Repository: liangbin77/certd Branch: v2 Commit: af54f48cec59 Files: 2332 Total size: 15.8 MB Directory structure: gitextract_oxfbws1h/ ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── workflows/ │ ├── build-image-for-test.yml │ ├── build-image.yml │ ├── deploy-demo.yml │ ├── sync-to-gitee-dev.yml │ └── sync-to-gitee.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── LICENSE.md ├── README.md ├── README_en.md ├── build-dev.trigger ├── build.trigger ├── deploy.js ├── deploy.trigger ├── docker/ │ └── run/ │ └── docker-compose.yaml ├── docs/ │ ├── .gitignore │ ├── .vitepress/ │ │ ├── config.ts │ │ └── theme/ │ │ ├── Layout.vue │ │ ├── index.ts │ │ ├── plugins/ │ │ │ └── baidutongji.ts │ │ └── style.css │ ├── guide/ │ │ ├── changelogs/ │ │ │ └── CHANGELOG.md │ │ ├── contact/ │ │ │ └── index.md │ │ ├── development/ │ │ │ ├── demo/ │ │ │ │ └── access.md │ │ │ └── index.md │ │ ├── donate/ │ │ │ └── index.md │ │ ├── feature/ │ │ │ ├── cname/ │ │ │ │ └── index.md │ │ │ └── safe/ │ │ │ ├── hidden/ │ │ │ │ └── index.md │ │ │ └── index.md │ │ ├── image.md │ │ ├── index.md │ │ ├── install/ │ │ │ ├── 1panel/ │ │ │ │ └── index.md │ │ │ ├── baota/ │ │ │ │ └── index.md │ │ │ ├── database.md │ │ │ ├── docker/ │ │ │ │ └── index.md │ │ │ ├── source/ │ │ │ │ └── index.md │ │ │ └── upgrade.md │ │ ├── license/ │ │ │ └── index.md │ │ ├── link/ │ │ │ └── index.md │ │ ├── open/ │ │ │ └── index.md │ │ ├── plugins/ │ │ │ ├── access.md │ │ │ ├── deploy.md │ │ │ ├── dns-provider.md │ │ │ └── notification.md │ │ ├── qa/ │ │ │ ├── index.md │ │ │ └── use.md │ │ ├── start.md │ │ ├── tutorial.md │ │ └── use/ │ │ ├── ESXi/ │ │ │ └── index.md │ │ ├── aliyun/ │ │ │ └── index.md │ │ ├── backup/ │ │ │ └── index.md │ │ ├── cert/ │ │ │ └── index.md │ │ ├── cf/ │ │ │ └── cf.md │ │ ├── comm/ │ │ │ ├── index.md │ │ │ └── payments/ │ │ │ ├── alipay.md │ │ │ ├── wxpay.md │ │ │ └── yizhifu.md │ │ ├── custom-script/ │ │ │ └── index.md │ │ ├── email/ │ │ │ └── index.md │ │ ├── forgotpasswd/ │ │ │ └── index.md │ │ ├── google/ │ │ │ └── index.md │ │ ├── host/ │ │ │ └── windows.md │ │ ├── https/ │ │ │ └── index.md │ │ ├── pretask/ │ │ │ └── index.md │ │ ├── rerun/ │ │ │ └── index.md │ │ ├── setting/ │ │ │ └── ipv6.md │ │ ├── synology/ │ │ │ └── index.md │ │ └── tencent/ │ │ └── index.md │ ├── index.md │ └── public/ │ └── robots.txt ├── index.ts ├── init.sh ├── lerna.json ├── package.json ├── packages/ │ ├── core/ │ │ ├── acme-client/ │ │ │ ├── .editorconfig │ │ │ ├── .eslintrc │ │ │ ├── .github/ │ │ │ │ ├── scripts/ │ │ │ │ │ ├── tests-install-coredns.sh │ │ │ │ │ ├── tests-install-cts.sh │ │ │ │ │ ├── tests-install-pebble.sh │ │ │ │ │ └── tests-wait-for-ca.sh │ │ │ │ └── workflows/ │ │ │ │ └── tests.yml │ │ │ ├── .gitignore │ │ │ ├── .prettierrc │ │ │ ├── CHANGELOG.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── build.md │ │ │ ├── docs/ │ │ │ │ ├── client.md │ │ │ │ ├── crypto.md │ │ │ │ ├── forge.md │ │ │ │ └── upgrade-v5.md │ │ │ ├── examples/ │ │ │ │ ├── README.md │ │ │ │ ├── api.js │ │ │ │ ├── auto.js │ │ │ │ ├── dns-01/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── dns-01.js │ │ │ │ ├── fallback.crt │ │ │ │ ├── fallback.key │ │ │ │ ├── http-01/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── http-01.js │ │ │ │ └── tls-alpn-01/ │ │ │ │ ├── README.md │ │ │ │ ├── haproxy.cfg │ │ │ │ ├── nginx.conf │ │ │ │ └── tls-alpn-01.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── api.js │ │ │ │ ├── auto.js │ │ │ │ ├── axios.js │ │ │ │ ├── client.js │ │ │ │ ├── crypto/ │ │ │ │ │ ├── forge.js │ │ │ │ │ └── index.js │ │ │ │ ├── error.js │ │ │ │ ├── http.js │ │ │ │ ├── index.js │ │ │ │ ├── logger.js │ │ │ │ ├── util.js │ │ │ │ ├── verify.js │ │ │ │ └── wait.js │ │ │ ├── test/ │ │ │ │ ├── 00-pebble.spec.js │ │ │ │ ├── 10-http.spec.js │ │ │ │ ├── 10-logger.spec.js │ │ │ │ ├── 10-util.spec.js │ │ │ │ ├── 10-verify.spec.js │ │ │ │ ├── 20-crypto-legacy.spec.js │ │ │ │ ├── 20-crypto.spec.js │ │ │ │ ├── 50-client.spec.js │ │ │ │ ├── 70-auto.spec.js │ │ │ │ ├── challtestsrv.js │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── certificate.crt │ │ │ │ │ ├── letsencrypt.crt │ │ │ │ │ ├── private.key │ │ │ │ │ └── san-certificate.crt │ │ │ │ ├── get-cert-issuers.js │ │ │ │ ├── retry.js │ │ │ │ ├── setup.js │ │ │ │ ├── soa.spec.mjs │ │ │ │ └── spec.js │ │ │ └── types/ │ │ │ ├── index.d.ts │ │ │ ├── index.test-d.ts │ │ │ └── rfc8555.d.ts │ │ ├── basic/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── .npmrc │ │ │ ├── .prettierrc │ │ │ ├── CHANGELOG.md │ │ │ ├── LICENSE │ │ │ ├── build.md │ │ │ ├── package.json │ │ │ ├── readme.md │ │ │ ├── src/ │ │ │ │ ├── index.ts │ │ │ │ └── utils/ │ │ │ │ ├── index.ts │ │ │ │ ├── util.amount.ts │ │ │ │ ├── util.cache.ts │ │ │ │ ├── util.domain.ts │ │ │ │ ├── util.env.ts │ │ │ │ ├── util.file.ts │ │ │ │ ├── util.hash.ts │ │ │ │ ├── util.id.ts │ │ │ │ ├── util.lock.ts │ │ │ │ ├── util.log.ts │ │ │ │ ├── util.merge.ts │ │ │ │ ├── util.mitter.ts │ │ │ │ ├── util.options.ts │ │ │ │ ├── util.promise.ts │ │ │ │ ├── util.request.ts │ │ │ │ ├── util.sleep.ts │ │ │ │ ├── util.sp.ts │ │ │ │ └── util.string.ts │ │ │ └── tsconfig.json │ │ └── pipeline/ │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .mocharc.json │ │ ├── .npmignore │ │ ├── .npmrc │ │ ├── .prettierrc │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── build.md │ │ ├── fix-esm-import-paths.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── access/ │ │ │ │ ├── api.ts │ │ │ │ ├── decorator.ts │ │ │ │ ├── index.ts │ │ │ │ └── registry.ts │ │ │ ├── context/ │ │ │ │ └── index.ts │ │ │ ├── core/ │ │ │ │ ├── context.ts │ │ │ │ ├── exceptions.ts │ │ │ │ ├── executor.ts │ │ │ │ ├── file-store.ts │ │ │ │ ├── index.ts │ │ │ │ ├── run-history.ts │ │ │ │ └── storage.ts │ │ │ ├── decorator/ │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── dt/ │ │ │ │ ├── fast-crud.ts │ │ │ │ ├── index.ts │ │ │ │ └── pipeline.ts │ │ │ ├── index.ts │ │ │ ├── notification/ │ │ │ │ ├── api.ts │ │ │ │ ├── decorator.ts │ │ │ │ ├── index.ts │ │ │ │ └── registry.ts │ │ │ ├── plugin/ │ │ │ │ ├── api.ts │ │ │ │ ├── decorator.ts │ │ │ │ ├── group.ts │ │ │ │ ├── index.ts │ │ │ │ └── registry.ts │ │ │ ├── registry/ │ │ │ │ ├── index.ts │ │ │ │ └── registry.ts │ │ │ └── service/ │ │ │ ├── cname.ts │ │ │ ├── config.ts │ │ │ ├── email.ts │ │ │ ├── emit.ts │ │ │ ├── index.ts │ │ │ └── url.ts │ │ ├── test/ │ │ │ ├── cert.fake.test.ts │ │ │ ├── index.test.ts │ │ │ └── pipeline/ │ │ │ ├── .gitignore │ │ │ ├── access-service-test.ts │ │ │ ├── init.test.ts │ │ │ ├── pipeline.define.ts │ │ │ └── pipeline.test.ts │ │ └── tsconfig.json │ ├── libs/ │ │ ├── lib-huawei/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── .prettierrc │ │ │ ├── CHANGELOG.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── fix-esm-import-paths.js │ │ │ ├── package.json │ │ │ ├── rollup.config.js │ │ │ ├── src/ │ │ │ │ ├── index.ts │ │ │ │ └── lib/ │ │ │ │ ├── APIGW-javascript-sdk-2.0.5/ │ │ │ │ │ ├── licenses/ │ │ │ │ │ │ ├── license-crypto-js │ │ │ │ │ │ └── license-node │ │ │ │ │ ├── node_demo.js │ │ │ │ │ └── signer.js │ │ │ │ ├── client.d.ts │ │ │ │ ├── client.js │ │ │ │ ├── signer.d.ts │ │ │ │ └── signer.js │ │ │ └── tsconfig.json │ │ ├── lib-iframe/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── .prettierrc │ │ │ ├── CHANGELOG.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── fix-esm-import-paths.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.ts │ │ │ │ └── lib/ │ │ │ │ └── iframe.client.ts │ │ │ └── tsconfig.json │ │ ├── lib-jdcloud/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── .travis.yml │ │ │ ├── CHANGELOG.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── rollup.config.js │ │ │ ├── src/ │ │ │ │ ├── global.js │ │ │ │ ├── index.ts │ │ │ │ ├── jdcloud.js │ │ │ │ ├── lib/ │ │ │ │ │ ├── browser.js │ │ │ │ │ ├── browser_loader.js │ │ │ │ │ ├── common.js │ │ │ │ │ ├── config.js │ │ │ │ │ ├── core.js │ │ │ │ │ ├── credentials.js │ │ │ │ │ ├── jc.js │ │ │ │ │ ├── node_loader.js │ │ │ │ │ ├── request.js │ │ │ │ │ ├── service.js │ │ │ │ │ ├── signers/ │ │ │ │ │ │ ├── request_signer.js │ │ │ │ │ │ ├── v2.js │ │ │ │ │ │ └── v2_credentials.js │ │ │ │ │ └── util.js │ │ │ │ ├── repo/ │ │ │ │ │ ├── ag/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── ag.js │ │ │ │ │ ├── ams/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── ams.js │ │ │ │ │ ├── antipro/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── antipro.js │ │ │ │ │ ├── apigateway/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── apigateway.js │ │ │ │ │ ├── asset/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── asset.js │ │ │ │ │ ├── assistant/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── assistant.js │ │ │ │ │ ├── autotaskpolicy/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── autotaskpolicy.js │ │ │ │ │ ├── baseanti/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── baseanti.js │ │ │ │ │ ├── bastion/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── bastion.js │ │ │ │ │ ├── bgw/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── bgw.js │ │ │ │ │ ├── billing/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── billing.js │ │ │ │ │ ├── bri/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── bri.js │ │ │ │ │ ├── captcha/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── captcha.js │ │ │ │ │ ├── cdn/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── cdn.js │ │ │ │ │ ├── censor/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── censor.js │ │ │ │ │ ├── clickhouse/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── clickhouse.js │ │ │ │ │ ├── cloudauth/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── cloudauth.js │ │ │ │ │ ├── clouddnsservice/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── clouddnsservice.js │ │ │ │ │ ├── cloudsign/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── cloudsign.js │ │ │ │ │ ├── compile/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── compile.js │ │ │ │ │ ├── containerregistry/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── containerregistry.js │ │ │ │ │ ├── cp/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── cp.js │ │ │ │ │ ├── cps/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── cps.js │ │ │ │ │ ├── cr/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── cr.js │ │ │ │ │ ├── datastar/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── datastar.js │ │ │ │ │ ├── dbaudit/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── dbaudit.js │ │ │ │ │ ├── dbs/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── dbs.js │ │ │ │ │ ├── dcap/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── dcap.js │ │ │ │ │ ├── deploy/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── deploy.js │ │ │ │ │ ├── detection/ │ │ │ │ │ │ ├── v2/ │ │ │ │ │ │ │ └── detection.js │ │ │ │ │ │ └── v3/ │ │ │ │ │ │ └── detection.js │ │ │ │ │ ├── dh/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── dh.js │ │ │ │ │ ├── disk/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── disk.js │ │ │ │ │ ├── dms/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── dms.js │ │ │ │ │ ├── domain/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── domain.js │ │ │ │ │ ├── domainservice/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── domainservice.js │ │ │ │ │ ├── dts/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── dts.js │ │ │ │ │ ├── edcps/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── edcps.js │ │ │ │ │ ├── eid/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── eid.js │ │ │ │ │ ├── elite/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── elite.js │ │ │ │ │ ├── es/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── es.js │ │ │ │ │ ├── fc/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── fc.js │ │ │ │ │ ├── flowlog/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── flowlog.js │ │ │ │ │ ├── function/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── function.js │ │ │ │ │ ├── gcs/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── gcs.js │ │ │ │ │ ├── httpdns/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── httpdns.js │ │ │ │ │ ├── hufu/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── hufu.js │ │ │ │ │ ├── iam/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── iam.js │ │ │ │ │ ├── ias/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── ias.js │ │ │ │ │ ├── industrydata/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── industrydata.js │ │ │ │ │ ├── instancevoucher/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── instancevoucher.js │ │ │ │ │ ├── invoice/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── invoice.js │ │ │ │ │ ├── iotcard/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── iotcard.js │ │ │ │ │ ├── iotcloudgateway/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── iotcloudgateway.js │ │ │ │ │ ├── iotcore/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── iotcore.js │ │ │ │ │ ├── iotedge/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── iotedge.js │ │ │ │ │ ├── iothub/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── iothub.js │ │ │ │ │ ├── iotlink/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── iotlink.js │ │ │ │ │ ├── ipanti/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── ipanti.js │ │ │ │ │ ├── iv/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── iv.js │ │ │ │ │ ├── jcq/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── jcq.js │ │ │ │ │ ├── jdccs/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── jdccs.js │ │ │ │ │ ├── jdfusion/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── jdfusion.js │ │ │ │ │ ├── jdro/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── jdro.js │ │ │ │ │ ├── jdw/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── jdw.js │ │ │ │ │ ├── jdworkspace/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── jdworkspace.js │ │ │ │ │ ├── jke/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── jke.js │ │ │ │ │ ├── jmr/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── jmr.js │ │ │ │ │ ├── kafka/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── kafka.js │ │ │ │ │ ├── kms/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── kms.js │ │ │ │ │ ├── kubernetes/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── kubernetes.js │ │ │ │ │ ├── lavm/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── lavm.js │ │ │ │ │ ├── lb/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── lb.js │ │ │ │ │ ├── live/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── live.js │ │ │ │ │ ├── mongodb/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── mongodb.js │ │ │ │ │ ├── monitor/ │ │ │ │ │ │ ├── v1/ │ │ │ │ │ │ │ └── monitor.js │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── monitor.js │ │ │ │ │ ├── monitorcm/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── monitorcm.js │ │ │ │ │ ├── mps/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── mps.js │ │ │ │ │ ├── nativecontainer/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── nativecontainer.js │ │ │ │ │ ├── nc/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── nc.js │ │ │ │ │ ├── openjrtc/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── openjrtc.js │ │ │ │ │ ├── oss/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── oss.js │ │ │ │ │ ├── ossopenapi/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── ossopenapi.js │ │ │ │ │ ├── partner/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── partner.js │ │ │ │ │ ├── pipeline/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── pipeline.js │ │ │ │ │ ├── pod/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── pod.js │ │ │ │ │ ├── portal/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── portal.js │ │ │ │ │ ├── privatezone/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── privatezone.js │ │ │ │ │ ├── quota/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── quota.js │ │ │ │ │ ├── rds/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── rds.js │ │ │ │ │ ├── rdts/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── rdts.js │ │ │ │ │ ├── redis/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── redis.js │ │ │ │ │ ├── refund/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── refund.js │ │ │ │ │ ├── reservedinstance/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── reservedinstance.js │ │ │ │ │ ├── resourcetag/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── resourcetag.js │ │ │ │ │ ├── rms/ │ │ │ │ │ │ ├── v1/ │ │ │ │ │ │ │ └── rms.js │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── rms.js │ │ │ │ │ ├── smartdba/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── smartdba.js │ │ │ │ │ ├── sms/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── sms.js │ │ │ │ │ ├── sop/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── sop.js │ │ │ │ │ ├── ssl/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── ssl.js │ │ │ │ │ ├── starshield/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── starshield.js │ │ │ │ │ ├── streambus/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── streambus.js │ │ │ │ │ ├── streamcomputer/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── streamcomputer.js │ │ │ │ │ ├── sts/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── sts.js │ │ │ │ │ ├── tidb/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── tidb.js │ │ │ │ │ ├── user/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── user.js │ │ │ │ │ ├── userpool/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── userpool.js │ │ │ │ │ ├── vm/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── vm.js │ │ │ │ │ ├── vod/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── vod.js │ │ │ │ │ ├── vpc/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── vpc.js │ │ │ │ │ ├── waf/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── waf.js │ │ │ │ │ ├── xdata/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── xdata.js │ │ │ │ │ ├── ydsms/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── ydsms.js │ │ │ │ │ ├── yunding/ │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ └── yunding.js │ │ │ │ │ ├── yundingdatapush/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ └── yundingdatapush.js │ │ │ │ │ └── zfs/ │ │ │ │ │ └── v1/ │ │ │ │ │ └── zfs.js │ │ │ │ └── services/ │ │ │ │ ├── .gitkeep │ │ │ │ ├── ag.js │ │ │ │ ├── all.js │ │ │ │ ├── ams.js │ │ │ │ ├── antipro.js │ │ │ │ ├── apigateway.js │ │ │ │ ├── asset.js │ │ │ │ ├── assistant.js │ │ │ │ ├── autotaskpolicy.js │ │ │ │ ├── baseanti.js │ │ │ │ ├── bastion.js │ │ │ │ ├── bgw.js │ │ │ │ ├── billing.js │ │ │ │ ├── bri.js │ │ │ │ ├── captcha.js │ │ │ │ ├── cdn.js │ │ │ │ ├── censor.js │ │ │ │ ├── clickhouse.js │ │ │ │ ├── cloudauth.js │ │ │ │ ├── clouddnsservice.js │ │ │ │ ├── cloudsign.js │ │ │ │ ├── compile.js │ │ │ │ ├── containerregistry.js │ │ │ │ ├── cp.js │ │ │ │ ├── cps.js │ │ │ │ ├── cr.js │ │ │ │ ├── datastar.js │ │ │ │ ├── dbaudit.js │ │ │ │ ├── dbs.js │ │ │ │ ├── dcap.js │ │ │ │ ├── deploy.js │ │ │ │ ├── detection.js │ │ │ │ ├── dh.js │ │ │ │ ├── disk.js │ │ │ │ ├── dms.js │ │ │ │ ├── domain.js │ │ │ │ ├── domainservice.js │ │ │ │ ├── dts.js │ │ │ │ ├── edcps.js │ │ │ │ ├── eid.js │ │ │ │ ├── elite.js │ │ │ │ ├── es.js │ │ │ │ ├── fc.js │ │ │ │ ├── flowlog.js │ │ │ │ ├── function.js │ │ │ │ ├── gcs.js │ │ │ │ ├── httpdns.js │ │ │ │ ├── hufu.js │ │ │ │ ├── iam.js │ │ │ │ ├── ias.js │ │ │ │ ├── industrydata.js │ │ │ │ ├── instancevoucher.js │ │ │ │ ├── invoice.js │ │ │ │ ├── iotcard.js │ │ │ │ ├── iotcloudgateway.js │ │ │ │ ├── iotcore.js │ │ │ │ ├── iotedge.js │ │ │ │ ├── iothub.js │ │ │ │ ├── iotlink.js │ │ │ │ ├── ipanti.js │ │ │ │ ├── iv.js │ │ │ │ ├── jcq.js │ │ │ │ ├── jdccs.js │ │ │ │ ├── jdfusion.js │ │ │ │ ├── jdro.js │ │ │ │ ├── jdw.js │ │ │ │ ├── jdworkspace.js │ │ │ │ ├── jke.js │ │ │ │ ├── jmr.js │ │ │ │ ├── kafka.js │ │ │ │ ├── kms.js │ │ │ │ ├── kubernetes.js │ │ │ │ ├── lavm.js │ │ │ │ ├── lb.js │ │ │ │ ├── live.js │ │ │ │ ├── logs.js │ │ │ │ ├── mongodb.js │ │ │ │ ├── monitor.js │ │ │ │ ├── monitorcm.js │ │ │ │ ├── mps.js │ │ │ │ ├── nativecontainer.js │ │ │ │ ├── nc.js │ │ │ │ ├── openjrtc.js │ │ │ │ ├── order.js │ │ │ │ ├── oss.js │ │ │ │ ├── ossopenapi.js │ │ │ │ ├── partner.js │ │ │ │ ├── pipeline.js │ │ │ │ ├── pod.js │ │ │ │ ├── portal.js │ │ │ │ ├── privatezone.js │ │ │ │ ├── quota.js │ │ │ │ ├── rds.js │ │ │ │ ├── rdts.js │ │ │ │ ├── redis.js │ │ │ │ ├── refund.js │ │ │ │ ├── renewal.js │ │ │ │ ├── reservedinstance.js │ │ │ │ ├── resourcetag.js │ │ │ │ ├── rms.js │ │ │ │ ├── smartdba.js │ │ │ │ ├── sms.js │ │ │ │ ├── sop.js │ │ │ │ ├── ssl.js │ │ │ │ ├── starshield.js │ │ │ │ ├── streambus.js │ │ │ │ ├── streamcomputer.js │ │ │ │ ├── sts.js │ │ │ │ ├── tidb.js │ │ │ │ ├── user.js │ │ │ │ ├── userpool.js │ │ │ │ ├── vm.js │ │ │ │ ├── vod.js │ │ │ │ ├── vpc.js │ │ │ │ ├── vqd.js │ │ │ │ ├── waf.js │ │ │ │ ├── xdata.js │ │ │ │ ├── ydsms.js │ │ │ │ ├── yunding.js │ │ │ │ ├── yundingdatapush.js │ │ │ │ └── zfs.js │ │ │ ├── test/ │ │ │ │ ├── callStyle.spec.js │ │ │ │ ├── config/ │ │ │ │ │ └── default.yml │ │ │ │ ├── config.spec.js │ │ │ │ └── services/ │ │ │ │ ├── monitor.spec.js │ │ │ │ ├── nc.spec.js │ │ │ │ ├── oss.spec.js │ │ │ │ └── vm.spec.js │ │ │ └── tsconfig.json │ │ ├── lib-k8s/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── .prettierrc │ │ │ ├── CHANGELOG.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── fix-esm-import-paths.js │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.ts │ │ │ │ └── lib/ │ │ │ │ └── k8s.client.ts │ │ │ └── tsconfig.json │ │ ├── lib-server/ │ │ │ ├── .dockerignore │ │ │ ├── .editorconfig │ │ │ ├── .eslintrc.json │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── .prettierrc │ │ │ ├── CHANGELOG.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── README_zhCN.md │ │ │ ├── jest.config.js │ │ │ ├── ormconfig.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── basic/ │ │ │ │ │ ├── base-controller.ts │ │ │ │ │ ├── base-service.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── crud-controller.ts │ │ │ │ │ ├── enum-item.ts │ │ │ │ │ ├── exception/ │ │ │ │ │ │ ├── auth-exception.ts │ │ │ │ │ │ ├── base-exception.ts │ │ │ │ │ │ ├── code-error-exception.ts │ │ │ │ │ │ ├── common-exception.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── login-error-exception.ts │ │ │ │ │ │ ├── not-found-exception.ts │ │ │ │ │ │ ├── param-exception.ts │ │ │ │ │ │ ├── permission-exception.ts │ │ │ │ │ │ ├── preview-exception.ts │ │ │ │ │ │ ├── site-off-exception.ts │ │ │ │ │ │ ├── validation-exception.ts │ │ │ │ │ │ └── vip-exception.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── result.ts │ │ │ │ ├── configuration.ts │ │ │ │ ├── index.ts │ │ │ │ ├── system/ │ │ │ │ │ ├── basic/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── service/ │ │ │ │ │ │ ├── encryptor.ts │ │ │ │ │ │ ├── file-service.ts │ │ │ │ │ │ └── plus-service.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── settings/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ └── sys-settings.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── service/ │ │ │ │ │ ├── models.ts │ │ │ │ │ └── sys-settings-service.ts │ │ │ │ └── user/ │ │ │ │ ├── access/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ └── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── service/ │ │ │ │ │ ├── access-getter.ts │ │ │ │ │ ├── access-service.ts │ │ │ │ │ ├── access-sys-getter.ts │ │ │ │ │ └── encrypt-service.ts │ │ │ │ └── index.ts │ │ │ ├── test.md │ │ │ └── tsconfig.json │ │ └── midway-flyway-js/ │ │ ├── .dockerignore │ │ ├── .editorconfig │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── .prettierrc │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── README_zhCN.md │ │ ├── jest.config.js │ │ ├── ormconfig.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── configuration.ts │ │ │ ├── entity.ts │ │ │ ├── flyway.ts │ │ │ └── index.ts │ │ ├── test/ │ │ │ ├── db/ │ │ │ │ ├── baseline/ │ │ │ │ │ ├── v0__baseline.sql │ │ │ │ │ └── v1__init.sql │ │ │ │ ├── blank/ │ │ │ │ │ └── v1__blank.sql │ │ │ │ ├── hash-check/ │ │ │ │ │ ├── v1__init.sql │ │ │ │ │ ├── v2__add_user.sql │ │ │ │ │ └── v3__add_role.sql │ │ │ │ ├── migration/ │ │ │ │ │ ├── v1__init.sql │ │ │ │ │ ├── v2__add_user.sql │ │ │ │ │ └── v3__add_role.sql │ │ │ │ ├── semicolon/ │ │ │ │ │ └── v1__init.sql │ │ │ │ └── split/ │ │ │ │ └── split.sql │ │ │ ├── flyway.test.js │ │ │ └── flyway.test.ts │ │ ├── test.md │ │ └── tsconfig.json │ ├── plugins/ │ │ ├── plugin-cert/ │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── .mocharc.json │ │ │ ├── .npmignore │ │ │ ├── .npmrc │ │ │ ├── .prettierrc │ │ │ ├── CHANGELOG.md │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── access/ │ │ │ │ │ ├── eab-access.ts │ │ │ │ │ ├── google-access.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── dns-provider/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── base.ts │ │ │ │ │ ├── decorator.ts │ │ │ │ │ ├── domain-parser.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── registry.ts │ │ │ │ ├── index.ts │ │ │ │ ├── libs/ │ │ │ │ │ └── google.ts │ │ │ │ └── plugin/ │ │ │ │ ├── cert-plugin/ │ │ │ │ │ ├── acme.ts │ │ │ │ │ ├── base-convert.ts │ │ │ │ │ ├── base.ts │ │ │ │ │ ├── cert-reader.ts │ │ │ │ │ ├── convert.ts │ │ │ │ │ ├── custom/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lego/ │ │ │ │ │ ├── dns.ts │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── test/ │ │ │ │ └── cert-plugin.test.mjs │ │ │ └── tsconfig.json │ │ └── plugin-lib/ │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .mocharc.json │ │ ├── .npmignore │ │ ├── .npmrc │ │ ├── .prettierrc │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── aliyun/ │ │ │ │ ├── access/ │ │ │ │ │ ├── alioss-access.ts │ │ │ │ │ ├── aliyun-access.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── lib/ │ │ │ │ ├── base-client.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oss-client.ts │ │ │ │ └── ssl-client.ts │ │ │ ├── common/ │ │ │ │ ├── index.ts │ │ │ │ └── util.ts │ │ │ ├── ctyun/ │ │ │ │ ├── access/ │ │ │ │ │ └── ctyun-access.ts │ │ │ │ └── index.ts │ │ │ ├── ftp/ │ │ │ │ ├── access.ts │ │ │ │ ├── client.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oss/ │ │ │ │ ├── api.ts │ │ │ │ ├── factory.ts │ │ │ │ ├── impls/ │ │ │ │ │ ├── alioss.ts │ │ │ │ │ ├── ftp.ts │ │ │ │ │ ├── qiniuoss.ts │ │ │ │ │ ├── s3.ts │ │ │ │ │ ├── sftp.ts │ │ │ │ │ ├── ssh.ts │ │ │ │ │ └── tencentcos.ts │ │ │ │ └── index.ts │ │ │ ├── qiniu/ │ │ │ │ ├── access-oss.ts │ │ │ │ ├── access.ts │ │ │ │ ├── index.ts │ │ │ │ └── lib/ │ │ │ │ └── sdk.ts │ │ │ ├── s3/ │ │ │ │ ├── access.ts │ │ │ │ └── index.ts │ │ │ ├── ssh/ │ │ │ │ ├── index.ts │ │ │ │ ├── sftp-access.ts │ │ │ │ ├── ssh-access.ts │ │ │ │ └── ssh.ts │ │ │ └── tencent/ │ │ │ ├── access-cos.ts │ │ │ ├── access.ts │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ ├── cos-client.ts │ │ │ ├── index.ts │ │ │ └── ssl-client.ts │ │ └── tsconfig.json │ └── ui/ │ ├── .dockerignore │ ├── Dockerfile │ ├── agent/ │ │ └── Dockerfile │ ├── certd-client/ │ │ ├── .browserslistrc │ │ ├── .eslintignore │ │ ├── .eslintrc.js │ │ ├── .github/ │ │ │ ├── ISSUE_TEMPLATE.md │ │ │ └── workflows/ │ │ │ └── sync-to-gitee.yml │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── .npmrc │ │ ├── .prettierrc │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── build/ │ │ │ ├── modify-vars.ts │ │ │ ├── tailwind-config/ │ │ │ │ ├── index.mjs │ │ │ │ ├── plugins/ │ │ │ │ │ └── entry.mjs │ │ │ │ └── postcss.config.mjs │ │ │ ├── theme-colors.ts │ │ │ └── theme-plugin.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── public/ │ │ │ ├── site-import-template.csv │ │ │ └── static/ │ │ │ ├── icons/ │ │ │ │ ├── demo.css │ │ │ │ ├── demo_index.html │ │ │ │ ├── iconfont.css │ │ │ │ ├── iconfont.js │ │ │ │ └── iconfont.json │ │ │ └── index.css │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── api/ │ │ │ │ ├── service.ts │ │ │ │ └── tools.ts │ │ │ ├── components/ │ │ │ │ ├── ai/ │ │ │ │ │ └── index.vue │ │ │ │ ├── code-editor/ │ │ │ │ │ ├── async-import.ts │ │ │ │ │ ├── import-works.ts │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── validators.ts │ │ │ │ │ ├── workers.ts │ │ │ │ │ └── yaml.worker.ts │ │ │ │ ├── container.vue │ │ │ │ ├── cron-editor/ │ │ │ │ │ ├── index.vue │ │ │ │ │ └── utils.ts │ │ │ │ ├── editable.vue │ │ │ │ ├── email-selector/ │ │ │ │ │ ├── api.ts │ │ │ │ │ └── index.vue │ │ │ │ ├── expires-time-text.vue │ │ │ │ ├── file-input.vue │ │ │ │ ├── fold-box.vue │ │ │ │ ├── highlight/ │ │ │ │ │ ├── index.vue │ │ │ │ │ └── libs/ │ │ │ │ │ └── htmlFormat.js │ │ │ │ ├── highlight-styles/ │ │ │ │ │ ├── agate.css │ │ │ │ │ ├── androidstudio.css │ │ │ │ │ ├── arduino-light.css │ │ │ │ │ ├── arta.css │ │ │ │ │ ├── ascetic.css │ │ │ │ │ ├── atelier-cave-dark.css │ │ │ │ │ ├── atelier-cave-light.css │ │ │ │ │ ├── atelier-dune-dark.css │ │ │ │ │ ├── atelier-dune-light.css │ │ │ │ │ ├── atelier-estuary-dark.css │ │ │ │ │ ├── atelier-estuary-light.css │ │ │ │ │ ├── atelier-forest-dark.css │ │ │ │ │ ├── atelier-forest-light.css │ │ │ │ │ ├── atelier-heath-dark.css │ │ │ │ │ ├── atelier-heath-light.css │ │ │ │ │ ├── atelier-lakeside-dark.css │ │ │ │ │ ├── atelier-lakeside-light.css │ │ │ │ │ ├── atelier-plateau-dark.css │ │ │ │ │ ├── atelier-plateau-light.css │ │ │ │ │ ├── atelier-savanna-dark.css │ │ │ │ │ ├── atelier-savanna-light.css │ │ │ │ │ ├── atelier-seaside-dark.css │ │ │ │ │ ├── atelier-seaside-light.css │ │ │ │ │ ├── atelier-sulphurpool-dark.css │ │ │ │ │ ├── atelier-sulphurpool-light.css │ │ │ │ │ ├── atom-one-dark.css │ │ │ │ │ ├── atom-one-light.css │ │ │ │ │ ├── brown-paper.css │ │ │ │ │ ├── codepen-embed.css │ │ │ │ │ ├── color-brewer.css │ │ │ │ │ ├── darcula.css │ │ │ │ │ ├── dark.css │ │ │ │ │ ├── darkula.css │ │ │ │ │ ├── default.css │ │ │ │ │ ├── docco.css │ │ │ │ │ ├── dracula.css │ │ │ │ │ ├── far.css │ │ │ │ │ ├── foundation.css │ │ │ │ │ ├── github-gist.css │ │ │ │ │ ├── github.css │ │ │ │ │ ├── googlecode.css │ │ │ │ │ ├── grayscale.css │ │ │ │ │ ├── gruvbox-dark.css │ │ │ │ │ ├── gruvbox-light.css │ │ │ │ │ ├── hopscotch.css │ │ │ │ │ ├── hybrid.css │ │ │ │ │ ├── idea.css │ │ │ │ │ ├── ir-black.css │ │ │ │ │ ├── kimbie.dark.css │ │ │ │ │ ├── kimbie.light.css │ │ │ │ │ ├── magula.css │ │ │ │ │ ├── mono-blue.css │ │ │ │ │ ├── monokai-sublime.css │ │ │ │ │ ├── monokai.css │ │ │ │ │ ├── obsidian.css │ │ │ │ │ ├── ocean.css │ │ │ │ │ ├── paraiso-dark.css │ │ │ │ │ ├── paraiso-light.css │ │ │ │ │ ├── pojoaque.css │ │ │ │ │ ├── purebasic.css │ │ │ │ │ ├── qtcreator_dark.css │ │ │ │ │ ├── qtcreator_light.css │ │ │ │ │ ├── railscasts.css │ │ │ │ │ ├── rainbow.css │ │ │ │ │ ├── routeros.css │ │ │ │ │ ├── school-book.css │ │ │ │ │ ├── solarized-dark.css │ │ │ │ │ ├── solarized-light.css │ │ │ │ │ ├── sunburst.css │ │ │ │ │ ├── tomorrow-night-blue.css │ │ │ │ │ ├── tomorrow-night-bright.css │ │ │ │ │ ├── tomorrow-night-eighties.css │ │ │ │ │ ├── tomorrow-night.css │ │ │ │ │ ├── tomorrow.css │ │ │ │ │ ├── vs.css │ │ │ │ │ ├── vs2015.css │ │ │ │ │ ├── xcode.css │ │ │ │ │ ├── xt256.css │ │ │ │ │ └── zenburn.css │ │ │ │ ├── icon-select.vue │ │ │ │ ├── index.ts │ │ │ │ ├── loading-button.vue │ │ │ │ ├── pem-input.vue │ │ │ │ ├── plugins/ │ │ │ │ │ ├── cert/ │ │ │ │ │ │ ├── dns-provider-selector/ │ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── domains-verify-plan-editor/ │ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ │ ├── cname-record-info.vue │ │ │ │ │ │ │ ├── cname-tip.vue │ │ │ │ │ │ │ ├── cname-verify-plan.vue │ │ │ │ │ │ │ ├── http-verify-plan.vue │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── type.ts │ │ │ │ │ │ │ └── validator.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── common/ │ │ │ │ │ │ ├── api-test.vue │ │ │ │ │ │ ├── cert-domains-getter.vue │ │ │ │ │ │ ├── input-password.vue │ │ │ │ │ │ ├── output-selector/ │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── remote-input.vue │ │ │ │ │ │ └── remote-select.vue │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── lib/ │ │ │ │ │ │ ├── dicts.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── synology/ │ │ │ │ │ └── device-id-getter.vue │ │ │ │ ├── tutorial/ │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── simple-steps.vue │ │ │ │ │ └── tutorial-steps.vue │ │ │ │ ├── valid-time-format.vue │ │ │ │ └── vip-button/ │ │ │ │ ├── api.ts │ │ │ │ ├── directive.ts │ │ │ │ ├── index.vue │ │ │ │ └── install.ts │ │ │ ├── constants/ │ │ │ │ └── index.ts │ │ │ ├── layout/ │ │ │ │ ├── components/ │ │ │ │ │ ├── footer/ │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── locale/ │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── menu/ │ │ │ │ │ │ ├── index.less │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ └── index1.tsx │ │ │ │ │ ├── source-link/ │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── tabs/ │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── theme/ │ │ │ │ │ │ ├── color-picker.vue │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ └── mode-set.vue │ │ │ │ │ └── user-info/ │ │ │ │ │ └── index.vue │ │ │ │ ├── layout-basic.vue │ │ │ │ ├── layout-framework.vue │ │ │ │ ├── layout-outside.vue │ │ │ │ └── layout-pass.vue │ │ │ ├── locales/ │ │ │ │ ├── antdv.ts │ │ │ │ ├── i18n.ts │ │ │ │ ├── index.ts │ │ │ │ ├── langs/ │ │ │ │ │ ├── en-US/ │ │ │ │ │ │ ├── authentication.ts │ │ │ │ │ │ ├── certd.ts │ │ │ │ │ │ ├── common.ts │ │ │ │ │ │ ├── guide.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── preferences.ts │ │ │ │ │ │ ├── tutorial.ts │ │ │ │ │ │ ├── ui.ts │ │ │ │ │ │ └── vip.ts │ │ │ │ │ └── zh-CN/ │ │ │ │ │ ├── authentication.ts │ │ │ │ │ ├── certd.ts │ │ │ │ │ ├── common.ts │ │ │ │ │ ├── guide.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── preferences.ts │ │ │ │ │ ├── tutorial.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── vip.ts │ │ │ │ └── typing.ts │ │ │ ├── main.ts │ │ │ ├── mock/ │ │ │ │ ├── base.ts │ │ │ │ ├── common/ │ │ │ │ │ ├── cascader-data.ts │ │ │ │ │ ├── mock.dict.ts │ │ │ │ │ ├── pca-data-little.ts │ │ │ │ │ └── pcas-data.ts │ │ │ │ └── index.ts │ │ │ ├── plugin/ │ │ │ │ ├── antdv-async/ │ │ │ │ │ ├── index-bak.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── fast-crud/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── iconfont/ │ │ │ │ │ ├── iconfont.js │ │ │ │ │ └── index.ts │ │ │ │ ├── iconify/ │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── permission/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── directive/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── permission.ts │ │ │ │ │ ├── errors.ts │ │ │ │ │ ├── hook.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── store.permission.ts │ │ │ │ │ ├── use-crud-permission.ts │ │ │ │ │ └── util.permission.ts │ │ │ │ └── validator/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── validator.spec.ts │ │ │ │ └── index.ts │ │ │ ├── router/ │ │ │ │ ├── access.ts │ │ │ │ ├── guard.ts │ │ │ │ ├── index.ts │ │ │ │ ├── resolve.ts │ │ │ │ └── source/ │ │ │ │ ├── framework.ts │ │ │ │ ├── header.ts │ │ │ │ ├── modules/ │ │ │ │ │ ├── about.tsx │ │ │ │ │ ├── certd.ts │ │ │ │ │ └── sys.ts │ │ │ │ └── outside.ts │ │ │ ├── shims-vue.d.ts │ │ │ ├── store/ │ │ │ │ ├── plugin/ │ │ │ │ │ ├── api.plugin.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── settings/ │ │ │ │ │ ├── api.basic.ts │ │ │ │ │ └── index.ts │ │ │ │ └── user/ │ │ │ │ ├── api.user.ts │ │ │ │ └── index.ts │ │ │ ├── style/ │ │ │ │ ├── antdv4.less │ │ │ │ ├── certd.less │ │ │ │ ├── common.less │ │ │ │ ├── fix-windicss.less │ │ │ │ ├── scroll.less │ │ │ │ ├── tailwind.less │ │ │ │ └── transition.less │ │ │ ├── style.css │ │ │ ├── types/ │ │ │ │ └── global.d.ts │ │ │ ├── use/ │ │ │ │ ├── use-modal.ts │ │ │ │ └── use-refrence.tsx │ │ │ ├── utils/ │ │ │ │ ├── index.ts │ │ │ │ ├── util.amount.ts │ │ │ │ ├── util.cache.ts │ │ │ │ ├── util.common.ts │ │ │ │ ├── util.env.ts │ │ │ │ ├── util.hash.ts │ │ │ │ ├── util.mitt.ts │ │ │ │ ├── util.router.ts │ │ │ │ ├── util.site.ts │ │ │ │ ├── util.storage.ts │ │ │ │ └── util.tree.ts │ │ │ ├── vben/ │ │ │ │ ├── access/ │ │ │ │ │ ├── access-control.vue │ │ │ │ │ ├── accessible.ts │ │ │ │ │ ├── directive.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── use-access.ts │ │ │ │ ├── common-ui/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── api-component/ │ │ │ │ │ │ │ ├── api-component.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── captcha/ │ │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ │ └── useCaptchaPoints.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── point-selection-captcha/ │ │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ │ └── point-selection-captcha-card.vue │ │ │ │ │ │ │ ├── slider-captcha/ │ │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ │ ├── slider-captcha-action.vue │ │ │ │ │ │ │ │ ├── slider-captcha-bar.vue │ │ │ │ │ │ │ │ └── slider-captcha-content.vue │ │ │ │ │ │ │ ├── slider-rotate-captcha/ │ │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── col-page/ │ │ │ │ │ │ │ ├── col-page.vue │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── count-to/ │ │ │ │ │ │ │ ├── count-to.vue │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── ellipsis-text/ │ │ │ │ │ │ │ ├── ellipsis-text.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── icon-picker/ │ │ │ │ │ │ │ ├── icon-picker.vue │ │ │ │ │ │ │ ├── icons.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── json-viewer/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ ├── style.scss │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── loading/ │ │ │ │ │ │ │ ├── directive.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── loading.vue │ │ │ │ │ │ │ └── spinner.vue │ │ │ │ │ │ ├── page/ │ │ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ │ │ └── page.test.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── page.vue │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── resize/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── resize.vue │ │ │ │ │ │ └── tippy/ │ │ │ │ │ │ ├── directive.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── ui/ │ │ │ │ │ ├── about/ │ │ │ │ │ │ ├── about.ts │ │ │ │ │ │ ├── about.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── authentication/ │ │ │ │ │ │ ├── auth-title.vue │ │ │ │ │ │ ├── code-login.vue │ │ │ │ │ │ ├── forget-password.vue │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── login-expired-modal.vue │ │ │ │ │ │ ├── login.vue │ │ │ │ │ │ ├── qrcode-login.vue │ │ │ │ │ │ ├── register.vue │ │ │ │ │ │ ├── third-party-login.vue │ │ │ │ │ │ └── types.ts │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── analysis/ │ │ │ │ │ │ │ ├── analysis-chart-card.vue │ │ │ │ │ │ │ ├── analysis-charts-tabs.vue │ │ │ │ │ │ │ ├── analysis-overview.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── typing.ts │ │ │ │ │ │ └── workbench/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── workbench-header.vue │ │ │ │ │ │ ├── workbench-project.vue │ │ │ │ │ │ ├── workbench-quick-nav.vue │ │ │ │ │ │ ├── workbench-todo.vue │ │ │ │ │ │ └── workbench-trends.vue │ │ │ │ │ ├── fallback/ │ │ │ │ │ │ ├── fallback.ts │ │ │ │ │ │ ├── fallback.vue │ │ │ │ │ │ ├── icons/ │ │ │ │ │ │ │ ├── icon-403.vue │ │ │ │ │ │ │ ├── icon-404.vue │ │ │ │ │ │ │ ├── icon-500.vue │ │ │ │ │ │ │ ├── icon-coming-soon.vue │ │ │ │ │ │ │ └── icon-offline.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── composables/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ └── use-sortable.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── use-is-mobile.ts │ │ │ │ │ ├── use-layout-style.ts │ │ │ │ │ ├── use-namespace.ts │ │ │ │ │ ├── use-priority-value.ts │ │ │ │ │ ├── use-scroll-lock.ts │ │ │ │ │ ├── use-simple-locale/ │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── messages.ts │ │ │ │ │ └── use-sortable.ts │ │ │ │ ├── constants/ │ │ │ │ │ ├── core.ts │ │ │ │ │ ├── globals.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── vben.ts │ │ │ │ ├── design/ │ │ │ │ │ ├── css/ │ │ │ │ │ │ ├── global.css │ │ │ │ │ │ ├── nprogress.css │ │ │ │ │ │ ├── transition.css │ │ │ │ │ │ └── ui.css │ │ │ │ │ ├── design-tokens/ │ │ │ │ │ │ ├── dark.css │ │ │ │ │ │ ├── default.css │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── scss-bem/ │ │ │ │ │ ├── bem.scss │ │ │ │ │ └── constants.scss │ │ │ │ ├── form-ui/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ └── form-actions.vue │ │ │ │ │ ├── config.ts │ │ │ │ │ ├── form-api.ts │ │ │ │ │ ├── form-render/ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ ├── dependencies.ts │ │ │ │ │ │ ├── expandable.ts │ │ │ │ │ │ ├── form-field.vue │ │ │ │ │ │ ├── form-label.vue │ │ │ │ │ │ ├── form.vue │ │ │ │ │ │ ├── helper.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── use-form-context.ts │ │ │ │ │ ├── use-vben-form.ts │ │ │ │ │ ├── vben-form.vue │ │ │ │ │ └── vben-use-form.vue │ │ │ │ ├── hooks/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── use-app-config.ts │ │ │ │ │ ├── use-content-maximize.ts │ │ │ │ │ ├── use-design-tokens.ts │ │ │ │ │ ├── use-hover-toggle.ts │ │ │ │ │ ├── use-pagination.ts │ │ │ │ │ ├── use-refresh.ts │ │ │ │ │ ├── use-tabs.ts │ │ │ │ │ └── use-watermark.ts │ │ │ │ ├── icons/ │ │ │ │ │ ├── create-icon.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lucide.ts │ │ │ │ ├── index.ts │ │ │ │ ├── layout-ui/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── layout-content.vue │ │ │ │ │ │ ├── layout-footer.vue │ │ │ │ │ │ ├── layout-header.vue │ │ │ │ │ │ ├── layout-sidebar.vue │ │ │ │ │ │ ├── layout-tabbar.vue │ │ │ │ │ │ └── widgets/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── sidebar-collapse-button.vue │ │ │ │ │ │ └── sidebar-fixed-button.vue │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ └── use-layout.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── vben-layout.ts │ │ │ │ │ └── vben-layout.vue │ │ │ │ ├── layouts/ │ │ │ │ │ ├── basic/ │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── content/ │ │ │ │ │ │ │ ├── content-spinner.vue │ │ │ │ │ │ │ ├── content.vue │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── use-content-spinner.ts │ │ │ │ │ │ ├── copyright/ │ │ │ │ │ │ │ ├── copyright.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── footer/ │ │ │ │ │ │ │ ├── footer.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── header/ │ │ │ │ │ │ │ ├── header.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── layout.vue │ │ │ │ │ │ ├── menu/ │ │ │ │ │ │ │ ├── extra-menu.vue │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── menu.vue │ │ │ │ │ │ │ ├── mixed-menu.vue │ │ │ │ │ │ │ ├── use-extra-menu.ts │ │ │ │ │ │ │ ├── use-mixed-menu.ts │ │ │ │ │ │ │ └── use-navigation.ts │ │ │ │ │ │ └── tabbar/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── tabbar.vue │ │ │ │ │ │ └── use-tabbar.ts │ │ │ │ │ ├── iframe/ │ │ │ │ │ │ ├── iframe-router-view.vue │ │ │ │ │ │ ├── iframe-view.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── widgets/ │ │ │ │ │ ├── breadcrumb.vue │ │ │ │ │ ├── check-updates/ │ │ │ │ │ │ ├── check-updates.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── color-toggle.vue │ │ │ │ │ ├── global-search/ │ │ │ │ │ │ ├── global-search.vue │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── search-panel.vue │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── language-toggle.vue │ │ │ │ │ ├── layout-toggle.vue │ │ │ │ │ ├── lock-screen/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── lock-screen-modal.vue │ │ │ │ │ │ └── lock-screen.vue │ │ │ │ │ ├── notification/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── notification.vue │ │ │ │ │ │ └── types.ts │ │ │ │ │ ├── preferences/ │ │ │ │ │ │ ├── blocks/ │ │ │ │ │ │ │ ├── block.vue │ │ │ │ │ │ │ ├── general/ │ │ │ │ │ │ │ │ ├── animation.vue │ │ │ │ │ │ │ │ └── general.vue │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── input-item.vue │ │ │ │ │ │ │ ├── layout/ │ │ │ │ │ │ │ │ ├── breadcrumb.vue │ │ │ │ │ │ │ │ ├── content.vue │ │ │ │ │ │ │ │ ├── copyright.vue │ │ │ │ │ │ │ │ ├── footer.vue │ │ │ │ │ │ │ │ ├── header.vue │ │ │ │ │ │ │ │ ├── layout.vue │ │ │ │ │ │ │ │ ├── navigation.vue │ │ │ │ │ │ │ │ ├── sidebar.vue │ │ │ │ │ │ │ │ ├── tabbar.vue │ │ │ │ │ │ │ │ └── widget.vue │ │ │ │ │ │ │ ├── number-field-item.vue │ │ │ │ │ │ │ ├── select-item.vue │ │ │ │ │ │ │ ├── shortcut-keys/ │ │ │ │ │ │ │ │ └── global.vue │ │ │ │ │ │ │ ├── switch-item.vue │ │ │ │ │ │ │ ├── theme/ │ │ │ │ │ │ │ │ ├── builtin.vue │ │ │ │ │ │ │ │ ├── color-mode.vue │ │ │ │ │ │ │ │ ├── radius.vue │ │ │ │ │ │ │ │ └── theme.vue │ │ │ │ │ │ │ └── toggle-item.vue │ │ │ │ │ │ ├── icons/ │ │ │ │ │ │ │ ├── content-compact.vue │ │ │ │ │ │ │ ├── full-content.vue │ │ │ │ │ │ │ ├── header-mixed-nav.vue │ │ │ │ │ │ │ ├── header-nav.vue │ │ │ │ │ │ │ ├── header-sidebar-nav.vue │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── mixed-nav.vue │ │ │ │ │ │ │ ├── setting.vue │ │ │ │ │ │ │ ├── sidebar-mixed-nav.vue │ │ │ │ │ │ │ └── sidebar-nav.vue │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── preferences-button.vue │ │ │ │ │ │ ├── preferences-drawer.vue │ │ │ │ │ │ ├── preferences.vue │ │ │ │ │ │ └── use-open-preferences.ts │ │ │ │ │ ├── theme-toggle/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── theme-button.vue │ │ │ │ │ │ └── theme-toggle.vue │ │ │ │ │ └── user-dropdown/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── user-dropdown.vue │ │ │ │ ├── menu-ui/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── collapse-transition.vue │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── menu-badge-dot.vue │ │ │ │ │ │ ├── menu-badge.vue │ │ │ │ │ │ ├── menu-item.vue │ │ │ │ │ │ ├── menu.vue │ │ │ │ │ │ ├── normal-menu/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── normal-menu.ts │ │ │ │ │ │ │ └── normal-menu.vue │ │ │ │ │ │ ├── sub-menu-content.vue │ │ │ │ │ │ └── sub-menu.vue │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── use-menu-context.ts │ │ │ │ │ │ └── use-menu.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── menu.vue │ │ │ │ │ ├── sub-menu.vue │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils/ │ │ │ │ │ └── index.ts │ │ │ │ ├── popup-ui/ │ │ │ │ │ ├── drawer/ │ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ │ └── drawer-api.test.ts │ │ │ │ │ │ ├── drawer-api.ts │ │ │ │ │ │ ├── drawer.ts │ │ │ │ │ │ ├── drawer.vue │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── use-drawer.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── modal/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ └── modal-api.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── modal-api.ts │ │ │ │ │ ├── modal.ts │ │ │ │ │ ├── modal.vue │ │ │ │ │ ├── use-modal-draggable.ts │ │ │ │ │ └── use-modal.ts │ │ │ │ ├── preferences/ │ │ │ │ │ ├── config.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── preferences.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── update-css-variables.ts │ │ │ │ │ └── use-preferences.ts │ │ │ │ ├── shadcn-ui/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── avatar/ │ │ │ │ │ │ │ ├── avatar.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── back-top/ │ │ │ │ │ │ │ ├── back-top.vue │ │ │ │ │ │ │ ├── backtop.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── use-backtop.ts │ │ │ │ │ │ ├── breadcrumb/ │ │ │ │ │ │ │ ├── breadcrumb-background.vue │ │ │ │ │ │ │ ├── breadcrumb-view.vue │ │ │ │ │ │ │ ├── breadcrumb.vue │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── button/ │ │ │ │ │ │ │ ├── button-group.vue │ │ │ │ │ │ │ ├── button.ts │ │ │ │ │ │ │ ├── button.vue │ │ │ │ │ │ │ ├── check-button-group.vue │ │ │ │ │ │ │ ├── icon-button.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── checkbox/ │ │ │ │ │ │ │ ├── checkbox.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── context-menu/ │ │ │ │ │ │ │ ├── context-menu.vue │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interface.ts │ │ │ │ │ │ ├── count-to-animator/ │ │ │ │ │ │ │ ├── count-to-animator.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── dropdown-menu/ │ │ │ │ │ │ │ ├── dropdown-menu.vue │ │ │ │ │ │ │ ├── dropdown-radio-menu.vue │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── interface.ts │ │ │ │ │ │ ├── expandable-arrow/ │ │ │ │ │ │ │ ├── expandable-arrow.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── full-screen/ │ │ │ │ │ │ │ ├── full-screen.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── hover-card/ │ │ │ │ │ │ │ ├── hover-card.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── icon/ │ │ │ │ │ │ │ ├── icon.vue │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── input-password/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── input-password.vue │ │ │ │ │ │ │ └── password-strength.vue │ │ │ │ │ │ ├── logo/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── logo.vue │ │ │ │ │ │ ├── pin-input/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── input.vue │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── popover/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── popover.vue │ │ │ │ │ │ ├── render-content/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── render-content.vue │ │ │ │ │ │ ├── scrollbar/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── scrollbar.vue │ │ │ │ │ │ ├── segmented/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── segmented.vue │ │ │ │ │ │ │ ├── tabs-indicator.vue │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ │ ├── select/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── select.vue │ │ │ │ │ │ ├── spine-text/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── spine-text.vue │ │ │ │ │ │ ├── spinner/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── loading.vue │ │ │ │ │ │ │ └── spinner.vue │ │ │ │ │ │ └── tooltip/ │ │ │ │ │ │ ├── help-tooltip.vue │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── tooltip.vue │ │ │ │ │ ├── index.ts │ │ │ │ │ └── ui/ │ │ │ │ │ ├── accordion/ │ │ │ │ │ │ ├── Accordion.vue │ │ │ │ │ │ ├── AccordionContent.vue │ │ │ │ │ │ ├── AccordionItem.vue │ │ │ │ │ │ ├── AccordionTrigger.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── avatar/ │ │ │ │ │ │ ├── Avatar.vue │ │ │ │ │ │ ├── AvatarFallback.vue │ │ │ │ │ │ ├── AvatarImage.vue │ │ │ │ │ │ ├── avatar.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── badge/ │ │ │ │ │ │ ├── Badge.vue │ │ │ │ │ │ ├── badge.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── breadcrumb/ │ │ │ │ │ │ ├── Breadcrumb.vue │ │ │ │ │ │ ├── BreadcrumbEllipsis.vue │ │ │ │ │ │ ├── BreadcrumbItem.vue │ │ │ │ │ │ ├── BreadcrumbLink.vue │ │ │ │ │ │ ├── BreadcrumbList.vue │ │ │ │ │ │ ├── BreadcrumbPage.vue │ │ │ │ │ │ ├── BreadcrumbSeparator.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── button/ │ │ │ │ │ │ ├── Button.vue │ │ │ │ │ │ ├── button.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ ├── card/ │ │ │ │ │ │ ├── Card.vue │ │ │ │ │ │ ├── CardContent.vue │ │ │ │ │ │ ├── CardDescription.vue │ │ │ │ │ │ ├── CardFooter.vue │ │ │ │ │ │ ├── CardHeader.vue │ │ │ │ │ │ ├── CardTitle.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── checkbox/ │ │ │ │ │ │ ├── Checkbox.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── context-menu/ │ │ │ │ │ │ ├── ContextMenu.vue │ │ │ │ │ │ ├── ContextMenuCheckboxItem.vue │ │ │ │ │ │ ├── ContextMenuContent.vue │ │ │ │ │ │ ├── ContextMenuGroup.vue │ │ │ │ │ │ ├── ContextMenuItem.vue │ │ │ │ │ │ ├── ContextMenuLabel.vue │ │ │ │ │ │ ├── ContextMenuPortal.vue │ │ │ │ │ │ ├── ContextMenuRadioGroup.vue │ │ │ │ │ │ ├── ContextMenuRadioItem.vue │ │ │ │ │ │ ├── ContextMenuSeparator.vue │ │ │ │ │ │ ├── ContextMenuShortcut.vue │ │ │ │ │ │ ├── ContextMenuSub.vue │ │ │ │ │ │ ├── ContextMenuSubContent.vue │ │ │ │ │ │ ├── ContextMenuSubTrigger.vue │ │ │ │ │ │ ├── ContextMenuTrigger.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── dialog/ │ │ │ │ │ │ ├── Dialog.vue │ │ │ │ │ │ ├── DialogClose.vue │ │ │ │ │ │ ├── DialogContent.vue │ │ │ │ │ │ ├── DialogDescription.vue │ │ │ │ │ │ ├── DialogFooter.vue │ │ │ │ │ │ ├── DialogHeader.vue │ │ │ │ │ │ ├── DialogOverlay.vue │ │ │ │ │ │ ├── DialogScrollContent.vue │ │ │ │ │ │ ├── DialogTitle.vue │ │ │ │ │ │ ├── DialogTrigger.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── dropdown-menu/ │ │ │ │ │ │ ├── DropdownMenu.vue │ │ │ │ │ │ ├── DropdownMenuCheckboxItem.vue │ │ │ │ │ │ ├── DropdownMenuContent.vue │ │ │ │ │ │ ├── DropdownMenuGroup.vue │ │ │ │ │ │ ├── DropdownMenuItem.vue │ │ │ │ │ │ ├── DropdownMenuLabel.vue │ │ │ │ │ │ ├── DropdownMenuRadioGroup.vue │ │ │ │ │ │ ├── DropdownMenuRadioItem.vue │ │ │ │ │ │ ├── DropdownMenuSeparator.vue │ │ │ │ │ │ ├── DropdownMenuShortcut.vue │ │ │ │ │ │ ├── DropdownMenuSub.vue │ │ │ │ │ │ ├── DropdownMenuSubContent.vue │ │ │ │ │ │ ├── DropdownMenuSubTrigger.vue │ │ │ │ │ │ ├── DropdownMenuTrigger.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── form/ │ │ │ │ │ │ ├── FormControl.vue │ │ │ │ │ │ ├── FormDescription.vue │ │ │ │ │ │ ├── FormItem.vue │ │ │ │ │ │ ├── FormLabel.vue │ │ │ │ │ │ ├── FormMessage.vue │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── injectionKeys.ts │ │ │ │ │ │ └── useFormField.ts │ │ │ │ │ ├── hover-card/ │ │ │ │ │ │ ├── HoverCard.vue │ │ │ │ │ │ ├── HoverCardContent.vue │ │ │ │ │ │ ├── HoverCardTrigger.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── input/ │ │ │ │ │ │ ├── Input.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── label/ │ │ │ │ │ │ ├── Label.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── number-field/ │ │ │ │ │ │ ├── NumberField.vue │ │ │ │ │ │ ├── NumberFieldContent.vue │ │ │ │ │ │ ├── NumberFieldDecrement.vue │ │ │ │ │ │ ├── NumberFieldIncrement.vue │ │ │ │ │ │ ├── NumberFieldInput.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── pagination/ │ │ │ │ │ │ ├── PaginationEllipsis.vue │ │ │ │ │ │ ├── PaginationFirst.vue │ │ │ │ │ │ ├── PaginationLast.vue │ │ │ │ │ │ ├── PaginationNext.vue │ │ │ │ │ │ ├── PaginationPrev.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── pin-input/ │ │ │ │ │ │ ├── PinInput.vue │ │ │ │ │ │ ├── PinInputGroup.vue │ │ │ │ │ │ ├── PinInputInput.vue │ │ │ │ │ │ ├── PinInputSeparator.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── popover/ │ │ │ │ │ │ ├── Popover.vue │ │ │ │ │ │ ├── PopoverContent.vue │ │ │ │ │ │ ├── PopoverTrigger.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── radio-group/ │ │ │ │ │ │ ├── RadioGroup.vue │ │ │ │ │ │ ├── RadioGroupItem.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── resizable/ │ │ │ │ │ │ ├── ResizableHandle.vue │ │ │ │ │ │ ├── ResizablePanelGroup.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── scroll-area/ │ │ │ │ │ │ ├── ScrollArea.vue │ │ │ │ │ │ ├── ScrollBar.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── select/ │ │ │ │ │ │ ├── Select.vue │ │ │ │ │ │ ├── SelectContent.vue │ │ │ │ │ │ ├── SelectGroup.vue │ │ │ │ │ │ ├── SelectItem.vue │ │ │ │ │ │ ├── SelectItemText.vue │ │ │ │ │ │ ├── SelectLabel.vue │ │ │ │ │ │ ├── SelectScrollDownButton.vue │ │ │ │ │ │ ├── SelectScrollUpButton.vue │ │ │ │ │ │ ├── SelectSeparator.vue │ │ │ │ │ │ ├── SelectTrigger.vue │ │ │ │ │ │ ├── SelectValue.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── separator/ │ │ │ │ │ │ ├── Separator.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── sheet/ │ │ │ │ │ │ ├── Sheet.vue │ │ │ │ │ │ ├── SheetClose.vue │ │ │ │ │ │ ├── SheetContent.vue │ │ │ │ │ │ ├── SheetDescription.vue │ │ │ │ │ │ ├── SheetFooter.vue │ │ │ │ │ │ ├── SheetHeader.vue │ │ │ │ │ │ ├── SheetOverlay.vue │ │ │ │ │ │ ├── SheetTitle.vue │ │ │ │ │ │ ├── SheetTrigger.vue │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── sheet.ts │ │ │ │ │ ├── switch/ │ │ │ │ │ │ ├── Switch.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── tabs/ │ │ │ │ │ │ ├── Tabs.vue │ │ │ │ │ │ ├── TabsContent.vue │ │ │ │ │ │ ├── TabsList.vue │ │ │ │ │ │ ├── TabsTrigger.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── textarea/ │ │ │ │ │ │ ├── Textarea.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── toggle/ │ │ │ │ │ │ ├── Toggle.vue │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── toggle.ts │ │ │ │ │ ├── toggle-group/ │ │ │ │ │ │ ├── ToggleGroup.vue │ │ │ │ │ │ ├── ToggleGroupItem.vue │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── tooltip/ │ │ │ │ │ ├── Tooltip.vue │ │ │ │ │ ├── TooltipContent.vue │ │ │ │ │ ├── TooltipProvider.vue │ │ │ │ │ ├── TooltipTrigger.vue │ │ │ │ │ └── index.ts │ │ │ │ ├── shared/ │ │ │ │ │ ├── cache/ │ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ │ └── storage-manager.test.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── storage-manager.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ ├── color/ │ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ │ └── convert.test.ts │ │ │ │ │ │ ├── color.ts │ │ │ │ │ │ ├── convert.ts │ │ │ │ │ │ ├── generator.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── constants/ │ │ │ │ │ │ ├── globals.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── vben.ts │ │ │ │ │ ├── global-state.ts │ │ │ │ │ ├── store.ts │ │ │ │ │ └── utils/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ ├── diff.test.ts │ │ │ │ │ │ ├── dom.test.ts │ │ │ │ │ │ ├── inference.test.ts │ │ │ │ │ │ ├── letter.test.ts │ │ │ │ │ │ ├── state-handler.test.ts │ │ │ │ │ │ ├── tree.test.ts │ │ │ │ │ │ ├── unique.test.ts │ │ │ │ │ │ ├── update-css-variables.test.ts │ │ │ │ │ │ ├── util.test.ts │ │ │ │ │ │ └── window.test.ts │ │ │ │ │ ├── cn.ts │ │ │ │ │ ├── date.ts │ │ │ │ │ ├── diff.ts │ │ │ │ │ ├── dom.ts │ │ │ │ │ ├── download.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── inference.ts │ │ │ │ │ ├── letter.ts │ │ │ │ │ ├── merge.ts │ │ │ │ │ ├── nprogress.ts │ │ │ │ │ ├── state-handler.ts │ │ │ │ │ ├── to.ts │ │ │ │ │ ├── tree.ts │ │ │ │ │ ├── unique.ts │ │ │ │ │ ├── update-css-variables.ts │ │ │ │ │ ├── util.ts │ │ │ │ │ └── window.ts │ │ │ │ ├── stores/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── modules/ │ │ │ │ │ │ ├── access.test.ts │ │ │ │ │ │ ├── access.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── lock.test.ts │ │ │ │ │ │ ├── lock.ts │ │ │ │ │ │ ├── tabbar.test.ts │ │ │ │ │ │ ├── tabbar.ts │ │ │ │ │ │ ├── user.test.ts │ │ │ │ │ │ └── user.ts │ │ │ │ │ └── setup.ts │ │ │ │ ├── styles/ │ │ │ │ │ ├── antd/ │ │ │ │ │ │ └── index.css │ │ │ │ │ ├── ele/ │ │ │ │ │ │ └── index.css │ │ │ │ │ ├── global/ │ │ │ │ │ │ └── index.scss │ │ │ │ │ ├── index.ts │ │ │ │ │ └── naive/ │ │ │ │ │ └── index.css │ │ │ │ ├── tabs-ui/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── tabs/ │ │ │ │ │ │ │ └── tabs.vue │ │ │ │ │ │ ├── tabs-chrome/ │ │ │ │ │ │ │ └── tabs.vue │ │ │ │ │ │ └── widgets/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── tool-more.vue │ │ │ │ │ │ └── tool-screen.vue │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tabs-view.vue │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── use-tabs-drag.ts │ │ │ │ │ └── use-tabs-view-scroll.ts │ │ │ │ ├── types/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── user.ts │ │ │ │ ├── typings/ │ │ │ │ │ ├── app.d.ts │ │ │ │ │ ├── basic.d.ts │ │ │ │ │ ├── helper.d.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── menu-record.ts │ │ │ │ │ ├── tabs.ts │ │ │ │ │ └── vue-router.d.ts │ │ │ │ └── utils/ │ │ │ │ ├── helpers/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ ├── find-menu-by-path.test.ts │ │ │ │ │ │ ├── generate-menus.test.ts │ │ │ │ │ │ ├── generate-routes-frontend.test.ts │ │ │ │ │ │ └── merge-route-modules.test.ts │ │ │ │ │ ├── find-menu-by-path.ts │ │ │ │ │ ├── generate-menus.ts │ │ │ │ │ ├── generate-routes-backend.ts │ │ │ │ │ ├── generate-routes-frontend.ts │ │ │ │ │ ├── get-popup-container.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── merge-route-modules.ts │ │ │ │ │ ├── reset-routes.ts │ │ │ │ │ └── unmount-global-loading.ts │ │ │ │ └── index.ts │ │ │ └── views/ │ │ │ ├── certd/ │ │ │ │ ├── access/ │ │ │ │ │ ├── access-selector/ │ │ │ │ │ │ ├── access/ │ │ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ └── secret-plain-getter.vue │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── common.tsx │ │ │ │ │ ├── crud.tsx │ │ │ │ │ └── index.vue │ │ │ │ ├── cert/ │ │ │ │ │ └── domain/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── crud.tsx │ │ │ │ │ └── index.vue │ │ │ │ ├── cname/ │ │ │ │ │ └── record/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── crud.tsx │ │ │ │ │ └── index.vue │ │ │ │ ├── history/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── crud.tsx │ │ │ │ │ └── index.vue │ │ │ │ ├── mine/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── change-password-button.vue │ │ │ │ │ ├── security/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── use.tsx │ │ │ │ │ └── user-profile.vue │ │ │ │ ├── monitor/ │ │ │ │ │ ├── cert/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ │ └── index.vue │ │ │ │ │ └── site/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── crud.tsx │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── ip/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ └── use.tsx │ │ │ │ │ ├── setting/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ └── index.vue │ │ │ │ │ └── use.tsx │ │ │ │ ├── notification/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── common.tsx │ │ │ │ │ ├── crud.tsx │ │ │ │ │ ├── index.vue │ │ │ │ │ └── notification-selector/ │ │ │ │ │ ├── index.vue │ │ │ │ │ └── modal/ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ └── index.vue │ │ │ │ ├── open/ │ │ │ │ │ └── openkey/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── crud.tsx │ │ │ │ │ └── index.vue │ │ │ │ ├── payment/ │ │ │ │ │ ├── api.ts │ │ │ │ │ └── return.vue │ │ │ │ ├── pipeline/ │ │ │ │ │ ├── api.history.ts │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── cert-upload/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ └── use.tsx │ │ │ │ │ ├── cert-view.vue │ │ │ │ │ ├── certd-form/ │ │ │ │ │ │ └── use.tsx │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── change-group.vue │ │ │ │ │ │ ├── change-notification.vue │ │ │ │ │ │ └── change-trigger.vue │ │ │ │ │ ├── crud.tsx │ │ │ │ │ ├── dash-roll.readme │ │ │ │ │ ├── detail.vue │ │ │ │ │ ├── group/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ │ ├── group-selector.vue │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── pipeline/ │ │ │ │ │ │ ├── component/ │ │ │ │ │ │ │ ├── history-timeline-item.vue │ │ │ │ │ │ │ ├── notification-form/ │ │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ │ └── pi-notification-form-email.vue │ │ │ │ │ │ │ ├── shortcut/ │ │ │ │ │ │ │ │ ├── task-shortcut.vue │ │ │ │ │ │ │ │ └── task-shortcuts.vue │ │ │ │ │ │ │ ├── status-show.vue │ │ │ │ │ │ │ ├── step-form/ │ │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ │ ├── task-form/ │ │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ │ ├── task-view/ │ │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ │ └── trigger-form/ │ │ │ │ │ │ │ └── index.vue │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ ├── plugin/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── type.ts │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ └── util.status.ts │ │ │ │ │ ├── sub-domain/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── template/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ │ ├── edit.vue │ │ │ │ │ │ ├── form.vue │ │ │ │ │ │ ├── import/ │ │ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ │ │ ├── form.tsx │ │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ │ └── table.vue │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ ├── use.tsx │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── use.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── suite/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── buy.vue │ │ │ │ │ ├── mine/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ └── user-suite-status.vue │ │ │ │ │ ├── order-modal.vue │ │ │ │ │ ├── pay-return.vue │ │ │ │ │ └── product-info.vue │ │ │ │ └── trade/ │ │ │ │ ├── api.ts │ │ │ │ ├── crud.tsx │ │ │ │ └── index.vue │ │ │ ├── framework/ │ │ │ │ ├── error/ │ │ │ │ │ ├── 403.vue │ │ │ │ │ └── 404.vue │ │ │ │ ├── home/ │ │ │ │ │ ├── content/ │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── dashboard/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── charts/ │ │ │ │ │ │ │ ├── d.ts │ │ │ │ │ │ │ ├── day-count.vue │ │ │ │ │ │ │ ├── expiring-list.vue │ │ │ │ │ │ │ └── pie-count.vue │ │ │ │ │ │ ├── index.vue │ │ │ │ │ │ ├── statistic-card.vue │ │ │ │ │ │ └── suite-card.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── login/ │ │ │ │ │ ├── image-code.vue │ │ │ │ │ ├── index.vue │ │ │ │ │ └── sms-code.vue │ │ │ │ └── register/ │ │ │ │ ├── email-code.vue │ │ │ │ └── index.vue │ │ │ └── sys/ │ │ │ ├── access/ │ │ │ │ └── index.vue │ │ │ ├── account/ │ │ │ │ ├── api.ts │ │ │ │ └── index.vue │ │ │ ├── authority/ │ │ │ │ ├── permission/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── crud.tsx │ │ │ │ │ ├── fs-permission-tree.vue │ │ │ │ │ └── index.vue │ │ │ │ ├── role/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── crud.tsx │ │ │ │ │ └── index.vue │ │ │ │ └── user/ │ │ │ │ ├── api.ts │ │ │ │ ├── crud.tsx │ │ │ │ └── index.vue │ │ │ ├── cname/ │ │ │ │ └── provider/ │ │ │ │ ├── api.ts │ │ │ │ ├── crud.tsx │ │ │ │ └── index.vue │ │ │ ├── console/ │ │ │ │ ├── api.ts │ │ │ │ └── index.vue │ │ │ ├── plugin/ │ │ │ │ ├── api.ts │ │ │ │ ├── components/ │ │ │ │ │ └── plugin-input.vue │ │ │ │ ├── config.vue │ │ │ │ ├── crud.tsx │ │ │ │ ├── demo/ │ │ │ │ │ ├── access.yaml │ │ │ │ │ ├── plugin.yaml │ │ │ │ │ └── sdk.yaml │ │ │ │ ├── edit.vue │ │ │ │ └── index.vue │ │ │ ├── settings/ │ │ │ │ ├── api.ts │ │ │ │ ├── email/ │ │ │ │ │ ├── api.email.ts │ │ │ │ │ └── index.vue │ │ │ │ ├── header-menus/ │ │ │ │ │ ├── crud.tsx │ │ │ │ │ └── index.vue │ │ │ │ ├── index.vue │ │ │ │ └── tabs/ │ │ │ │ ├── base.vue │ │ │ │ ├── payment.vue │ │ │ │ ├── register.vue │ │ │ │ └── safe.vue │ │ │ ├── site/ │ │ │ │ ├── api.ts │ │ │ │ └── index.vue │ │ │ └── suite/ │ │ │ ├── product/ │ │ │ │ ├── api.ts │ │ │ │ ├── crud.tsx │ │ │ │ ├── duration-price-value.vue │ │ │ │ ├── duration-value.vue │ │ │ │ ├── index.vue │ │ │ │ ├── price-edit.vue │ │ │ │ ├── price-input.vue │ │ │ │ ├── suite-value-edit.vue │ │ │ │ └── suite-value.vue │ │ │ ├── setting/ │ │ │ │ ├── index.vue │ │ │ │ └── suite-duration-selector.vue │ │ │ ├── trade/ │ │ │ │ ├── api.ts │ │ │ │ ├── crud.tsx │ │ │ │ └── index.vue │ │ │ └── user-suite/ │ │ │ ├── api.ts │ │ │ ├── crud.tsx │ │ │ └── index.vue │ │ ├── tailwind.config.mjs │ │ ├── tests/ │ │ │ └── unit/ │ │ │ └── example.spec.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── certd-server/ │ │ ├── .dockerignore │ │ ├── .editorconfig │ │ ├── .eslintrc │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── .mocharc.json │ │ ├── .npmrc │ │ ├── .prettierrc │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── README.zh-CN.md │ │ ├── app.js │ │ ├── before-build.js │ │ ├── bootstrap.js │ │ ├── db/ │ │ │ ├── migration/ │ │ │ │ ├── v00001__init.sql │ │ │ │ ├── v00002__for_pre.sql │ │ │ │ ├── v10000__certd.sql │ │ │ │ ├── v10001__certdv2.sql │ │ │ │ ├── v10002__settings.sql │ │ │ │ ├── v10003__role_user.sql │ │ │ │ ├── v10004__settings.sql │ │ │ │ ├── v10005__password2.sql │ │ │ │ ├── v10006__pipeline_title.sql │ │ │ │ ├── v10007__access_text.sql │ │ │ │ ├── v10008__access_encrypt.sql │ │ │ │ ├── v10009__cname.sql │ │ │ │ ├── v10010__plugin.sql │ │ │ │ ├── v10011__cname_provider_user_id.sql │ │ │ │ ├── v10012__disabled_readonly_user.sql │ │ │ │ ├── v10013__notification.sql │ │ │ │ ├── v10014__notification_default.sql │ │ │ │ ├── v10015__pipeline_group.sql │ │ │ │ ├── v10016__index.sql │ │ │ │ ├── v10017__field_type.sql │ │ │ │ ├── v10018__suite.sql │ │ │ │ ├── v10019__text_too_short.sql │ │ │ │ ├── v10020__open.sql │ │ │ │ ├── v10021__plugin.sql │ │ │ │ ├── v10022__plugin.sql │ │ │ │ ├── v10023__site_ip.sql │ │ │ │ ├── v10024__cname_error.sql │ │ │ │ ├── v10025__history_trigger_type.sql │ │ │ │ ├── v10026__template.sql │ │ │ │ └── v10027__auto.sql │ │ │ ├── migration-mysql/ │ │ │ │ ├── v00001__init.sql │ │ │ │ ├── v00002__for_pre.sql │ │ │ │ ├── v10000__certd.sql │ │ │ │ ├── v10001__certdv2.sql │ │ │ │ ├── v10002__settings.sql │ │ │ │ ├── v10003__role_user.sql │ │ │ │ ├── v10004__settings.sql │ │ │ │ ├── v10005__password2.sql │ │ │ │ ├── v10006__pipeline_title.sql │ │ │ │ ├── v10007__access_text.sql │ │ │ │ ├── v10008__access_encrypt.sql │ │ │ │ ├── v10009__cname.sql │ │ │ │ ├── v10010__plugin.sql │ │ │ │ ├── v10011__cname_provider_user_id.sql │ │ │ │ ├── v10012__disabled_readonly_user.sql │ │ │ │ ├── v10013__notification.sql │ │ │ │ ├── v10014__notification_default.sql │ │ │ │ ├── v10015__pipeline_group.sql │ │ │ │ ├── v10016__index.sql │ │ │ │ ├── v10017__field_type.sql │ │ │ │ ├── v10018__suite.sql │ │ │ │ ├── v10019__text_too_short.sql │ │ │ │ ├── v10020__open.sql │ │ │ │ ├── v10021__plugin.sql │ │ │ │ ├── v10022__plugin.sql │ │ │ │ ├── v10023__site_ip.sql │ │ │ │ ├── v10024__cname_error.sql │ │ │ │ ├── v10025__history_trigger_type.sql │ │ │ │ ├── v10026__template.sql │ │ │ │ └── v10027__auto.sql │ │ │ ├── migration-pg/ │ │ │ │ ├── v00001__init.sql │ │ │ │ ├── v00002__for_pre.sql │ │ │ │ ├── v10000__certd.sql │ │ │ │ ├── v10001__certdv2.sql │ │ │ │ ├── v10002__settings.sql │ │ │ │ ├── v10003__role_user.sql │ │ │ │ ├── v10004__settings.sql │ │ │ │ ├── v10005__password2.sql │ │ │ │ ├── v10006__pipeline_title.sql │ │ │ │ ├── v10007__access_text.sql │ │ │ │ ├── v10008__access_encrypt.sql │ │ │ │ ├── v10009__cname.sql │ │ │ │ ├── v10010__plugin.sql │ │ │ │ ├── v10011__cname_provider_user_id.sql │ │ │ │ ├── v10012__disabled_readonly_user.sql │ │ │ │ ├── v10013__notification.sql │ │ │ │ ├── v10014__notification_default.sql │ │ │ │ ├── v10015__pipeline_group.sql │ │ │ │ ├── v10016__index.sql │ │ │ │ ├── v10017__field_type.sql │ │ │ │ ├── v10018__suite.sql │ │ │ │ ├── v10019__text_too_short.sql │ │ │ │ ├── v10020__open.sql │ │ │ │ ├── v10021__plugin.sql │ │ │ │ ├── v10022__plugin.sql │ │ │ │ ├── v10023__site_ip.sql │ │ │ │ ├── v10024__cname_error.sql │ │ │ │ ├── v10025__history_trigger_type.sql │ │ │ │ ├── v10026__template.sql │ │ │ │ └── v10027__auto.sql │ │ │ ├── readme.md │ │ │ └── transform.js │ │ ├── export-plugin-md.js │ │ ├── export-plugin-yaml.js │ │ ├── f.yaml │ │ ├── jest.config.js │ │ ├── metadata/ │ │ │ ├── access_51dns.yaml │ │ │ ├── access_CacheFly.yaml │ │ │ ├── access_Gcore.yaml │ │ │ ├── access_aws.yaml │ │ │ ├── access_cloudflare.yaml │ │ │ ├── access_demo.yaml │ │ │ ├── access_dnsla.yaml │ │ │ ├── access_dnspod.yaml │ │ │ ├── access_dogecloud.yaml │ │ │ ├── access_huawei.yaml │ │ │ ├── access_jdcloud.yaml │ │ │ ├── access_namesilo.yaml │ │ │ ├── access_proxmox.yaml │ │ │ ├── access_upyun.yaml │ │ │ ├── access_volcengine.yaml │ │ │ ├── access_west.yaml │ │ │ ├── access_woai.yaml │ │ │ ├── deploy_AliyunDeployCertToALB.yaml │ │ │ ├── deploy_AliyunDeployCertToFC.yaml │ │ │ ├── deploy_AliyunDeployCertToNLB.yaml │ │ │ ├── deploy_AliyunDeployCertToSLB.yaml │ │ │ ├── deploy_AliyunDeployCertToWaf.yaml │ │ │ ├── deploy_AwsDeployToCloudFront.yaml │ │ │ ├── deploy_AwsUploadToACM.yaml │ │ │ ├── deploy_CacheFly.yaml │ │ │ ├── deploy_CopyToLocal.yaml │ │ │ ├── deploy_CustomScript.yaml │ │ │ ├── deploy_DBBackupPlugin.yaml │ │ │ ├── deploy_DemoTest.yaml │ │ │ ├── deploy_DeployCertToAliyunCDN.yaml │ │ │ ├── deploy_DeployCertToAliyunDCDN.yaml │ │ │ ├── deploy_DeployCertToAliyunOSS.yaml │ │ │ ├── deploy_DeployCertToTencentAll.yaml │ │ │ ├── deploy_DeployCertToTencentCDN.yaml │ │ │ ├── deploy_DeployCertToTencentCLB.yaml │ │ │ ├── deploy_DeployCertToTencentCosPlugin.yaml │ │ │ ├── deploy_DeployCertToTencentEO.yaml │ │ │ ├── deploy_DeployCertToTencentTKEIngress.yaml │ │ │ ├── deploy_DogeCloudDeployToCDN.yaml │ │ │ ├── deploy_Gcoreflush.yaml │ │ │ ├── deploy_Gcoreupload.yaml │ │ │ ├── deploy_HauweiDeployCertToCDN.yaml │ │ │ ├── deploy_HauweiUploadToCCM.yaml │ │ │ ├── deploy_JDCloudDeployToCDN.yaml │ │ │ ├── deploy_JDCloudUpdateCert.yaml │ │ │ ├── deploy_JDCloudUploadCert.yaml │ │ │ ├── deploy_ProxmoxUploadCert.yaml │ │ │ ├── deploy_QiniuCertUpload.yaml │ │ │ ├── deploy_QiniuDeployCertToCDN.yaml │ │ │ ├── deploy_QnapDeploy.yaml │ │ │ ├── deploy_RestartCertd.yaml │ │ │ ├── deploy_TencentActionInstancesPlugin.yaml │ │ │ ├── deploy_TencentDeleteExpiringCert.yaml │ │ │ ├── deploy_TencentDeployCertToCDNv2.yaml │ │ │ ├── deploy_TencentDeployCertToLive.yaml │ │ │ ├── deploy_UploadCertToTencent.yaml │ │ │ ├── deploy_UpyunDeployToCdn.yaml │ │ │ ├── deploy_VolcengineDeployToALB.yaml │ │ │ ├── deploy_VolcengineDeployToCDN.yaml │ │ │ ├── deploy_VolcengineDeployToCLB.yaml │ │ │ ├── deploy_VolcengineDeployToLive.yaml │ │ │ ├── deploy_VolcengineDeployToVOD.yaml │ │ │ ├── deploy_VolcengineUploadToCertCenter.yaml │ │ │ ├── deploy_WaitPlugin.yaml │ │ │ ├── deploy_WoaiCDN.yaml │ │ │ ├── deploy_hostShellExecute.yaml │ │ │ ├── deploy_uploadCertToAliyun.yaml │ │ │ ├── deploy_uploadCertToHost.yaml │ │ │ ├── dnsProvider_51dns.yaml │ │ │ ├── dnsProvider_aliyun.yaml │ │ │ ├── dnsProvider_cloudflare.yaml │ │ │ ├── dnsProvider_demo.yaml │ │ │ ├── dnsProvider_dnsla.yaml │ │ │ ├── dnsProvider_dnspod.yaml │ │ │ ├── dnsProvider_huawei.yaml │ │ │ ├── dnsProvider_jdcloud.yaml │ │ │ ├── dnsProvider_namesilo.yaml │ │ │ ├── dnsProvider_tencent.yaml │ │ │ ├── dnsProvider_volcengine.yaml │ │ │ ├── dnsProvider_west.yaml │ │ │ ├── notification_anpush.yaml │ │ │ ├── notification_bark.yaml │ │ │ ├── notification_dingtalk.yaml │ │ │ ├── notification_discord.yaml │ │ │ ├── notification_email.yaml │ │ │ ├── notification_feishu.yaml │ │ │ ├── notification_iyuu.yaml │ │ │ ├── notification_qywx.yaml │ │ │ ├── notification_serverchan.yaml │ │ │ ├── notification_serverchan3.yaml │ │ │ ├── notification_slack.yaml │ │ │ ├── notification_telegram.yaml │ │ │ ├── notification_vocechat.yaml │ │ │ └── notification_webhook.yaml │ │ ├── package.json │ │ ├── plugin-doc-gen.mjs │ │ ├── public/ │ │ │ ├── .gitignore │ │ │ └── index.html │ │ ├── src/ │ │ │ ├── config/ │ │ │ │ ├── config.default.ts │ │ │ │ └── loader.ts │ │ │ ├── configuration.ts │ │ │ ├── controller/ │ │ │ │ ├── basic/ │ │ │ │ │ ├── app-controller.ts │ │ │ │ │ ├── code-controller.ts │ │ │ │ │ ├── file-controller.ts │ │ │ │ │ ├── home-controller.ts │ │ │ │ │ ├── root-controller.ts │ │ │ │ │ ├── settings-controller.ts │ │ │ │ │ └── unhidden-controller.ts │ │ │ │ ├── openapi/ │ │ │ │ │ ├── base-open-controller.ts │ │ │ │ │ └── v1/ │ │ │ │ │ └── cert-controller.ts │ │ │ │ ├── sys/ │ │ │ │ │ ├── access/ │ │ │ │ │ │ └── access-controller.ts │ │ │ │ │ ├── account/ │ │ │ │ │ │ └── account-controller.ts │ │ │ │ │ ├── authority/ │ │ │ │ │ │ ├── permission-controller.ts │ │ │ │ │ │ ├── role-controller.ts │ │ │ │ │ │ └── user-controller.ts │ │ │ │ │ ├── cname/ │ │ │ │ │ │ └── cname-provider-controller.ts │ │ │ │ │ ├── console/ │ │ │ │ │ │ └── statistic-controller.ts │ │ │ │ │ ├── plugin/ │ │ │ │ │ │ └── plugin-controller.ts │ │ │ │ │ ├── plus/ │ │ │ │ │ │ └── plus-controller.ts │ │ │ │ │ └── settings/ │ │ │ │ │ ├── sys-safe-settings-controller.ts │ │ │ │ │ └── sys-settings-controller.ts │ │ │ │ └── user/ │ │ │ │ ├── cert/ │ │ │ │ │ └── domain-controller.ts │ │ │ │ ├── cname/ │ │ │ │ │ ├── cname-provider-controller.ts │ │ │ │ │ └── cname-record-controller.ts │ │ │ │ ├── dashboard/ │ │ │ │ │ └── statistic-controller.ts │ │ │ │ ├── login/ │ │ │ │ │ ├── login-controller.ts │ │ │ │ │ └── register-controller.ts │ │ │ │ ├── mine/ │ │ │ │ │ ├── email-controller.ts │ │ │ │ │ ├── mine-controller.ts │ │ │ │ │ ├── setting-two-factor-controller.ts │ │ │ │ │ └── user-settings-controller.ts │ │ │ │ ├── monitor/ │ │ │ │ │ ├── cert-info-controller.ts │ │ │ │ │ ├── site-info-controller.ts │ │ │ │ │ └── site-ip-controller.ts │ │ │ │ ├── open/ │ │ │ │ │ └── open-key-controller.ts │ │ │ │ └── pipeline/ │ │ │ │ ├── access-controller.ts │ │ │ │ ├── cert-controller.ts │ │ │ │ ├── dns-provider-controller.ts │ │ │ │ ├── handle-controller.ts │ │ │ │ ├── history-controller.ts │ │ │ │ ├── notification-controller.ts │ │ │ │ ├── pipeline-controller.ts │ │ │ │ ├── pipeline-group-controller.ts │ │ │ │ ├── plugin-controller.ts │ │ │ │ ├── sub-domain-controller.ts │ │ │ │ └── template-controller.ts │ │ │ ├── filter/ │ │ │ │ ├── default.filter.ts │ │ │ │ └── notfound.filter.ts │ │ │ ├── middleware/ │ │ │ │ ├── authority.ts │ │ │ │ ├── global-exception.ts │ │ │ │ ├── hidden.ts │ │ │ │ ├── preview.ts │ │ │ │ └── reset-passwd/ │ │ │ │ └── middleware.ts │ │ │ ├── modules/ │ │ │ │ ├── auto/ │ │ │ │ │ ├── auto-a-init-site.ts │ │ │ │ │ ├── auto-b-load-plugins.ts │ │ │ │ │ ├── auto-c-register-cron.ts │ │ │ │ │ ├── auto-d-mitter-register.ts │ │ │ │ │ ├── auto-e-pipeline-emitter-register.ts │ │ │ │ │ ├── auto-z.ts │ │ │ │ │ └── https/ │ │ │ │ │ ├── self-certificate.ts │ │ │ │ │ └── server.ts │ │ │ │ ├── basic/ │ │ │ │ │ ├── service/ │ │ │ │ │ │ ├── code-service.ts │ │ │ │ │ │ └── email-service.ts │ │ │ │ │ └── sms/ │ │ │ │ │ ├── aliyun-sms.ts │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── factory.ts │ │ │ │ │ └── yfy-sms.ts │ │ │ │ ├── cert/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ └── domain.ts │ │ │ │ │ └── service/ │ │ │ │ │ └── domain-service.ts │ │ │ │ ├── cname/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ ├── cname-provider.ts │ │ │ │ │ │ └── cname-record.ts │ │ │ │ │ └── service/ │ │ │ │ │ ├── cname-provider-service.ts │ │ │ │ │ ├── cname-record-service.ts │ │ │ │ │ └── common-provider.ts │ │ │ │ ├── cron/ │ │ │ │ │ ├── configuration.ts │ │ │ │ │ ├── cron.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── db/ │ │ │ │ │ ├── d.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── mysql.ts │ │ │ │ │ ├── postgresql.ts │ │ │ │ │ └── sqlite.ts │ │ │ │ ├── login/ │ │ │ │ │ └── service/ │ │ │ │ │ └── login-service.ts │ │ │ │ ├── mine/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ └── user-settings.ts │ │ │ │ │ └── service/ │ │ │ │ │ ├── models.ts │ │ │ │ │ ├── two-factor-service.ts │ │ │ │ │ └── user-settings-service.ts │ │ │ │ ├── monitor/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ ├── cert-info.ts │ │ │ │ │ │ ├── site-info.ts │ │ │ │ │ │ └── site-ip.ts │ │ │ │ │ ├── facade/ │ │ │ │ │ │ └── cert-info-facade.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── service/ │ │ │ │ │ ├── cert-info-service.ts │ │ │ │ │ ├── dns-custom.ts │ │ │ │ │ ├── site-info-service.ts │ │ │ │ │ ├── site-ip-service.ts │ │ │ │ │ └── site-tester.ts │ │ │ │ ├── open/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ └── open-key.ts │ │ │ │ │ └── service/ │ │ │ │ │ └── open-key-service.ts │ │ │ │ ├── pipeline/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ ├── history-log.ts │ │ │ │ │ │ ├── history.ts │ │ │ │ │ │ ├── notification.ts │ │ │ │ │ │ ├── pipeline-group.ts │ │ │ │ │ │ ├── pipeline.ts │ │ │ │ │ │ ├── storage.ts │ │ │ │ │ │ ├── sub-domain.ts │ │ │ │ │ │ ├── template.ts │ │ │ │ │ │ └── vo/ │ │ │ │ │ │ ├── history-detail.ts │ │ │ │ │ │ └── pipeline-detail.ts │ │ │ │ │ └── service/ │ │ │ │ │ ├── builtin-plugin-service.ts │ │ │ │ │ ├── db-storage.ts │ │ │ │ │ ├── dns-provider-service.ts │ │ │ │ │ ├── getter/ │ │ │ │ │ │ ├── cname-proxy-service.ts │ │ │ │ │ │ ├── domain-verifier-getter.ts │ │ │ │ │ │ ├── notification-getter.ts │ │ │ │ │ │ ├── sub-domain-getter.ts │ │ │ │ │ │ └── task-service-getter.ts │ │ │ │ │ ├── history-log-service.ts │ │ │ │ │ ├── history-service.ts │ │ │ │ │ ├── notification-service.ts │ │ │ │ │ ├── pipeline-group-service.ts │ │ │ │ │ ├── pipeline-service.ts │ │ │ │ │ ├── storage-service.ts │ │ │ │ │ ├── sub-domain-service.ts │ │ │ │ │ ├── template-service.ts │ │ │ │ │ └── url-service.ts │ │ │ │ ├── plugin/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ └── plugin.ts │ │ │ │ │ └── service/ │ │ │ │ │ ├── default-plugin.ts │ │ │ │ │ ├── plugin-config-getter.ts │ │ │ │ │ ├── plugin-config-service.ts │ │ │ │ │ └── plugin-service.ts │ │ │ │ ├── suite/ │ │ │ │ │ └── service/ │ │ │ │ │ └── my-count-service.ts │ │ │ │ └── sys/ │ │ │ │ ├── authority/ │ │ │ │ │ ├── entity/ │ │ │ │ │ │ ├── permission.ts │ │ │ │ │ │ ├── role-permission.ts │ │ │ │ │ │ ├── role.ts │ │ │ │ │ │ ├── user-role.ts │ │ │ │ │ │ └── user.ts │ │ │ │ │ ├── enums/ │ │ │ │ │ │ └── ResourceTypeEnum.ts │ │ │ │ │ └── service/ │ │ │ │ │ ├── auth-service.ts │ │ │ │ │ ├── permission-service.ts │ │ │ │ │ ├── role-permission-service.ts │ │ │ │ │ ├── role-service.ts │ │ │ │ │ ├── user-role-service.ts │ │ │ │ │ └── user-service.ts │ │ │ │ └── settings/ │ │ │ │ ├── fix.ts │ │ │ │ └── safe-service.ts │ │ │ ├── plugins/ │ │ │ │ ├── index.ts │ │ │ │ ├── plugin-51dns/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── dns-provider.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-aliyun/ │ │ │ │ │ ├── dns-provider/ │ │ │ │ │ │ ├── aliyun-dns-provider.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── plugin/ │ │ │ │ │ │ ├── deploy-to-alb/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── deploy-to-cdn/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── deploy-to-dcdn/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── deploy-to-esa/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── deploy-to-fc/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── deploy-to-nlb/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── deploy-to-oss/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── deploy-to-slb/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── deploy-to-vod/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── deploy-to-waf/ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── upload-to-aliyun/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── utils/ │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-aws/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── libs/ │ │ │ │ │ │ └── aws-acm-client.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── plugin-deploy-to-cloudfront.ts │ │ │ │ │ └── plugin-upload-to-acm.ts │ │ │ │ ├── plugin-aws-cn/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── libs/ │ │ │ │ │ │ └── aws-iam-client.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-deploy-to-cloudfront.ts │ │ │ │ ├── plugin-cachefly/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-deploy-to-cdn.ts │ │ │ │ ├── plugin-cloudflare/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── dns-provider.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-demo/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── dns-provider.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-test.ts │ │ │ │ ├── plugin-dnsla/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── dns-provider.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-doge/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── lib/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── deploy-to-cdn/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-farcdn/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-refresh-cert.ts │ │ │ │ ├── plugin-flex/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-refresh-cert.ts │ │ │ │ ├── plugin-fnos/ │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-gcore/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── plugin-flush.ts │ │ │ │ │ └── plugin-upload.ts │ │ │ │ ├── plugin-github/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-check-release.ts │ │ │ │ ├── plugin-host/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin/ │ │ │ │ │ ├── copy-to-local/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── host-shell-execute/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── plugin-upload-to-oss.ts │ │ │ │ │ └── upload-to-host/ │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-huawei/ │ │ │ │ │ ├── access/ │ │ │ │ │ │ ├── huawei-access.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── dns-provider/ │ │ │ │ │ │ ├── huawei-dns-provider.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── deploy-to-cdn/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── upload-to-ccm/ │ │ │ │ │ ├── ccm-client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-jdcloud/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── dns-provider.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── plugin-deploy-to-cdn.ts │ │ │ │ │ ├── plugin-update-cert.ts │ │ │ │ │ └── plugin-upload-cert.ts │ │ │ │ ├── plugin-namesilo/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── dns-provider.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-notification/ │ │ │ │ │ ├── anpush/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── bark/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── dingtalk/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── discord/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── email/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── feishu/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── iyuu/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── qywx/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── serverchan/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── serverchan3/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── slack/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── telegram/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── vocechat/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── webhook/ │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-other/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── plugin-db-backup.ts │ │ │ │ │ ├── plugin-deploy-to-mail.ts │ │ │ │ │ ├── plugin-restart.ts │ │ │ │ │ ├── plugin-script.ts │ │ │ │ │ └── plugin-wait.ts │ │ │ │ ├── plugin-proxmox/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-upload.ts │ │ │ │ ├── plugin-qiniu/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin/ │ │ │ │ │ ├── deploy-to-cdn/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── upload-cert/ │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-qnap/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-qnap.ts │ │ │ │ ├── plugin-rainyun/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── dns-provider.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-refresh-cert.ts │ │ │ │ ├── plugin-tencent/ │ │ │ │ │ ├── access/ │ │ │ │ │ │ ├── dnspod-access.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── dns-provider/ │ │ │ │ │ │ ├── dnspod-dns-provider.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── tencent-dns-provider.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin/ │ │ │ │ │ ├── delete-expiring-cert/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── deploy-to-all/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── deploy-to-cdn/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── deploy-to-cdn-v2/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── deploy-to-clb/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── deploy-to-cos/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── deploy-to-eo/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── deploy-to-live/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── deploy-to-tke-ingress/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── start-instances/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── upload-to-tencent/ │ │ │ │ │ └── index.ts │ │ │ │ ├── plugin-upyun/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-depoy-to-cdn.ts │ │ │ │ ├── plugin-volcengine/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── cdn-client.ts │ │ │ │ │ ├── dns-client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── plugins/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── plugin-deploy-to-alb.ts │ │ │ │ │ │ ├── plugin-deploy-to-cdn.ts │ │ │ │ │ │ ├── plugin-deploy-to-clb.ts │ │ │ │ │ │ ├── plugin-deploy-to-dcdn.ts │ │ │ │ │ │ ├── plugin-deploy-to-live.ts │ │ │ │ │ │ ├── plugin-deploy-to-vod.ts │ │ │ │ │ │ └── plugin-upload-to-cert-center.ts │ │ │ │ │ ├── ve-client.ts │ │ │ │ │ └── volcengine-dns-provider.ts │ │ │ │ ├── plugin-wangsu/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── lib/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ └── AkSkAuth.ts │ │ │ │ │ │ ├── common/ │ │ │ │ │ │ │ └── Constant.ts │ │ │ │ │ │ ├── exception/ │ │ │ │ │ │ │ └── ApiAuthException.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── model/ │ │ │ │ │ │ │ ├── AkSkConfig.ts │ │ │ │ │ │ │ └── HttpRequestMsg.ts │ │ │ │ │ │ └── util/ │ │ │ │ │ │ ├── CryptoUtils.ts │ │ │ │ │ │ └── HttpUtils.ts │ │ │ │ │ └── plugins/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── plugin-refresh-cert.ts │ │ │ │ ├── plugin-west/ │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── dns-provider.ts │ │ │ │ │ └── index.ts │ │ │ │ └── plugin-woai/ │ │ │ │ ├── access.ts │ │ │ │ ├── index.ts │ │ │ │ └── plugins/ │ │ │ │ ├── index.ts │ │ │ │ └── plugin-deploy-to-cdn.ts │ │ │ └── utils/ │ │ │ ├── env.ts │ │ │ ├── http.ts │ │ │ ├── random.ts │ │ │ └── version.ts │ │ ├── test/ │ │ │ ├── controller/ │ │ │ │ ├── api.test.ts │ │ │ │ └── home.test.ts │ │ │ └── plugins/ │ │ │ └── 51dns.test.mjs │ │ ├── tools/ │ │ │ └── lego/ │ │ │ └── readme.md │ │ └── tsconfig.json │ └── docker-compose.yaml ├── pnpm-workspace.yaml ├── publish-check.js ├── start.sh ├── step.md ├── test/ │ └── docker/ │ ├── Dockerfile │ └── docker-compose.yaml └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ > 感谢您支持certd,请按如下规范提交issue > 如果有条件,请尽量在[github上提交](https://github.com/certd/certd/issues) ## 一、问题描述 `请在此处简要描述你所遇到的问题,必要时请贴出相关截图辅助理解和定位` ### 复现步骤 `请描述复现问题的详细步骤` `如果非示例页面的问题,最好能提供最小复现示例的代码、或者仓库链接` ### 报错截图 `请贴出报错日志截图` ### 效果截图 `请贴出效果截图` #### 1. 期望效果 #### 2. 实际效果 ================================================ FILE: .github/workflows/build-image-for-test.yml ================================================ name: build-image-for-test on: push: branches: ['v2-dev'] paths: - "build-dev.trigger" # schedule: # - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间 # - cron: '17 19 * * *' permissions: contents: read packages: write jobs: build-certd-image: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 0 ref: v2-dev - name: get_certd_version id: get_certd_version uses: actions/github-script@v6 with: result-encoding: string script: | const fs = require('fs'); const path = require('path'); const pnpmWorkspace = "./pnpm-workspace.yaml"; fs.unlinkSync(pnpmWorkspace) const jsonFilePath = "./packages/ui/certd-server/package.json"; const jsonContent = fs.readFileSync(jsonFilePath, 'utf-8'); const pkg = JSON.parse(jsonContent) console.log("certd_version:",pkg.version); return pkg.version # - name: Use Node.js # uses: actions/setup-node@v4 # with: # node-version: 18 # cache: 'npm' # working-directory: ./packages/ui/certd-client - run: | npm install -g pnpm pnpm install npm run build working-directory: ./packages/ui/certd-client - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to aliyun container Registry uses: docker/login-action@v3 with: registry: registry.cn-shenzhen.aliyuncs.com username: ${{ secrets.aliyun_cs_username }} password: ${{ secrets.aliyun_cs_password }} - name: Login to GitHub Packages uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.dockerhub_username }} password: ${{ secrets.dockerhub_password }} # - name: Build default platforms # uses: docker/build-push-action@v6 # with: # platforms: linux/amd64,linux/arm64 # push: true # context: ./packages/ui/ # tags: | # registry.cn-shenzhen.aliyuncs.com/handsfree/certd-dev:latest # greper/certd-dev:latest # ghcr.io/${{ github.repository }}:dev-latest - name: Build armv7 uses: docker/build-push-action@v6 with: platforms: linux/arm/v7 push: true context: ./packages/ui/ tags: | registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7 registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7 greper/certd:armv7 greper/certd:${{steps.get_certd_version.outputs.result}}-armv7 ================================================ FILE: .github/workflows/build-image.yml ================================================ name: build-image on: push: branches: ['v2-dev'] paths: - "build.trigger" # schedule: # - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间 # - cron: '17 19 * * *' permissions: contents: read packages: write jobs: build-certd-image: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 0 lfs: true - name: get_certd_version id: get_certd_version uses: actions/github-script@v6 with: result-encoding: string script: | const fs = require('fs'); const path = require('path'); const pnpmWorkspace = "./pnpm-workspace.yaml"; fs.unlinkSync(pnpmWorkspace) const jsonFilePath = "./packages/ui/certd-server/package.json"; const jsonContent = fs.readFileSync(jsonFilePath, 'utf-8'); const pkg = JSON.parse(jsonContent) console.log("certd_version:",pkg.version); return pkg.version # - name: Use Node.js # uses: actions/setup-node@v4 # with: # node-version: 18 # cache: 'npm' # working-directory: ./packages/ui/certd-client - run: | npm install -g pnpm pnpm install npm run build working-directory: ./packages/ui/certd-client - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to aliyun container Registry uses: docker/login-action@v3 with: registry: registry.cn-shenzhen.aliyuncs.com username: ${{ secrets.aliyun_cs_username }} password: ${{ secrets.aliyun_cs_password }} - name: Login to GitHub Packages uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.dockerhub_username }} password: ${{ secrets.dockerhub_password }} - name: Build default platforms uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 push: true context: ./packages/ui/ tags: | registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}} greper/certd:latest greper/certd:${{steps.get_certd_version.outputs.result}} ghcr.io/${{ github.repository }}:latest ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}} - name: Build armv7 uses: docker/build-push-action@v6 with: platforms: linux/arm/v7 push: true context: ./packages/ui/ tags: | registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7 registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7 greper/certd:armv7 greper/certd:${{steps.get_certd_version.outputs.result}}-armv7 ghcr.io/${{ github.repository }}:armv7 ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7 # - name: Build agent # uses: docker/build-push-action@v6 # with: # platforms: linux/amd64,linux/arm64 # push: true # context: ./packages/ui/agent/ # tags: | # registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:latest # registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:${{steps.get_certd_version.outputs.result}} # greper/certd-agent:latest # greper/certd-agent:${{steps.get_certd_version.outputs.result}} ================================================ FILE: .github/workflows/deploy-demo.yml ================================================ name: deploy-demo on: push: branches: ['v2-dev'] paths: - "deploy.trigger" workflow_run: workflows: [ "build-image" ] types: - completed # schedule: # - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间 # - cron: '17 19 * * *' permissions: contents: read jobs: deploy-certd-demo: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 with: fetch-depth: 0 ref: v2-dev - name: get_certd_version id: get_certd_version uses: actions/github-script@v6 with: result-encoding: string script: | const fs = require('fs'); const path = require('path'); const jsonFilePath = "./packages/ui/certd-server/package.json"; const jsonContent = fs.readFileSync(jsonFilePath, 'utf-8'); const pkg = JSON.parse(jsonContent) console.log("certd_version:",pkg.version); return pkg.version - uses: GuillaumeFalourd/wait-sleep-action@v1 with: time: '10' # for 60 seconds - name: deploy-certd-demo uses: tyrrrz/action-http-request@master with: url: http://flow-openapi.aliyun.com/pipeline/webhook/lzCzlGrLCOHQaTMMt0mG method: POST headers: | Content-Type: application/json body: | { "CERTD_VERSION": "${{steps.get_certd_version.outputs.result}}" } retry-count: 3 retry-delay: 5000 - name: deploy-certd-doc uses: tyrrrz/action-http-request@master with: url: http://flow-openapi.aliyun.com/pipeline/webhook/IiSxLDp9aOhgDUxJPytv method: POST body: | {} headers: | Content-Type: application/json retry-count: 3 retry-delay: 5000 ================================================ FILE: .github/workflows/sync-to-gitee-dev.yml ================================================ name: sync-to-gitee-dev on: push: branches: ['v2-dev'] # schedule: # - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间 # - cron: '17 19 * * *' permissions: contents: read jobs: sync: runs-on: ubuntu-latest steps: - name: Checkout work repo # 1. 检出当前仓库(certd-sync-work) uses: actions/checkout@v4 with: fetch-depth: 0 lfs: true - name: Set git user # 2. 给git命令设置用户名和邮箱,↙↙↙ 改成你的name和email run: | git config --global user.name "xiaojunnuo" git config --global user.email "xiaojunnuo@qq.com" - name: Set git token # 3. 给git命令设置token,用于push到目标仓库 uses: de-vri-es/setup-git-credentials@v2 with: # token 格式为: username:password credentials: https://${{secrets.PUSH_TOKEN_GITEE}}@gitee.com - name: push to gitee # 4. 执行同步 run: | git remote add upstream https://gitee.com/certd/certd git push --set-upstream upstream v2-dev ================================================ FILE: .github/workflows/sync-to-gitee.yml ================================================ name: sync-to-gitee on: push: branches: ['v2'] # schedule: # - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间 # - cron: '17 19 * * *' permissions: contents: read jobs: sync: runs-on: ubuntu-latest steps: - name: Checkout work repo # 1. 检出当前仓库(certd-sync-work) uses: actions/checkout@v4 with: fetch-depth: 0 lfs: true - name: Set git user # 2. 给git命令设置用户名和邮箱,↙↙↙ 改成你的name和email run: | git config --global user.name "xiaojunnuo" git config --global user.email "xiaojunnuo@qq.com" - name: Set git token # 3. 给git命令设置token,用于push到目标仓库 uses: de-vri-es/setup-git-credentials@v2 with: # token 格式为: username:password credentials: https://${{secrets.PUSH_TOKEN_GITEE}}@gitee.com - name: push to gitee # 4. 执行同步 run: | git remote add upstream https://gitee.com/certd/certd git push --set-upstream upstream v2 ================================================ FILE: .gitignore ================================================ ./packages/core/lego # IntelliJ project files .vscode/ node_modules/ npm-debug.log yarn-error.log yarn.lock package-lock.json /.idea/ */**/dist */**/pnpm-lock.yaml */**/stats.html .idea *.iml out gen /test/*.private.* /*.log nohup.out /packages/ui/*/.idea /packages/ui/*/node_modules /packages/*/node_modules #/pnpm-lock.yaml tsconfig.tsbuildinfo test/**/*.js /packages/ui/certd-server/data/db.sqlite /packages/ui/certd-server/data/keys.yaml /packages/pro/ test.js ================================================ FILE: .npmrc ================================================ link-workspace-packages=deep prefer-workspace-packages=true ================================================ FILE: .prettierrc ================================================ { "printWidth": 160, "bracketSpacing": true, "singleQuote": true, "trailingComma": "es5", "arrowParens": "avoid" } ================================================ FILE: CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [1.36.10](https://github.com/certd/certd/compare/v1.36.9...v1.36.10) (2025-07-18) ### Bug Fixes * 企业微信通知改成text类型,因为markdown类型不支持@用户 ([747d266](https://github.com/certd/certd/commit/747d26674248082e678a3fd5ecc94712641a2716)) * api接口获取不到证书的bug ([05a33a0](https://github.com/certd/certd/commit/05a33a0ec9999e2802f6c7b23cc1c61a2b9e963d)) ### Performance Improvements * 部署到阿里云oss插件支持选择上传到阿里云cas中的证书 ([2ea2c8c](https://github.com/certd/certd/commit/2ea2c8c05fc40f79595f1bbde67c1413558bf684)) * 优化子域名托管的说明 ([b15f514](https://github.com/certd/certd/commit/b15f514018b728acb0922ee3f93c1f302eb5d471)) * 账号即将过期通知 ([e403450](https://github.com/certd/certd/commit/e40345095f31e2fb8e2333a6647466659133fa0c)) * 子域名托管重复域名不允许添加 ([ffc0c7b](https://github.com/certd/certd/commit/ffc0c7bb7b16d9904fd2d905d1c4e1d4854e92a9)) ## [1.36.9](https://github.com/certd/certd/compare/v1.36.7...v1.36.9) (2025-07-15) ### Bug Fixes * 修复ssh无法执行命令的bug ([9763cb0](https://github.com/certd/certd/commit/9763cb00e5d95b2fa5d1c2d3d4a8eecac71600e6)) ## [1.36.7](https://github.com/certd/certd/compare/v1.36.6...v1.36.7) (2025-07-15) ### Bug Fixes * 修复流水线列表页报length错误的bug ([9864792](https://github.com/certd/certd/commit/9864792bbfd149e770d6e1ffa809573694f99dd3)) * 修复流水线页面状态没有刷新的bug ([93e9498](https://github.com/certd/certd/commit/93e9498b410353f504e11e264db62468895d7290)) * 修复自定义证书检查时间重启之后不生效的bug ([38e867c](https://github.com/certd/certd/commit/38e867c917bbc68bd228bdd8064f3e7358d6413d)) ### Performance Improvements * 支持上传证书到各种对象存储,oss、cos、七牛、s3、minio等 ([1da8617](https://github.com/certd/certd/commit/1da8617a53a675776635bbc3bcb3c6d7dff83e27)) * 支持邮箱发送证书 ([95332d5](https://github.com/certd/certd/commit/95332d5db96cd54ddab6ab737332417a09169b39)) ## [1.36.6](https://github.com/certd/certd/compare/v1.36.5...v1.36.6) (2025-07-14) ### Bug Fixes * 修复某些页面翻译不全显示错误的bug ([0b3158f](https://github.com/certd/certd/commit/0b3158fdd5fe5bb0a98c4e65715dbc3de2c38047)) * 修复运行流水线后会闪烁一下的bug ([dfc9362](https://github.com/certd/certd/commit/dfc9362084082ee535b898f23b2609c1d946a6fd)) ### Performance Improvements * 部署plesk证书,支持删除未使用的证书 ([902d246](https://github.com/certd/certd/commit/902d246d1a7473ad90f604028c4eb09c8c67d99c)) * 通知和定时器的删除按钮显示为红色更显眼 ([61ba83c](https://github.com/certd/certd/commit/61ba83c77546c3d505d081e19a3d68c127662bf1)) * 优化流水线列表页面、详情页面性能,精简返回数据 ([609ac9c](https://github.com/certd/certd/commit/609ac9c9a2dde605eb09834ae59693c1cb238765)) * 支持自动选择校验方式申请证书 ([3f99432](https://github.com/certd/certd/commit/3f9943270cfb12946e38e6272bc5e8d95ad6ab9e)) * OpenAPI支持autoApply参数 ([42f4d14](https://github.com/certd/certd/commit/42f4d1477dc791520a874aed56035abcbc8c433b)) ## [1.36.5](https://github.com/certd/certd/compare/v1.36.4...v1.36.5) (2025-07-11) ### Bug Fixes * 某些插件找不到的bug ([4b3f4a8](https://github.com/certd/certd/commit/4b3f4a868a8b0800c5c59de9d0f47bddc079e7b3)) ## [1.36.4](https://github.com/certd/certd/compare/v1.36.3...v1.36.4) (2025-07-10) ### Bug Fixes * 修复查看证书对话框翻译错误的bug ([8626b6d](https://github.com/certd/certd/commit/8626b6d9f235c511766f2ae98e0a37f6cebb621c)) * 修复translation后分组编辑打不开的bug ([46a1b74](https://github.com/certd/certd/commit/46a1b7479923d2feb2dece202a5932b99981b2cd)) * 执行windows nginx命令时,改为return code判断是否执行成功 ([b37cffd](https://github.com/certd/certd/commit/b37cffd704cd08b8bdd68a6e284706eabe59e78d)) ### Performance Improvements * 优化证书进度条颜色 ([2af91db](https://github.com/certd/certd/commit/2af91dbf2ae36a4ed17c6788bc2a2a79a3bb29f8)) * 站点证书即将过期通知标题颜色优化为红色 ([80c5331](https://github.com/certd/certd/commit/80c5331a5d4c320323d9b9b800e4ea3b72577b33)) * 支持部署到阿里云vod ([98da4e1](https://github.com/certd/certd/commit/98da4e1791ed8bb21daf2a9914fda875d14633c9)) * 支持部署证书到网宿CDN ([c3da026](https://github.com/certd/certd/commit/c3da026b33106f5195959825a68cadbe49efef00)) * 重置管理员密码同时可以清除管理员的2FA设置 ([1ece091](https://github.com/certd/certd/commit/1ece0915f172d5f8b8adb866434e7efcc5c8c46d)) * output-selector from参数支持更丰富的过滤规则 ([87853a2](https://github.com/certd/certd/commit/87853a201535f3bfe8505c40f8f5229d82ffcc73)) ## [1.36.3](https://github.com/certd/certd/compare/v1.36.2...v1.36.3) (2025-07-07) ### Bug Fixes * 修复开放接口添加按钮文本显示问题 ([f93ba99](https://github.com/certd/certd/commit/f93ba9970c12680f38eba2a7abd4b72cf3f5b6a6)) ### Performance Improvements * 优化部署到腾讯TKE插件,支持Opaque类型选择,优化填写说明 ([1445325](https://github.com/certd/certd/commit/144532530a865b634e68539e4888e26f52f73492)) ## [1.36.2](https://github.com/certd/certd/compare/v1.36.1...v1.36.2) (2025-07-06) ### Bug Fixes * 修复notification编辑按钮无法打开对话框的bug ([0cea26c](https://github.com/certd/certd/commit/0cea26c6287f52adf273b4a525c37bea8555c68c)) * 优化更新飞牛os证书有效期,修复某些情况下部署证书后飞牛无法访问https的bug ([610c919](https://github.com/certd/certd/commit/610c919c72037becc0ed326f5d5b18c963dfcb3a)) ### Performance Improvements * 证书检查支持自定义dns服务器 ([c53bb7c](https://github.com/certd/certd/commit/c53bb7cf677faa32729709ae0c10359db5194d7a)) ## [1.36.1](https://github.com/certd/certd/compare/v1.36.0...v1.36.1) (2025-07-02) ### Bug Fixes * 修复通知和触发器无法编辑的bug ([a2e0951](https://github.com/certd/certd/commit/a2e09510426680eb425c0d7ad337f39d3f052054)) ### Performance Improvements * 支持部署到七牛云DCDN ([bde601b](https://github.com/certd/certd/commit/bde601bfffb4f7345d97e1e3b064520816d31555)) # [1.36.0](https://github.com/certd/certd/compare/v1.35.5...v1.36.0) (2025-07-01) ### Bug Fixes * 支持自定义证书生成插件 ([481cc02](https://github.com/certd/certd/commit/481cc029fafaf280aa844cd3ca30f4653ec35d55)) ### Features * 支持模版创建流水线 ([2559f0e](https://github.com/certd/certd/commit/2559f0e822db095d1d26a7f1d517622dce22a5c2)) ### Performance Improvements * 阿里云waf cname站点选择支持翻页及域名查询 ([4cf9858](https://github.com/certd/certd/commit/4cf98584dacc5999752732f136246647a2f1f07d)) * 部署到ssh主机命令支持前置命令 ([991b741](https://github.com/certd/certd/commit/991b741cbe223b342f534157da63b71e81661f8e)) * 模版导入流水线 ([dcc8c56](https://github.com/certd/certd/commit/dcc8c569693432579709ce63656665a76bcf9a44)) * 添加用户资料编辑功能 ([7c0f43c](https://github.com/certd/certd/commit/7c0f43c8a3052f73afee3e93c9fcbc43c44ab690)) * 优化阿里云waf的日志信息 ([821c6d8](https://github.com/certd/certd/commit/821c6d807d4b3cc5092d09a6282b8cbafb9e7c9f)) * 优化中英文翻译与切换 ([acaa8b1](https://github.com/certd/certd/commit/acaa8b173183b4423584ee070e6e332e0ac0eb2d)) * 站点IP监控前先同步一下IP ([a080b60](https://github.com/certd/certd/commit/a080b606ab6e289d96b17ef7d2879b4603f889ba)) * 支持选择运行策略设置 ([60f055f](https://github.com/certd/certd/commit/60f055f293ce237c21cd9050333dad9609eceac1)) ## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20) ### Bug Fixes * 腾讯云授权支持设置是否国际站,部署到EO插件支持国际站 ([5cd3968](https://github.com/certd/certd/commit/5cd3968929acef333cf30d3b20cf21cea6c82c5f)) * 修复邮箱包含.号校验失败的bug ([65dcae7](https://github.com/certd/certd/commit/65dcae79f8faa7a6cb425e10a0fdb6758b0719f3)) ### Performance Improvements * 首次打开任务日志查看页面,自动滚动到底部 ([43fee42](https://github.com/certd/certd/commit/43fee42198e8697185b427b1fa3eb79409603393)) * 支持批量修改通知和定时 ([e11b3be](https://github.com/certd/certd/commit/e11b3becfd4abe6547e84d09adc38ebd6e1c4b87)) ## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13) ### Performance Improvements * 支持s3 access做测试 ([f00aeac](https://github.com/certd/certd/commit/f00aeacb8b5c81f0bafa4c1b76723dec2b6b7784)) ## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12) ### Bug Fixes * 修复消息内容存在()<>等括号情况下无法发送tg通知的bug ([c937583](https://github.com/certd/certd/commit/c937583a50d8513d76adead3648f83eee2fcc6f9)) * 修复重试次数设置无效的bug ([e2099ac](https://github.com/certd/certd/commit/e2099ac9ca344bc70bfa4219002e9138708973ae)) ### Performance Improvements * 授权列表类型颜色优化 ([1e86338](https://github.com/certd/certd/commit/1e863382d3d1a8cc95a1abf51e75bf6eaea3244f)) * 支持雨云dns解析 ([8354348](https://github.com/certd/certd/commit/83543487e7418683bd79cfe3b9e0d792bdb977f7)) * 支持雨云dns解析以及雨云证书更新 ([43c7a19](https://github.com/certd/certd/commit/43c7a1984926f5d4647760cc134bb0aede3a7b7a)) * github 版本检查支持执行脚本 ([bad3504](https://github.com/certd/certd/commit/bad3504d4a15e6989b967b66aa9da8c6981f25bf)) ## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09) ### Bug Fixes * 修复阿里云新加坡clb无法部署证书的bug ([c1fbc8c](https://github.com/certd/certd/commit/c1fbc8cd68ae020ef342e4e92f4d9b4869ca1ead)) * 修复阿里云新加坡clb无法部署证书的bug ([3e84e11](https://github.com/certd/certd/commit/3e84e116e863b54c6b4d7db160af372dacc5857f)) * 修复检查github release 插件无法保存最后版本的bug ([a92107c](https://github.com/certd/certd/commit/a92107cc47133883b099d5228b06373e84c8bb50)) * 修复站点监控定时器多次添加的bug ([9361679](https://github.com/certd/certd/commit/936167972fe83e519bc01a0dd961d9c0635d24ab)) ### Performance Improvements * 阿里云dns操作增加重试机制 ([424fd96](https://github.com/certd/certd/commit/424fd96615c05e949af8c837c261c1400bdffba2)) * 优化阿里云nlb支持部署扩展证书 ([9cbdfda](https://github.com/certd/certd/commit/9cbdfda829b231733d54c66c5024d46e6fc11af3)) * 子域名托管帮助链接优化为打开新窗口 ([7c0cdd1](https://github.com/certd/certd/commit/7c0cdd169e2f943e703e433677f2f437d4aa02ee)) * history增加触发类型显示 ([7f6070c](https://github.com/certd/certd/commit/7f6070c960ed7bf02add5ab36436de6573f2f1fa)) ## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07) ### Bug Fixes * 某些证书提供商的证书确实commonName导致无法转换证书的问题 ([ac87bc5](https://github.com/certd/certd/commit/ac87bc57e957ea4679707bfd38d6840e26319bed)) * 修复站点监控通知渠道设置无效的bug ([a00453c](https://github.com/certd/certd/commit/a00453c83a58114ce2873dd6e6aaf313f1ce0f87)) ### Performance Improvements * 修改 HTTPS 服务器监听地址 ([e1cf64a](https://github.com/certd/certd/commit/e1cf64ae16d4abfe4299ff16d5088c30cf3c6365)) * 优化流水线页面,增加下次执行时间、查看证书显示 ([c820315](https://github.com/certd/certd/commit/c8203154094fae3d17198747f49f5f41ddf29a4e)) * 站点证书监控支持定时设置,重试次数设置 ([d3c2f8e](https://github.com/certd/certd/commit/d3c2f8eb436e670772d14a54acd6b541c5aa3978)) * 证书申请支持letencrypt profile选项 ([2eb0e54](https://github.com/certd/certd/commit/2eb0e54909d8ad36708e07c12fd598998159bc43)) * aliyun alb支持部署扩展证书 ([2a19b61](https://github.com/certd/certd/commit/2a19b61b7a78620c06396c2cc37cc77d738b6d12)) # [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05) ### Features * 完善注释 ([6702ca1](https://github.com/certd/certd/commit/6702ca10a17f5d7dbff789b039f7269496f66b97)) * AWS 中国区 CloudFront 证书部署(IAM 证书) ([8a55bed](https://github.com/certd/certd/commit/8a55beda924b3be2a53b9ba80d9487cefa8bf887)) * **lego:** support for command options ([b84159f](https://github.com/certd/certd/commit/b84159f2f11531f058837c2e82d66499f3740f20)) ## [1.34.11](https://github.com/certd/certd/compare/v1.34.10...v1.34.11) (2025-06-05) ### Bug Fixes * 修复用户最大流水线数量校验的问题 ([919f70a](https://github.com/certd/certd/commit/919f70a5fd2842ca69f96f1659bb5a7ba3f73776)) * 修复中文域名使用cname方式校验无法通过的问题 ([f7d5baa](https://github.com/certd/certd/commit/f7d5baa6d04cb83c572b06e62f885890cfa0143a)) * 修复cv4pve sdk (proxmox插件连接失败时无法正常结束任务的bug) ([49f26b4](https://github.com/certd/certd/commit/49f26b4049a0549b0270395157e96e8f04a68bc4)) * 修复flexcdn部署证书的顶级CA名称显示 ([6467edb](https://github.com/certd/certd/commit/6467edb84324d7c80a85212675dbacedc459df83)) * 修复flexcdn证书commonNames错误的问题 ([ace363f](https://github.com/certd/certd/commit/ace363fa355436e769b27f71cc487d30d6441780)) ### Performance Improvements * 分组选择支持清空选项 ([03e2e99](https://github.com/certd/certd/commit/03e2e9949837b34eb3ea56d14a9e8a5dabc96063)) * 优化cname检查,当有冲突的cname记录时,给出提示 ([e639a8f](https://github.com/certd/certd/commit/e639a8f9f12640ffcca69f1a6a0324459924afbd)) * 增加下载日志按钮 ([6ff509d](https://github.com/certd/certd/commit/6ff509d263c0182645b4692c10b5fedb192db964)) * 站点监控支持批量导入域名和ip ([2d7729d](https://github.com/certd/certd/commit/2d7729dbe98f29088f5f317db2b52cc1ede223a6)) * 支持设置用户有效期 ([6ac3bc5](https://github.com/certd/certd/commit/6ac3bc564f407dad2cd0b0b0744e887387aa5da3)) ## [1.34.10](https://github.com/certd/certd/compare/v1.34.9...v1.34.10) (2025-06-03) ### Bug Fixes * **flexcdn:** fix cert upload and skipSslVerify required ([c48da5d](https://github.com/certd/certd/commit/c48da5dea7f0f0cdeae643b106b4a678acc3b14b)) ### Performance Improvements * 阿里云CLB支持部署到扩展域名 ([0e8339c](https://github.com/certd/certd/commit/0e8339c70190890d449099e1d26e5ed06ff135fb)) * 优化流水线名称过长时的显示 ([6a0cc1b](https://github.com/certd/certd/commit/6a0cc1b1f3ad508f9e4093b3b682b163f12389eb)) * 支持部署到飞牛OS ([ddfd0fb](https://github.com/certd/certd/commit/ddfd0fb81d6638352920261065f1ab8e27bdd564)) * 支持日志写入文件 ([37edbf5](https://github.com/certd/certd/commit/37edbf5824d6aaae68ea1ef7259c6f739d418d2c)) ## [1.34.9](https://github.com/certd/certd/compare/v1.34.8...v1.34.9) (2025-05-30) ### Bug Fixes * 修复Farcdn证书有效期错误的问题 ([1fe4c36](https://github.com/certd/certd/commit/1fe4c367f7128de9ba5e3395ae06bc81e63a7d5a)) ### Performance Improvements * 不止证书自动化,插件解锁无限可能 ([a9b302e](https://github.com/certd/certd/commit/a9b302e38d3328d75df8b2da3d8b914851e55e9c)) * 邮箱支持保存和选择 ([f7b0b44](https://github.com/certd/certd/commit/f7b0b44ef6044bec36510a6f0b06d8dca5bfce49)) * 支持github 新版本检查并发布通知 ([356703c](https://github.com/certd/certd/commit/356703c83ea18c6efb8931402e181280d7b7e696)) ## [1.34.8](https://github.com/certd/certd/compare/v1.34.7...v1.34.8) (2025-05-28) ### Bug Fixes * 更新 1panel API 版本支持v1/v2设置 ([e6195ad](https://github.com/certd/certd/commit/e6195ade3ec54b138825b8d6738f86eb8afdd720)) * 同步更新namesilo接口,修复无法创建和删除dns记录的问题 ([36b02c2](https://github.com/certd/certd/commit/36b02c2cec145c13d4ef29d49aba5b6b4f697df2)) * 修复阿里云 esa 证书获取站点列表错误的问题 ([0c2ea5d](https://github.com/certd/certd/commit/0c2ea5da4c836f8a0df132a3f22d399bd9ee1de9)) * 修复部署到华为cdn,子账号ak查询不到域名的bug ([ebb292a](https://github.com/certd/certd/commit/ebb292a2f7a425c1bc810f59468beb3f1d5bc3f0)) * 修复证书申请任务无法修改dns提供商类型的bug ([8802274](https://github.com/certd/certd/commit/88022747bebe2054223e0241d68d410771405e68)) ### Performance Improvements * 关闭腾讯云证书通知提醒 ([231a875](https://github.com/certd/certd/commit/231a875bb481420c39bf76ec9ff4e50954ab9fe4)) * 优化站点选择组件,切换选择时不刷新列表 ([3a14714](https://github.com/certd/certd/commit/3a147141b1a5d67c92a5ce88a5313eaa62859e03)) * 优化站点ip检查 ([a463711](https://github.com/certd/certd/commit/a463711b03a20120f2a298be15d71ca152d27f21)) * 站点监控支持监控IP ([9cc4c01](https://github.com/certd/certd/commit/9cc4c017ae646a18284e732769b82636feda01d3)) * 支持批量重新运行 ([8189982](https://github.com/certd/certd/commit/818998259ddc75e722196ac5c365038818539b9b)) * farcdn优化 ([a06ef07](https://github.com/certd/certd/commit/a06ef07178ed73c537e21c7d57e5e5144d2c056d)) ## [1.34.7](https://github.com/certd/certd/compare/v1.34.6...v1.34.7) (2025-05-26) ### Performance Improvements * 优化阿里云DCDN插件,支持多选 ([b091657](https://github.com/certd/certd/commit/b091657b5c537acf2442a2bfc345d0a77f5e2c50)) * 支持部署到farcdn ([e08cf57](https://github.com/certd/certd/commit/e08cf57b72128998f487ab6469868052fbce0dba)) ## [1.34.6](https://github.com/certd/certd/compare/v1.34.5...v1.34.6) (2025-05-25) ### Bug Fixes * 修复公共插件配置修改不生效的bug,优化系统设置参数注入时机 ([e1e510c](https://github.com/certd/certd/commit/e1e510ce1e37a5ae82478226b6987a83f22d1ecb)) * 修复又拍云 CDN 设置证书参数和强制 HTTPS 配置报错的bug ([7984b62](https://github.com/certd/certd/commit/7984b625ba6727132f205db8e25f790bce27b2f7)) * 修复lego模式下每次都重新申请证书的bug ([f807b8c](https://github.com/certd/certd/commit/f807b8cb465cc329fa034ecbef94e18ef394f870)) * 优化 RunnableError错误信息展示 ([36bc3ff](https://github.com/certd/certd/commit/36bc3ff22da93ba342c3c1103d7ee2bbcecf44f2)) * **cert:** 修正证书过期时间计算逻辑 ([a3086e6](https://github.com/certd/certd/commit/a3086e6a5bec8b07f5e1d21a2ca8bd969c75bd5c)) ### Performance Improvements * 二次认证页面中,添加动态验证码输入框的焦点控制,提升用户体验 ([bb22f06](https://github.com/certd/certd/commit/bb22f062ed4ab4b5b71938270fe4cc666af6b8e7)) * 添加阿里云 ESA证书部署插件 ([1db1ffd](https://github.com/certd/certd/commit/1db1ffde99ac7e4684fa606ebc4c327f829b3a26)) * 站点证书监控增加通知设置 ([3422a1a](https://github.com/certd/certd/commit/3422a1a59fd0d2c0f17fa9c7e8988ac527ecfdd9)) ## [1.34.5](https://github.com/certd/certd/compare/v1.34.4...v1.34.5) (2025-05-19) ### Performance Improvements * 1panel增加授权测试按钮 ([566b12f](https://github.com/certd/certd/commit/566b12f5d14ce10e8f5cf1807c58f7bf27f0d199)) * 优化钉钉通知标题颜色 ([a560999](https://github.com/certd/certd/commit/a560999d13eed18d08dd32ee530166569e3f8746)) * 优化飞书通知为卡片模式 ([a818a3d](https://github.com/certd/certd/commit/a818a3d293e22fb46979bc77055c05621a6fed81)) * 支持部署到宝塔aaWAF ([094565c](https://github.com/certd/certd/commit/094565ccd619ef671c6c11ce5fb7fd54a7a21d1c)) * aaWaf、cdnfly站点选择支持查询 ([8af3463](https://github.com/certd/certd/commit/8af3463668a40b9b99febb02e3b4e0d9d8d719b4)) ## [1.34.4](https://github.com/certd/certd/compare/v1.34.3...v1.34.4) (2025-05-16) ### Bug Fixes * 修复部署flexcdn问题 ([76b19a4](https://github.com/certd/certd/commit/76b19a4980f8edba5238543b82a7811e1003746c)) * 修复插件导入的bug ([677fec0](https://github.com/certd/certd/commit/677fec0a0b6fceb4966705e471bbfeeda91610c7)) * 修复导入在线插件不生效的bug ([fcf8309](https://github.com/certd/certd/commit/fcf8309c238208281ecb4575b2c3cfe50c11d783)) * 修复自建插件保存丢失部署策略的bug ([863e74d](https://github.com/certd/certd/commit/863e74dd2e3912f950ff5025b5ed0070aeb37035)) ### Performance Improvements * 调整小助手,仅在登录之后显示 ([aebb07c](https://github.com/certd/certd/commit/aebb07c5cc8b1f233b9d203ff017ac60e6971a85)) ## [1.34.3](https://github.com/certd/certd/compare/v1.34.2...v1.34.3) (2025-05-15) ### Performance Improvements * 宝塔插件、1panel 改成完全免费版 ([a53b6cd](https://github.com/certd/certd/commit/a53b6cd28ff2ce5662ada82379ea44a06b179b81)) * 添加 FlexCDN 更新证书插件 ([bf040d4](https://github.com/certd/certd/commit/bf040d4c428d29c06fbaca5e29100e0c583b2b0b)) * 小助手可以关闭 ([3e2101a](https://github.com/certd/certd/commit/3e2101aa5b56548614102e900d59819ce8c7e97c)) * 支持部署到maoyun cdn ([68f333f](https://github.com/certd/certd/commit/68f333fb87ce85eed27436ecb0f76351c0ccb0d1)) * 支持AI分析报错 ([aa96859](https://github.com/certd/certd/commit/aa96859798166426e485947a6590464de189de05)) ## [1.34.2](https://github.com/certd/certd/compare/v1.34.1...v1.34.2) (2025-05-11) ### Bug Fixes * 修复部署到又拍云强制https无效的bug ([2397097](https://github.com/certd/certd/commit/2397097e4ddcb6f593210598e8779ffd44ac3f8f)) * 修复刷新流水线页面后,日志不自动更新的bug ([0b2e28b](https://github.com/certd/certd/commit/0b2e28b62dd5eb6804c602083e65c87a9d1d72d2)) ### Performance Improvements * 集成智能问答机器人 ([9dd4905](https://github.com/certd/certd/commit/9dd49054d18ec436a5029444ca55a38adc682933)) * 支持设置网安备案号 ([d18e431](https://github.com/certd/certd/commit/d18e431e2f08e6b37704032c4ea6fbdd8e971442)) * http方式支持校验443端口 ([d75fcb7](https://github.com/certd/certd/commit/d75fcb7fec421a9a638eaa27fe9378c84b5e0f19)) ## [1.34.1](https://github.com/certd/certd/compare/v1.34.0...v1.34.1) (2025-05-05) ### Bug Fixes * 根据SOA记录判断子域名托管有缺陷,改回手动配置子域名托管记录的方式 ([1b280a2](https://github.com/certd/certd/commit/1b280a2940f9e2d919b0bf23b89cc185be1fa498)) * 修复宝塔授权测试按钮显示错误的bug ([048696e](https://github.com/certd/certd/commit/048696ee9386491bb68592fb3a47d1c900bb68bf)) ### Performance Improvements * 支持部署证书到火山dcdn ([5f85219](https://github.com/certd/certd/commit/5f852194953dc1b4e6336770f417507b8f5a33ad)) * 支持部署证书到unicloud ([a63d687](https://github.com/certd/certd/commit/a63d687f1c573159f0857693f37602b0e1e44072)) # [1.34.0](https://github.com/certd/certd/compare/v1.33.8...v1.34.0) (2025-04-28) ### Bug Fixes * 修复二次认证登录进入错误账号的bug ([e3930e0](https://github.com/certd/certd/commit/e3930e07172dd7903cb0f6ff26e0e3e828ba3e77)) ### Features * 从yaml文件注册插件 ([deb3893](https://github.com/certd/certd/commit/deb38938204b29543f36d3266249958faaaa6b66)) ### Performance Improvements * 优化cdnfly插件,支持自动匹配域名部署 ([afd59e9](https://github.com/certd/certd/commit/afd59e9933b2650f41c5d47684c171b93b962065)) ## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26) ### Bug Fixes * 服务器时间获取不准确的bug ([5d10cbf](https://github.com/certd/certd/commit/5d10cbf18daf94a90a7551641a3b13e3c5fec611)) * 修复复制流水线无效的bug ([3df20a9](https://github.com/certd/certd/commit/3df20a924f32970b052e2588ea20de095f0ea693)) * 修复http上传方式无法清除记录文件的bug ([72a7b51](https://github.com/certd/certd/commit/72a7b51d479602b2c54c6c3ac8d8a0dcb9664e73)) * 修复token过期后,疯狂打印token过期信息的bug ([50a5fa1](https://github.com/certd/certd/commit/50a5fa15bb240a125bbc91d2ce1ff3c835888a77)) ### Performance Improvements * 从域名的soa获取主域名,子域名托管无需额外配置 ([a586a92](https://github.com/certd/certd/commit/a586a92d5e32ea846ac37be52a7ad8c328d89966)) * 七牛oss支持删除过期备份 ([b7113bd](https://github.com/certd/certd/commit/b7113bda2378116d6c116dc583f563cce7cf9f00)) * 数据库备份支持oss ([308d460](https://github.com/certd/certd/commit/308d4600efe2002f199c33b4594d3071784e58ea)) * 支持阿里云中文域名申请 ([b3468cf](https://github.com/certd/certd/commit/b3468cf7f28228d7c9cf68de6b5a9bbeb67f2c6d)) * 支持反向代理增加contextPath路径 ([0088929](https://github.com/certd/certd/commit/0088929622160cc922995de9a563e8061686ff34)) * 支持中文域名 ([162ebfd](https://github.com/certd/certd/commit/162ebfd4e0c25727efb33952d3bbf7420a02e2c3)) ## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22) ### Performance Improvements * 添加部署证书至火山 Live ([abea80e](https://github.com/certd/certd/commit/abea80e3ab9b1672aebe1c5d5e856693b29931a8)) * 优化首页插件列表展示 ([9b8f60b](https://github.com/certd/certd/commit/9b8f60b64b5f9a3db7dfa9b3dcbd9201984358d0)) * 证书申请支持51dns ([8638fc9](https://github.com/certd/certd/commit/8638fc91ff34fccaf12ff9874fd3fa9d2a8c18b7)) * 支持51dns ([96a0900](https://github.com/certd/certd/commit/96a0900edc95dcfd9acccf9d13592f12f5a09b3d)) * ssh PTY模式登录设置 ([8385bcc](https://github.com/certd/certd/commit/8385bcc2d7f2411a07748bb5c53f9eaf4d38d7cc)) * ssh伪终端模式优化,windows下不开启 ([42dfe93](https://github.com/certd/certd/commit/42dfe936b773b7bdd82ca3378363252ffffd7b71)) ## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20) ### Bug Fixes * 上传商用证书,直接粘贴文本报错的问题;修复无法上传ec加密证书的bug ([5750bb7](https://github.com/certd/certd/commit/5750bb706779da274d8e7a87e71416cb64d2df79)) * 修复下载证书时提示token已过期的问题 ([0e07ae6](https://github.com/certd/certd/commit/0e07ae6ce84dcb9279d3c44060d621566afa593c)) ### Performance Improvements * 更新license时同时绑定url ([78367af](https://github.com/certd/certd/commit/78367af8307f801e778c76d49f0918c21ffe032f)) * 切换到不同的分组后再打开创建对话框,会自动选择分组 ([893dcd4](https://github.com/certd/certd/commit/893dcd4f2487891199ed3e5a3d47a79a75efc942)) * 新增部署到火山引擎ALB/CLB、上传到证书中心 ([c9a3e3d](https://github.com/certd/certd/commit/c9a3e3d9d26f964c7af7b56667936f1414fbf42a)) * 优化/api缓存为0 ([dc05cd4](https://github.com/certd/certd/commit/dc05cd481f186b13375192be965000e6b4b429a5)) * 优化华为cdn插件引用ccm证书 ([b565b4b](https://github.com/certd/certd/commit/b565b4b3b919b71b98ea2517670bc1ef00e00dc9)) * 优化证书流水线创建,支持选择分组 ([d613aa8](https://github.com/certd/certd/commit/d613aa8f3e85d8dc475ef1b62d49394ce7fd7d24)) ## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17) ### Performance Improvements * 登录支持双重认证 ([48aef25](https://github.com/certd/certd/commit/48aef25b3f6499d674ca4e4ef16f4c62399fb735)) * 多重认证登录 ([0f82cf4](https://github.com/certd/certd/commit/0f82cf409bc60706ab07e4ca4f272b9a1ca7eecb)) * 优化部署到华为云CDN,支持先上传到ccm,再使用证书id部署,修复offline状态下导致部署报错的bug ([79df39a](https://github.com/certd/certd/commit/79df39acabab10ae7e1864dadcdc186bb007a3c5)) ## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15) ### Bug Fixes * 补充类型断言 ([2143dff](https://github.com/certd/certd/commit/2143dff2ae96e6a78bef9f0498e36f8cd9e6941f)) * 修复腾讯云部署到任意资源插件,无法使用之前已上传的腾讯云证书问题 ([32c714d](https://github.com/certd/certd/commit/32c714d1b6e68c71a74a7452115040c87ac4bfdc)) ### Performance Improvements * 插件支持导入导出 ([cf8abb4](https://github.com/certd/certd/commit/cf8abb45282070c8ba91469f93fd379fabf1f74a)) * 支持上传证书到华为云CCM ([cfd3b66](https://github.com/certd/certd/commit/cfd3b66be9ebf53a26693057e70ed60c3f116be9)) ## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14) ### Bug Fixes * 修复登录错误次数过多阻止再次登录逻辑 ([bf4d191](https://github.com/certd/certd/commit/bf4d191c8bd2f9209eb6768f662b9c77de99e998)) ## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12) ### Bug Fixes * 修复某些情况下无法输出日志的bug ([70101bf](https://github.com/certd/certd/commit/70101bfa7ade65678d9202c804bbae2cb808b594)) ### Performance Improvements * 修复内置插件分页查询逻辑 ([a2710dd](https://github.com/certd/certd/commit/a2710ddc2525e4e637fd157f0180e6d3b801c8be)) ## [1.33.1](https://github.com/certd/certd/compare/v1.33.0...v1.33.1) (2025-04-12) ### Bug Fixes * 修复阿里云cdn证书部署失败问题,增加certname参数传入 ([965dc2c](https://github.com/certd/certd/commit/965dc2cb476f690af716f291c6b20ba98be0c8f0)) * 修复ssh插件报length空指针的bug ([9c4cbe1](https://github.com/certd/certd/commit/9c4cbe17a22b548611cf1fbefecc83a421788e42)) ### Performance Improvements * 镜像支持armv7 ([f78cbed](https://github.com/certd/certd/commit/f78cbed4d817859721fdafe7d348864848d0dfbf)) # [1.33.0](https://github.com/certd/certd/compare/v1.32.0...v1.33.0) (2025-04-11) ### Bug Fixes * 升级mysql驱动,支持mysql8最新版本的认证 ([2f5ed3a](https://github.com/certd/certd/commit/2f5ed3aead97641f2c80d692a50226839016df0b)) * 修复eab授权,没有email绑定的bug ([2f1683b](https://github.com/certd/certd/commit/2f1683b26acebbfb7d6e2d751435be04a4e7cab4)) ### Features * 支持在线自定义插件,无需源码开发 ([d0d9d68](https://github.com/certd/certd/commit/d0d9d68fe6740f6ff49fe40b7c9917c5a2e4b442)) * **lego:** support set key type ([f3bf4fa](https://github.com/certd/certd/commit/f3bf4faee0be5bdbfdbcf70a502849ed4c8ed4c4)) * release image to ghcr ([9b536af](https://github.com/certd/certd/commit/9b536af9e656dc89e2a87078c129cad6f591e467)) ### Performance Improvements * 修复tab页缓存问题 ([64e5449](https://github.com/certd/certd/commit/64e5449ab3c6b219b0e89eddad14bfb6b71a0650)) * 隐藏运行策略选项 ([2951df0](https://github.com/certd/certd/commit/2951df0cd94c23e2efee84ff1b843055aac56cae)) * 增加手动上传证书功能说明 ([5d083a1](https://github.com/certd/certd/commit/5d083a153637caddbc6f44e915d9fb2d1ae87b33)) # [1.32.0](https://github.com/certd/certd/compare/v1.31.11...v1.32.0) (2025-04-04) ### Bug Fixes * 创建cname记录移除域名两端的空格 ([903a413](https://github.com/certd/certd/commit/903a4131ab5f42c8286cd2150ed1032d486fda2f)) * 修复从本地dns获取记录报错的bug ([c39b1bf](https://github.com/certd/certd/commit/c39b1bf823ddc6216bed2049e4c87e6107def08a)) ### Features * 优化证书申请速度,修复某些情况下letsencrypt 校验失败的问题 ([857589b](https://github.com/certd/certd/commit/857589b365c6f709e0ae67914d2f50ce182e6dd6)) ### Performance Improvements * 优化华为dns解析记录创建和删除问题 ([0948c5b](https://github.com/certd/certd/commit/0948c5bc691d2ee6eb47c72a85da1b7453361878)) * 又拍云支持云存储 ([9339b78](https://github.com/certd/certd/commit/9339b78f801d193472c0af25749e8e7a27ffb7af)) * 又拍云支持云存储 ([8449f85](https://github.com/certd/certd/commit/8449f8580da90c1f6b5d02d07c3236ebaf6cf161)) ## [1.31.11](https://github.com/certd/certd/compare/v1.31.10...v1.31.11) (2025-04-02) ### Bug Fixes * 修复ssh支持键盘事件登录 ([8145808](https://github.com/certd/certd/commit/8145808c4370364377b4ffe3ae88ff465b49f20b)) ### Performance Improvements * 支持部署到京东云cdn ([6f17c70](https://github.com/certd/certd/commit/6f17c700b84965baa01b40fe2abaa0a91bcbaffd)) * 支持京东云dns申请证书 ([04d79f9](https://github.com/certd/certd/commit/04d79f9117670be504960b018fd49ae3bf7c1c11)) ## [1.31.10](https://github.com/certd/certd/compare/v1.31.9...v1.31.10) (2025-03-29) ### Performance Improvements * tab增加图标显示 ([a03ae5a](https://github.com/certd/certd/commit/a03ae5a216a1df2c1d3da12ae18dcd0f089a92d3)) * 升级lego版本到4.22.2 ([4e15556](https://github.com/certd/certd/commit/4e15556e5e8100719497edb1729570d5a29668e1)) * 优化华为dns接口报错信息输出 ([bf30b7a](https://github.com/certd/certd/commit/bf30b7afaef623dd8126570344f1fcc2c06f1215)) ## [1.31.9](https://github.com/certd/certd/compare/v1.31.8...v1.31.9) (2025-03-28) ### Bug Fixes * 修复华为云dns接口请求出错的bug ([caa15b4](https://github.com/certd/certd/commit/caa15b47355363cbb8847f415ff12363cd53eeda)) * 修复某些情况下站点证书监控报undefined.includes的错误 ([0b6618f](https://github.com/certd/certd/commit/0b6618ff709322a0eeba78953c8c6e9d073d083a)) * 修复网站证书监控https port设置无效的bug ([cc8da0c](https://github.com/certd/certd/commit/cc8da0cf130f0c469371b59ac5bd04567f4a4414)) ### Performance Improvements * 站点监控保存时异步检查 ([993bc74](https://github.com/certd/certd/commit/993bc7432fce2d954e9897ed85b54f22150bfc7e)) * dns支持火山引擎 ([99ff879](https://github.com/certd/certd/commit/99ff879d93658c29ea493a4bde7e9e3f85996d64)) ## [1.31.8](https://github.com/certd/certd/compare/v1.31.7...v1.31.8) (2025-03-26) ### Bug Fixes * 修复编辑通知勾选默认,导致出现多个默认通知的bug ([6cd7bdd](https://github.com/certd/certd/commit/6cd7bddc37da8b0d7b9860fd9a26ddfe84c869a7)) * 修复网站监控无法设置端口的bug ([27a8a57](https://github.com/certd/certd/commit/27a8a57cf52b4bf83d628aa3049be1efaa74f29c)) * 修复lego模式无法创建流水线的bug ([687bb8a](https://github.com/certd/certd/commit/687bb8a2376d0de7b72739a174e4a9560581f866)) ### Performance Improvements * 优化通知格式 ([c3c5006](https://github.com/certd/certd/commit/c3c5006daa39c20624cb58864f2b92b230a38a7a)) * 优化scp上传 ([e51123a](https://github.com/certd/certd/commit/e51123a95131cc76d655937488caf08956a67020)) * 优化txt本地校验效率 ([fd507f2](https://github.com/certd/certd/commit/fd507f269253607e68c5c099c99e0de11636f229)) * 支持又拍云cdn ([fd0536b](https://github.com/certd/certd/commit/fd0536bd4b41f15b6b5d42e0b447f0dcbf73b8a8)) * 支持又拍云cdn ([57389a7](https://github.com/certd/certd/commit/57389a79a1a61c45d081712562f8b33c9633158e)) ## [1.31.7](https://github.com/certd/certd/compare/v1.31.6...v1.31.7) (2025-03-24) ### Performance Improvements * 增加服务器时间警告 ([d66ade4](https://github.com/certd/certd/commit/d66ade4e4783850b6c7625c6f164a5a0fc0aa509)) * 支持部署到lucky ([e18e399](https://github.com/certd/certd/commit/e18e399ce6529e8c7e36b56c5f674cfdbbd3d3d1)) ## [1.31.6](https://github.com/certd/certd/compare/v1.31.5...v1.31.6) (2025-03-24) ### Bug Fixes * 修复dns.la无法申请证书的bug ([90b045a](https://github.com/certd/certd/commit/90b045af6d1a4f46986e4b118885c1f050df067c)) ### Performance Improvements * 上传到主机支持scp方式 ([05b6159](https://github.com/certd/certd/commit/05b6159802b9e85b6a410361b60b5c28875b48e7)) * 优化图标 ([c56f48c](https://github.com/certd/certd/commit/c56f48c1e3c54c4e203fafb380d9091d75681b7e)) ## [1.31.5](https://github.com/certd/certd/compare/v1.31.4...v1.31.5) (2025-03-22) ### Bug Fixes * 修复通知选择器无法选择的bug ([f7b88f9](https://github.com/certd/certd/commit/f7b88f9e3b7d9d9122e4fd2003a20c555bd50c7d)) * 修复证书流水线创建失败的bug ([736fe03](https://github.com/certd/certd/commit/736fe038ebda56648bcc4c12884a700341d2c049)) ## [1.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21) ### Bug Fixes * 修复站点监控通知通过webhook发送失败的bug ([9be1ecc](https://github.com/certd/certd/commit/9be1ecc8aab3ea23dd0dc2dab3688f4edb90ef2c)) * 修复dns.la域名申请失败的bug ([1de8eee](https://github.com/certd/certd/commit/1de8eee6ea8307f3c11626af75303d3cc104bb95)) ### Performance Improvements * 宝塔支持doker站点证书部署 ([589a373](https://github.com/certd/certd/commit/589a373142ef7f50d64d3aa767a90b1f4b64da93)) * 保存调整后的列宽 ([873f2b6](https://github.com/certd/certd/commit/873f2b618b9d7320045baf69d6da83afe48a780f)) * 创建证书流水线时,支持更多参数展开 ([36aa7f8](https://github.com/certd/certd/commit/36aa7f82b078a053a102331b3c6f132fb9d492f9)) * 流水线页面可以鼠标按住左右拖动 ([d85a02f](https://github.com/certd/certd/commit/d85a02feeb3183c5abd6c1ea790d5923a32d7271)) * 流水线增加上传证书快捷方式 ([425bba6](https://github.com/certd/certd/commit/425bba67c539b734e2a85a83a4f9ecc9b2434fb4)) * 手动上传证书部署流水线 ([fbb66f3](https://github.com/certd/certd/commit/fbb66f3c4389489aa8a43b194d82bc8cf391607b)) * 优化选择任务时手机版展示效果 ([d01004d](https://github.com/certd/certd/commit/d01004d53071a75ac91ee21cc96bde9369f77ff3)) * 站点监控,手动测试也发通知 ([729b19c](https://github.com/certd/certd/commit/729b19c8da60d5efb5baef7cf8df0518e7f6b471)) * 站点证书监控支持模糊查询 ([0069c0e](https://github.com/certd/certd/commit/0069c0e3992946a8dd6410f299d4fc974ef0e76b)) * 支持飞书通知 ([b82e1dc](https://github.com/certd/certd/commit/b82e1dcd6217b09a7d7e21cd648bb31de320cadf)) * 支持手动上传证书并部署 ([a9fffa5](https://github.com/certd/certd/commit/a9fffa5180c83da27b35886aa2e858a92a2c5f94)) ## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13) ### Bug Fixes * 修复阿里云fc获取不到列表的bug ([474b337](https://github.com/certd/certd/commit/474b3372d8ce98e6d45900bf8046bc0b3f220686)) ### Performance Improvements * 1panel支持 apikey方式授权 ([170b2af](https://github.com/certd/certd/commit/170b2afb0e3b125e4ed057f633fe895b5ac3ac22)) * 套餐支持3天7天等选项 ([0d71a8e](https://github.com/certd/certd/commit/0d71a8ee501a0e5bb69decf07e8729026e9d85bf)) * 证书仓库增加有效期显示 ([be87124](https://github.com/certd/certd/commit/be87124ada7a093f281ca29a45c86b4ea4644ead)) * 支持部署到天翼云CDN ([82a72e0](https://github.com/certd/certd/commit/82a72e0b497efa043d342ad0e33c083a2de79a05)) * 支持dns.la ([ee8af18](https://github.com/certd/certd/commit/ee8af18d0ac0af82544d6dda1e4b4c678b733041)) * cf授权支持配置http代理 ([27386ea](https://github.com/certd/certd/commit/27386ea04d3c1a5aebe3cfdd7ac48185eaa76629)) ## [1.31.2](https://github.com/certd/certd/compare/v1.31.1...v1.31.2) (2025-03-12) ### Bug Fixes * 修复cname记录查找bug ([95fb4e3](https://github.com/certd/certd/commit/95fb4e3e8be6ca13cc43b451f6141d62190ba453)) ## [1.31.1](https://github.com/certd/certd/compare/v1.31.0...v1.31.1) (2025-03-11) ### Performance Improvements * 一些手机端适配优化 ([5b8d5dd](https://github.com/certd/certd/commit/5b8d5dd97536456a9d5d1384216eac1093b2dc3d)) # [1.31.0](https://github.com/certd/certd/compare/v1.30.6...v1.31.0) (2025-03-10) ### Bug Fixes * 修复CDN插件我爱云因更换接口导致部署失败的问题 ([5641c19](https://github.com/certd/certd/commit/5641c19502970f67af19709bddf8c781b1a25bdc)) * 修复CDN插件我爱云因更换接口导致部署失败的问题 ([0110dfd](https://github.com/certd/certd/commit/0110dfdb70b12dfb0a7a067717f3773ed75aae7c)) * 修复webhook headers value中带等号是解析错误的bug ([1fe3365](https://github.com/certd/certd/commit/1fe3365e10c464c4c60c82f424cf74fe35b883e0)) * ProxmoxUploadCert 增加强制部署证书 ([441b15e](https://github.com/certd/certd/commit/441b15ed2fe5a143a5bd5508613b3816ddbff596)) ### Performance Improvements * 历史记录查看详情,可以切换到对应的历史记录日志上去 ([082802e](https://github.com/certd/certd/commit/082802e1197156837800f814728ee0f6b300b18c)) * 流水线同一个阶段任务优化为并行执行 ([efa9c74](https://github.com/certd/certd/commit/efa9c748c5c07fc950af3db742ef9310f1ac9a4b)) * 升级midwayjs版本 ([057b0b4](https://github.com/certd/certd/commit/057b0b4565e19bb93195633f767b2942e8e40e59)) * 是否允许爬虫爬取增加ui设置选项 ([779db9d](https://github.com/certd/certd/commit/779db9da705d2dfef36fec21f52bd38af9fc5f2e)) * 通知支持钉钉群聊机器人 ([fc8bef5](https://github.com/certd/certd/commit/fc8bef5aae522d75d408d8c3aa74543269da5398)) * 易支付支持固定支付方式,适合没有收银台版本使用 ([81df96b](https://github.com/certd/certd/commit/81df96bf4542ce8d8ef4a428a4460dd554e4719a)) * 支持易盾RCDN部署 ([065713c](https://github.com/certd/certd/commit/065713cdb6953d16df08585c316c1a7a8eaec437)) ## [1.30.6](https://github.com/certd/certd/compare/v1.30.5...v1.30.6) (2025-02-24) ### Performance Improvements * 禁止爬虫爬取本网站 ([5164116](https://github.com/certd/certd/commit/5164116bde60dabac774cdf94f5317ff386e95ca)) * 上传到阿里云证书名称后缀增加毫秒时间戳 ([9f0ee21](https://github.com/certd/certd/commit/9f0ee219d02907ffe128a5cf10173397d934ccd7)) * 支持部署到阿里云FC3.0 ([bcaf54d](https://github.com/certd/certd/commit/bcaf54d4cb7bc469486aae6cdb127ae017eb3abb)) * 支持新版本LeCDN ([44d43f4](https://github.com/certd/certd/commit/44d43f45cb9094619df7494c2a64a51ba77ad116)) ## [1.30.5](https://github.com/certd/certd/compare/v1.30.4...v1.30.5) (2025-02-14) **Note:** Version bump only for package root ## [1.30.4](https://github.com/certd/certd/compare/v1.30.3...v1.30.4) (2025-02-14) ### Bug Fixes * 适配最新版1panel密码编码方式 ([78044c0](https://github.com/certd/certd/commit/78044c062e20cdd04f08baef9fb6745bf25eddcf)) ## [1.30.3](https://github.com/certd/certd/compare/v1.30.2...v1.30.3) (2025-02-13) ### Bug Fixes * 修复腾讯云CLB多域名同证书部署报错的bug ([c3a5542](https://github.com/certd/certd/commit/c3a55429357e78f4b78c9592d3e5897db2d4d549)) * 修复新版本1panel密码需要加密,无法登录的问题 ([ada0b71](https://github.com/certd/certd/commit/ada0b7106e97e551783829e4e719f76793a7123d)) ## [1.30.2](https://github.com/certd/certd/compare/v1.30.1...v1.30.2) (2025-02-09) ### Bug Fixes * 当前置任务被删除时进行校验 ([c89686a](https://github.com/certd/certd/commit/c89686a2fda251484930f0ae715417b618c21690)) * 修复cloudflare删除解析记录报错的bug ([00c2da4](https://github.com/certd/certd/commit/00c2da444f84adb89f3f1226d03294d7c6e3e4f1)) ### Performance Improvements * 上传自定义证书 ([75a38d9](https://github.com/certd/certd/commit/75a38d95f305b4271d9106babe7cffc1c89ae8f3)) ## [1.30.1](https://github.com/certd/certd/compare/v1.30.0...v1.30.1) (2025-01-20) ### Bug Fixes * 修复部署到阿里云ALB、NLB插件加载混乱的bug ([6ab83b6](https://github.com/certd/certd/commit/6ab83b662a2c5e715b9cb7eb1244de2ebb7f47b0)) * 修复腾讯clb重复执行会报错的bug ([e95d29f](https://github.com/certd/certd/commit/e95d29f446d06eced315a3087fc9e105a30b20bd)) * 修复tg消息内容中存在.和*就会发送失败的bug ([ae5dfc3](https://github.com/certd/certd/commit/ae5dfc3bee950267123ae2fbd1c11e7ce36626ea)) ### Performance Improvements * 创建流水线时,默认成功时也发送通知 ([52ae690](https://github.com/certd/certd/commit/52ae6902d203ca56e0312692b50c55cb6ddd3e39)) * http方式校验,选择sftp时,支持修改文件访问权限比如777 ([15d6eaf](https://github.com/certd/certd/commit/15d6eaf5532ed25acd4f8d58c429353a2f44206c)) # [1.30.0](https://github.com/certd/certd/compare/v1.29.5...v1.30.0) (2025-01-19) ### Bug Fixes * 修复查看任务日志偶发性无法自动滚动底部的bug ([7e482f7](https://github.com/certd/certd/commit/7e482f798c0142bce1866f84676cb40210f9638a)) * 修复namesilo ttl太短的问题 ([865f26d](https://github.com/certd/certd/commit/865f26d75c0d3dd4dc8b41448f8830068e45957c)) ### Features * 支持open api接口,根据域名获取证书 ([52a4fd3](https://github.com/certd/certd/commit/52a4fd33180e9b3f71b8dc9f7671d7cd8e448c3b)) ### Performance Improvements * 证书仓库 ([91e7f45](https://github.com/certd/certd/commit/91e7f45a1c5ea1e0ec0aa3236b80028f03a6d0aa)) * 支持部署到阿里云ALB ([653940a](https://github.com/certd/certd/commit/653940a0ca64fc380178c1b0b58ae0af64dfaf07)) * 支持部署到阿里云NLB、SLB ([c085bac](https://github.com/certd/certd/commit/c085bac5d877c4250a8a79e17eb8673b8e4fc89c)) * 支持部署到腾讯云直播 ([417d37b](https://github.com/certd/certd/commit/417d37b199b79a42f790f9edab8f178eedf8fbf7)) * 支持部署证书到proxmox ([d10795e](https://github.com/certd/certd/commit/d10795ecd97eb8cf2ffa46aabfdbfc6812636396)) ## [1.29.5](https://github.com/certd/certd/compare/v1.29.4...v1.29.5) (2025-01-07) ### Bug Fixes * 修复复制到本机插件,pfx格式复制时报错的bug ([f57116d](https://github.com/certd/certd/commit/f57116d2bebf33e47ad93e0b39c4efe8e4aea25c)) * 修复授权管理,点击了查看原文按钮后,无法修改值的bug ([85c99f7](https://github.com/certd/certd/commit/85c99f7f80761ac6efaf3255c03b933442db1686)) ## [1.29.4](https://github.com/certd/certd/compare/v1.29.3...v1.29.4) (2025-01-06) ### Bug Fixes * 修复站点监控域名校验无法通过的bug ([1cb4a53](https://github.com/certd/certd/commit/1cb4a539cc523721ffd4b22d40d0e3d2d68cd915)) ### Performance Improvements * 优化腾讯云CLB插件,支持非sni情况,sni情况支持填写多个域名 ([635b042](https://github.com/certd/certd/commit/635b042690637bff85e97e07c7aac4b87a8a124b)) ## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04) ### Bug Fixes * 修复系统级授权无法查看密钥的bug ([8644348](https://github.com/certd/certd/commit/8644348fc41ae2e1672f946ca37e5d3a674e0218)) ### Performance Improvements * 优化站点证书检查页面,检查增加3次重试 ([e6dd7cd](https://github.com/certd/certd/commit/e6dd7cd54a3e23897031b5df6e0c3cdc0545d35a)) * 优化acme sdk ([54db744](https://github.com/certd/certd/commit/54db74428259de64d12230c2ab7353ae11197bbc)) * 支持http校验方式申请证书 ([405591c](https://github.com/certd/certd/commit/405591c5d08fa1a3b228ee3980199e7731cfec4a)) * http校验方式,支持七牛云oss、阿里云oss、腾讯云cos ([3f74d4d](https://github.com/certd/certd/commit/3f74d4d9e5f5d0e629b44cff1895b3f7a8fbcafc)) ## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25) ### Bug Fixes * 修复套餐关闭状态下,仍然限制用户流水线数量的bug ([66fb9e5](https://github.com/certd/certd/commit/66fb9e5f49491f9c159363b48af14720a37673b1)) ## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25) ### Bug Fixes * 免费套餐支持购买 ([f5ec987](https://github.com/certd/certd/commit/f5ec9870fd6af1f0c9099852bbdb4d07813ccce8)) * 修复某处金额转换丢失精度的bug ([d2d6f12](https://github.com/certd/certd/commit/d2d6f12218cbe7bd55f4ae082b93084be85f0a7b)) * 修复新版本小红点显示错误问题 ([fe4786e](https://github.com/certd/certd/commit/fe4786e168afe03a5243dd67971476c348339809)) ### Performance Improvements * 用户创建证书流水线没有购买套餐或者超限时提前报错 ([472f06c](https://github.com/certd/certd/commit/472f06c2d190d0ae48e8b53c18bc278437656a1c)) * 优化插件名称显示 ([26adf7d](https://github.com/certd/certd/commit/26adf7d437e674385f26a8f92fded6521a620671)) # [1.29.0](https://github.com/certd/certd/compare/v1.28.4...v1.29.0) (2024-12-24) ### Bug Fixes * 修复手机模式下,查询框被文字遮盖的bug ([040788c](https://github.com/certd/certd/commit/040788c793642c3bb2a3ede87fe30fcf3be471bd)) * 修复左侧菜单收起时无法展开子菜单的bug ([0056223](https://github.com/certd/certd/commit/005622307e612717a5408aa1484717ef03003a22)) ### Features * 基础版不再限制流水线数量 ([cb27d4b](https://github.com/certd/certd/commit/cb27d4b4906b2782eaceb0a95bbdc5d0534370d2)) * 套餐购买支持易支付、支付宝支付 ([faa28f8](https://github.com/certd/certd/commit/faa28f88f954cba4c1dd29125562e5acd2fd99af)) * 用户套餐,用户支付功能 ([a019956](https://github.com/certd/certd/commit/a019956698acaf2c4beb620b5ad8c18918ead6a1)) * 站点证书监控 ([9c8c7a7](https://github.com/certd/certd/commit/9c8c7a781223f4217f45510db1e89495600e3cd5)) * 支持微信支付 ([45d6347](https://github.com/certd/certd/commit/45d6347f5b6199493b11aabdd74177f6dca2cea4)) ### Performance Improvements * 调整创建证书表单字段的顺序 ([d393521](https://github.com/certd/certd/commit/d3935219f2aa50d6662c5b5ebf7ee25ad696ab2b)) * 同一时间只允许一个套餐生效 ([8ebf95a](https://github.com/certd/certd/commit/8ebf95a222a900d1707716c7b1f3b39f8a6d8f94)) * 用户名支持修改 ([89c7f07](https://github.com/certd/certd/commit/89c7f070343e86453c84677ebe1669f9b266d871)) * 优化证书申请跳过的状态显示,成功通知现在在跳过时不会发送 ([67d762b](https://github.com/certd/certd/commit/67d762b6a520f1fa24719a124e5ae975a81f5f82)) * 站点证书监控通知发送,每天定时检查 ([bb4910f](https://github.com/certd/certd/commit/bb4910f4e57234e42b44505f4620ae7af66025c5)) * 支持一体证书 ([53c38cf](https://github.com/certd/certd/commit/53c38cf714a6f7486abbf1d71c9f48f56a790100)) * 支持plesk网站证书部署 ([eda45c1](https://github.com/certd/certd/commit/eda45c1528199648b3970505e87f492d398226cd)) ## [1.28.4](https://github.com/certd/certd/compare/v1.28.3...v1.28.4) (2024-12-12) ### Bug Fixes * 修复证书成功通知发送失败的bug ([0f5c690](https://github.com/certd/certd/commit/0f5c69040ba77340c909813220a26bc7ddada3ea)) ### Performance Improvements * 群晖支持6.x ([79f7ec4](https://github.com/certd/certd/commit/79f7ec4672f4fd5744cc45e4a6f104da943f4026)) ## [1.28.3](https://github.com/certd/certd/compare/v1.28.2...v1.28.3) (2024-12-12) ### Bug Fixes * 修复没有配置eab时,报order无法读取的问题 ([657a2ae](https://github.com/certd/certd/commit/657a2ae032e6f61ac27fbdd26c7bf169c041219e)) * 修复授权被删除后,无法清空的bug ([b45977c](https://github.com/certd/certd/commit/b45977c29a29084c11e496bec3415eaaebafdd74)) * mysql下access.setting字段改成text ([b7f5740](https://github.com/certd/certd/commit/b7f5740c57743914f754f3b4fdd94b59a2e8338c)) ### Performance Improvements * 点击版本红点按钮,跳转到升级帮助页面 ([454fbda](https://github.com/certd/certd/commit/454fbda581bbe22abca5b91e5086ea9d9d58a020)) * 通知标题优化 ([ff083ce](https://github.com/certd/certd/commit/ff083ce6848a8bee3c8248e4b881086ae1517c28)) * 支持腾讯虚拟机开关机([@wujingke](https://github.com/wujingke)) ([8039e8b](https://github.com/certd/certd/commit/8039e8baf83c82d03f1a6198cf61c372026b962b)) * 支持aws cloudfront ([0ae39f1](https://github.com/certd/certd/commit/0ae39f160a7c6b6696b3bf513d68aa28905810ad)) ## [1.28.2](https://github.com/certd/certd/compare/v1.28.1...v1.28.2) (2024-12-09) ### Bug Fixes * 修复创建流水线通知设置无效的bug ([498cf34](https://github.com/certd/certd/commit/498cf34999fddfa24ce088e2e678469fa669abb8)) * 修复流水线分组可以被所有人看见的bug ([a0e838d](https://github.com/certd/certd/commit/a0e838d1eec918e5dc92fe95dc72ac14facb930e)) ### Performance Improvements * 优化数据表索引 ([228fdf0](https://github.com/certd/certd/commit/228fdf0a0d28013f5dd156a97bbde80537e8e97e)) * 支持mysql ([7cde1fd](https://github.com/certd/certd/commit/7cde1fdc4a9ed851900d231a5460c8dbfbcd148e)) ## [1.28.1](https://github.com/certd/certd/compare/v1.28.0...v1.28.1) (2024-12-08) ### Bug Fixes * 修复cname排查方法 nslookup命令显示黑色的问题 ([3dfeeec](https://github.com/certd/certd/commit/3dfeeec899d7d0d7292695ce410f78548e076c03)) ### Performance Improvements * 通知选择器优化 ([2c0cbdd](https://github.com/certd/certd/commit/2c0cbdd29ecb74cc939b2ae7ee86b8d40f70ba31)) * 新增七牛云插件分组 ([49e7dc5](https://github.com/certd/certd/commit/49e7dc56e1a95fbdea3e30cdeb945b48415b69e3)) * 新增server酱3通知 ([6aa4872](https://github.com/certd/certd/commit/6aa487269c9f6862e188b37a0d6c73f79c937d94)) * 支持邀请奖励 ([618ec93](https://github.com/certd/certd/commit/618ec937866b24ebcf8164db43acb1ed66a5b329)) * 支持易发云短信 ([94fa77f](https://github.com/certd/certd/commit/94fa77fcd2b9bea294fb05736c0d8cdc81f56103)) * cname value优化 ([e8c9c2a](https://github.com/certd/certd/commit/e8c9c2a47d47048ae743b16f7bc932dbe18a89e9)) * favicon支持自定义 ([8b9c47d](https://github.com/certd/certd/commit/8b9c47daf194515006689a212ae9cf586bdf5993)) # [1.28.0](https://github.com/certd/certd/compare/v1.27.9...v1.28.0) (2024-11-30) ### Bug Fixes * 修复自定义webhook contextType的bug ([7e5ea0c](https://github.com/certd/certd/commit/7e5ea0cee003acda952d922ca70592f1e8a2ed80)) ### Features * 手机号登录、邮箱验证码注册 ([7b55337](https://github.com/certd/certd/commit/7b55337c5edb470cca7aa62201eda8d274784004)) ### Performance Improvements * 部署到IIS插件 ([1534f45](https://github.com/certd/certd/commit/1534f4523633265d219d7b3a249a9ea1af99c512)) * 登录失败增加重试次数限制及冷却时间 ([954b6df](https://github.com/certd/certd/commit/954b6df3608695fe074130f8149a33e311d80cc4)) * 流水线支持批量修改分组,批量删除 ([a847e66](https://github.com/certd/certd/commit/a847e66c4fc843b98f1520b2b8072d3586ce8b81)) * 取消docker-compose的dns配置 ([87bbf6f](https://github.com/certd/certd/commit/87bbf6f14080b9fa287c250d7fc4d33279c83ff7)) * 首页新增修改密码提示 ([0772d3b](https://github.com/certd/certd/commit/0772d3b3fd24afdde4086d9f09ef19d037b431b4)) * 选项显示图标 ([aedc462](https://github.com/certd/certd/commit/aedc46213571a3bd93809b7af7fa17a08d546237)) * 优化七牛云cdn,获取域名列表可以选择 ([5a20242](https://github.com/certd/certd/commit/5a20242111d6bd255b25dac86fe1f062c8543096)) * 优化七牛云cdn部署,保持http2和forceHttp设置,当未开启https时,主动开启https ([196f7d9](https://github.com/certd/certd/commit/196f7d9dc23d7dd96b663c686542e85270b81aef)) * 优化证书申请成功通知发送方式 ([8002a56](https://github.com/certd/certd/commit/8002a56efc5998aa03db5711ae87f9eb4bc9e160)) * 支持短信验证码登录 ([387bcc5](https://github.com/certd/certd/commit/387bcc5fa418cdeea81a06da5e3f8cd6b43cd082)) * 支持威联通证书部署 ([0d8913e](https://github.com/certd/certd/commit/0d8913ea2f56fdebbcc9bb207eae59e8ddbb8cad)) * 自定义webhook显示详细的错误信息 ([3254afc](https://github.com/certd/certd/commit/3254afc75640eed3729d0fc02a818fefbe5c7fc3)) ## [1.27.9](https://github.com/certd/certd/compare/v1.27.8...v1.27.9) (2024-11-26) ### Performance Improvements * 通知支持自定义webhook、anpush、iyuu、server酱 ([cbccd9e](https://github.com/certd/certd/commit/cbccd9e3d0a4c24aba772af62734666d40b22c57)) * 通知支持vocechat、bark、telegram、discord、slack ([642f57f](https://github.com/certd/certd/commit/642f57ff6d7152a9e14f59c7fc0e32a6b1751fb7)) ## [1.27.8](https://github.com/certd/certd/compare/v1.27.7...v1.27.8) (2024-11-25) **Note:** Version bump only for package root ## [1.27.7](https://github.com/certd/certd/compare/v1.27.6...v1.27.7) (2024-11-25) ### Bug Fixes * 修复关键字查询bug ([fab6660](https://github.com/certd/certd/commit/fab66606b35a540fac31fee902331ba1ffdebc16)) * 修复CNAME时子域名级数超出限制的问题 ([3af6d96](https://github.com/certd/certd/commit/3af6d96e6e353c9b2111cff81679b79c55195a0a)) ### Performance Improvements * 谷歌EAB绑定邮箱改成必填 ([81a8123](https://github.com/certd/certd/commit/81a8123725d7bf4bd6a32a64a066bd760b7b6a7f)) * 华为云密钥获取提示及访问链接 ([de43391](https://github.com/certd/certd/commit/de43391e4c12dc3ad976f8fa8787f4eb70a41e75)) * 通知管理 ([d9a00ee](https://github.com/certd/certd/commit/d9a00eeaf72735ced67c59d7983d84e3c730064a)) * 通知渠道支持测试按钮 ([b54ae27](https://github.com/certd/certd/commit/b54ae272ebc2d31b32b049d44e2299a6be7f153c)) * 优化插件开发,dnsProvider无需写http logger 变量 ([fcbb5e4](https://github.com/certd/certd/commit/fcbb5e46a112174150a62648319b8224fce3b7ed)) * 支持部署到阿里云WAF ([c96fcb7](https://github.com/certd/certd/commit/c96fcb7afced979435cffa73591275008033c90d)) * 支持企业微信群聊机器人通知 ([b805a29](https://github.com/certd/certd/commit/b805a2925984144a31575b8aaa622f0c30d41b56)) ## [1.27.6](https://github.com/certd/certd/compare/v1.27.5...v1.27.6) (2024-11-19) ### Bug Fixes * .env 读取 \r 问题 ([0e33dfa](https://github.com/certd/certd/commit/0e33dfa019a55ea76193c428ec756af386adeb9d)) * 修复vip试用secret报错的bug ([018dee6](https://github.com/certd/certd/commit/018dee6c383233560f078dfd30f6c2857a7e15ee)) ### Performance Improvements * 当步骤全部都禁用时,任务本身显示删除线 ([9ab9a6e](https://github.com/certd/certd/commit/9ab9a6e8b083e19793894f23e59f29c604ec98e5)) ## [1.27.5](https://github.com/certd/certd/compare/v1.27.4...v1.27.5) (2024-11-18) ### Bug Fixes * 修复1Panel面板本身证书更新导致判定执行失败的问题 ([2689e6d](https://github.com/certd/certd/commit/2689e6d6c03aba21da90d5d45232c6ba08696be1)) * 修复角色无法删除的bug ([66629a5](https://github.com/certd/certd/commit/66629a591aecc2d8364ea415c7afc3f9d0406562)) * 修复Cname情况下,无法使用DNS类型的bug ([26dad39](https://github.com/certd/certd/commit/26dad399d5768b3205da099ddc11809aef7d6224)) ### Performance Improvements * 日志查看自动滚动到底部 ([4a2f7eb](https://github.com/certd/certd/commit/4a2f7ebf87b7c027cebff7cb763f8f35f6d2aa36)) * 系统设置中的代理设置优化为可全局生效,环境变量中的https_proxy设置将无效 ([381a37f](https://github.com/certd/certd/commit/381a37fbaa6b61c887eda743897ae00afb825bdf)) * 新手导航在非编辑模式下不显示 ([18bfcc2](https://github.com/certd/certd/commit/18bfcc24ad0bde57bb04db8a4209861ec6b8ff1d)) * 优化腾讯云 cloudflare 重复解析记录时的返回值 ([90d1b68](https://github.com/certd/certd/commit/90d1b68bd6cf232fbe085234efe07d29b7690044)) * 支持namesilo ([80159ec](https://github.com/certd/certd/commit/80159ecca895103d0495f3217311199e66056572)) * 专业版试用,无需绑定账号 ([c7c4318](https://github.com/certd/certd/commit/c7c4318c11b65a76089787aa58939832d338a232)) ## [1.27.4](https://github.com/certd/certd/compare/v1.27.3...v1.27.4) (2024-11-14) ### Bug Fixes * 修复未设置pfx密码,导致jks转换报错的bug ([c3cfbd8](https://github.com/certd/certd/commit/c3cfbd8474155aed4379f91075de37d5d8c73ef0)) ### Performance Improvements * 公共cname服务支持关闭 ([f4ae512](https://github.com/certd/certd/commit/f4ae5125dc4cd97816976779cb3586b5ee78947e)) ## [1.27.3](https://github.com/certd/certd/compare/v1.27.2...v1.27.3) (2024-11-13) ### Bug Fixes * 修复偶发性cname一直验证超时的bug ([d2ce72e](https://github.com/certd/certd/commit/d2ce72e4aaacdf726ba8b91fcd71db40a27714ba)) * 修复邮件配置,忽略证书校验设置不生效的bug ([66a9690](https://github.com/certd/certd/commit/66a9690dc958732e1b3c672d965db502296446f9)) * 修复ipv6未开启情况下,请求带有ipv6地址域名报ETIMEDOUT的bug ([a9a0967](https://github.com/certd/certd/commit/a9a0967a6f1d0bd27e69f3ec52c31d90d470bc23)) ### Performance Improvements * 修复站点个性化,浏览器标题没有生效的bug ([bcfac02](https://github.com/certd/certd/commit/bcfac02c96ceaf23d1a0b05b48d8047da933beaf)) * 优化上传到主机插 路径选择,根据证书格式显示 ([8c3f86c](https://github.com/certd/certd/commit/8c3f86c6909ed91f48bb2880e78834e22f6f6a29)) * 支持jks ([889eaae](https://github.com/certd/certd/commit/889eaaea92818f628b922dae540c026630611707)) * ipv6支持 ([da6ac16](https://github.com/certd/certd/commit/da6ac1626b3574be2fabeeb18a1f10d60bdcbe49)) ## [1.27.2](https://github.com/certd/certd/compare/v1.27.1...v1.27.2) (2024-11-08) ### Bug Fixes * 修复某些容器管理ui无法识别端口列表的bug ([576e60a](https://github.com/certd/certd/commit/576e60a2b52315909e659d2a58cf98b130e69e6f)) * 修复删除腾讯云过期证书时间判断上的bug,导致已过期仍然没有删除证书 ([1ba1007](https://github.com/certd/certd/commit/1ba10072615015d91b81fc56a3b01dae6a2ae9d1)) ### Performance Improvements * 优化部署到阿里云CDN插件,支持多域名,更易用 ([80c500f](https://github.com/certd/certd/commit/80c500f618b169a1f64c57fe442242a4d0d9d833)) * 优化流水线页面切换回来不丢失查询条件 ([4dcf6e8](https://github.com/certd/certd/commit/4dcf6e87bc5f7657ce8a56c5331e8723a0fee8ee)) * 支持公共cname服务 ([3c919ee](https://github.com/certd/certd/commit/3c919ee5d1aef5d26cf3620a7c49d920786bc941)) * 执行历史支持点击查看流水线详情 ([8968639](https://github.com/certd/certd/commit/89686399f90058835435b92872fc236fac990148)) * 专业版7天试用 ([c58250e](https://github.com/certd/certd/commit/c58250e1f065a9bd8b4e82acc1df754504c0010c)) ## [1.27.1](https://github.com/certd/certd/compare/v1.27.0...v1.27.1) (2024-11-04) ### Bug Fixes * 修复头像没有更新的bug ([9b4a31f](https://github.com/certd/certd/commit/9b4a31fa6a32b9cab2e22bd141cf96ca29120445)) ### Performance Improvements * 禁止页面缓存,点击tab页签可以刷新数据 ([7ad4b55](https://github.com/certd/certd/commit/7ad4b55ee000c1dd0747832b11107f32b0ffb889)) * 优化时间选择器,自动填写分钟和秒钟 ([396dc34](https://github.com/certd/certd/commit/396dc34a841c7d016b033736afdba8366fb2d211)) * cname 域名映射记录可读性优化 ([b1117ed](https://github.com/certd/certd/commit/b1117ed54a3ef015752999324ff72b821ef5e4b9)) # [1.27.0](https://github.com/certd/certd/compare/v1.26.16...v1.27.0) (2024-10-31) ### Bug Fixes * 修复历史记录不能按名称查询的bug ([6113c38](https://github.com/certd/certd/commit/6113c388b7fc58b11ca19ff05cc1286d096c8d28)) * pfx兼容windows server 2016 ([e5e468a](https://github.com/certd/certd/commit/e5e468a463f66d02f235de54b7c1e09ace5f1cb1)) ### Features * 首页全新改版 ([63ec5b5](https://github.com/certd/certd/commit/63ec5b5519c760a3330569c0da6dac157302a330)) ### Performance Improvements * 管理控制台数据统计 ([babd589](https://github.com/certd/certd/commit/babd5897ae013ff7c04ebfcbfac8a00d84dd627c)) * 增加向导 ([6d9ef26](https://github.com/certd/certd/commit/6d9ef26ecab71d752c2c55d75aed4fb5f6c05a39)) * lego 升级到 4.19.2 ([129bf53](https://github.com/certd/certd/commit/129bf53edc9bbb001fe49fbd7e239bd1d09cc128)) ## [1.26.16](https://github.com/certd/certd/compare/v1.26.15...v1.26.16) (2024-10-30) ### Bug Fixes * 修复lego No help topic for 错误 ([aaaf8d7](https://github.com/certd/certd/commit/aaaf8d7db34896cf8f2ff8f12eec1ab0cae58f0f)) ### Performance Improvements * 支持白山云cdn部署 ([b1b2cd0](https://github.com/certd/certd/commit/b1b2cd088b684eda764962abd61754c26a204d1c)) * 支持华为云cdn ([81a3fdb](https://github.com/certd/certd/commit/81a3fdbc29b71f380762008cc151493ec97458f9)) ## [1.26.15](https://github.com/certd/certd/compare/v1.26.14...v1.26.15) (2024-10-28) ### Bug Fixes * 顶部菜单变...的bug ([6dabad7](https://github.com/certd/certd/commit/6dabad76baba96be0f8af36a3fbfb9f5182aecf1)) ### Performance Improvements * 默认证书更新时间设置为35天,增加腾讯云删除过期证书插件,可以避免腾讯云过期证书邮件 ([51b6fed](https://github.com/certd/certd/commit/51b6fed468eaa6f28ce4497ce303ace1a52abb96)) * 授权加密支持解密查看 ([5575c83](https://github.com/certd/certd/commit/5575c839705f6987ad2bdcd33256b0962c6a9c6a)) * 重置管理员密码同时启用管理员账户,避免之前禁用了,重置密码还是登录不进去 ([f92d918](https://github.com/certd/certd/commit/f92d918a1e28e29b794ad4754661ea760c18af46)) ## [1.26.14](https://github.com/certd/certd/compare/v1.26.13...v1.26.14) (2024-10-26) ### Bug Fixes * 修复阿里云部署大杀器报插件_还未注册错误的bug ([abd2dcf](https://github.com/certd/certd/commit/abd2dcf2e85a545321bae6451406d081f773b132)) * 修复启动时自签证书无法保存的bug ([526c484](https://github.com/certd/certd/commit/526c48450bcd37b3ccded9b448f17de8140bdc6e)) ### Performance Improvements * 顶部菜单自定义 ([54d136c](https://github.com/certd/certd/commit/54d136cc6ae122f7c891b7a5c7232fe5de8e5cb5)) * 禁用readonly用户 ([d10d42e](https://github.com/certd/certd/commit/d10d42e20619bb55a50d636b8867ff33db4e3b4b)) * 限制其他用户流水线数量 ([315e437](https://github.com/certd/certd/commit/315e43746baf01682737f82e41579237a48409af)) * 用户管理优化头像上传 ([661293c](https://github.com/certd/certd/commit/661293c189a3abf3cdc953b5225192372f57930d)) ## [1.26.13](https://github.com/certd/certd/compare/v1.26.12...v1.26.13) (2024-10-26) ### Bug Fixes * 修复对话框全屏按钮与关闭按钮重叠的bug ([95df56c](https://github.com/certd/certd/commit/95df56cc5ca5e3eb843cd17cb7078cde47729f1e)) * deprecated的运行时不要报错,只报警告 ([bcbefaa](https://github.com/certd/certd/commit/bcbefaaa35cf6d0eec085b3a2c5bfc7c6a8de9e1)) ### Performance Improvements * 更新certd本身的证书文档说明 ([0c50ede](https://github.com/certd/certd/commit/0c50ede129337b82df54575cbd2f4c2a783a0732)) * 支持同时监听https端口,7002 ([d5a17f9](https://github.com/certd/certd/commit/d5a17f9e6afd63fda2df0981118480f25a1fac2e)) ## [1.26.12](https://github.com/certd/certd/compare/v1.26.11...v1.26.12) (2024-10-25) ### Performance Improvements * 部署到阿里云任意云资源,阿里云部署大杀器 ([4075be7](https://github.com/certd/certd/commit/4075be7849b140acb92bd8da8a9acbf4eef85180)) * 文件名特殊字符限制输入 ([c4164c6](https://github.com/certd/certd/commit/c4164c66e29f3ec799f98108a344806ca61e94ff)) * 新增部署到百度云CDN插件 ([f126f9f](https://github.com/certd/certd/commit/f126f9f932d37fa01fff1accc7bdd17d349f8db5)) * 新增部署到腾讯云CDN-v2,推荐使用 ([d782655](https://github.com/certd/certd/commit/d782655cb4dfbb74138178afbffeee76fc755115)) * 优化cron选择器,增加下次触发时间显示 ([5b148b7](https://github.com/certd/certd/commit/5b148b7ed960ca6f7f5b733b2eadd56eeecbd4c2)) * 支持部署到腾讯云COS ([a8a45d7](https://github.com/certd/certd/commit/a8a45d7f757820990e278533277a3deda5ba48f3)) * 支持配置公共ZeroSSL授权 ([a90d1e6](https://github.com/certd/certd/commit/a90d1e68ee9cbc3705223457b8a86f071b150968)) ## [1.26.11](https://github.com/certd/certd/compare/v1.26.10...v1.26.11) (2024-10-23) ### Bug Fixes * 申请证书没有使用到系统设置的http代理的bug ([3db216f](https://github.com/certd/certd/commit/3db216f515ba404cb4330fdab452971b22a50f08)) * 修复移动任务后出现空阶段的bug ([4ea3edd](https://github.com/certd/certd/commit/4ea3edd59e93ca4f5b2e43b20dd4ef33909caddb)) * 修复google证书*.xx.com与xx.com同时申请时报错的bug ([f8b99b8](https://github.com/certd/certd/commit/f8b99b81a23e7e9fd5e05ebd5caf355c41d67a90)) * 允许七牛云cdn插件输入.号开头的通配符域名 ([18ee87d](https://github.com/certd/certd/commit/18ee87daff6eafc2201b58e28d85aafd3cb7a5b9)) ### Performance Improvements * 申请证书启用新的反代地址 ([a705182](https://github.com/certd/certd/commit/a705182b85e51157883e48f23463263793bf3c12)) * 优化日志颜色 ([1291e98](https://github.com/certd/certd/commit/1291e98e821c5b1810aab7f0aebe3f5f5cd44a20)) * 优化证书申请速度和成功率,反代地址优化,google基本可以稳定请求。增加请求重试。 ([41d9c3a](https://github.com/certd/certd/commit/41d9c3ac8398def541e65351cbe920d4a927182d)) * 优化pfx密码密码输入框,让浏览器不自动填写密码 ([ffeede3](https://github.com/certd/certd/commit/ffeede38afa70c5ff6f2015516bead23d2c4df87)) ## [1.26.10](https://github.com/certd/certd/compare/v1.26.9...v1.26.10) (2024-10-20) ### Bug Fixes * 修复cname服务普通用户access访问权限问题 ([c1e3e2e](https://github.com/certd/certd/commit/c1e3e2ee1f923ee5806479dd5f178c3286a01ae0)) ## [1.26.9](https://github.com/certd/certd/compare/v1.26.8...v1.26.9) (2024-10-19) ### Bug Fixes * 修复普通用户无法校验cname配置的bug ([6285497](https://github.com/certd/certd/commit/62854978bf0bdbe749b42f8e40ab227ab31ec92f)) * 修复切换普通用户登录时,左侧菜单没有同步更新的bug ([12116a8](https://github.com/certd/certd/commit/12116a89f43cf8b98f16d2ea6073f6b72a643215)) * 修正邮箱设置跳转路由 ([17d8890](https://github.com/certd/certd/commit/17d88900a1f0e3af609b74597f5b1978230db32d)) ### Performance Improvements * 触发证书重新申请input变化对比规则优化,减少升级版本后触发申请证书的情况 ([c46a2a9](https://github.com/certd/certd/commit/c46a2a9a399c2a9a8bb59a48b9fb6e93227cce9b)) * 任务下所有步骤都跳过时,整个任务显示跳过 ([84fd3b2](https://github.com/certd/certd/commit/84fd3b250dd1161ea06c5582fdadece4b29c2e53)) * 授权配置去除前后空格 ([57d8d48](https://github.com/certd/certd/commit/57d8d48046fbf51c52b041d2dec03d51fb018587)) * 数据库备份插件,先压缩再备份 ([304ef49](https://github.com/certd/certd/commit/304ef494fd5787c996ad0dcb6edd2f517afce9e2)) * 优化菜单 ([1f4f157](https://github.com/certd/certd/commit/1f4f15757de1015cf7563f7022599eef58cc93d7)) * 增加文档站 https://certd.docmirror.cn ([6e2ac1c](https://github.com/certd/certd/commit/6e2ac1c089f6ddccb396f1f2738509c05333e1bb)) ## [1.26.8](https://github.com/certd/certd/compare/v1.26.7...v1.26.8) (2024-10-15) ### Bug Fixes * 修复无法设置角色的bug ([02fe704](https://github.com/certd/certd/commit/02fe704769edb25fea5ffd85a51a5530864b37b3)) ### Performance Improvements * 角色删除安全 ([28bb485](https://github.com/certd/certd/commit/28bb4856bee03569153f6471527c9b9f28cb3d14)) * 密钥备份 ([1c6028a](https://github.com/certd/certd/commit/1c6028abcf8849163462bb2f8441b6838357e09b)) * 证书直接查看 ([5dde5bd](https://github.com/certd/certd/commit/5dde5bd3f76db3959d411619d29bfb8064e3b307)) * sqlite数据库备份插件 ([77f1631](https://github.com/certd/certd/commit/77f163144f7dcfb0431475c55508fecfd6d969f8)) ## [1.26.7](https://github.com/certd/certd/compare/v1.26.6...v1.26.7) (2024-10-14) ### Bug Fixes * 修复siteInfo每次都要重新设置的bug ([36b26ae](https://github.com/certd/certd/commit/36b26ae9f5c7a53c1c2546fb79b2ea451b854abf)) ## [1.26.6](https://github.com/certd/certd/compare/v1.26.5...v1.26.6) (2024-10-14) ### Bug Fixes * 修复排序失效的bug ([1f0742e](https://github.com/certd/certd/commit/1f0742ef9f0caae0c7e713acf0fd3cebf5d63875)) ## [1.26.5](https://github.com/certd/certd/compare/v1.26.4...v1.26.5) (2024-10-14) ### Bug Fixes * 修复版本号获取错误的bug ([8851870](https://github.com/certd/certd/commit/8851870400df86e496198ad509061b8989fcc44f)) ## [1.26.4](https://github.com/certd/certd/compare/v1.26.3...v1.26.4) (2024-10-14) ### Performance Improvements * [comm] 支持插件管理 ([e8b617b](https://github.com/certd/certd/commit/e8b617b80ce882dd63006f0cfc719a80a1cc6acc)) * 新增代理设置功能 ([273ab61](https://github.com/certd/certd/commit/273ab6139f5807f4d7fe865cc353b97f51b9a668)) * EAB授权支持绑定邮箱,支持公共EAB设置 ([07043af](https://github.com/certd/certd/commit/07043aff0ca7fd29c56dd3c363002cb15d78b464)) ## [1.26.3](https://github.com/certd/certd/compare/v1.26.2...v1.26.3) (2024-10-12) ### Performance Improvements * 优化系统设置加载时机 ([7396253](https://github.com/certd/certd/commit/73962536d5a4769902d760d005f3f879465addcc)) ## [1.26.2](https://github.com/certd/certd/compare/v1.26.1...v1.26.2) (2024-10-11) ### Bug Fixes * 修复某些情况下bindUrl失败的bug ([91fc1cd](https://github.com/certd/certd/commit/91fc1cd7353be4a22be951239ed70b38baebc74e)) ### Performance Improvements * 邮箱设置改为系统设置,普通用户无需配置发件邮箱 ([4244569](https://github.com/certd/certd/commit/42445692117184a3293e63bef84a74cbb5984b0e)) ## [1.26.1](https://github.com/certd/certd/compare/v1.26.0...v1.26.1) (2024-10-10) **Note:** Version bump only for package root # [1.26.0](https://github.com/certd/certd/compare/v1.25.9...v1.26.0) (2024-10-10) ### Bug Fixes * 修复管理员编辑其他用户流水线任务时归属userid也被修改的bug ([e85c477](https://github.com/certd/certd/commit/e85c47744cf740b4af3b93dca7c2f0ccc818ec2f)) * 修复历史记录根据流水线名称查询报错的bug ([ce9a986](https://github.com/certd/certd/commit/ce9a9862f122fce2186e7727eaa4b251b59e6032)) * 修复某些代理情况下 报 400 The plain HTTP request was sent to HTTPS port use proxy 的bug ([a13203f](https://github.com/certd/certd/commit/a13203fb3f48c427d0d81a504912248dcc07df1a)) ### Features * 域名验证方法支持CNAME间接方式,此方式支持所有域名注册商,且无需提供Access授权,但是需要手动添加cname解析 ([f3d3508](https://github.com/certd/certd/commit/f3d35084ed44f9f33845f7045e520be5c27eed93)) * 站点个性化设置 ([11a9fe9](https://github.com/certd/certd/commit/11a9fe9014d96cba929e5a066e78f2af7ae59d14)) ### Performance Improvements * 并行任务名称改成添加任务,取消并行,可以在同一个阶段获取上一个task的输出 ([c5e5877](https://github.com/certd/certd/commit/c5e58770d1c5edc19c6f9ea1618f44b68e091f35)) * 调整静态资源到static目录 ([0584b36](https://github.com/certd/certd/commit/0584b3672b40f9042a2ed87e5627022606d046cd)) * 调整全部静态资源到static目录 ([a218890](https://github.com/certd/certd/commit/a21889080d6c7ffdf0af526a3a21f0b2d1c77288)) * 检查cname是否正确配置 ([b5d8935](https://github.com/certd/certd/commit/b5d8935159374fbe7fc7d4c48ae0ed9396861bdd)) * 七牛云cdn支持配置多个域名 ([88d745e](https://github.com/certd/certd/commit/88d745e29063a089864fb9c6705be7b8d4c2669a)) * 上传到主机插件支持注入环境变量 ([81fac73](https://github.com/certd/certd/commit/81fac736f9ccc8d1cda7ef4178752239cec20849)) * 优化宝塔网站部署插件远程获取数据的提示 ([2a3ca9f](https://github.com/certd/certd/commit/2a3ca9f552d96594ec6690a1c4c91f598451b9a1)) * 优化缩短首页缓存时间 ([49395e8](https://github.com/certd/certd/commit/49395e8cb65f4b30c0145329ed5de48be4ef3842)) * 域名输入增加校验提示,避免输入错误的域名 ([0c8e83e](https://github.com/certd/certd/commit/0c8e83e1254a9ce4d5a4e7888eb1710394a4b77c)) * cname校验配置增加未校验通过提示 ([77cc3c4](https://github.com/certd/certd/commit/77cc3c4a5cbd81f8233a8e0bb33fab0621c0905f)) * google eab授权支持自动获取,不过要配置代理 ([592791d](https://github.com/certd/certd/commit/592791d1356fc252fbb70d7f168567aee9585507)) ## [1.25.9](https://github.com/certd/certd/compare/v1.25.8...v1.25.9) (2024-10-01) ### Bug Fixes * 修复西部数码账户级别apikey不可用的bug ([f8f3e8b](https://github.com/certd/certd/commit/f8f3e8b43fd5d815887bcb53b95f46dc96424b79)) ### Performance Improvements * 增加等待插件 ([3ef0541](https://github.com/certd/certd/commit/3ef0541cc85ab6abf698ead3b258ae1ac156ef98)) ## [1.25.8](https://github.com/certd/certd/compare/v1.25.7...v1.25.8) (2024-09-30) ### Bug Fixes * 修复pfxPassword无效的bug ([251e450](https://github.com/certd/certd/commit/251e450fabfe62405bac13e39f2153736c081ef0)) ### Performance Improvements * 群晖获取deviceid优化 ([8d42273](https://github.com/certd/certd/commit/8d4227366548eb70f6bc04303829e6933168f906)) ## [1.25.7](https://github.com/certd/certd/compare/v1.25.6...v1.25.7) (2024-09-29) ### Bug Fixes * 修复某些地区被屏蔽无法激活专业版的bug ([7532a96](https://github.com/certd/certd/commit/7532a960851b84d4f2cc3dba02353c5235e1a364)) ### Performance Improvements * 上传到主机,支持socks代理 ([d91026d](https://github.com/certd/certd/commit/d91026dc4fbfe5fedc4ee8e43dc0d08f1cf88356)) * 支持上传到七牛云oss ([bf024bd](https://github.com/certd/certd/commit/bf024bdda8bc2a463475be5761acf0da7317a08a)) ## [1.25.6](https://github.com/certd/certd/compare/v1.25.5...v1.25.6) (2024-09-29) ### Bug Fixes * 修复中间证书复制错误的bug ([76e86ea](https://github.com/certd/certd/commit/76e86ea283ecbe4ec76cdc92b98457d0fef544ac)) ### Performance Improvements * 部署支持1Panel ([d047234](https://github.com/certd/certd/commit/d047234d98d31504f2e5a472b66e1b75806af26e)) * 增加使用教程 ([9d9c021](https://github.com/certd/certd/commit/9d9c0218195af5b9896cce7109b26a433480571d)) ## [1.25.5](https://github.com/certd/certd/compare/v1.25.4...v1.25.5) (2024-09-26) **Note:** Version bump only for package root ## [1.25.4](https://github.com/certd/certd/compare/v1.25.3...v1.25.4) (2024-09-25) ### Bug Fixes * 修复启动报授权验证失败的bug ([3460d3d](https://github.com/certd/certd/commit/3460d3ddca222ea702816ab805909d489eff957f)) ## [1.25.3](https://github.com/certd/certd/compare/v1.25.2...v1.25.3) (2024-09-24) ### Bug Fixes * 修复upload to host trim错误 ([0f0ddb9](https://github.com/certd/certd/commit/0f0ddb9c5963fd643d6d203334efac471c43ec3b)) ## [1.25.2](https://github.com/certd/certd/compare/v1.25.1...v1.25.2) (2024-09-24) **Note:** Version bump only for package root ## [1.25.1](https://github.com/certd/certd/compare/v1.25.0...v1.25.1) (2024-09-24) **Note:** Version bump only for package root # [1.25.0](https://github.com/certd/certd/compare/v1.24.4...v1.25.0) (2024-09-24) ### Bug Fixes * 修复首次创建任务运行时不自动设置当前运行情况的bug ([ecd83ee](https://github.com/certd/certd/commit/ecd83ee136abdd3df9ed2f21ec2ff0f24c0ed9d9)) ### Features * 账号绑定 ([e046640](https://github.com/certd/certd/commit/e0466409d0c021bb415abd94df448c8a0d4799e9)) * 支持中间证书 ([e86756e](https://github.com/certd/certd/commit/e86756e4c65a53dd23106d7ecbfe2fa987cc13f3)) * 支持vip转移 ([361e8fe](https://github.com/certd/certd/commit/361e8fe7ae5877e23fd5de31bc919bedd09c57f5)) ### Performance Improvements * 群晖支持OTP双重验证登录 ([8b8039f](https://github.com/certd/certd/commit/8b8039f42bbce10a4d0e737cdeeeef9bb17bee5a)) * 任务支持禁用 ([8ed16b3](https://github.com/certd/certd/commit/8ed16b3ea2dfe847357863a0bfa614e4fa5fc041)) * 优化收件邮箱输入 ([22ef28f](https://github.com/certd/certd/commit/22ef28f6338a78465bd52ccbad13e66e80263b2f)) * 优化主机登录失败提示 ([9de77b3](https://github.com/certd/certd/commit/9de77b327d39cff5ed6660ec53b58ba0eea18e5a)) * 增加重启certd插件 ([48238d9](https://github.com/certd/certd/commit/48238d929e6c4afa1d428e4d35b9159d37a47ae0)) * 证书支持旧版RSA,pkcs1 ([3d9c3ec](https://github.com/certd/certd/commit/3d9c3ecb3eb604b2458154f608bde0f01915d116)) * 支持阿里云ACK证书部署 ([d331fea](https://github.com/certd/certd/commit/d331fea47789122650e057ec7c9e85ee8e66f09b)) * 支持七牛云 ([8ecc2f9](https://github.com/certd/certd/commit/8ecc2f9446a9ebd11b9bfbffbb6cf7812a043495)) * 支持k8s ingress secret ([e5a5d0a](https://github.com/certd/certd/commit/e5a5d0a607bb6b4e1a1f7a1a419bada5f2dee59f)) * http请求增加默认超时时间 ([664bd86](https://github.com/certd/certd/commit/664bd863e5b4895aabe2384277c0c65f5902fdb2)) * plugins增加图标 ([a8da658](https://github.com/certd/certd/commit/a8da658a9723342b4f43a579f7805bfef0648efb)) ## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09) ### Bug Fixes * 修复腾讯云cdn证书部署后会自动关闭hsts,http2.0等配置的bug ([7908ab7](https://github.com/certd/certd/commit/7908ab79da624c94fa05849925b15e480e3317c4)) * 修复腾讯云tke证书部署报错的bug ([653f409](https://github.com/certd/certd/commit/653f409d91a441850d6381f89a8dd390831f0d5e)) ### Performance Improvements * 插件选择支持搜索 ([d1498a7](https://github.com/certd/certd/commit/d1498a71601b74d38343b1d070eadd03705dd9d5)) * 前置任务步骤增加错误提示 ([ae3daa9](https://github.com/certd/certd/commit/ae3daa9bcf4fc363825aad9b77f5d3879aeeff70)) * 群晖部署教程 ([0f0af2f](https://github.com/certd/certd/commit/0f0af2f309390f388e7a272cea3a1dd30c01977d)) * 支持群晖 ([5c270b6](https://github.com/certd/certd/commit/5c270b6b9d45a2152f9fdb3c07bd98b7c803cb8e)) ## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06) ### Performance Improvements * 支持多吉云cdn证书部署 ([65ef685](https://github.com/certd/certd/commit/65ef6857296784ca765926e09eafcb6fc8b6ecde)) ## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06) ### Bug Fixes * 修复复制流水线出现的各种问题 ([6314e8d](https://github.com/certd/certd/commit/6314e8d7eb58cd52e2a7bd3b5ffb9112b0b69577)) * 修复windows下无法执行第二条命令的bug ([71ac8aa](https://github.com/certd/certd/commit/71ac8aae4aa694e1a23761e9761c9fba30b43a21)) ### Performance Improvements * 阶段、任务、步骤全面支持拖动排序 ([bd73a16](https://github.com/certd/certd/commit/bd73a163cd0497f062bd424ddc6bc9bbc95f81ea)) * 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac)) * 任务支持拖动排序 ([1e9b563](https://github.com/certd/certd/commit/1e9b5638aa36a8ce70019a9c750230ba41938327)) * 西部数据支持用户级的apikey ([1c17b41](https://github.com/certd/certd/commit/1c17b41e160944b073e1849e6f9467c3659a4bfc)) * 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/certd/certd/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5)) * 优化跳过处理逻辑 ([b80210f](https://github.com/certd/certd/commit/b80210f24bf5db1c958d06ab27c9e5d3db452eda)) * 支持阿里云oss ([87a2673](https://github.com/certd/certd/commit/87a2673e8c33dff6eda1b836d92ecc121564ed78)) * 支持西部数码DNS ([c59cab1](https://github.com/certd/certd/commit/c59cab1aaeb19f86df8e3e0d8127cbd0a9ef77f3)) * 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723)) * client 请求超时时间延长为10s ([ff46771](https://github.com/certd/certd/commit/ff46771d8dd43e71c1ca70e3ba783945750342cc)) ## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02) ### Bug Fixes * 激活仅限管理员 ([1c17970](https://github.com/certd/certd/commit/1c17970b981f0987c506744ee6b2283fd5e40493)) * 修复在没有勾选使用代理的情况下,仍然会使用代理的bug ([0f66794](https://github.com/certd/certd/commit/0f6679425f6a736bb0128527dd99c085fac17d84)) ### Performance Improvements * 部署插件支持宝塔、易盾云等 ([ee61709](https://github.com/certd/certd/commit/ee617095efa1171548cf52fd45f0f98a368555a3)) * 授权配置支持加密 ([42a56b5](https://github.com/certd/certd/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb)) * 优化内存占用 ([db61033](https://github.com/certd/certd/commit/db6103363364440b650bc10bb334834e4a9470c7)) * 支持阿里云 DCDN ([98b77f8](https://github.com/certd/certd/commit/98b77f80843834616fb26f83b4c42245326abd06)) * 支持已跳过的步骤重新运行 ([ea775ad](https://github.com/certd/certd/commit/ea775adae18d57a04470cfba6b9460d761d74035)) * 支持cdnfly ([724a850](https://github.com/certd/certd/commit/724a85028b4a7146c9e3b4df4497dcf2a7bf7c67)) * 支持ftp上传 ([b9bddbf](https://github.com/certd/certd/commit/b9bddbfabb5664365f1232e9432532187c98006c)) # [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25) ### Bug Fixes * 部署到腾讯云cdn选择证书任务步骤限制只能选证书 ([3345c14](https://github.com/certd/certd/commit/3345c145b802170f75a098a35d0c4b8312efcd17)) * 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/certd/certd/commit/37eb762afe25c5896b75dee25f32809f8426e7b7)) * 修复创建流水线后立即运行时报no id错误的bug ([17ead54](https://github.com/certd/certd/commit/17ead547aab25333603980304aa3aad3db1f73d5)) * 修复使用代理的情况下申请证书失败的bug ([95122e2](https://github.com/certd/certd/commit/95122e28609333f4df55c266e5434897954c0fb3)) * 修复执行日志没有清理的bug ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5)) * 修复重置密码参数配置后无效的bug ([e358a88](https://github.com/certd/certd/commit/e358a8869696578687306e4cd0dcda53f898fe13)) * 修复ssh无法连接成功,无法执行命令的bug ([41b9837](https://github.com/certd/certd/commit/41b9837582323fb400ef8525ce65e8b37ad4b36f)) ### Features * 支持ECC类型 ([a7424e0](https://github.com/certd/certd/commit/a7424e02f5c7e02ac1688791040785920ce67473)) * 支持google证书申请(需要使用代理) ([a593056](https://github.com/certd/certd/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe)) ### Performance Improvements * 更新k8s底层api库 ([746bb9d](https://github.com/certd/certd/commit/746bb9d385e2f397daef4976eca1d4782a2f5ebd)) * 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b)) * 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345)) * 优化dnspod的token id 说明 ([790bf11](https://github.com/certd/certd/commit/790bf11af06d6264ef74bc1bb919661f0354239a)) * email proxy ([453f1ba](https://github.com/certd/certd/commit/453f1baa0b9eb0f648aa1b71ccf5a95b202ce13f)) ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06) ### Bug Fixes * 修复模糊查询无效的bug ([9355917](https://github.com/certd/certd/commit/93559174c780173f0daec7cdbd1f72f8d5c504d5)) ### Performance Improvements * 优化插件字段的default value ([24c7be2](https://github.com/certd/certd/commit/24c7be2c9cb39c14f7a97b674127c88033280b02)) * 优化默认值设置 ([1af19f0](https://github.com/certd/certd/commit/1af19f0ac053fe109782882964533636b5969d6b)) # [1.23.0](https://github.com/certd/certd/compare/v1.22.9...v1.23.0) (2024-08-05) ### Bug Fixes * 修复环境变量多个下划线不生效的bug ([7ec2218](https://github.com/certd/certd/commit/7ec2218c9fee5bee2bf0aa31f3e3a4301575f247)) ### Features * use node 20 ([e8ed972](https://github.com/certd/certd/commit/e8ed97206bf28e83f942db2ef4ea07fa76fd3567)) ## [1.22.9](https://github.com/certd/certd/compare/v1.22.8...v1.22.9) (2024-08-05) ### Performance Improvements * 优化定时任务 ([87e440e](https://github.com/certd/certd/commit/87e440ee2a8b10dc571ce619f28bc83c1e5eb147)) ## [1.22.8](https://github.com/certd/certd/compare/v1.22.7...v1.22.8) (2024-08-05) ### Performance Improvements * 修复删除历史记录没有删除log的bug,新增history管理页面,演示站点启动时不自动启动非管理员用户的定时任务 ([f78ae93](https://github.com/certd/certd/commit/f78ae93eedfe214008c3d071ca3d77c962137a64)) * 优化pipeline删除时,删除其他history ([b425203](https://github.com/certd/certd/commit/b4252033d56a9ad950f3e204ff021497c3978015)) ## [1.22.7](https://github.com/certd/certd/compare/v1.22.6...v1.22.7) (2024-08-04) ### Bug Fixes * 修复保存配置报id不能为空的bug ([367f807](https://github.com/certd/certd/commit/367f80731396003416665c22853dfbc09c2c03a0)) ## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03) ### Bug Fixes * 修复在相同的cron时偶尔无法触发定时任务的bug ([680941a](https://github.com/certd/certd/commit/680941af119619006b592e3ab6fb112cb5556a8b)) * 修复pg下pipeline title 类型问题 ([a9717b9](https://github.com/certd/certd/commit/a9717b9a0df7b5a64d4fe03314fecad4f59774cc)) ### Performance Improvements * 流水线支持名称模糊查询 ([59897c4](https://github.com/certd/certd/commit/59897c4ceae992ebe2972ca9e8f9196616ffdfd7)) * 腾讯云clb支持更多大区选择 ([e4f4570](https://github.com/certd/certd/commit/e4f4570b29f26c60f1ee9660a4c507cbeaba3d7e)) * 优化前置任务输出为空的提示 ([6ed1e18](https://github.com/certd/certd/commit/6ed1e18c7d9c46d964ecc6abc90f3908297b7632)) ## [1.22.5](https://github.com/certd/certd/compare/v1.22.4...v1.22.5) (2024-07-26) ### Bug Fixes * 修复用户管理无法添加用户的bug ([e7e89b8](https://github.com/certd/certd/commit/e7e89b8de7386e84c0d6b8e217e2034909657d68)) ## [1.22.4](https://github.com/certd/certd/compare/v1.22.3...v1.22.4) (2024-07-26) ### Performance Improvements * 证书申请支持反向代理,letsencrypt无法访问时的备用方案 ([b7b5df0](https://github.com/certd/certd/commit/b7b5df0587e0f7ea288c1b2af6f87211f207395f)) * 支持arm64 ([fa14f87](https://github.com/certd/certd/commit/fa14f87a8093ef3addc5e5f3315ce1bfc9982782)) ## [1.22.3](https://github.com/certd/certd/compare/v1.22.2...v1.22.3) (2024-07-25) ### Bug Fixes * lege 无执行权限问题 ([338eb3b](https://github.com/certd/certd/commit/338eb3bdfeb461e9b3bc7eee97b97a59f5642ffe)) ## [1.22.2](https://github.com/certd/certd/compare/v1.22.1...v1.22.2) (2024-07-23) ### Bug Fixes * 修复创建流水线时,无法根据dns类型默认正确的dns授权的bug ([a2c43b5](https://github.com/certd/certd/commit/a2c43b50a6069ed48958fd142844a8568c2af452)) ## [1.22.1](https://github.com/certd/certd/compare/v1.22.0...v1.22.1) (2024-07-20) ### Performance Improvements * 创建证书任务可以选择lege插件 ([affef13](https://github.com/certd/certd/commit/affef130378030c517250c58a4e787b0fc85d7d1)) * 创建证书任务增加定时任务和邮件通知输入 ([427620d](https://github.com/certd/certd/commit/427620d34f3b8ad6933005faf1878908441a2453)) * 支持配置启动后自动触发一次任务 ([a5a0c1f](https://github.com/certd/certd/commit/a5a0c1f6e7a3f05e581005e491d5b102ee854412)) # [1.22.0](https://github.com/certd/certd/compare/v1.21.2...v1.22.0) (2024-07-19) ### Features * 升级midway,支持esm ([485e603](https://github.com/certd/certd/commit/485e603b5165c28bc08694997726eaf2a585ebe7)) * 支持lego,海量DNS提供商 ([0bc6d0a](https://github.com/certd/certd/commit/0bc6d0a211920fb0084d705e1db67ee1e7262c44)) * 支持postgresql ([3b19bfb](https://github.com/certd/certd/commit/3b19bfb4291e89064b3b407a80dae092d54747d5)) ### Performance Improvements * 优化一些小细节 ([b168852](https://github.com/certd/certd/commit/b1688525dbbbfd67e0ab1cf5b4ddfbe9d394f370)) * 增加备案号设置 ([bd3d959](https://github.com/certd/certd/commit/bd3d959944db63a5690b55ee150e1007133868b9)) * 自动生成jwtkey,无需手动配置 ([390e485](https://github.com/certd/certd/commit/390e4853a570390a97df6a3b3882579f9547eeb4)) ## [1.21.2](https://github.com/certd/certd/compare/v1.21.1...v1.21.2) (2024-07-08) ### Performance Improvements * 申请证书时可以选择跳过本地dns校验 ([fe91d94](https://github.com/certd/certd/commit/fe91d94090d22ed0a3ea753ba74dfaa1bf057c17)) ## [1.21.1](https://github.com/certd/certd/compare/v1.21.0...v1.21.1) (2024-07-08) ### Performance Improvements * 上传到主机,支持设置不mkdirs ([5ba9831](https://github.com/certd/certd/commit/5ba9831ed1aa6ec6057df246f1035b36b9c41d2e)) * 说明优化,默认值优化 ([970c7fd](https://github.com/certd/certd/commit/970c7fd8a0f557770e973d8462ee5684ef742810)) # [1.21.0](https://github.com/certd/certd/compare/v1.20.17...v1.21.0) (2024-07-03) ### Features * 支持zero ssl ([eade2c2](https://github.com/certd/certd/commit/eade2c2b681569f03e9cd466e7d5bcd6703ed492)) ## [1.20.17](https://github.com/certd/certd/compare/v1.20.16...v1.20.17) (2024-07-03) ### Performance Improvements * 创建dns解析后,强制等待60s ([f47b35f](https://github.com/certd/certd/commit/f47b35f6d5bd7d675005c3e286b7e9a029201f8b)) * 文件上传提示由cert.crt改为cert.pem ([a09b0e4](https://github.com/certd/certd/commit/a09b0e48c176f3ed763791bd50322c29729f7c1c)) * 优化cname verify ([eba333d](https://github.com/certd/certd/commit/eba333de7a5b5ef4b0b7eaa904f578720102fa61)) ## [1.20.16](https://github.com/certd/certd/compare/v1.20.15...v1.20.16) (2024-07-01) ### Bug Fixes * 修复配置了cdn cname后申请失败的bug ([4a5fa76](https://github.com/certd/certd/commit/4a5fa767edc347d03d29a467e86c9a4d70b0220c)) ## [1.20.15](https://github.com/certd/certd/compare/v1.20.14...v1.20.15) (2024-06-28) ### Bug Fixes * 修复无法强制取消任务的bug ([9cc01db](https://github.com/certd/certd/commit/9cc01db1d569a5c45bb3e731f35d85df324a8e62)) ### Performance Improvements * 腾讯云dns provider 支持腾讯云的accessId ([e0eb3a4](https://github.com/certd/certd/commit/e0eb3a441384d474fe2923c69b25318264bdc9df)) * 支持windows文件上传 ([7f61cab](https://github.com/certd/certd/commit/7f61cab101fa13b4e88234e9ad47434e6130fed2)) ## [1.20.14](https://github.com/certd/certd/compare/v1.20.13...v1.20.14) (2024-06-23) ### Bug Fixes * 修复修改密码功能异常问题 ([f740ff5](https://github.com/certd/certd/commit/f740ff517f521dce361284c2c54bccc68aee0ea2)) ## [1.20.13](https://github.com/certd/certd/compare/v1.20.12...v1.20.13) (2024-06-18) ### Bug Fixes * 日志高度越界 ([c4c9adb](https://github.com/certd/certd/commit/c4c9adb8bfd513f57252e523794e3799a9b220f8)) * 修复邮箱设置页面SMTP拼写错误的问题 ([b98f1c0](https://github.com/certd/certd/commit/b98f1c0dd0bc6c6b4f814c578692afdf6d90b88d)) * 修复logo问题 ([7e483e6](https://github.com/certd/certd/commit/7e483e60913d509b113148c735fe13ba1d72dddf)) ### Performance Improvements * 增加警告,修复一些样式错乱问题 ([fd54c2f](https://github.com/certd/certd/commit/fd54c2ffac492222e85ff2f5f49a9ee5cfc73588)) * ssh登录支持openssh格式私钥、支持私钥密码 ([5c2c508](https://github.com/certd/certd/commit/5c2c50839a9076004f9034d754ac6deb531acdfb)) ## [1.20.12](https://github.com/certd/certd/compare/v1.20.10...v1.20.12) (2024-06-17) ### Bug Fixes * 修复aliyun域名超过100个找不到域名的bug ([5b1494b](https://github.com/certd/certd/commit/5b1494b3ce93d1026dc56ee741342fbb8bf7be24)) ### Performance Improvements * 增加系统设置,可以关闭自助注册功能 ([20feace](https://github.com/certd/certd/commit/20feacea12d43386540db6a600f391d786be4014)) * 增加cloudflare access token说明 ([934e6e2](https://github.com/certd/certd/commit/934e6e2bd05387cd50ffab95f230933543954098)) * 支持重置管理员密码,忘记密码的补救方案 ([732cbc5](https://github.com/certd/certd/commit/732cbc5e927b526850724594830392b2f10c6705)) * 支持cloudflare域名 ([fbb9a47](https://github.com/certd/certd/commit/fbb9a47e8f7bb805289b9ee64bd46ffee0f01c06)) ## [1.20.10](https://github.com/certd/certd/compare/v1.20.9...v1.20.10) (2024-05-30) ### Bug Fixes * 增加权限相关helper说明 ([83e4083](https://github.com/certd/certd/commit/83e40836ebff10bec60efe8933183e1ba1c22bf9)) * 增加权限相关helper说明 ([4304c94](https://github.com/certd/certd/commit/4304c9443ad9248f63dd6d8c512d8d6f32f90d37)) ### Performance Improvements * 上传到主机插件支持复制到本机路径 ([92446c3](https://github.com/certd/certd/commit/92446c339936f98f08f654b8971a7393d8435224)) * 优化文件下载包名 ([d9eb927](https://github.com/certd/certd/commit/d9eb927b0a1445feab08b1958aa9ea80637a5ae6)) * 增加任务复制功能 ([39ad759](https://github.com/certd/certd/commit/39ad7597fa0e19cc1f7631bbd6fea0a9e05a62c9)) ## [1.20.9](https://github.com/certd/certd/compare/v1.20.8...v1.20.9) (2024-03-22) **Note:** Version bump only for package root ## [1.20.8](https://github.com/certd/certd/compare/v1.20.7...v1.20.8) (2024-03-22) **Note:** Version bump only for package root ## [1.20.7](https://github.com/certd/certd/compare/v1.20.6...v1.20.7) (2024-03-22) **Note:** Version bump only for package root ## [1.20.6](https://github.com/certd/certd/compare/v1.20.5...v1.20.6) (2024-03-21) ### Bug Fixes * 调整按钮图标到居中位置 ([836d18f](https://github.com/certd/certd/commit/836d18f07e22d00faf2f213bc3301a6672b5bafc)) ### Performance Improvements * 插件贡献文档及示例 ([72fb20a](https://github.com/certd/certd/commit/72fb20abf3ba5bdd862575d2907703a52fd7eb17)) ## [1.20.5](https://github.com/certd/certd/compare/v1.20.2...v1.20.5) (2024-03-11) ### Bug Fixes * 修复腾讯云cdn部署无法选择端点的bug ([154409b](https://github.com/certd/certd/commit/154409b1dfee3ea1caae740ad9c1f99a6e7a9814)) ## [1.20.2](https://github.com/certd/certd/compare/v1.2.1...v1.20.2) (2024-02-28) ### Bug Fixes * 临时修复阿里云domainlist接口返回域名列表不全的问题,后续还需要增加翻页查询 ([849c145](https://github.com/certd/certd/commit/849c145926984762bd9dbec87bd91cd047fc0855)) ## [1.2.1](https://github.com/certd/certd/compare/v1.2.0...v1.2.1) (2023-12-12) ### Bug Fixes * 修复邮箱设置无效的bug ([aaa3224](https://github.com/certd/certd/commit/aaa322464d0f65e924d1850995540d396ee24d25)) **Note:** Version bump only for package root # [1.2.0](https://github.com/certd/certd/compare/v1.1.6...v1.2.0) (2023-10-27) * 🔱: [client] sync upgrade with 2 commits [trident-sync] ([aa3207f](https://github.com/certd/certd/commit/aa3207fca5f15f7c3da789989d99c8ae7d1c4551)) ### BREAKING CHANGES * search支持自定义布局,search.layout、search.collapse转移到 search.container之下。如果想使用原来的search组件,请配置search.is=fs-search-v1 ## [1.1.6](https://github.com/certd/certd/compare/v1.1.5...v1.1.6) (2023-07-10) ### Bug Fixes * 修复上传证书到腾讯云失败的bug ([e950322](https://github.com/certd/certd/commit/e950322232e19d1263b8552eefa5b0150fd7864e)) ## [1.1.5](https://github.com/certd/certd/compare/v1.1.4...v1.1.5) (2023-07-03) **Note:** Version bump only for package root ## [1.1.4](https://github.com/certd/certd/compare/v1.1.3...v1.1.4) (2023-07-03) ### Bug Fixes * 成功图标转动的问题 ([f87eee3](https://github.com/certd/certd/commit/f87eee3b9ff1ef9874e79a81fe0ed7104cb9ee8c)) ### Performance Improvements * cancel task ([bc65c0a](https://github.com/certd/certd/commit/bc65c0a786360c087fe95cad93ec6a87804cc5ee)) * flush log ([891a43a](https://github.com/certd/certd/commit/891a43ae6716ff98ed06643f7da2e35199ee195c)) * flush logger ([91be682](https://github.com/certd/certd/commit/91be6826b902e0f302b1a6cbdb1d24e15914c18d)) * timeout ([3eeb1f7](https://github.com/certd/certd/commit/3eeb1f77aa2922f3545f3d2067f561d95621d54f)) ## [1.1.3](https://github.com/certd/certd/compare/v1.1.2...v1.1.3) (2023-07-03) **Note:** Version bump only for package root ## [1.1.2](https://github.com/certd/certd/compare/v1.1.1...v1.1.2) (2023-07-03) **Note:** Version bump only for package root ## [1.1.1](https://github.com/certd/certd/compare/v1.1.0...v1.1.1) (2023-06-28) **Note:** Version bump only for package root # [1.1.0](https://github.com/certd/certd/compare/v1.0.6...v1.1.0) (2023-06-28) ### Bug Fixes * 修复access选择类型trigger ([2851a33](https://github.com/certd/certd/commit/2851a33eb2510f038fadb55da29512597a4ba512)) ### Features * 权限控制 ([27a4c81](https://github.com/certd/certd/commit/27a4c81c6d70e70abb3892c3ea58d4719988808a)) * 邮件通知 ([937e3fa](https://github.com/certd/certd/commit/937e3fac19cd03b8aa91db8ba03fda7fcfbacea2)) * cert download ([5a51c14](https://github.com/certd/certd/commit/5a51c14de521cb8075a80d2ae41a16e6d5281259)) * config merge ([fdc25dc](https://github.com/certd/certd/commit/fdc25dc0d795555cffacc4572648ec158988fbbb)) * save files ([99522fb](https://github.com/certd/certd/commit/99522fb49adb42c1dfdf7bec3dd52d641158285b)) * save files ([671d273](https://github.com/certd/certd/commit/671d273e2f9136d16896536b0ca127cf372f1619)) ## [1.0.6](https://github.com/certd/certd/compare/v1.0.5...v1.0.6) (2023-05-25) **Note:** Version bump only for package root ## [1.0.5](https://github.com/certd/certd/compare/v1.0.4...v1.0.5) (2023-05-25) **Note:** Version bump only for package root ## [1.0.4](https://github.com/certd/certd/compare/v1.0.3...v1.0.4) (2023-05-25) **Note:** Version bump only for package root ## [1.0.3](https://github.com/certd/certd/compare/v1.0.2...v1.0.3) (2023-05-25) **Note:** Version bump only for package root ## [1.0.2](https://github.com/certd/certd/compare/v1.0.1...v1.0.2) (2023-05-24) **Note:** Version bump only for package root ## [1.0.1](https://github.com/certd/certd/compare/v1.0.0...v1.0.1) (2023-05-24) **Note:** Version bump only for package root ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: LICENSE.md ================================================ Certd Open Source License - This project is licensed under the **GNU Affero General Public License (AGPL)** with the following additional terms. - 本项目遵循 GNU Affero General Public License(AGPL),并附加以下条款。 ## 1. License Terms ( 许可证条款 ) 1. **Freedom to Use** (自由使用) - You are free to use, copy, modify, and distribute the source code of this project for personal or organizational use, provided that you comply with the terms of this license. - 您可以自由使用、复制、修改和分发本项目的源代码,前提是您遵循本许可证的条款。 2. **Modification for Personal Use** (个人使用的修改) - Individuals and companies are allowed to modify the project according to their needs for non-commercial purposes. However, modifications to the logo, copyright information, or any code related to licensing are strictly prohibited. - 个人和公司允许根据自身需求对本项目进行修改以供非商业用途。但任何对logo、版权信息或与许可相关代码的修改都是严格禁止的。 3. **Commercial Authorization** (商业授权) - If you wish to make any form of monetary gain from this project, you must first obtain commercial authorization from the original author. Users should contact the author directly to negotiate the relevant licensing terms. - 如果您希望从本项目获得任何形式的经济收益,您必须首先从原作者处获得商业授权,用户应直接与作者联系,以协商相关许可条款。 4. **Retention of Rights** (保留权利) - All rights, title, and interest in the project remain with the original author. - 本项目的所有权利、标题和利益仍归原作者所有。 ## 2. As a contributor ( 作为贡献者 ) - you should agree that your contributed code: - 您应同意您贡献的代码: 1. - The original author can adjust the open-source agreement to be more strict or relaxed. - 原作者可以调整开源协议以使其更严格或更宽松。 2. - Can be used for commercial purposes. - 可用于商业用途。 ================================================ FILE: README.md ================================================ # Certd [English](./README_en.md) | [中文](./README.md) Certd® 是一个免费的全自动证书管理系统,让你的网站证书永不过期。 后缀d取自linux守护进程的命名风格,意为证书守护进程 >首创流水线申请部署证书模式,已被多个项目“借鉴”,被抄也是一种成功。 > 关于证书续期: >* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。 >* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。 >* 免费证书过期时间90天,以后可能还会缩短,所以自动化部署必不可少 > 流水线数量现已调整为无限制,欢迎大家使用 ## 一、特性 本项目不仅支持证书申请过程自动化,还可以自动化部署更新证书,让你的证书永不过期。 * 全自动申请证书(支持所有注册商注册的域名,支持DNS-01、HTTP-01、CNAME代理等多种域名验证方式) * 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等70+部署插件) * 支持通配符域名/泛域名,支持多个域名打到一个证书上,支持pem、pfx、der、jks等多种证书格式 * 邮件通知、webhook通知、企微、钉钉、飞书、anpush等多种通知方式 * 私有化部署,数据保存本地,安装简单快捷,镜像由Github Actions构建,过程公开透明 * 授权加密,站点隐藏,2FA,密码防爆破等多重安全保障 * 支持SQLite,PostgreSQL、MySQL多种数据库 * 开放接口支持 * 站点证书监控 * 多用户管理 * 多语言支持(中英双语切换) * 各版本向下兼容,一键无忧升级 ![](./docs/images/intro/intro.svg) ## 二、在线体验 官方Demo地址,自助注册后体验 https://certd.handfree.work/ > 注意数据将不定期清理,不定期停止定时任务,生产使用请自行部署 > 包含敏感信息,务必自己本地部署进行生产使用 ![首页](./docs/images/start/home.png) ## 三、使用教程 仅需3步,让你的证书永不过期 ### 1. 创建证书流水线 ![演示](packages/ui/certd-client/public/static/doc/images/1-add.png) > 添加成功后,就可以直接运行流水线申请证书了 ### 2. 添加部署任务 当然我们一般需要把证书部署到应用上,certd支持海量的部署插件,您可以根据自身实际情况进行选择,比如部署到Nginx、阿里云、腾讯云、K8S、CDN、宝塔、1Panel等等 此处演示部署证书到主机的nginx上 ![演示](packages/ui/certd-client/public/static/doc/images/5-1-add-host.png) 如果目前的部署插件都无法满足,您也可以手动下载,然后自行部署 ![演示](packages/ui/certd-client/public/static/doc/images/13-3-download.png) ### 3. 定时运行 ![演示](packages/ui/certd-client/public/static/doc/images/12-1-log-success.png) ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ -------> [点我查看详细使用步骤演示](./step.md) <-------- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 更多教程请访问官方文档 [certd.docmirror.cn](https://certd.docmirror.cn/guide/) ## 四、私有化部署 由于证书、授权信息等属于高度敏感数据,请务必私有化部署,保障数据安全 您可以根据实际情况从如下方式中选择一种方式进行私有化部署: 1. 【推荐】[Docker方式部署 ](https://certd.docmirror.cn/guide/install/docker/) 2. 【推荐】[宝塔面板方式部署 ](https://certd.docmirror.cn/guide/install/docker/) 3. 【推荐】[1Panel面板方式部署](https://certd.docmirror.cn/guide/install/1panel/) 4. 【推荐】[雨云一键部署](https://app.rainyun.com/apps/rca/store/6646/?ref=NzExMDQ2_) : 首充翻倍,每月仅需2.2元 [](https://app.rainyun.com/apps/rca/store/6646/?ref=NzExMDQ2_) 5. 【不推荐】[源码方式部署 ](https://certd.docmirror.cn/guide/install/source/) #### Docker镜像说明: * 国内镜像地址: * `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` * `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7`、`[version]-armv7` * DockerHub地址: * `https://hub.docker.com/r/greper/certd` * `greper/certd:latest` * `greper/certd:armv7`、`greper/certd:[version]-armv7` * GitHub Packages地址: * `ghcr.io/certd/certd:latest` * `ghcr.io/certd/certd:armv7`、`ghcr.io/certd/certd:[version]-armv7` * 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用 * [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml) ![](./docs/images/action/action-build.jpg) > 注意: > * 本应用存储的证书、授权信息等属于高度敏感数据,请做好安全防护 > * 请务必使用HTTPS协议访问本应用,避免被中间人攻击 > * 请务必使用web应用防火墙防护本应用,防止XSS、SQL注入等攻击 > * 请务必做好服务器本身的安全防护,防止数据库泄露 > * 请务必做好数据备份,避免数据丢失 > * [更多安全生产建议点我](https://certd.docmirror.cn/guide/feature/safe/) ## 五、生态 ### 1. 客户端工具 SSL-Assistant `SSL Assistant` 是一个运行于主机上的证书部署管理助手客户端。 支持自动扫描主机`Nginx`配置,然后从`Certd`拉取证书并部署。 在不想暴露ssh主机密码情况下,该工具非常好用。 开源地址: https://github.com/Youngxj/SSL-Assistant ## 六、更多帮助 请访问官方文档:[https://certd.docmirror.cn/](https://certd.docmirror.cn/guide/) * 升级方法:[升级方法](https://certd.docmirror.cn/guide/install/upgrade/) * 常见问题:[忘记密码](https://certd.docmirror.cn/guide/use/forgotpasswd/) * 多数据库:[多数据库配置](https://certd.docmirror.cn/guide/install/database/) * 站点安全:[站点安全特性](https://certd.docmirror.cn/guide/feature/safe/) * 更新日志:[CHANGELOG](./CHANGELOG.md) ## 七、联系作者 如有疑问,欢迎加入群聊(请备注certd) | 加群 | 微信群 | QQ群 | |---------|-------|-------| | 二维码 | | | 也可以加作者好友 | 加作者好友 | 微信 QQ | |---------|-------------------------------------------------------------| | 二维码 | | ## 八、捐赠 ************************ 支持开源,为爱发电,我已入驻爱发电 https://afdian.com/a/greper 发电权益: 1. 可加入发电专属群,可以获得作者一对一技术支持 2. 您的需求我们将优先实现,并且将作为专业版功能提供 3. 一年期专业版激活码 专业版特权对比 | 功能 | 免费版 | 专业版 | |---------|---------------------------------------|--------------------------------| | 免费证书申请 | 免费无限制 | 免费无限制 | | 域名数量 | 无限制 | 无限制 | | 证书流水线条数 | 无限制 | 无限制 | | 站点证书监控 | 限制1条 | 无限制 | | 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖 | | 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | ************************ ## 九、贡献代码 1. 本地开发请参考 [贡献插件向导](https://certd.docmirror.cn/guide/development/) 2. 作为贡献者,代表您同意您贡献的代码如下许可: 1. 可以调整开源协议以使其更严格或更宽松。 2. 可以用于商业用途。 感谢以下贡献者做出的贡献。 ## 十、 开源许可 * 本项目遵循 GNU Affero General Public License(AGPL)开源协议。 * 允许个人和公司内部自由使用、复制、修改和分发本项目,未获得商业授权情况下禁止任何形式的商业用途 * 未获得商业授权情况下,禁止任何对logo、版权信息及授权许可相关代码的修改。 * 如需商业授权,请联系作者。 ## 十一、我的其他项目(求Star) | 项目名称 | stars | 项目描述 | | --------- |--------- |----------- | | [fast-crud](https://gitee.com/fast-crud/fast-crud/) | GitHub stars | 基于vue3的crud快速开发框架 | | [dev-sidecar](https://github.com/docmirror/dev-sidecar/) | GitHub stars | 直连访问github工具,无需FQ,解决github无法访问的问题 | ================================================ FILE: README_en.md ================================================ # Certd [English](./README_en.md) | [中文](./README.md) Certd® is a free, fully automated certificate management system that ensures your website certificates never expire. The suffix 'd' is inspired by the naming convention of Linux daemons, representing a certificate daemon. > We pioneered the pipeline-based certificate application and deployment model, which has been "referenced" by multiple projects. Being copied is also a form of success. > Regarding certificate renewal: >* In fact, it's impossible to renew or reissue a certificate without modifying the certificate file itself. >* What we refer to as renewal is essentially applying for a new certificate following the full process and redeploying it. >* Free certificates expire in 90 days, which may be shortened in the future. Therefore, automated deployment is essential. > The number of pipelines is now unlimited. Welcome to use it. ## 1. Features This project not only supports automated certificate application but also automated certificate deployment and updates, ensuring your certificates never expire. * Fully automated certificate application (supports domains registered with all registrars and multiple domain verification methods such as DNS-01, HTTP-01, and CNAME proxy). * Fully automated certificate deployment and updates (currently supports deployment to over 70 plugins, including hosts, Alibaba Cloud, Tencent Cloud, etc.). * Supports wildcard domains/pan-domains, allows multiple domains in a single certificate, and supports various certificate formats such as pem, pfx, der, and jks. * Multiple notification methods, including email, webhook, WeChat Work, DingTalk, Lark, and anpush. * On-premises deployment, local data storage, simple and quick installation. Images are built by Github Actions, with a transparent process. * Multiple security measures, including authorization encryption, site hiding, 2FA, and password brute-force protection. * Supports multiple databases such as SQLite, PostgreSQL, and MySQL. * Open API support. * Site certificate monitoring. * Multi-user management. * Multi-language support (Chinese and English switching). * Downward compatibility across all versions, with one-click worry-free upgrades. ![](./docs/images/intro/intro.svg) ## 2. Online Experience Visit the official demo site and register to experience it. https://certd.handfree.work/ > Note: Data will be cleaned up irregularly, and scheduled tasks may be stopped. For production use, please deploy it yourself. > The content contains sensitive information. Make sure to deploy it locally for production use. ![Home Page](./docs/images/start/home.png) ## 3. Usage Tutorial Just 3 steps to ensure your certificates never expire. ### 1. Create a Certificate Pipeline ![Demonstration](packages/ui/certd-client/public/static/doc/images/1-add.png) > After successful addition, you can directly run the pipeline to apply for a certificate. ### 2. Add a Deployment Task Normally, we need to deploy certificates to applications. Certd supports a wide range of deployment plugins. You can choose based on your needs, such as deploying to Nginx, Alibaba Cloud, Tencent Cloud, K8S, CDN, Baota, 1Panel, etc. Here's a demonstration of deploying certificates to a host's Nginx: ![Demonstration](packages/ui/certd-client/public/static/doc/images/5-1-add-host.png) If the current deployment plugins don't meet your needs, you can also download them manually and deploy them yourself. ![Demonstration](packages/ui/certd-client/public/static/doc/images/13-3-download.png) ### 3. Run Scheduled Tasks ![Demonstration](packages/ui/certd-client/public/static/doc/images/12-1-log-success.png) ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ -------> [Click here to view detailed usage steps](./step.md) <-------- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ For more tutorials, please visit the official documentation [certd.docmirror.cn](https://certd.docmirror.cn/guide/). ## 4. On-Premises Deployment Since certificates, authorization information, and other data are highly sensitive, please make sure to deploy them on-premises to ensure data security. You can choose one of the following deployment methods based on your needs: 1. 【Recommended】[Docker Deployment](https://certd.docmirror.cn/guide/install/docker/) 2. 【Recommended】[BT Panel Deployment](https://certd.docmirror.cn/guide/install/docker/) 3. 【Recommended】[1Panel Deployment](https://certd.docmirror.cn/guide/install/1panel/) 4. 【Recommended】[Rainyun One-Click Deployment](https://app.rainyun.com/apps/rca/store/6646/?ref=NzExMDQ2_): Double your first recharge, only $2.2 per month. [](https://app.rainyun.com/apps/rca/store/6646/?ref=NzExMDQ2_) 5. 【Not Recommended】[Source Code Deployment](https://certd.docmirror.cn/guide/install/source/) #### Docker Image Information: * Domestic Image Addresses: * `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` * `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7`, `[version]-armv7` * DockerHub Addresses: * `https://hub.docker.com/r/greper/certd` * `greper/certd:latest` * `greper/certd:armv7`, `greper/certd:[version]-armv7` * GitHub Packages Addresses: * `ghcr.io/certd/certd:latest` * `ghcr.io/certd/certd:armv7`, `ghcr.io/certd/certd:[version]-armv7` * Images are built automatically by `Actions`, with a transparent process. Please use them with confidence. * [Click here to view image build logs](https://github.com/certd/certd/actions/workflows/build-image.yml) ![](./docs/images/action/action-build.jpg) > Note: > * The certificates, authorization information, and other data stored in this application are highly sensitive. Please take appropriate security measures. > * Make sure to use the HTTPS protocol to access this application to avoid man-in-the-middle attacks. > * Make sure to use a web application firewall to protect this application from attacks such as XSS and SQL injection. > * Make sure to secure the server itself to prevent database leakage. > * Make sure to back up your data to avoid data loss. > * [Click here for more production safety suggestions](https://certd.docmirror.cn/guide/feature/safe/) ## 5. Ecosystem ### 1. Client Tool: SSL-Assistant `SSL Assistant` is a certificate deployment and management assistant client that runs on hosts. It supports automatic scanning of the host's `Nginx` configuration and pulling certificates from `Certd` for deployment. This tool is very useful when you don't want to expose your SSH host password. Open-source Address: https://github.com/Youngxj/SSL-Assistant ## 6. More Help Please visit the official documentation: [https://certd.docmirror.cn/](https://certd.docmirror.cn/guide/). * Upgrade Method: [Upgrade Guide](https://certd.docmirror.cn/guide/install/upgrade/) * Common Issues: [Forgot Password](https://certd.docmirror.cn/guide/use/forgotpasswd/) * Multi-Database: [Multi-Database Configuration](https://certd.docmirror.cn/guide/install/database/) * Site Security: [Site Security Features](https://certd.docmirror.cn/guide/feature/safe/) * Changelog: [CHANGELOG](./CHANGELOG.md) ## 7. Contact the Author If you have any questions, feel free to join the group chat (please mention 'certd' in your message). | Join Group | WeChat Group | QQ Group | |---------|-------|-------| | QR Code | | | You can also add the author as a friend. | Add Author as Friend | WeChat QQ | |---------|-------|-------| | QR Code | | ## 8. Donation ************************ Support open-source projects and contribute with love. I've joined Afdian. https://afdian.com/a/greper Benefits of Contribution: 1. Join the exclusive contributor group and get one-on-one technical support from the author. 2. Your requests will be prioritized and implemented as professional edition features. 3. Receive a one-year professional edition activation code. Comparison of Professional Edition Privileges: | Feature | Free Edition | Professional Edition | |---------|---------------------------------------|--------------------------------| | Free Certificate Application | Unlimited for free | Unlimited for free | | Number of Domains | Unlimited | Unlimited | | Number of Certificate Pipelines | Unlimited | Unlimited | | Site Certificate Monitoring | Limited to 1 | Unlimited | | Automatic Deployment Plugins | Most plugins such as Alibaba Cloud CDN, Tencent Cloud, QiNiu CDN, Host Deployment, Baota, 1Panel | Synology | | Notifications | Email, Custom Webhook | Email without configuration, WeChat Work, DingTalk, Lark, anpush, ServerChan, etc. | ************************ ## 9. Contribute Code 1. For local development, please refer to the [Plugin Contribution Guide](https://certd.docmirror.cn/guide/development/). 2. As a contributor, you agree that your contributed code is subject to the following license: 1. The open-source license can be adjusted to be more or less restrictive. 2. It can be used for commercial purposes. Thank you to the following contributors. ## 10. Open-Source License * This project follows the GNU Affero General Public License (AGPL). * Individuals and companies are allowed to use, copy, modify, and distribute this project freely for internal use. Any form of commercial use is prohibited without obtaining commercial authorization. * Without commercial authorization, any modification of the logo, copyright information, and license-related code is prohibited. * For commercial authorization, please contact the author. ## 11. My Other Projects (Please Star) | Project Name | Stars | Project Description | |----------------|---------------|--------------| | [fast-crud](https://gitee.com/fast-crud/fast-crud/) | GitHub stars | A fast CRUD development framework based on Vue3. | | [dev-sidecar](https://github.com/docmirror/dev-sidecar/) | GitHub stars | A tool to access GitHub directly without a VPN, solving the problem of inaccessible GitHub. | ================================================ FILE: build-dev.trigger ================================================ 2 ================================================ FILE: build.trigger ================================================ 23:09 ================================================ FILE: deploy.js ================================================ import http from 'axios' import fs from 'fs' //读取 packages/core/pipline/package.json的版本号 import {default as packageJson} from './packages/core/pipeline/package.json' assert { type: "json" }; const certdVersion = packageJson.version console.log("certdVersion", certdVersion) // 同步npmmirror的包 async function getPackages(directoryPath) { return new Promise((resolve, reject) => { // 读取目录下的文件和目录列表 fs.readdir(directoryPath, {withFileTypes: true}, (err, files) => { if (err) { console.log('无法读取目录:', err); reject(err) return; } // 过滤仅保留目录 const directories = files .filter(file => file.isDirectory()) .map(directory => directory.name); console.log('目录列表:', directories); resolve(directories) }); }) } async function getAllPackages() { const base = await getPackages("./packages/core") const plugins = await getPackages("./packages/plugins") const libs = await getPackages("./packages/libs") return base.concat(plugins).concat(libs) } async function sync() { const packages = await getAllPackages() for (const pkg of packages) { await http({ url: `http://registry-direct.npmmirror.com/@certd/${pkg}/sync?sync_upstream=true`, method: 'PUT', headers: { "Content-Type": "application/json" }, data: {} }) console.log(`sync success:${pkg}`) await sleep(30*1000) } } // curl -X PUT https://registry-direct.npmmirror.com/@certd/plugin-cert/sync?sync_upstream=true const certdImageBuild = "http://flow-openapi.aliyun.com/pipeline/webhook/4zgFk3i4RZEMGuQzlOcI" const certdImageRun = "http://flow-openapi.aliyun.com/pipeline/webhook/lzCzlGrLCOHQaTMMt0mG" const webhooks = [certdImageBuild,certdImageRun] async function sleep(time) { return new Promise(resolve => { setTimeout(resolve, time) }) } async function triggerBuild() { await sleep(60000) for (const webhook of webhooks) { await http({ url: webhook, method: 'POST', headers: { "Content-Type": "application/json" }, data: { 'CERTD_VERSION': certdVersion } }) console.log(`webhook success:${webhook}`) await sleep(30*60*1000) } } async function start() { // await build() console.log("等待60秒") await sleep(100* 1000) await sync() await sleep(100 * 1000) await triggerBuild() } start() /** * 打包前 修改 lerna * nodemodules里面搜索如下 * return childProcess.exec("git", ["add", "--", ...files], execOpts); * * ('git', ['add', '--', ...files] * ('git', ['add', '.'] */ ================================================ FILE: deploy.trigger ================================================ 5 ================================================ FILE: docker/run/docker-compose.yaml ================================================ version: '3.3' # 兼容旧版docker-compose services: certd: # 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0 image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest # image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源 # image: greper/certd:latest container_name: certd # 容器名 restart: unless-stopped # 自动重启 volumes: # ↓↓↓↓↓ -------------------------------------------------------- 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下,【您需要定时备份此目录,以保障数据容灾】 # 只要修改冒号前面的,冒号后面的/app/data不要动 - /data/certd:/app/data ports: # 端口映射 # ↓↓↓↓ ---------------------------------------------------------- 如果端口有冲突,可以修改第一个7001为其他不冲突的端口号,第二个7001不要动 - "7001:7001" # ↓↓↓↓ ---------------------------------------------------------- https端口,可以根据实际情况,是否暴露该端口 - "7002:7002" #↓↓↓↓ -------------------------------------------------------------- 如果出现getaddrinfo EAI_AGAIN 或 getaddrinfo ENOTFOUND 错误,可以尝试设置dns # dns: # - 223.5.5.5 # 阿里云公共dns # - 223.6.6.6 # # ↓↓↓↓ --------------------------------------------------------- 如果你服务器在腾讯云,可以用这个替换上面阿里云的公共dns # - 119.29.29.29 # 腾讯云公共dns # - 182.254.116.116 # # ↓↓↓↓ --------------------------------------------------------- 如果你服务器部署在国外,可以用这个替换上面阿里云的公共dns # - 8.8.8.8 # 谷歌公共dns # - 8.8.4.4 # extra_hosts: # # ↓↓↓↓ -------------------------------------------------------- 这里可以配置自定义hosts,外网域名可以指向本地局域网ip地址 # - "localdomain.com:192.168.1.3" # # ↓↓↓↓ ------------------------------------------------ 直接使用主机的网络,如果网络问题实在找不到原因,可以尝试打开此参数 # network_mode: host labels: com.centurylinklabs.watchtower.enable: "true" # ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络,还需要把下面networks的注释放开 # networks: # - ip6net environment: # ↓↓↓↓ ----------------------------------------------------- 使用上海东八时区 # - TZ=Asia/Shanghai # 设置环境变量即可自定义certd配置 # 配置项见: packages/ui/certd-server/src/config/config.default.ts # 配置规则: certd_ + 配置项, 点号用_代替 # #↓↓↓↓ ----------------------------- 如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false - certd_system_resetAdminPasswd=false # 默认使用sqlite文件数据库,如果需要使用其他数据库,请设置以下环境变量 # 注意: 选定使用一种数据库之后,不支持更换数据库。 # 数据库迁移方法:1、使用新数据库重新部署一套,然后将旧数据同步过去,注意flyway_history表的数据不要同步 # #↓↓↓↓ ----------------------------- 使用postgresql数据库,需要提前创建数据库 # - certd_flyway_scriptDir=./db/migration-pg # 升级脚本目录 # - certd_typeorm_dataSource_default_type=postgres # 数据库类型 # - certd_typeorm_dataSource_default_host=localhost # 数据库地址 # - certd_typeorm_dataSource_default_port=5433 # 数据库端口 # - certd_typeorm_dataSource_default_username=postgres # 用户名 # - certd_typeorm_dataSource_default_password=yourpasswd # 密码 # - certd_typeorm_dataSource_default_database=certd # 数据库名 # #↓↓↓↓ ----------------------------- 使用mysql数据库,需要提前创建数据库 charset=utf8mb4, collation=utf8mb4_bin # - certd_flyway_scriptDir=./db/migration-mysql # 升级脚本目录 # - certd_typeorm_dataSource_default_type=mysql # 数据库类型, 或者 mariadb # - certd_typeorm_dataSource_default_host=localhost # 数据库地址 # - certd_typeorm_dataSource_default_port=3306 # 数据库端口 # - certd_typeorm_dataSource_default_username=root # 用户名 # - certd_typeorm_dataSource_default_password=yourpasswd # 密码 # - certd_typeorm_dataSource_default_database=certd # 数据库名 # ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest # certd-updater: # 添加 Watchtower 服务 # image: containrrr/watchtower:latest # container_name: certd-updater # restart: unless-stopped # volumes: # - /var/run/docker.sock:/var/run/docker.sock # # 配置 自动更新 # environment: # - WATCHTOWER_CLEANUP=true # 自动清理旧版本容器 # - WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器 # - WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新 # - WATCHTOWER_POLL_INTERVAL=600 # 每 10 分钟检查一次更新 # ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络,还需要把上面networks的注释放开 #networks: # ip6net: # enable_ipv6: true # ipam: # config: # - subnet: 2001:db8::/64 ================================================ FILE: docs/.gitignore ================================================ .vitepress/cache dist ================================================ FILE: docs/.vitepress/config.ts ================================================ import {defineConfig} from "vitepress"; // Import lightbox plugin import lightbox from "vitepress-plugin-lightbox"; // https://vitepress.dev/reference/site-config export default defineConfig({ title: "Certd", titleTemplate: "开源SSL证书管理工具,证书自动化申请部署,让你的网站证书永不过期", description: "Certd帮助文档,Certd是一款开源免费的全自动SSL证书管理工具;证书自动化申请部署流水线;自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔。", markdown: { config: (md) => { // Use lightbox plugin md.use(lightbox, {}); } }, sitemap: { hostname: 'https://certd.docmirror.cn' }, head: [ // [ // 'meta', // { // name: 'viewport', // content: // 'width=device-width,initial-scale=1,minimfast-cum-scale=1.0,maximum-scale=1.0,user-scalable=no', // }, // ], ["meta", { name: "keywords", content: "证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具、Certd、SSL证书自动部署、证书自动化,https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署,SSL平台,证书管理平台,证书流水线" }], // ["meta", { name: "google-site-verification",content: "V5XLTSnXoT15uQotwpxJoQolUo2d5UbSL-TacsyOsC0"}], // // ["meta", {name: "baidu-site-verification",content: "codeva-MiWN8Y07Ua"}], ["link", {rel: "icon", href: "/static/logo/logo.svg"}] ], themeConfig: { logo: "/static/logo/logo.svg", search: { provider: "local", options: { detailedView: true, translations: { button: { buttonText: "搜索文档", buttonAriaLabel: "搜索文档" }, modal: { noResultsText: "无法找到相关结果", resetButtonTitle: "清除查询条件", footer: { selectText: "选择", closeText: "关闭", navigateText: "切换" } } } } }, // https://vitepress.dev/reference/default-theme-config nav: [ {text: "首页", link: "/"}, {text: "指南", link: "/guide/"}, {text: "Demo体验", link: "https://certd.handfree.work"} ], sidebar: { "/guide/": [ { text: "入门", items: [ {text: "简介", link: "/guide/"}, {text: "快速开始", link: "/guide/start.md"}, { text: "私有化部署", items: [ {text: "docker部署", link: "/guide/install/docker/"}, {text: "宝塔面板部署", link: "/guide/install/baota/"}, {text: "1Panel部署", link: "/guide/install/1panel/"}, {text: "群晖部署", link: "/guide/use/synology/"}, {text: "源码部署", link: "/guide/install/source/"} ] }, {text: "演示教程", link: "/guide/tutorial.md"}, {text: "版本升级", link: "/guide/install/upgrade.md"} ] }, { text: "特性", items: [ {text: "CNAME代理校验", link: "/guide/feature/cname/index.md"}, {text: "多数据库支持", link: "/guide/install/database.md"}, {text: "开放接口", link: "/guide/open/index.md"}, { text: "站点安全", link: "/guide/feature/safe/" }, { text: "插件列表", items: [ {text: "授权提供商", link: "/guide/plugins/access.md"}, {text: "DNS提供商", link: "/guide/plugins/dns-provider.md"}, {text: "任务插件", link: "/guide/plugins/deploy.md"}, {text: "通知插件", link: "/guide/plugins/notification.md"}, ] }, ] }, { text: "常见问题", items: [ {text: "QA", link: "/guide/qa/use.md"}, {text: "常见报错处理", link: "/guide/qa/"}, {text: "群晖证书部署", link: "/guide/use/synology/"}, {text: "腾讯云密钥获取", link: "/guide/use/tencent/"}, {text: "连接windows主机", link: "/guide/use/host/windows.md"}, {text: "Google EAB获取", link: "/guide/use/google/"}, {text: "阿里云相关", link: "/guide/use/aliyun/"}, {text: "忘记密码", link: "/guide/use/forgotpasswd/"}, {text: "数据备份", link: "/guide/use/backup/"}, {text: "Certd本身的证书更新", link: "/guide/use/https/index.md"}, {text: "js脚本插件使用", link: "/guide/use/custom-script/index.md"}, {text: "邮箱配置", link: "/guide/use/email/index.md"}, {text: "IPv6支持", link: "/guide/use/setting/ipv6.md"}, {text: "ESXi", link: "/guide/use/ESXi/index.md"}, ] }, { text: "商业版配置", link: "/guide/use/comm/", items: [ {text: "支付宝配置", link: "/guide/use/comm/payments/alipay.md"}, {text: "微信支付配置", link: "/guide/use/comm/payments/wxpay.md"}, {text: "彩虹易支付配置", link: "/guide/use/comm/payments/yizhifu.md"}, ] }, { text: "其他", items: [ {text: "贡献代码", link: "/guide/development/index.md"}, {text: "更新日志", link: "/guide/changelogs/CHANGELOG.md"}, {text: "镜像说明", link: "/guide/image.md"}, {text: "联系我们", link: "/guide/contact/"}, {text: "捐赠", link: "/guide/donate/"}, {text: "开源协议", link: "/guide/license/"}, {text: "我的其他开源项目", link: "/guide/link/"}, ] } ], }, socialLinks: [ {icon: "github", link: "https://github.com/certd/certd"} ], footer: { message: "Certd帮助文档 | 粤ICP备14088435号 ", copyright: "Copyright © 2021-present handfree.work " } } }); ================================================ FILE: docs/.vitepress/theme/Layout.vue ================================================ ================================================ FILE: docs/.vitepress/theme/index.ts ================================================ // https://vitepress.dev/guide/custom-theme // import { h } from 'vue' import type { Theme } from 'vitepress' import DefaultTheme from 'vitepress/theme' import './style.css' import Layout from './Layout.vue' import { registerAnalytics, siteIds, trackPageview } from './plugins/baidutongji' import { inBrowser } from "vitepress"; export default { extends: DefaultTheme, Layout, // Layout: () => { // return h(DefaultTheme.Layout, null, { // // https://vitepress.dev/guide/extending-default-theme#layout-slots // }) // }, enhanceApp({ app, router, siteData }) { // ... if (inBrowser) { registerAnalytics(siteIds) window.addEventListener('hashchange', () => { const { href: url } = window.location trackPageview(siteIds, url) }) router.onAfterRouteChanged = (to) => { trackPageview(siteIds, to) } } } } satisfies Theme ================================================ FILE: docs/.vitepress/theme/plugins/baidutongji.ts ================================================ import { inBrowser } from 'vitepress' /** * 统计站点的 ID 列表 */ export const siteIds = 'a6ce877a899ae44292e4f854a53d688e' declare global { interface Window { _hmt: any } } /** * 注册统计 */ export function registerAnalytics(siteId: string) { if (!inBrowser) return if (document.querySelector(`#analytics-plugin-${siteId}`)) return window._hmt = window._hmt ? window._hmt : [] const script = document.createElement('script') script.id = `analytics-${siteId}` script.async = true script.src = `https://hm.baidu.com/hm.js?${siteId}` document.querySelector('head')?.appendChild(script) } /** * 上报 PV 数据 * @param siteId - 站点 ID * @param pageUrl - 页面 URL */ export function trackPageview(siteId: string, pageUrl: string) { if (!inBrowser) return if (!pageUrl || typeof pageUrl !== 'string') pageUrl = '/' if (pageUrl.startsWith('http')) { const urlFragment = pageUrl.split('/') const origin = `${urlFragment[0]}//${urlFragment[2]}` pageUrl = pageUrl.replace(origin, '') } window._hmt.push(['_setAccount', siteId]) window._hmt.push(['_trackPageview', pageUrl]) } ================================================ FILE: docs/.vitepress/theme/style.css ================================================ /** * Customize default theme styling by overriding CSS variables: * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */ /** * Colors * * Each colors have exact same color scale system with 3 levels of solid * colors with different brightness, and 1 soft color. * * - `XXX-1`: The most solid color used mainly for colored text. It must * satisfy the contrast ratio against when used on top of `XXX-soft`. * * - `XXX-2`: The color used mainly for hover state of the button. * * - `XXX-3`: The color for solid background, such as bg color of the button. * It must satisfy the contrast ratio with pure white (#ffffff) text on * top of it. * * - `XXX-soft`: The color used for subtle background such as custom container * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors * on top of it. * * The soft color must be semi transparent alpha channel. This is crucial * because it allows adding multiple "soft" colors on top of each other * to create a accent, such as when having inline code block inside * custom containers. * * - `default`: The color used purely for subtle indication without any * special meanings attached to it such as bg color for menu hover state. * * - `brand`: Used for primary brand colors, such as link text, button with * brand theme, etc. * * - `tip`: Used to indicate useful information. The default theme uses the * brand color for this by default. * * - `warning`: Used to indicate warning to the users. Used in custom * container, badges, etc. * * - `danger`: Used to show error, or dangerous message to the users. Used * in custom container, badges, etc. * -------------------------------------------------------------------------- */ :root { --vp-c-default-1: var(--vp-c-gray-1); --vp-c-default-2: var(--vp-c-gray-2); --vp-c-default-3: var(--vp-c-gray-3); --vp-c-default-soft: var(--vp-c-gray-soft); --vp-c-brand-1: var(--vp-c-indigo-1); --vp-c-brand-2: var(--vp-c-indigo-2); --vp-c-brand-3: var(--vp-c-indigo-3); --vp-c-brand-soft: var(--vp-c-indigo-soft); --vp-c-tip-1: var(--vp-c-brand-1); --vp-c-tip-2: var(--vp-c-brand-2); --vp-c-tip-3: var(--vp-c-brand-3); --vp-c-tip-soft: var(--vp-c-brand-soft); --vp-c-warning-1: var(--vp-c-yellow-1); --vp-c-warning-2: var(--vp-c-yellow-2); --vp-c-warning-3: var(--vp-c-yellow-3); --vp-c-warning-soft: var(--vp-c-yellow-soft); --vp-c-danger-1: var(--vp-c-red-1); --vp-c-danger-2: var(--vp-c-red-2); --vp-c-danger-3: var(--vp-c-red-3); --vp-c-danger-soft: var(--vp-c-red-soft); } /** * Component: Button * -------------------------------------------------------------------------- */ :root { --vp-button-brand-border: transparent; --vp-button-brand-text: var(--vp-c-white); --vp-button-brand-bg: var(--vp-c-brand-3); --vp-button-brand-hover-border: transparent; --vp-button-brand-hover-text: var(--vp-c-white); --vp-button-brand-hover-bg: var(--vp-c-brand-2); --vp-button-brand-active-border: transparent; --vp-button-brand-active-text: var(--vp-c-white); --vp-button-brand-active-bg: var(--vp-c-brand-1); } /** * Component: Home * -------------------------------------------------------------------------- */ :root { --vp-home-hero-name-color: transparent; --vp-home-hero-name-background: -webkit-linear-gradient( 120deg, #bd34fe 30%, #41d1ff ); --vp-home-hero-image-background-image: linear-gradient( -45deg, #bd34fe 50%, #47caff 50% ); --vp-home-hero-image-filter: blur(44px); } @media (min-width: 640px) { :root { --vp-home-hero-image-filter: blur(56px); } } @media (min-width: 960px) { :root { --vp-home-hero-image-filter: blur(68px); } } /** * Component: Custom Block * -------------------------------------------------------------------------- */ :root { --vp-custom-block-tip-border: transparent; --vp-custom-block-tip-text: var(--vp-c-text-1); --vp-custom-block-tip-bg: var(--vp-c-brand-soft); --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); } /** * Component: Algolia * -------------------------------------------------------------------------- */ .DocSearch { --docsearch-primary-color: var(--vp-c-brand-1) !important; } ================================================ FILE: docs/guide/changelogs/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [1.36.10](https://github.com/certd/certd/compare/v1.36.9...v1.36.10) (2025-07-18) ### Bug Fixes * 企业微信通知改成text类型,因为markdown类型不支持@用户 ([747d266](https://github.com/certd/certd/commit/747d26674248082e678a3fd5ecc94712641a2716)) * api接口获取不到证书的bug ([05a33a0](https://github.com/certd/certd/commit/05a33a0ec9999e2802f6c7b23cc1c61a2b9e963d)) ### Performance Improvements * 部署到阿里云oss插件支持选择上传到阿里云cas中的证书 ([2ea2c8c](https://github.com/certd/certd/commit/2ea2c8c05fc40f79595f1bbde67c1413558bf684)) * 优化子域名托管的说明 ([b15f514](https://github.com/certd/certd/commit/b15f514018b728acb0922ee3f93c1f302eb5d471)) * 账号即将过期通知 ([e403450](https://github.com/certd/certd/commit/e40345095f31e2fb8e2333a6647466659133fa0c)) * 子域名托管重复域名不允许添加 ([ffc0c7b](https://github.com/certd/certd/commit/ffc0c7bb7b16d9904fd2d905d1c4e1d4854e92a9)) ## [1.36.9](https://github.com/certd/certd/compare/v1.36.7...v1.36.9) (2025-07-15) ### Bug Fixes * 修复ssh无法执行命令的bug ([9763cb0](https://github.com/certd/certd/commit/9763cb00e5d95b2fa5d1c2d3d4a8eecac71600e6)) ## [1.36.7](https://github.com/certd/certd/compare/v1.36.6...v1.36.7) (2025-07-15) ### Bug Fixes * 修复流水线列表页报length错误的bug ([9864792](https://github.com/certd/certd/commit/9864792bbfd149e770d6e1ffa809573694f99dd3)) * 修复流水线页面状态没有刷新的bug ([93e9498](https://github.com/certd/certd/commit/93e9498b410353f504e11e264db62468895d7290)) * 修复自定义证书检查时间重启之后不生效的bug ([38e867c](https://github.com/certd/certd/commit/38e867c917bbc68bd228bdd8064f3e7358d6413d)) ### Performance Improvements * 支持上传证书到各种对象存储,oss、cos、七牛、s3、minio等 ([1da8617](https://github.com/certd/certd/commit/1da8617a53a675776635bbc3bcb3c6d7dff83e27)) * 支持邮箱发送证书 ([95332d5](https://github.com/certd/certd/commit/95332d5db96cd54ddab6ab737332417a09169b39)) ## [1.36.6](https://github.com/certd/certd/compare/v1.36.5...v1.36.6) (2025-07-14) ### Bug Fixes * 修复某些页面翻译不全显示错误的bug ([0b3158f](https://github.com/certd/certd/commit/0b3158fdd5fe5bb0a98c4e65715dbc3de2c38047)) * 修复运行流水线后会闪烁一下的bug ([dfc9362](https://github.com/certd/certd/commit/dfc9362084082ee535b898f23b2609c1d946a6fd)) ### Performance Improvements * 部署plesk证书,支持删除未使用的证书 ([902d246](https://github.com/certd/certd/commit/902d246d1a7473ad90f604028c4eb09c8c67d99c)) * 通知和定时器的删除按钮显示为红色更显眼 ([61ba83c](https://github.com/certd/certd/commit/61ba83c77546c3d505d081e19a3d68c127662bf1)) * 优化流水线列表页面、详情页面性能,精简返回数据 ([609ac9c](https://github.com/certd/certd/commit/609ac9c9a2dde605eb09834ae59693c1cb238765)) * 支持自动选择校验方式申请证书 ([3f99432](https://github.com/certd/certd/commit/3f9943270cfb12946e38e6272bc5e8d95ad6ab9e)) * OpenAPI支持autoApply参数 ([42f4d14](https://github.com/certd/certd/commit/42f4d1477dc791520a874aed56035abcbc8c433b)) ## [1.36.5](https://github.com/certd/certd/compare/v1.36.4...v1.36.5) (2025-07-11) ### Bug Fixes * 某些插件找不到的bug ([4b3f4a8](https://github.com/certd/certd/commit/4b3f4a868a8b0800c5c59de9d0f47bddc079e7b3)) ## [1.36.4](https://github.com/certd/certd/compare/v1.36.3...v1.36.4) (2025-07-10) ### Bug Fixes * 修复查看证书对话框翻译错误的bug ([8626b6d](https://github.com/certd/certd/commit/8626b6d9f235c511766f2ae98e0a37f6cebb621c)) * 修复translation后分组编辑打不开的bug ([46a1b74](https://github.com/certd/certd/commit/46a1b7479923d2feb2dece202a5932b99981b2cd)) * 执行windows nginx命令时,改为return code判断是否执行成功 ([b37cffd](https://github.com/certd/certd/commit/b37cffd704cd08b8bdd68a6e284706eabe59e78d)) ### Performance Improvements * 优化证书进度条颜色 ([2af91db](https://github.com/certd/certd/commit/2af91dbf2ae36a4ed17c6788bc2a2a79a3bb29f8)) * 站点证书即将过期通知标题颜色优化为红色 ([80c5331](https://github.com/certd/certd/commit/80c5331a5d4c320323d9b9b800e4ea3b72577b33)) * 支持部署到阿里云vod ([98da4e1](https://github.com/certd/certd/commit/98da4e1791ed8bb21daf2a9914fda875d14633c9)) * 支持部署证书到网宿CDN ([c3da026](https://github.com/certd/certd/commit/c3da026b33106f5195959825a68cadbe49efef00)) * 重置管理员密码同时可以清除管理员的2FA设置 ([1ece091](https://github.com/certd/certd/commit/1ece0915f172d5f8b8adb866434e7efcc5c8c46d)) * output-selector from参数支持更丰富的过滤规则 ([87853a2](https://github.com/certd/certd/commit/87853a201535f3bfe8505c40f8f5229d82ffcc73)) ## [1.36.3](https://github.com/certd/certd/compare/v1.36.2...v1.36.3) (2025-07-07) ### Bug Fixes * 修复开放接口添加按钮文本显示问题 ([f93ba99](https://github.com/certd/certd/commit/f93ba9970c12680f38eba2a7abd4b72cf3f5b6a6)) ### Performance Improvements * 优化部署到腾讯TKE插件,支持Opaque类型选择,优化填写说明 ([1445325](https://github.com/certd/certd/commit/144532530a865b634e68539e4888e26f52f73492)) ## [1.36.2](https://github.com/certd/certd/compare/v1.36.1...v1.36.2) (2025-07-06) ### Bug Fixes * 修复notification编辑按钮无法打开对话框的bug ([0cea26c](https://github.com/certd/certd/commit/0cea26c6287f52adf273b4a525c37bea8555c68c)) * 优化更新飞牛os证书有效期,修复某些情况下部署证书后飞牛无法访问https的bug ([610c919](https://github.com/certd/certd/commit/610c919c72037becc0ed326f5d5b18c963dfcb3a)) ### Performance Improvements * 证书检查支持自定义dns服务器 ([c53bb7c](https://github.com/certd/certd/commit/c53bb7cf677faa32729709ae0c10359db5194d7a)) ## [1.36.1](https://github.com/certd/certd/compare/v1.36.0...v1.36.1) (2025-07-02) ### Bug Fixes * 修复通知和触发器无法编辑的bug ([a2e0951](https://github.com/certd/certd/commit/a2e09510426680eb425c0d7ad337f39d3f052054)) ### Performance Improvements * 支持部署到七牛云DCDN ([bde601b](https://github.com/certd/certd/commit/bde601bfffb4f7345d97e1e3b064520816d31555)) # [1.36.0](https://github.com/certd/certd/compare/v1.35.5...v1.36.0) (2025-07-01) ### Bug Fixes * 支持自定义证书生成插件 ([481cc02](https://github.com/certd/certd/commit/481cc029fafaf280aa844cd3ca30f4653ec35d55)) ### Features * 支持模版创建流水线 ([2559f0e](https://github.com/certd/certd/commit/2559f0e822db095d1d26a7f1d517622dce22a5c2)) ### Performance Improvements * 阿里云waf cname站点选择支持翻页及域名查询 ([4cf9858](https://github.com/certd/certd/commit/4cf98584dacc5999752732f136246647a2f1f07d)) * 部署到ssh主机命令支持前置命令 ([991b741](https://github.com/certd/certd/commit/991b741cbe223b342f534157da63b71e81661f8e)) * 模版导入流水线 ([dcc8c56](https://github.com/certd/certd/commit/dcc8c569693432579709ce63656665a76bcf9a44)) * 添加用户资料编辑功能 ([7c0f43c](https://github.com/certd/certd/commit/7c0f43c8a3052f73afee3e93c9fcbc43c44ab690)) * 优化阿里云waf的日志信息 ([821c6d8](https://github.com/certd/certd/commit/821c6d807d4b3cc5092d09a6282b8cbafb9e7c9f)) * 优化中英文翻译与切换 ([acaa8b1](https://github.com/certd/certd/commit/acaa8b173183b4423584ee070e6e332e0ac0eb2d)) * 站点IP监控前先同步一下IP ([a080b60](https://github.com/certd/certd/commit/a080b606ab6e289d96b17ef7d2879b4603f889ba)) * 支持选择运行策略设置 ([60f055f](https://github.com/certd/certd/commit/60f055f293ce237c21cd9050333dad9609eceac1)) ## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20) ### Bug Fixes * 腾讯云授权支持设置是否国际站,部署到EO插件支持国际站 ([5cd3968](https://github.com/certd/certd/commit/5cd3968929acef333cf30d3b20cf21cea6c82c5f)) * 修复邮箱包含.号校验失败的bug ([65dcae7](https://github.com/certd/certd/commit/65dcae79f8faa7a6cb425e10a0fdb6758b0719f3)) ### Performance Improvements * 首次打开任务日志查看页面,自动滚动到底部 ([43fee42](https://github.com/certd/certd/commit/43fee42198e8697185b427b1fa3eb79409603393)) * 支持批量修改通知和定时 ([e11b3be](https://github.com/certd/certd/commit/e11b3becfd4abe6547e84d09adc38ebd6e1c4b87)) ## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13) ### Performance Improvements * 支持s3 access做测试 ([f00aeac](https://github.com/certd/certd/commit/f00aeacb8b5c81f0bafa4c1b76723dec2b6b7784)) ## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12) ### Bug Fixes * 修复消息内容存在()<>等括号情况下无法发送tg通知的bug ([c937583](https://github.com/certd/certd/commit/c937583a50d8513d76adead3648f83eee2fcc6f9)) * 修复重试次数设置无效的bug ([e2099ac](https://github.com/certd/certd/commit/e2099ac9ca344bc70bfa4219002e9138708973ae)) ### Performance Improvements * 授权列表类型颜色优化 ([1e86338](https://github.com/certd/certd/commit/1e863382d3d1a8cc95a1abf51e75bf6eaea3244f)) * 支持雨云dns解析 ([8354348](https://github.com/certd/certd/commit/83543487e7418683bd79cfe3b9e0d792bdb977f7)) * 支持雨云dns解析以及雨云证书更新 ([43c7a19](https://github.com/certd/certd/commit/43c7a1984926f5d4647760cc134bb0aede3a7b7a)) * github 版本检查支持执行脚本 ([bad3504](https://github.com/certd/certd/commit/bad3504d4a15e6989b967b66aa9da8c6981f25bf)) ## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09) ### Bug Fixes * 修复阿里云新加坡clb无法部署证书的bug ([c1fbc8c](https://github.com/certd/certd/commit/c1fbc8cd68ae020ef342e4e92f4d9b4869ca1ead)) * 修复阿里云新加坡clb无法部署证书的bug ([3e84e11](https://github.com/certd/certd/commit/3e84e116e863b54c6b4d7db160af372dacc5857f)) * 修复检查github release 插件无法保存最后版本的bug ([a92107c](https://github.com/certd/certd/commit/a92107cc47133883b099d5228b06373e84c8bb50)) * 修复站点监控定时器多次添加的bug ([9361679](https://github.com/certd/certd/commit/936167972fe83e519bc01a0dd961d9c0635d24ab)) ### Performance Improvements * 阿里云dns操作增加重试机制 ([424fd96](https://github.com/certd/certd/commit/424fd96615c05e949af8c837c261c1400bdffba2)) * 优化阿里云nlb支持部署扩展证书 ([9cbdfda](https://github.com/certd/certd/commit/9cbdfda829b231733d54c66c5024d46e6fc11af3)) * 子域名托管帮助链接优化为打开新窗口 ([7c0cdd1](https://github.com/certd/certd/commit/7c0cdd169e2f943e703e433677f2f437d4aa02ee)) * history增加触发类型显示 ([7f6070c](https://github.com/certd/certd/commit/7f6070c960ed7bf02add5ab36436de6573f2f1fa)) ## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07) ### Bug Fixes * 某些证书提供商的证书确实commonName导致无法转换证书的问题 ([ac87bc5](https://github.com/certd/certd/commit/ac87bc57e957ea4679707bfd38d6840e26319bed)) * 修复站点监控通知渠道设置无效的bug ([a00453c](https://github.com/certd/certd/commit/a00453c83a58114ce2873dd6e6aaf313f1ce0f87)) ### Performance Improvements * 修改 HTTPS 服务器监听地址 ([e1cf64a](https://github.com/certd/certd/commit/e1cf64ae16d4abfe4299ff16d5088c30cf3c6365)) * 优化流水线页面,增加下次执行时间、查看证书显示 ([c820315](https://github.com/certd/certd/commit/c8203154094fae3d17198747f49f5f41ddf29a4e)) * 站点证书监控支持定时设置,重试次数设置 ([d3c2f8e](https://github.com/certd/certd/commit/d3c2f8eb436e670772d14a54acd6b541c5aa3978)) * 证书申请支持letencrypt profile选项 ([2eb0e54](https://github.com/certd/certd/commit/2eb0e54909d8ad36708e07c12fd598998159bc43)) * aliyun alb支持部署扩展证书 ([2a19b61](https://github.com/certd/certd/commit/2a19b61b7a78620c06396c2cc37cc77d738b6d12)) # [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05) ### Features * 完善注释 ([6702ca1](https://github.com/certd/certd/commit/6702ca10a17f5d7dbff789b039f7269496f66b97)) * AWS 中国区 CloudFront 证书部署(IAM 证书) ([8a55bed](https://github.com/certd/certd/commit/8a55beda924b3be2a53b9ba80d9487cefa8bf887)) * **lego:** support for command options ([b84159f](https://github.com/certd/certd/commit/b84159f2f11531f058837c2e82d66499f3740f20)) ## [1.34.11](https://github.com/certd/certd/compare/v1.34.10...v1.34.11) (2025-06-05) ### Bug Fixes * 修复用户最大流水线数量校验的问题 ([919f70a](https://github.com/certd/certd/commit/919f70a5fd2842ca69f96f1659bb5a7ba3f73776)) * 修复中文域名使用cname方式校验无法通过的问题 ([f7d5baa](https://github.com/certd/certd/commit/f7d5baa6d04cb83c572b06e62f885890cfa0143a)) * 修复cv4pve sdk (proxmox插件连接失败时无法正常结束任务的bug) ([49f26b4](https://github.com/certd/certd/commit/49f26b4049a0549b0270395157e96e8f04a68bc4)) * 修复flexcdn部署证书的顶级CA名称显示 ([6467edb](https://github.com/certd/certd/commit/6467edb84324d7c80a85212675dbacedc459df83)) * 修复flexcdn证书commonNames错误的问题 ([ace363f](https://github.com/certd/certd/commit/ace363fa355436e769b27f71cc487d30d6441780)) ### Performance Improvements * 分组选择支持清空选项 ([03e2e99](https://github.com/certd/certd/commit/03e2e9949837b34eb3ea56d14a9e8a5dabc96063)) * 优化cname检查,当有冲突的cname记录时,给出提示 ([e639a8f](https://github.com/certd/certd/commit/e639a8f9f12640ffcca69f1a6a0324459924afbd)) * 增加下载日志按钮 ([6ff509d](https://github.com/certd/certd/commit/6ff509d263c0182645b4692c10b5fedb192db964)) * 站点监控支持批量导入域名和ip ([2d7729d](https://github.com/certd/certd/commit/2d7729dbe98f29088f5f317db2b52cc1ede223a6)) * 支持设置用户有效期 ([6ac3bc5](https://github.com/certd/certd/commit/6ac3bc564f407dad2cd0b0b0744e887387aa5da3)) ## [1.34.10](https://github.com/certd/certd/compare/v1.34.9...v1.34.10) (2025-06-03) ### Bug Fixes * **flexcdn:** fix cert upload and skipSslVerify required ([c48da5d](https://github.com/certd/certd/commit/c48da5dea7f0f0cdeae643b106b4a678acc3b14b)) ### Performance Improvements * 阿里云CLB支持部署到扩展域名 ([0e8339c](https://github.com/certd/certd/commit/0e8339c70190890d449099e1d26e5ed06ff135fb)) * 优化流水线名称过长时的显示 ([6a0cc1b](https://github.com/certd/certd/commit/6a0cc1b1f3ad508f9e4093b3b682b163f12389eb)) * 支持部署到飞牛OS ([ddfd0fb](https://github.com/certd/certd/commit/ddfd0fb81d6638352920261065f1ab8e27bdd564)) * 支持日志写入文件 ([37edbf5](https://github.com/certd/certd/commit/37edbf5824d6aaae68ea1ef7259c6f739d418d2c)) ## [1.34.9](https://github.com/certd/certd/compare/v1.34.8...v1.34.9) (2025-05-30) ### Bug Fixes * 修复Farcdn证书有效期错误的问题 ([1fe4c36](https://github.com/certd/certd/commit/1fe4c367f7128de9ba5e3395ae06bc81e63a7d5a)) ### Performance Improvements * 不止证书自动化,插件解锁无限可能 ([a9b302e](https://github.com/certd/certd/commit/a9b302e38d3328d75df8b2da3d8b914851e55e9c)) * 邮箱支持保存和选择 ([f7b0b44](https://github.com/certd/certd/commit/f7b0b44ef6044bec36510a6f0b06d8dca5bfce49)) * 支持github 新版本检查并发布通知 ([356703c](https://github.com/certd/certd/commit/356703c83ea18c6efb8931402e181280d7b7e696)) ## [1.34.8](https://github.com/certd/certd/compare/v1.34.7...v1.34.8) (2025-05-28) ### Bug Fixes * 更新 1panel API 版本支持v1/v2设置 ([e6195ad](https://github.com/certd/certd/commit/e6195ade3ec54b138825b8d6738f86eb8afdd720)) * 同步更新namesilo接口,修复无法创建和删除dns记录的问题 ([36b02c2](https://github.com/certd/certd/commit/36b02c2cec145c13d4ef29d49aba5b6b4f697df2)) * 修复阿里云 esa 证书获取站点列表错误的问题 ([0c2ea5d](https://github.com/certd/certd/commit/0c2ea5da4c836f8a0df132a3f22d399bd9ee1de9)) * 修复部署到华为cdn,子账号ak查询不到域名的bug ([ebb292a](https://github.com/certd/certd/commit/ebb292a2f7a425c1bc810f59468beb3f1d5bc3f0)) * 修复证书申请任务无法修改dns提供商类型的bug ([8802274](https://github.com/certd/certd/commit/88022747bebe2054223e0241d68d410771405e68)) ### Performance Improvements * 关闭腾讯云证书通知提醒 ([231a875](https://github.com/certd/certd/commit/231a875bb481420c39bf76ec9ff4e50954ab9fe4)) * 优化站点选择组件,切换选择时不刷新列表 ([3a14714](https://github.com/certd/certd/commit/3a147141b1a5d67c92a5ce88a5313eaa62859e03)) * 优化站点ip检查 ([a463711](https://github.com/certd/certd/commit/a463711b03a20120f2a298be15d71ca152d27f21)) * 站点监控支持监控IP ([9cc4c01](https://github.com/certd/certd/commit/9cc4c017ae646a18284e732769b82636feda01d3)) * 支持批量重新运行 ([8189982](https://github.com/certd/certd/commit/818998259ddc75e722196ac5c365038818539b9b)) * farcdn优化 ([a06ef07](https://github.com/certd/certd/commit/a06ef07178ed73c537e21c7d57e5e5144d2c056d)) ## [1.34.7](https://github.com/certd/certd/compare/v1.34.6...v1.34.7) (2025-05-26) ### Performance Improvements * 优化阿里云DCDN插件,支持多选 ([b091657](https://github.com/certd/certd/commit/b091657b5c537acf2442a2bfc345d0a77f5e2c50)) * 支持部署到farcdn ([e08cf57](https://github.com/certd/certd/commit/e08cf57b72128998f487ab6469868052fbce0dba)) ## [1.34.6](https://github.com/certd/certd/compare/v1.34.5...v1.34.6) (2025-05-25) ### Bug Fixes * 修复公共插件配置修改不生效的bug,优化系统设置参数注入时机 ([e1e510c](https://github.com/certd/certd/commit/e1e510ce1e37a5ae82478226b6987a83f22d1ecb)) * 修复又拍云 CDN 设置证书参数和强制 HTTPS 配置报错的bug ([7984b62](https://github.com/certd/certd/commit/7984b625ba6727132f205db8e25f790bce27b2f7)) * 修复lego模式下每次都重新申请证书的bug ([f807b8c](https://github.com/certd/certd/commit/f807b8cb465cc329fa034ecbef94e18ef394f870)) * 优化 RunnableError错误信息展示 ([36bc3ff](https://github.com/certd/certd/commit/36bc3ff22da93ba342c3c1103d7ee2bbcecf44f2)) * **cert:** 修正证书过期时间计算逻辑 ([a3086e6](https://github.com/certd/certd/commit/a3086e6a5bec8b07f5e1d21a2ca8bd969c75bd5c)) ### Performance Improvements * 二次认证页面中,添加动态验证码输入框的焦点控制,提升用户体验 ([bb22f06](https://github.com/certd/certd/commit/bb22f062ed4ab4b5b71938270fe4cc666af6b8e7)) * 添加阿里云 ESA证书部署插件 ([1db1ffd](https://github.com/certd/certd/commit/1db1ffde99ac7e4684fa606ebc4c327f829b3a26)) * 站点证书监控增加通知设置 ([3422a1a](https://github.com/certd/certd/commit/3422a1a59fd0d2c0f17fa9c7e8988ac527ecfdd9)) ## [1.34.5](https://github.com/certd/certd/compare/v1.34.4...v1.34.5) (2025-05-19) ### Performance Improvements * 1panel增加授权测试按钮 ([566b12f](https://github.com/certd/certd/commit/566b12f5d14ce10e8f5cf1807c58f7bf27f0d199)) * 优化钉钉通知标题颜色 ([a560999](https://github.com/certd/certd/commit/a560999d13eed18d08dd32ee530166569e3f8746)) * 优化飞书通知为卡片模式 ([a818a3d](https://github.com/certd/certd/commit/a818a3d293e22fb46979bc77055c05621a6fed81)) * 支持部署到宝塔aaWAF ([094565c](https://github.com/certd/certd/commit/094565ccd619ef671c6c11ce5fb7fd54a7a21d1c)) * aaWaf、cdnfly站点选择支持查询 ([8af3463](https://github.com/certd/certd/commit/8af3463668a40b9b99febb02e3b4e0d9d8d719b4)) ## [1.34.4](https://github.com/certd/certd/compare/v1.34.3...v1.34.4) (2025-05-16) ### Bug Fixes * 修复部署flexcdn问题 ([76b19a4](https://github.com/certd/certd/commit/76b19a4980f8edba5238543b82a7811e1003746c)) * 修复插件导入的bug ([677fec0](https://github.com/certd/certd/commit/677fec0a0b6fceb4966705e471bbfeeda91610c7)) * 修复导入在线插件不生效的bug ([fcf8309](https://github.com/certd/certd/commit/fcf8309c238208281ecb4575b2c3cfe50c11d783)) * 修复自建插件保存丢失部署策略的bug ([863e74d](https://github.com/certd/certd/commit/863e74dd2e3912f950ff5025b5ed0070aeb37035)) ### Performance Improvements * 调整小助手,仅在登录之后显示 ([aebb07c](https://github.com/certd/certd/commit/aebb07c5cc8b1f233b9d203ff017ac60e6971a85)) ## [1.34.3](https://github.com/certd/certd/compare/v1.34.2...v1.34.3) (2025-05-15) ### Performance Improvements * 宝塔插件、1panel 改成完全免费版 ([a53b6cd](https://github.com/certd/certd/commit/a53b6cd28ff2ce5662ada82379ea44a06b179b81)) * 添加 FlexCDN 更新证书插件 ([bf040d4](https://github.com/certd/certd/commit/bf040d4c428d29c06fbaca5e29100e0c583b2b0b)) * 小助手可以关闭 ([3e2101a](https://github.com/certd/certd/commit/3e2101aa5b56548614102e900d59819ce8c7e97c)) * 支持部署到maoyun cdn ([68f333f](https://github.com/certd/certd/commit/68f333fb87ce85eed27436ecb0f76351c0ccb0d1)) * 支持AI分析报错 ([aa96859](https://github.com/certd/certd/commit/aa96859798166426e485947a6590464de189de05)) ## [1.34.2](https://github.com/certd/certd/compare/v1.34.1...v1.34.2) (2025-05-11) ### Bug Fixes * 修复部署到又拍云强制https无效的bug ([2397097](https://github.com/certd/certd/commit/2397097e4ddcb6f593210598e8779ffd44ac3f8f)) * 修复刷新流水线页面后,日志不自动更新的bug ([0b2e28b](https://github.com/certd/certd/commit/0b2e28b62dd5eb6804c602083e65c87a9d1d72d2)) ### Performance Improvements * 集成智能问答机器人 ([9dd4905](https://github.com/certd/certd/commit/9dd49054d18ec436a5029444ca55a38adc682933)) * 支持设置网安备案号 ([d18e431](https://github.com/certd/certd/commit/d18e431e2f08e6b37704032c4ea6fbdd8e971442)) * http方式支持校验443端口 ([d75fcb7](https://github.com/certd/certd/commit/d75fcb7fec421a9a638eaa27fe9378c84b5e0f19)) ## [1.34.1](https://github.com/certd/certd/compare/v1.34.0...v1.34.1) (2025-05-05) ### Bug Fixes * 根据SOA记录判断子域名托管有缺陷,改回手动配置子域名托管记录的方式 ([1b280a2](https://github.com/certd/certd/commit/1b280a2940f9e2d919b0bf23b89cc185be1fa498)) * 修复宝塔授权测试按钮显示错误的bug ([048696e](https://github.com/certd/certd/commit/048696ee9386491bb68592fb3a47d1c900bb68bf)) ### Performance Improvements * 支持部署证书到火山dcdn ([5f85219](https://github.com/certd/certd/commit/5f852194953dc1b4e6336770f417507b8f5a33ad)) * 支持部署证书到unicloud ([a63d687](https://github.com/certd/certd/commit/a63d687f1c573159f0857693f37602b0e1e44072)) # [1.34.0](https://github.com/certd/certd/compare/v1.33.8...v1.34.0) (2025-04-28) ### Bug Fixes * 修复二次认证登录进入错误账号的bug ([e3930e0](https://github.com/certd/certd/commit/e3930e07172dd7903cb0f6ff26e0e3e828ba3e77)) ### Features * 从yaml文件注册插件 ([deb3893](https://github.com/certd/certd/commit/deb38938204b29543f36d3266249958faaaa6b66)) ### Performance Improvements * 优化cdnfly插件,支持自动匹配域名部署 ([afd59e9](https://github.com/certd/certd/commit/afd59e9933b2650f41c5d47684c171b93b962065)) ## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26) ### Bug Fixes * 服务器时间获取不准确的bug ([5d10cbf](https://github.com/certd/certd/commit/5d10cbf18daf94a90a7551641a3b13e3c5fec611)) * 修复复制流水线无效的bug ([3df20a9](https://github.com/certd/certd/commit/3df20a924f32970b052e2588ea20de095f0ea693)) * 修复http上传方式无法清除记录文件的bug ([72a7b51](https://github.com/certd/certd/commit/72a7b51d479602b2c54c6c3ac8d8a0dcb9664e73)) * 修复token过期后,疯狂打印token过期信息的bug ([50a5fa1](https://github.com/certd/certd/commit/50a5fa15bb240a125bbc91d2ce1ff3c835888a77)) ### Performance Improvements * 从域名的soa获取主域名,子域名托管无需额外配置 ([a586a92](https://github.com/certd/certd/commit/a586a92d5e32ea846ac37be52a7ad8c328d89966)) * 七牛oss支持删除过期备份 ([b7113bd](https://github.com/certd/certd/commit/b7113bda2378116d6c116dc583f563cce7cf9f00)) * 数据库备份支持oss ([308d460](https://github.com/certd/certd/commit/308d4600efe2002f199c33b4594d3071784e58ea)) * 支持阿里云中文域名申请 ([b3468cf](https://github.com/certd/certd/commit/b3468cf7f28228d7c9cf68de6b5a9bbeb67f2c6d)) * 支持反向代理增加contextPath路径 ([0088929](https://github.com/certd/certd/commit/0088929622160cc922995de9a563e8061686ff34)) * 支持中文域名 ([162ebfd](https://github.com/certd/certd/commit/162ebfd4e0c25727efb33952d3bbf7420a02e2c3)) ## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22) ### Performance Improvements * 添加部署证书至火山 Live ([abea80e](https://github.com/certd/certd/commit/abea80e3ab9b1672aebe1c5d5e856693b29931a8)) * 优化首页插件列表展示 ([9b8f60b](https://github.com/certd/certd/commit/9b8f60b64b5f9a3db7dfa9b3dcbd9201984358d0)) * 证书申请支持51dns ([8638fc9](https://github.com/certd/certd/commit/8638fc91ff34fccaf12ff9874fd3fa9d2a8c18b7)) * 支持51dns ([96a0900](https://github.com/certd/certd/commit/96a0900edc95dcfd9acccf9d13592f12f5a09b3d)) * ssh PTY模式登录设置 ([8385bcc](https://github.com/certd/certd/commit/8385bcc2d7f2411a07748bb5c53f9eaf4d38d7cc)) * ssh伪终端模式优化,windows下不开启 ([42dfe93](https://github.com/certd/certd/commit/42dfe936b773b7bdd82ca3378363252ffffd7b71)) ## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20) ### Bug Fixes * 上传商用证书,直接粘贴文本报错的问题;修复无法上传ec加密证书的bug ([5750bb7](https://github.com/certd/certd/commit/5750bb706779da274d8e7a87e71416cb64d2df79)) * 修复下载证书时提示token已过期的问题 ([0e07ae6](https://github.com/certd/certd/commit/0e07ae6ce84dcb9279d3c44060d621566afa593c)) ### Performance Improvements * 更新license时同时绑定url ([78367af](https://github.com/certd/certd/commit/78367af8307f801e778c76d49f0918c21ffe032f)) * 切换到不同的分组后再打开创建对话框,会自动选择分组 ([893dcd4](https://github.com/certd/certd/commit/893dcd4f2487891199ed3e5a3d47a79a75efc942)) * 新增部署到火山引擎ALB/CLB、上传到证书中心 ([c9a3e3d](https://github.com/certd/certd/commit/c9a3e3d9d26f964c7af7b56667936f1414fbf42a)) * 优化/api缓存为0 ([dc05cd4](https://github.com/certd/certd/commit/dc05cd481f186b13375192be965000e6b4b429a5)) * 优化华为cdn插件引用ccm证书 ([b565b4b](https://github.com/certd/certd/commit/b565b4b3b919b71b98ea2517670bc1ef00e00dc9)) * 优化证书流水线创建,支持选择分组 ([d613aa8](https://github.com/certd/certd/commit/d613aa8f3e85d8dc475ef1b62d49394ce7fd7d24)) ## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17) ### Performance Improvements * 登录支持双重认证 ([48aef25](https://github.com/certd/certd/commit/48aef25b3f6499d674ca4e4ef16f4c62399fb735)) * 多重认证登录 ([0f82cf4](https://github.com/certd/certd/commit/0f82cf409bc60706ab07e4ca4f272b9a1ca7eecb)) * 优化部署到华为云CDN,支持先上传到ccm,再使用证书id部署,修复offline状态下导致部署报错的bug ([79df39a](https://github.com/certd/certd/commit/79df39acabab10ae7e1864dadcdc186bb007a3c5)) ## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15) ### Bug Fixes * 补充类型断言 ([2143dff](https://github.com/certd/certd/commit/2143dff2ae96e6a78bef9f0498e36f8cd9e6941f)) * 修复腾讯云部署到任意资源插件,无法使用之前已上传的腾讯云证书问题 ([32c714d](https://github.com/certd/certd/commit/32c714d1b6e68c71a74a7452115040c87ac4bfdc)) ### Performance Improvements * 插件支持导入导出 ([cf8abb4](https://github.com/certd/certd/commit/cf8abb45282070c8ba91469f93fd379fabf1f74a)) * 支持上传证书到华为云CCM ([cfd3b66](https://github.com/certd/certd/commit/cfd3b66be9ebf53a26693057e70ed60c3f116be9)) ## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14) ### Bug Fixes * 修复登录错误次数过多阻止再次登录逻辑 ([bf4d191](https://github.com/certd/certd/commit/bf4d191c8bd2f9209eb6768f662b9c77de99e998)) ## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12) ### Bug Fixes * 修复某些情况下无法输出日志的bug ([70101bf](https://github.com/certd/certd/commit/70101bfa7ade65678d9202c804bbae2cb808b594)) ### Performance Improvements * 修复内置插件分页查询逻辑 ([a2710dd](https://github.com/certd/certd/commit/a2710ddc2525e4e637fd157f0180e6d3b801c8be)) ## [1.33.1](https://github.com/certd/certd/compare/v1.33.0...v1.33.1) (2025-04-12) ### Bug Fixes * 修复阿里云cdn证书部署失败问题,增加certname参数传入 ([965dc2c](https://github.com/certd/certd/commit/965dc2cb476f690af716f291c6b20ba98be0c8f0)) * 修复ssh插件报length空指针的bug ([9c4cbe1](https://github.com/certd/certd/commit/9c4cbe17a22b548611cf1fbefecc83a421788e42)) ### Performance Improvements * 镜像支持armv7 ([f78cbed](https://github.com/certd/certd/commit/f78cbed4d817859721fdafe7d348864848d0dfbf)) # [1.33.0](https://github.com/certd/certd/compare/v1.32.0...v1.33.0) (2025-04-11) ### Bug Fixes * 升级mysql驱动,支持mysql8最新版本的认证 ([2f5ed3a](https://github.com/certd/certd/commit/2f5ed3aead97641f2c80d692a50226839016df0b)) * 修复eab授权,没有email绑定的bug ([2f1683b](https://github.com/certd/certd/commit/2f1683b26acebbfb7d6e2d751435be04a4e7cab4)) ### Features * 支持在线自定义插件,无需源码开发 ([d0d9d68](https://github.com/certd/certd/commit/d0d9d68fe6740f6ff49fe40b7c9917c5a2e4b442)) * **lego:** support set key type ([f3bf4fa](https://github.com/certd/certd/commit/f3bf4faee0be5bdbfdbcf70a502849ed4c8ed4c4)) * release image to ghcr ([9b536af](https://github.com/certd/certd/commit/9b536af9e656dc89e2a87078c129cad6f591e467)) ### Performance Improvements * 修复tab页缓存问题 ([64e5449](https://github.com/certd/certd/commit/64e5449ab3c6b219b0e89eddad14bfb6b71a0650)) * 隐藏运行策略选项 ([2951df0](https://github.com/certd/certd/commit/2951df0cd94c23e2efee84ff1b843055aac56cae)) * 增加手动上传证书功能说明 ([5d083a1](https://github.com/certd/certd/commit/5d083a153637caddbc6f44e915d9fb2d1ae87b33)) # [1.32.0](https://github.com/certd/certd/compare/v1.31.11...v1.32.0) (2025-04-04) ### Bug Fixes * 创建cname记录移除域名两端的空格 ([903a413](https://github.com/certd/certd/commit/903a4131ab5f42c8286cd2150ed1032d486fda2f)) * 修复从本地dns获取记录报错的bug ([c39b1bf](https://github.com/certd/certd/commit/c39b1bf823ddc6216bed2049e4c87e6107def08a)) ### Features * 优化证书申请速度,修复某些情况下letsencrypt 校验失败的问题 ([857589b](https://github.com/certd/certd/commit/857589b365c6f709e0ae67914d2f50ce182e6dd6)) ### Performance Improvements * 优化华为dns解析记录创建和删除问题 ([0948c5b](https://github.com/certd/certd/commit/0948c5bc691d2ee6eb47c72a85da1b7453361878)) * 又拍云支持云存储 ([9339b78](https://github.com/certd/certd/commit/9339b78f801d193472c0af25749e8e7a27ffb7af)) * 又拍云支持云存储 ([8449f85](https://github.com/certd/certd/commit/8449f8580da90c1f6b5d02d07c3236ebaf6cf161)) ## [1.31.11](https://github.com/certd/certd/compare/v1.31.10...v1.31.11) (2025-04-02) ### Bug Fixes * 修复ssh支持键盘事件登录 ([8145808](https://github.com/certd/certd/commit/8145808c4370364377b4ffe3ae88ff465b49f20b)) ### Performance Improvements * 支持部署到京东云cdn ([6f17c70](https://github.com/certd/certd/commit/6f17c700b84965baa01b40fe2abaa0a91bcbaffd)) * 支持京东云dns申请证书 ([04d79f9](https://github.com/certd/certd/commit/04d79f9117670be504960b018fd49ae3bf7c1c11)) ## [1.31.10](https://github.com/certd/certd/compare/v1.31.9...v1.31.10) (2025-03-29) ### Performance Improvements * tab增加图标显示 ([a03ae5a](https://github.com/certd/certd/commit/a03ae5a216a1df2c1d3da12ae18dcd0f089a92d3)) * 升级lego版本到4.22.2 ([4e15556](https://github.com/certd/certd/commit/4e15556e5e8100719497edb1729570d5a29668e1)) * 优化华为dns接口报错信息输出 ([bf30b7a](https://github.com/certd/certd/commit/bf30b7afaef623dd8126570344f1fcc2c06f1215)) ## [1.31.9](https://github.com/certd/certd/compare/v1.31.8...v1.31.9) (2025-03-28) ### Bug Fixes * 修复华为云dns接口请求出错的bug ([caa15b4](https://github.com/certd/certd/commit/caa15b47355363cbb8847f415ff12363cd53eeda)) * 修复某些情况下站点证书监控报undefined.includes的错误 ([0b6618f](https://github.com/certd/certd/commit/0b6618ff709322a0eeba78953c8c6e9d073d083a)) * 修复网站证书监控https port设置无效的bug ([cc8da0c](https://github.com/certd/certd/commit/cc8da0cf130f0c469371b59ac5bd04567f4a4414)) ### Performance Improvements * 站点监控保存时异步检查 ([993bc74](https://github.com/certd/certd/commit/993bc7432fce2d954e9897ed85b54f22150bfc7e)) * dns支持火山引擎 ([99ff879](https://github.com/certd/certd/commit/99ff879d93658c29ea493a4bde7e9e3f85996d64)) ## [1.31.8](https://github.com/certd/certd/compare/v1.31.7...v1.31.8) (2025-03-26) ### Bug Fixes * 修复编辑通知勾选默认,导致出现多个默认通知的bug ([6cd7bdd](https://github.com/certd/certd/commit/6cd7bddc37da8b0d7b9860fd9a26ddfe84c869a7)) * 修复网站监控无法设置端口的bug ([27a8a57](https://github.com/certd/certd/commit/27a8a57cf52b4bf83d628aa3049be1efaa74f29c)) * 修复lego模式无法创建流水线的bug ([687bb8a](https://github.com/certd/certd/commit/687bb8a2376d0de7b72739a174e4a9560581f866)) ### Performance Improvements * 优化通知格式 ([c3c5006](https://github.com/certd/certd/commit/c3c5006daa39c20624cb58864f2b92b230a38a7a)) * 优化scp上传 ([e51123a](https://github.com/certd/certd/commit/e51123a95131cc76d655937488caf08956a67020)) * 优化txt本地校验效率 ([fd507f2](https://github.com/certd/certd/commit/fd507f269253607e68c5c099c99e0de11636f229)) * 支持又拍云cdn ([fd0536b](https://github.com/certd/certd/commit/fd0536bd4b41f15b6b5d42e0b447f0dcbf73b8a8)) * 支持又拍云cdn ([57389a7](https://github.com/certd/certd/commit/57389a79a1a61c45d081712562f8b33c9633158e)) ## [1.31.7](https://github.com/certd/certd/compare/v1.31.6...v1.31.7) (2025-03-24) ### Performance Improvements * 增加服务器时间警告 ([d66ade4](https://github.com/certd/certd/commit/d66ade4e4783850b6c7625c6f164a5a0fc0aa509)) * 支持部署到lucky ([e18e399](https://github.com/certd/certd/commit/e18e399ce6529e8c7e36b56c5f674cfdbbd3d3d1)) ## [1.31.6](https://github.com/certd/certd/compare/v1.31.5...v1.31.6) (2025-03-24) ### Bug Fixes * 修复dns.la无法申请证书的bug ([90b045a](https://github.com/certd/certd/commit/90b045af6d1a4f46986e4b118885c1f050df067c)) ### Performance Improvements * 上传到主机支持scp方式 ([05b6159](https://github.com/certd/certd/commit/05b6159802b9e85b6a410361b60b5c28875b48e7)) * 优化图标 ([c56f48c](https://github.com/certd/certd/commit/c56f48c1e3c54c4e203fafb380d9091d75681b7e)) ## [1.31.5](https://github.com/certd/certd/compare/v1.31.4...v1.31.5) (2025-03-22) ### Bug Fixes * 修复通知选择器无法选择的bug ([f7b88f9](https://github.com/certd/certd/commit/f7b88f9e3b7d9d9122e4fd2003a20c555bd50c7d)) * 修复证书流水线创建失败的bug ([736fe03](https://github.com/certd/certd/commit/736fe038ebda56648bcc4c12884a700341d2c049)) ## [1.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21) ### Bug Fixes * 修复站点监控通知通过webhook发送失败的bug ([9be1ecc](https://github.com/certd/certd/commit/9be1ecc8aab3ea23dd0dc2dab3688f4edb90ef2c)) * 修复dns.la域名申请失败的bug ([1de8eee](https://github.com/certd/certd/commit/1de8eee6ea8307f3c11626af75303d3cc104bb95)) ### Performance Improvements * 宝塔支持doker站点证书部署 ([589a373](https://github.com/certd/certd/commit/589a373142ef7f50d64d3aa767a90b1f4b64da93)) * 保存调整后的列宽 ([873f2b6](https://github.com/certd/certd/commit/873f2b618b9d7320045baf69d6da83afe48a780f)) * 创建证书流水线时,支持更多参数展开 ([36aa7f8](https://github.com/certd/certd/commit/36aa7f82b078a053a102331b3c6f132fb9d492f9)) * 流水线页面可以鼠标按住左右拖动 ([d85a02f](https://github.com/certd/certd/commit/d85a02feeb3183c5abd6c1ea790d5923a32d7271)) * 流水线增加上传证书快捷方式 ([425bba6](https://github.com/certd/certd/commit/425bba67c539b734e2a85a83a4f9ecc9b2434fb4)) * 手动上传证书部署流水线 ([fbb66f3](https://github.com/certd/certd/commit/fbb66f3c4389489aa8a43b194d82bc8cf391607b)) * 优化选择任务时手机版展示效果 ([d01004d](https://github.com/certd/certd/commit/d01004d53071a75ac91ee21cc96bde9369f77ff3)) * 站点监控,手动测试也发通知 ([729b19c](https://github.com/certd/certd/commit/729b19c8da60d5efb5baef7cf8df0518e7f6b471)) * 站点证书监控支持模糊查询 ([0069c0e](https://github.com/certd/certd/commit/0069c0e3992946a8dd6410f299d4fc974ef0e76b)) * 支持飞书通知 ([b82e1dc](https://github.com/certd/certd/commit/b82e1dcd6217b09a7d7e21cd648bb31de320cadf)) * 支持手动上传证书并部署 ([a9fffa5](https://github.com/certd/certd/commit/a9fffa5180c83da27b35886aa2e858a92a2c5f94)) ## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13) ### Bug Fixes * 修复阿里云fc获取不到列表的bug ([474b337](https://github.com/certd/certd/commit/474b3372d8ce98e6d45900bf8046bc0b3f220686)) ### Performance Improvements * 1panel支持 apikey方式授权 ([170b2af](https://github.com/certd/certd/commit/170b2afb0e3b125e4ed057f633fe895b5ac3ac22)) * 套餐支持3天7天等选项 ([0d71a8e](https://github.com/certd/certd/commit/0d71a8ee501a0e5bb69decf07e8729026e9d85bf)) * 证书仓库增加有效期显示 ([be87124](https://github.com/certd/certd/commit/be87124ada7a093f281ca29a45c86b4ea4644ead)) * 支持部署到天翼云CDN ([82a72e0](https://github.com/certd/certd/commit/82a72e0b497efa043d342ad0e33c083a2de79a05)) * 支持dns.la ([ee8af18](https://github.com/certd/certd/commit/ee8af18d0ac0af82544d6dda1e4b4c678b733041)) * cf授权支持配置http代理 ([27386ea](https://github.com/certd/certd/commit/27386ea04d3c1a5aebe3cfdd7ac48185eaa76629)) ## [1.31.2](https://github.com/certd/certd/compare/v1.31.1...v1.31.2) (2025-03-12) ### Bug Fixes * 修复cname记录查找bug ([95fb4e3](https://github.com/certd/certd/commit/95fb4e3e8be6ca13cc43b451f6141d62190ba453)) ## [1.31.1](https://github.com/certd/certd/compare/v1.31.0...v1.31.1) (2025-03-11) ### Performance Improvements * 一些手机端适配优化 ([5b8d5dd](https://github.com/certd/certd/commit/5b8d5dd97536456a9d5d1384216eac1093b2dc3d)) # [1.31.0](https://github.com/certd/certd/compare/v1.30.6...v1.31.0) (2025-03-10) ### Bug Fixes * 修复CDN插件我爱云因更换接口导致部署失败的问题 ([5641c19](https://github.com/certd/certd/commit/5641c19502970f67af19709bddf8c781b1a25bdc)) * 修复CDN插件我爱云因更换接口导致部署失败的问题 ([0110dfd](https://github.com/certd/certd/commit/0110dfdb70b12dfb0a7a067717f3773ed75aae7c)) * 修复webhook headers value中带等号是解析错误的bug ([1fe3365](https://github.com/certd/certd/commit/1fe3365e10c464c4c60c82f424cf74fe35b883e0)) * ProxmoxUploadCert 增加强制部署证书 ([441b15e](https://github.com/certd/certd/commit/441b15ed2fe5a143a5bd5508613b3816ddbff596)) ### Performance Improvements * 历史记录查看详情,可以切换到对应的历史记录日志上去 ([082802e](https://github.com/certd/certd/commit/082802e1197156837800f814728ee0f6b300b18c)) * 流水线同一个阶段任务优化为并行执行 ([efa9c74](https://github.com/certd/certd/commit/efa9c748c5c07fc950af3db742ef9310f1ac9a4b)) * 升级midwayjs版本 ([057b0b4](https://github.com/certd/certd/commit/057b0b4565e19bb93195633f767b2942e8e40e59)) * 是否允许爬虫爬取增加ui设置选项 ([779db9d](https://github.com/certd/certd/commit/779db9da705d2dfef36fec21f52bd38af9fc5f2e)) * 通知支持钉钉群聊机器人 ([fc8bef5](https://github.com/certd/certd/commit/fc8bef5aae522d75d408d8c3aa74543269da5398)) * 易支付支持固定支付方式,适合没有收银台版本使用 ([81df96b](https://github.com/certd/certd/commit/81df96bf4542ce8d8ef4a428a4460dd554e4719a)) * 支持易盾RCDN部署 ([065713c](https://github.com/certd/certd/commit/065713cdb6953d16df08585c316c1a7a8eaec437)) ## [1.30.6](https://github.com/certd/certd/compare/v1.30.5...v1.30.6) (2025-02-24) ### Performance Improvements * 禁止爬虫爬取本网站 ([5164116](https://github.com/certd/certd/commit/5164116bde60dabac774cdf94f5317ff386e95ca)) * 上传到阿里云证书名称后缀增加毫秒时间戳 ([9f0ee21](https://github.com/certd/certd/commit/9f0ee219d02907ffe128a5cf10173397d934ccd7)) * 支持部署到阿里云FC3.0 ([bcaf54d](https://github.com/certd/certd/commit/bcaf54d4cb7bc469486aae6cdb127ae017eb3abb)) * 支持新版本LeCDN ([44d43f4](https://github.com/certd/certd/commit/44d43f45cb9094619df7494c2a64a51ba77ad116)) ## [1.30.5](https://github.com/certd/certd/compare/v1.30.4...v1.30.5) (2025-02-14) **Note:** Version bump only for package root ## [1.30.4](https://github.com/certd/certd/compare/v1.30.3...v1.30.4) (2025-02-14) ### Bug Fixes * 适配最新版1panel密码编码方式 ([78044c0](https://github.com/certd/certd/commit/78044c062e20cdd04f08baef9fb6745bf25eddcf)) ## [1.30.3](https://github.com/certd/certd/compare/v1.30.2...v1.30.3) (2025-02-13) ### Bug Fixes * 修复腾讯云CLB多域名同证书部署报错的bug ([c3a5542](https://github.com/certd/certd/commit/c3a55429357e78f4b78c9592d3e5897db2d4d549)) * 修复新版本1panel密码需要加密,无法登录的问题 ([ada0b71](https://github.com/certd/certd/commit/ada0b7106e97e551783829e4e719f76793a7123d)) ## [1.30.2](https://github.com/certd/certd/compare/v1.30.1...v1.30.2) (2025-02-09) ### Bug Fixes * 当前置任务被删除时进行校验 ([c89686a](https://github.com/certd/certd/commit/c89686a2fda251484930f0ae715417b618c21690)) * 修复cloudflare删除解析记录报错的bug ([00c2da4](https://github.com/certd/certd/commit/00c2da444f84adb89f3f1226d03294d7c6e3e4f1)) ### Performance Improvements * 上传自定义证书 ([75a38d9](https://github.com/certd/certd/commit/75a38d95f305b4271d9106babe7cffc1c89ae8f3)) ## [1.30.1](https://github.com/certd/certd/compare/v1.30.0...v1.30.1) (2025-01-20) ### Bug Fixes * 修复部署到阿里云ALB、NLB插件加载混乱的bug ([6ab83b6](https://github.com/certd/certd/commit/6ab83b662a2c5e715b9cb7eb1244de2ebb7f47b0)) * 修复腾讯clb重复执行会报错的bug ([e95d29f](https://github.com/certd/certd/commit/e95d29f446d06eced315a3087fc9e105a30b20bd)) * 修复tg消息内容中存在.和*就会发送失败的bug ([ae5dfc3](https://github.com/certd/certd/commit/ae5dfc3bee950267123ae2fbd1c11e7ce36626ea)) ### Performance Improvements * 创建流水线时,默认成功时也发送通知 ([52ae690](https://github.com/certd/certd/commit/52ae6902d203ca56e0312692b50c55cb6ddd3e39)) * http方式校验,选择sftp时,支持修改文件访问权限比如777 ([15d6eaf](https://github.com/certd/certd/commit/15d6eaf5532ed25acd4f8d58c429353a2f44206c)) # [1.30.0](https://github.com/certd/certd/compare/v1.29.5...v1.30.0) (2025-01-19) ### Bug Fixes * 修复查看任务日志偶发性无法自动滚动底部的bug ([7e482f7](https://github.com/certd/certd/commit/7e482f798c0142bce1866f84676cb40210f9638a)) * 修复namesilo ttl太短的问题 ([865f26d](https://github.com/certd/certd/commit/865f26d75c0d3dd4dc8b41448f8830068e45957c)) ### Features * 支持open api接口,根据域名获取证书 ([52a4fd3](https://github.com/certd/certd/commit/52a4fd33180e9b3f71b8dc9f7671d7cd8e448c3b)) ### Performance Improvements * 证书仓库 ([91e7f45](https://github.com/certd/certd/commit/91e7f45a1c5ea1e0ec0aa3236b80028f03a6d0aa)) * 支持部署到阿里云ALB ([653940a](https://github.com/certd/certd/commit/653940a0ca64fc380178c1b0b58ae0af64dfaf07)) * 支持部署到阿里云NLB、SLB ([c085bac](https://github.com/certd/certd/commit/c085bac5d877c4250a8a79e17eb8673b8e4fc89c)) * 支持部署到腾讯云直播 ([417d37b](https://github.com/certd/certd/commit/417d37b199b79a42f790f9edab8f178eedf8fbf7)) * 支持部署证书到proxmox ([d10795e](https://github.com/certd/certd/commit/d10795ecd97eb8cf2ffa46aabfdbfc6812636396)) ## [1.29.5](https://github.com/certd/certd/compare/v1.29.4...v1.29.5) (2025-01-07) ### Bug Fixes * 修复复制到本机插件,pfx格式复制时报错的bug ([f57116d](https://github.com/certd/certd/commit/f57116d2bebf33e47ad93e0b39c4efe8e4aea25c)) * 修复授权管理,点击了查看原文按钮后,无法修改值的bug ([85c99f7](https://github.com/certd/certd/commit/85c99f7f80761ac6efaf3255c03b933442db1686)) ## [1.29.4](https://github.com/certd/certd/compare/v1.29.3...v1.29.4) (2025-01-06) ### Bug Fixes * 修复站点监控域名校验无法通过的bug ([1cb4a53](https://github.com/certd/certd/commit/1cb4a539cc523721ffd4b22d40d0e3d2d68cd915)) ### Performance Improvements * 优化腾讯云CLB插件,支持非sni情况,sni情况支持填写多个域名 ([635b042](https://github.com/certd/certd/commit/635b042690637bff85e97e07c7aac4b87a8a124b)) ## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04) ### Bug Fixes * 修复系统级授权无法查看密钥的bug ([8644348](https://github.com/certd/certd/commit/8644348fc41ae2e1672f946ca37e5d3a674e0218)) ### Performance Improvements * 优化站点证书检查页面,检查增加3次重试 ([e6dd7cd](https://github.com/certd/certd/commit/e6dd7cd54a3e23897031b5df6e0c3cdc0545d35a)) * 优化acme sdk ([54db744](https://github.com/certd/certd/commit/54db74428259de64d12230c2ab7353ae11197bbc)) * 支持http校验方式申请证书 ([405591c](https://github.com/certd/certd/commit/405591c5d08fa1a3b228ee3980199e7731cfec4a)) * http校验方式,支持七牛云oss、阿里云oss、腾讯云cos ([3f74d4d](https://github.com/certd/certd/commit/3f74d4d9e5f5d0e629b44cff1895b3f7a8fbcafc)) ## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25) ### Bug Fixes * 修复套餐关闭状态下,仍然限制用户流水线数量的bug ([66fb9e5](https://github.com/certd/certd/commit/66fb9e5f49491f9c159363b48af14720a37673b1)) ## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25) ### Bug Fixes * 免费套餐支持购买 ([f5ec987](https://github.com/certd/certd/commit/f5ec9870fd6af1f0c9099852bbdb4d07813ccce8)) * 修复某处金额转换丢失精度的bug ([d2d6f12](https://github.com/certd/certd/commit/d2d6f12218cbe7bd55f4ae082b93084be85f0a7b)) * 修复新版本小红点显示错误问题 ([fe4786e](https://github.com/certd/certd/commit/fe4786e168afe03a5243dd67971476c348339809)) ### Performance Improvements * 用户创建证书流水线没有购买套餐或者超限时提前报错 ([472f06c](https://github.com/certd/certd/commit/472f06c2d190d0ae48e8b53c18bc278437656a1c)) * 优化插件名称显示 ([26adf7d](https://github.com/certd/certd/commit/26adf7d437e674385f26a8f92fded6521a620671)) # [1.29.0](https://github.com/certd/certd/compare/v1.28.4...v1.29.0) (2024-12-24) ### Bug Fixes * 修复手机模式下,查询框被文字遮盖的bug ([040788c](https://github.com/certd/certd/commit/040788c793642c3bb2a3ede87fe30fcf3be471bd)) * 修复左侧菜单收起时无法展开子菜单的bug ([0056223](https://github.com/certd/certd/commit/005622307e612717a5408aa1484717ef03003a22)) ### Features * 基础版不再限制流水线数量 ([cb27d4b](https://github.com/certd/certd/commit/cb27d4b4906b2782eaceb0a95bbdc5d0534370d2)) * 套餐购买支持易支付、支付宝支付 ([faa28f8](https://github.com/certd/certd/commit/faa28f88f954cba4c1dd29125562e5acd2fd99af)) * 用户套餐,用户支付功能 ([a019956](https://github.com/certd/certd/commit/a019956698acaf2c4beb620b5ad8c18918ead6a1)) * 站点证书监控 ([9c8c7a7](https://github.com/certd/certd/commit/9c8c7a781223f4217f45510db1e89495600e3cd5)) * 支持微信支付 ([45d6347](https://github.com/certd/certd/commit/45d6347f5b6199493b11aabdd74177f6dca2cea4)) ### Performance Improvements * 调整创建证书表单字段的顺序 ([d393521](https://github.com/certd/certd/commit/d3935219f2aa50d6662c5b5ebf7ee25ad696ab2b)) * 同一时间只允许一个套餐生效 ([8ebf95a](https://github.com/certd/certd/commit/8ebf95a222a900d1707716c7b1f3b39f8a6d8f94)) * 用户名支持修改 ([89c7f07](https://github.com/certd/certd/commit/89c7f070343e86453c84677ebe1669f9b266d871)) * 优化证书申请跳过的状态显示,成功通知现在在跳过时不会发送 ([67d762b](https://github.com/certd/certd/commit/67d762b6a520f1fa24719a124e5ae975a81f5f82)) * 站点证书监控通知发送,每天定时检查 ([bb4910f](https://github.com/certd/certd/commit/bb4910f4e57234e42b44505f4620ae7af66025c5)) * 支持一体证书 ([53c38cf](https://github.com/certd/certd/commit/53c38cf714a6f7486abbf1d71c9f48f56a790100)) * 支持plesk网站证书部署 ([eda45c1](https://github.com/certd/certd/commit/eda45c1528199648b3970505e87f492d398226cd)) ## [1.28.4](https://github.com/certd/certd/compare/v1.28.3...v1.28.4) (2024-12-12) ### Bug Fixes * 修复证书成功通知发送失败的bug ([0f5c690](https://github.com/certd/certd/commit/0f5c69040ba77340c909813220a26bc7ddada3ea)) ### Performance Improvements * 群晖支持6.x ([79f7ec4](https://github.com/certd/certd/commit/79f7ec4672f4fd5744cc45e4a6f104da943f4026)) ## [1.28.3](https://github.com/certd/certd/compare/v1.28.2...v1.28.3) (2024-12-12) ### Bug Fixes * 修复没有配置eab时,报order无法读取的问题 ([657a2ae](https://github.com/certd/certd/commit/657a2ae032e6f61ac27fbdd26c7bf169c041219e)) * 修复授权被删除后,无法清空的bug ([b45977c](https://github.com/certd/certd/commit/b45977c29a29084c11e496bec3415eaaebafdd74)) * mysql下access.setting字段改成text ([b7f5740](https://github.com/certd/certd/commit/b7f5740c57743914f754f3b4fdd94b59a2e8338c)) ### Performance Improvements * 点击版本红点按钮,跳转到升级帮助页面 ([454fbda](https://github.com/certd/certd/commit/454fbda581bbe22abca5b91e5086ea9d9d58a020)) * 通知标题优化 ([ff083ce](https://github.com/certd/certd/commit/ff083ce6848a8bee3c8248e4b881086ae1517c28)) * 支持腾讯虚拟机开关机([@wujingke](https://github.com/wujingke)) ([8039e8b](https://github.com/certd/certd/commit/8039e8baf83c82d03f1a6198cf61c372026b962b)) * 支持aws cloudfront ([0ae39f1](https://github.com/certd/certd/commit/0ae39f160a7c6b6696b3bf513d68aa28905810ad)) ## [1.28.2](https://github.com/certd/certd/compare/v1.28.1...v1.28.2) (2024-12-09) ### Bug Fixes * 修复创建流水线通知设置无效的bug ([498cf34](https://github.com/certd/certd/commit/498cf34999fddfa24ce088e2e678469fa669abb8)) * 修复流水线分组可以被所有人看见的bug ([a0e838d](https://github.com/certd/certd/commit/a0e838d1eec918e5dc92fe95dc72ac14facb930e)) ### Performance Improvements * 优化数据表索引 ([228fdf0](https://github.com/certd/certd/commit/228fdf0a0d28013f5dd156a97bbde80537e8e97e)) * 支持mysql ([7cde1fd](https://github.com/certd/certd/commit/7cde1fdc4a9ed851900d231a5460c8dbfbcd148e)) ## [1.28.1](https://github.com/certd/certd/compare/v1.28.0...v1.28.1) (2024-12-08) ### Bug Fixes * 修复cname排查方法 nslookup命令显示黑色的问题 ([3dfeeec](https://github.com/certd/certd/commit/3dfeeec899d7d0d7292695ce410f78548e076c03)) ### Performance Improvements * 通知选择器优化 ([2c0cbdd](https://github.com/certd/certd/commit/2c0cbdd29ecb74cc939b2ae7ee86b8d40f70ba31)) * 新增七牛云插件分组 ([49e7dc5](https://github.com/certd/certd/commit/49e7dc56e1a95fbdea3e30cdeb945b48415b69e3)) * 新增server酱3通知 ([6aa4872](https://github.com/certd/certd/commit/6aa487269c9f6862e188b37a0d6c73f79c937d94)) * 支持邀请奖励 ([618ec93](https://github.com/certd/certd/commit/618ec937866b24ebcf8164db43acb1ed66a5b329)) * 支持易发云短信 ([94fa77f](https://github.com/certd/certd/commit/94fa77fcd2b9bea294fb05736c0d8cdc81f56103)) * cname value优化 ([e8c9c2a](https://github.com/certd/certd/commit/e8c9c2a47d47048ae743b16f7bc932dbe18a89e9)) * favicon支持自定义 ([8b9c47d](https://github.com/certd/certd/commit/8b9c47daf194515006689a212ae9cf586bdf5993)) # [1.28.0](https://github.com/certd/certd/compare/v1.27.9...v1.28.0) (2024-11-30) ### Bug Fixes * 修复自定义webhook contextType的bug ([7e5ea0c](https://github.com/certd/certd/commit/7e5ea0cee003acda952d922ca70592f1e8a2ed80)) ### Features * 手机号登录、邮箱验证码注册 ([7b55337](https://github.com/certd/certd/commit/7b55337c5edb470cca7aa62201eda8d274784004)) ### Performance Improvements * 部署到IIS插件 ([1534f45](https://github.com/certd/certd/commit/1534f4523633265d219d7b3a249a9ea1af99c512)) * 登录失败增加重试次数限制及冷却时间 ([954b6df](https://github.com/certd/certd/commit/954b6df3608695fe074130f8149a33e311d80cc4)) * 流水线支持批量修改分组,批量删除 ([a847e66](https://github.com/certd/certd/commit/a847e66c4fc843b98f1520b2b8072d3586ce8b81)) * 取消docker-compose的dns配置 ([87bbf6f](https://github.com/certd/certd/commit/87bbf6f14080b9fa287c250d7fc4d33279c83ff7)) * 首页新增修改密码提示 ([0772d3b](https://github.com/certd/certd/commit/0772d3b3fd24afdde4086d9f09ef19d037b431b4)) * 选项显示图标 ([aedc462](https://github.com/certd/certd/commit/aedc46213571a3bd93809b7af7fa17a08d546237)) * 优化七牛云cdn,获取域名列表可以选择 ([5a20242](https://github.com/certd/certd/commit/5a20242111d6bd255b25dac86fe1f062c8543096)) * 优化七牛云cdn部署,保持http2和forceHttp设置,当未开启https时,主动开启https ([196f7d9](https://github.com/certd/certd/commit/196f7d9dc23d7dd96b663c686542e85270b81aef)) * 优化证书申请成功通知发送方式 ([8002a56](https://github.com/certd/certd/commit/8002a56efc5998aa03db5711ae87f9eb4bc9e160)) * 支持短信验证码登录 ([387bcc5](https://github.com/certd/certd/commit/387bcc5fa418cdeea81a06da5e3f8cd6b43cd082)) * 支持威联通证书部署 ([0d8913e](https://github.com/certd/certd/commit/0d8913ea2f56fdebbcc9bb207eae59e8ddbb8cad)) * 自定义webhook显示详细的错误信息 ([3254afc](https://github.com/certd/certd/commit/3254afc75640eed3729d0fc02a818fefbe5c7fc3)) ## [1.27.9](https://github.com/certd/certd/compare/v1.27.8...v1.27.9) (2024-11-26) ### Performance Improvements * 通知支持自定义webhook、anpush、iyuu、server酱 ([cbccd9e](https://github.com/certd/certd/commit/cbccd9e3d0a4c24aba772af62734666d40b22c57)) * 通知支持vocechat、bark、telegram、discord、slack ([642f57f](https://github.com/certd/certd/commit/642f57ff6d7152a9e14f59c7fc0e32a6b1751fb7)) ## [1.27.8](https://github.com/certd/certd/compare/v1.27.7...v1.27.8) (2024-11-25) **Note:** Version bump only for package root ## [1.27.7](https://github.com/certd/certd/compare/v1.27.6...v1.27.7) (2024-11-25) ### Bug Fixes * 修复关键字查询bug ([fab6660](https://github.com/certd/certd/commit/fab66606b35a540fac31fee902331ba1ffdebc16)) * 修复CNAME时子域名级数超出限制的问题 ([3af6d96](https://github.com/certd/certd/commit/3af6d96e6e353c9b2111cff81679b79c55195a0a)) ### Performance Improvements * 谷歌EAB绑定邮箱改成必填 ([81a8123](https://github.com/certd/certd/commit/81a8123725d7bf4bd6a32a64a066bd760b7b6a7f)) * 华为云密钥获取提示及访问链接 ([de43391](https://github.com/certd/certd/commit/de43391e4c12dc3ad976f8fa8787f4eb70a41e75)) * 通知管理 ([d9a00ee](https://github.com/certd/certd/commit/d9a00eeaf72735ced67c59d7983d84e3c730064a)) * 通知渠道支持测试按钮 ([b54ae27](https://github.com/certd/certd/commit/b54ae272ebc2d31b32b049d44e2299a6be7f153c)) * 优化插件开发,dnsProvider无需写http logger 变量 ([fcbb5e4](https://github.com/certd/certd/commit/fcbb5e46a112174150a62648319b8224fce3b7ed)) * 支持部署到阿里云WAF ([c96fcb7](https://github.com/certd/certd/commit/c96fcb7afced979435cffa73591275008033c90d)) * 支持企业微信群聊机器人通知 ([b805a29](https://github.com/certd/certd/commit/b805a2925984144a31575b8aaa622f0c30d41b56)) ## [1.27.6](https://github.com/certd/certd/compare/v1.27.5...v1.27.6) (2024-11-19) ### Bug Fixes * .env 读取 \r 问题 ([0e33dfa](https://github.com/certd/certd/commit/0e33dfa019a55ea76193c428ec756af386adeb9d)) * 修复vip试用secret报错的bug ([018dee6](https://github.com/certd/certd/commit/018dee6c383233560f078dfd30f6c2857a7e15ee)) ### Performance Improvements * 当步骤全部都禁用时,任务本身显示删除线 ([9ab9a6e](https://github.com/certd/certd/commit/9ab9a6e8b083e19793894f23e59f29c604ec98e5)) ## [1.27.5](https://github.com/certd/certd/compare/v1.27.4...v1.27.5) (2024-11-18) ### Bug Fixes * 修复1Panel面板本身证书更新导致判定执行失败的问题 ([2689e6d](https://github.com/certd/certd/commit/2689e6d6c03aba21da90d5d45232c6ba08696be1)) * 修复角色无法删除的bug ([66629a5](https://github.com/certd/certd/commit/66629a591aecc2d8364ea415c7afc3f9d0406562)) * 修复Cname情况下,无法使用DNS类型的bug ([26dad39](https://github.com/certd/certd/commit/26dad399d5768b3205da099ddc11809aef7d6224)) ### Performance Improvements * 日志查看自动滚动到底部 ([4a2f7eb](https://github.com/certd/certd/commit/4a2f7ebf87b7c027cebff7cb763f8f35f6d2aa36)) * 系统设置中的代理设置优化为可全局生效,环境变量中的https_proxy设置将无效 ([381a37f](https://github.com/certd/certd/commit/381a37fbaa6b61c887eda743897ae00afb825bdf)) * 新手导航在非编辑模式下不显示 ([18bfcc2](https://github.com/certd/certd/commit/18bfcc24ad0bde57bb04db8a4209861ec6b8ff1d)) * 优化腾讯云 cloudflare 重复解析记录时的返回值 ([90d1b68](https://github.com/certd/certd/commit/90d1b68bd6cf232fbe085234efe07d29b7690044)) * 支持namesilo ([80159ec](https://github.com/certd/certd/commit/80159ecca895103d0495f3217311199e66056572)) * 专业版试用,无需绑定账号 ([c7c4318](https://github.com/certd/certd/commit/c7c4318c11b65a76089787aa58939832d338a232)) ## [1.27.4](https://github.com/certd/certd/compare/v1.27.3...v1.27.4) (2024-11-14) ### Bug Fixes * 修复未设置pfx密码,导致jks转换报错的bug ([c3cfbd8](https://github.com/certd/certd/commit/c3cfbd8474155aed4379f91075de37d5d8c73ef0)) ### Performance Improvements * 公共cname服务支持关闭 ([f4ae512](https://github.com/certd/certd/commit/f4ae5125dc4cd97816976779cb3586b5ee78947e)) ## [1.27.3](https://github.com/certd/certd/compare/v1.27.2...v1.27.3) (2024-11-13) ### Bug Fixes * 修复偶发性cname一直验证超时的bug ([d2ce72e](https://github.com/certd/certd/commit/d2ce72e4aaacdf726ba8b91fcd71db40a27714ba)) * 修复邮件配置,忽略证书校验设置不生效的bug ([66a9690](https://github.com/certd/certd/commit/66a9690dc958732e1b3c672d965db502296446f9)) * 修复ipv6未开启情况下,请求带有ipv6地址域名报ETIMEDOUT的bug ([a9a0967](https://github.com/certd/certd/commit/a9a0967a6f1d0bd27e69f3ec52c31d90d470bc23)) ### Performance Improvements * 修复站点个性化,浏览器标题没有生效的bug ([bcfac02](https://github.com/certd/certd/commit/bcfac02c96ceaf23d1a0b05b48d8047da933beaf)) * 优化上传到主机插 路径选择,根据证书格式显示 ([8c3f86c](https://github.com/certd/certd/commit/8c3f86c6909ed91f48bb2880e78834e22f6f6a29)) * 支持jks ([889eaae](https://github.com/certd/certd/commit/889eaaea92818f628b922dae540c026630611707)) * ipv6支持 ([da6ac16](https://github.com/certd/certd/commit/da6ac1626b3574be2fabeeb18a1f10d60bdcbe49)) ## [1.27.2](https://github.com/certd/certd/compare/v1.27.1...v1.27.2) (2024-11-08) ### Bug Fixes * 修复某些容器管理ui无法识别端口列表的bug ([576e60a](https://github.com/certd/certd/commit/576e60a2b52315909e659d2a58cf98b130e69e6f)) * 修复删除腾讯云过期证书时间判断上的bug,导致已过期仍然没有删除证书 ([1ba1007](https://github.com/certd/certd/commit/1ba10072615015d91b81fc56a3b01dae6a2ae9d1)) ### Performance Improvements * 优化部署到阿里云CDN插件,支持多域名,更易用 ([80c500f](https://github.com/certd/certd/commit/80c500f618b169a1f64c57fe442242a4d0d9d833)) * 优化流水线页面切换回来不丢失查询条件 ([4dcf6e8](https://github.com/certd/certd/commit/4dcf6e87bc5f7657ce8a56c5331e8723a0fee8ee)) * 支持公共cname服务 ([3c919ee](https://github.com/certd/certd/commit/3c919ee5d1aef5d26cf3620a7c49d920786bc941)) * 执行历史支持点击查看流水线详情 ([8968639](https://github.com/certd/certd/commit/89686399f90058835435b92872fc236fac990148)) * 专业版7天试用 ([c58250e](https://github.com/certd/certd/commit/c58250e1f065a9bd8b4e82acc1df754504c0010c)) ## [1.27.1](https://github.com/certd/certd/compare/v1.27.0...v1.27.1) (2024-11-04) ### Bug Fixes * 修复头像没有更新的bug ([9b4a31f](https://github.com/certd/certd/commit/9b4a31fa6a32b9cab2e22bd141cf96ca29120445)) ### Performance Improvements * 禁止页面缓存,点击tab页签可以刷新数据 ([7ad4b55](https://github.com/certd/certd/commit/7ad4b55ee000c1dd0747832b11107f32b0ffb889)) * 优化时间选择器,自动填写分钟和秒钟 ([396dc34](https://github.com/certd/certd/commit/396dc34a841c7d016b033736afdba8366fb2d211)) * cname 域名映射记录可读性优化 ([b1117ed](https://github.com/certd/certd/commit/b1117ed54a3ef015752999324ff72b821ef5e4b9)) # [1.27.0](https://github.com/certd/certd/compare/v1.26.16...v1.27.0) (2024-10-31) ### Bug Fixes * 修复历史记录不能按名称查询的bug ([6113c38](https://github.com/certd/certd/commit/6113c388b7fc58b11ca19ff05cc1286d096c8d28)) * pfx兼容windows server 2016 ([e5e468a](https://github.com/certd/certd/commit/e5e468a463f66d02f235de54b7c1e09ace5f1cb1)) ### Features * 首页全新改版 ([63ec5b5](https://github.com/certd/certd/commit/63ec5b5519c760a3330569c0da6dac157302a330)) ### Performance Improvements * 管理控制台数据统计 ([babd589](https://github.com/certd/certd/commit/babd5897ae013ff7c04ebfcbfac8a00d84dd627c)) * 增加向导 ([6d9ef26](https://github.com/certd/certd/commit/6d9ef26ecab71d752c2c55d75aed4fb5f6c05a39)) * lego 升级到 4.19.2 ([129bf53](https://github.com/certd/certd/commit/129bf53edc9bbb001fe49fbd7e239bd1d09cc128)) ## [1.26.16](https://github.com/certd/certd/compare/v1.26.15...v1.26.16) (2024-10-30) ### Bug Fixes * 修复lego No help topic for 错误 ([aaaf8d7](https://github.com/certd/certd/commit/aaaf8d7db34896cf8f2ff8f12eec1ab0cae58f0f)) ### Performance Improvements * 支持白山云cdn部署 ([b1b2cd0](https://github.com/certd/certd/commit/b1b2cd088b684eda764962abd61754c26a204d1c)) * 支持华为云cdn ([81a3fdb](https://github.com/certd/certd/commit/81a3fdbc29b71f380762008cc151493ec97458f9)) ## [1.26.15](https://github.com/certd/certd/compare/v1.26.14...v1.26.15) (2024-10-28) ### Bug Fixes * 顶部菜单变...的bug ([6dabad7](https://github.com/certd/certd/commit/6dabad76baba96be0f8af36a3fbfb9f5182aecf1)) ### Performance Improvements * 默认证书更新时间设置为35天,增加腾讯云删除过期证书插件,可以避免腾讯云过期证书邮件 ([51b6fed](https://github.com/certd/certd/commit/51b6fed468eaa6f28ce4497ce303ace1a52abb96)) * 授权加密支持解密查看 ([5575c83](https://github.com/certd/certd/commit/5575c839705f6987ad2bdcd33256b0962c6a9c6a)) * 重置管理员密码同时启用管理员账户,避免之前禁用了,重置密码还是登录不进去 ([f92d918](https://github.com/certd/certd/commit/f92d918a1e28e29b794ad4754661ea760c18af46)) ## [1.26.14](https://github.com/certd/certd/compare/v1.26.13...v1.26.14) (2024-10-26) ### Bug Fixes * 修复阿里云部署大杀器报插件_还未注册错误的bug ([abd2dcf](https://github.com/certd/certd/commit/abd2dcf2e85a545321bae6451406d081f773b132)) * 修复启动时自签证书无法保存的bug ([526c484](https://github.com/certd/certd/commit/526c48450bcd37b3ccded9b448f17de8140bdc6e)) ### Performance Improvements * 顶部菜单自定义 ([54d136c](https://github.com/certd/certd/commit/54d136cc6ae122f7c891b7a5c7232fe5de8e5cb5)) * 禁用readonly用户 ([d10d42e](https://github.com/certd/certd/commit/d10d42e20619bb55a50d636b8867ff33db4e3b4b)) * 限制其他用户流水线数量 ([315e437](https://github.com/certd/certd/commit/315e43746baf01682737f82e41579237a48409af)) * 用户管理优化头像上传 ([661293c](https://github.com/certd/certd/commit/661293c189a3abf3cdc953b5225192372f57930d)) ## [1.26.13](https://github.com/certd/certd/compare/v1.26.12...v1.26.13) (2024-10-26) ### Bug Fixes * 修复对话框全屏按钮与关闭按钮重叠的bug ([95df56c](https://github.com/certd/certd/commit/95df56cc5ca5e3eb843cd17cb7078cde47729f1e)) * deprecated的运行时不要报错,只报警告 ([bcbefaa](https://github.com/certd/certd/commit/bcbefaaa35cf6d0eec085b3a2c5bfc7c6a8de9e1)) ### Performance Improvements * 更新certd本身的证书文档说明 ([0c50ede](https://github.com/certd/certd/commit/0c50ede129337b82df54575cbd2f4c2a783a0732)) * 支持同时监听https端口,7002 ([d5a17f9](https://github.com/certd/certd/commit/d5a17f9e6afd63fda2df0981118480f25a1fac2e)) ## [1.26.12](https://github.com/certd/certd/compare/v1.26.11...v1.26.12) (2024-10-25) ### Performance Improvements * 部署到阿里云任意云资源,阿里云部署大杀器 ([4075be7](https://github.com/certd/certd/commit/4075be7849b140acb92bd8da8a9acbf4eef85180)) * 文件名特殊字符限制输入 ([c4164c6](https://github.com/certd/certd/commit/c4164c66e29f3ec799f98108a344806ca61e94ff)) * 新增部署到百度云CDN插件 ([f126f9f](https://github.com/certd/certd/commit/f126f9f932d37fa01fff1accc7bdd17d349f8db5)) * 新增部署到腾讯云CDN-v2,推荐使用 ([d782655](https://github.com/certd/certd/commit/d782655cb4dfbb74138178afbffeee76fc755115)) * 优化cron选择器,增加下次触发时间显示 ([5b148b7](https://github.com/certd/certd/commit/5b148b7ed960ca6f7f5b733b2eadd56eeecbd4c2)) * 支持部署到腾讯云COS ([a8a45d7](https://github.com/certd/certd/commit/a8a45d7f757820990e278533277a3deda5ba48f3)) * 支持配置公共ZeroSSL授权 ([a90d1e6](https://github.com/certd/certd/commit/a90d1e68ee9cbc3705223457b8a86f071b150968)) ## [1.26.11](https://github.com/certd/certd/compare/v1.26.10...v1.26.11) (2024-10-23) ### Bug Fixes * 申请证书没有使用到系统设置的http代理的bug ([3db216f](https://github.com/certd/certd/commit/3db216f515ba404cb4330fdab452971b22a50f08)) * 修复移动任务后出现空阶段的bug ([4ea3edd](https://github.com/certd/certd/commit/4ea3edd59e93ca4f5b2e43b20dd4ef33909caddb)) * 修复google证书*.xx.com与xx.com同时申请时报错的bug ([f8b99b8](https://github.com/certd/certd/commit/f8b99b81a23e7e9fd5e05ebd5caf355c41d67a90)) * 允许七牛云cdn插件输入.号开头的通配符域名 ([18ee87d](https://github.com/certd/certd/commit/18ee87daff6eafc2201b58e28d85aafd3cb7a5b9)) ### Performance Improvements * 申请证书启用新的反代地址 ([a705182](https://github.com/certd/certd/commit/a705182b85e51157883e48f23463263793bf3c12)) * 优化日志颜色 ([1291e98](https://github.com/certd/certd/commit/1291e98e821c5b1810aab7f0aebe3f5f5cd44a20)) * 优化证书申请速度和成功率,反代地址优化,google基本可以稳定请求。增加请求重试。 ([41d9c3a](https://github.com/certd/certd/commit/41d9c3ac8398def541e65351cbe920d4a927182d)) * 优化pfx密码密码输入框,让浏览器不自动填写密码 ([ffeede3](https://github.com/certd/certd/commit/ffeede38afa70c5ff6f2015516bead23d2c4df87)) ## [1.26.10](https://github.com/certd/certd/compare/v1.26.9...v1.26.10) (2024-10-20) ### Bug Fixes * 修复cname服务普通用户access访问权限问题 ([c1e3e2e](https://github.com/certd/certd/commit/c1e3e2ee1f923ee5806479dd5f178c3286a01ae0)) ## [1.26.9](https://github.com/certd/certd/compare/v1.26.8...v1.26.9) (2024-10-19) ### Bug Fixes * 修复普通用户无法校验cname配置的bug ([6285497](https://github.com/certd/certd/commit/62854978bf0bdbe749b42f8e40ab227ab31ec92f)) * 修复切换普通用户登录时,左侧菜单没有同步更新的bug ([12116a8](https://github.com/certd/certd/commit/12116a89f43cf8b98f16d2ea6073f6b72a643215)) * 修正邮箱设置跳转路由 ([17d8890](https://github.com/certd/certd/commit/17d88900a1f0e3af609b74597f5b1978230db32d)) ### Performance Improvements * 触发证书重新申请input变化对比规则优化,减少升级版本后触发申请证书的情况 ([c46a2a9](https://github.com/certd/certd/commit/c46a2a9a399c2a9a8bb59a48b9fb6e93227cce9b)) * 任务下所有步骤都跳过时,整个任务显示跳过 ([84fd3b2](https://github.com/certd/certd/commit/84fd3b250dd1161ea06c5582fdadece4b29c2e53)) * 授权配置去除前后空格 ([57d8d48](https://github.com/certd/certd/commit/57d8d48046fbf51c52b041d2dec03d51fb018587)) * 数据库备份插件,先压缩再备份 ([304ef49](https://github.com/certd/certd/commit/304ef494fd5787c996ad0dcb6edd2f517afce9e2)) * 优化菜单 ([1f4f157](https://github.com/certd/certd/commit/1f4f15757de1015cf7563f7022599eef58cc93d7)) * 增加文档站 https://certd.docmirror.cn ([6e2ac1c](https://github.com/certd/certd/commit/6e2ac1c089f6ddccb396f1f2738509c05333e1bb)) ## [1.26.8](https://github.com/certd/certd/compare/v1.26.7...v1.26.8) (2024-10-15) ### Bug Fixes * 修复无法设置角色的bug ([02fe704](https://github.com/certd/certd/commit/02fe704769edb25fea5ffd85a51a5530864b37b3)) ### Performance Improvements * 角色删除安全 ([28bb485](https://github.com/certd/certd/commit/28bb4856bee03569153f6471527c9b9f28cb3d14)) * 密钥备份 ([1c6028a](https://github.com/certd/certd/commit/1c6028abcf8849163462bb2f8441b6838357e09b)) * 证书直接查看 ([5dde5bd](https://github.com/certd/certd/commit/5dde5bd3f76db3959d411619d29bfb8064e3b307)) * sqlite数据库备份插件 ([77f1631](https://github.com/certd/certd/commit/77f163144f7dcfb0431475c55508fecfd6d969f8)) ## [1.26.7](https://github.com/certd/certd/compare/v1.26.6...v1.26.7) (2024-10-14) ### Bug Fixes * 修复siteInfo每次都要重新设置的bug ([36b26ae](https://github.com/certd/certd/commit/36b26ae9f5c7a53c1c2546fb79b2ea451b854abf)) ## [1.26.6](https://github.com/certd/certd/compare/v1.26.5...v1.26.6) (2024-10-14) ### Bug Fixes * 修复排序失效的bug ([1f0742e](https://github.com/certd/certd/commit/1f0742ef9f0caae0c7e713acf0fd3cebf5d63875)) ## [1.26.5](https://github.com/certd/certd/compare/v1.26.4...v1.26.5) (2024-10-14) ### Bug Fixes * 修复版本号获取错误的bug ([8851870](https://github.com/certd/certd/commit/8851870400df86e496198ad509061b8989fcc44f)) ## [1.26.4](https://github.com/certd/certd/compare/v1.26.3...v1.26.4) (2024-10-14) ### Performance Improvements * [comm] 支持插件管理 ([e8b617b](https://github.com/certd/certd/commit/e8b617b80ce882dd63006f0cfc719a80a1cc6acc)) * 新增代理设置功能 ([273ab61](https://github.com/certd/certd/commit/273ab6139f5807f4d7fe865cc353b97f51b9a668)) * EAB授权支持绑定邮箱,支持公共EAB设置 ([07043af](https://github.com/certd/certd/commit/07043aff0ca7fd29c56dd3c363002cb15d78b464)) ## [1.26.3](https://github.com/certd/certd/compare/v1.26.2...v1.26.3) (2024-10-12) ### Performance Improvements * 优化系统设置加载时机 ([7396253](https://github.com/certd/certd/commit/73962536d5a4769902d760d005f3f879465addcc)) ## [1.26.2](https://github.com/certd/certd/compare/v1.26.1...v1.26.2) (2024-10-11) ### Bug Fixes * 修复某些情况下bindUrl失败的bug ([91fc1cd](https://github.com/certd/certd/commit/91fc1cd7353be4a22be951239ed70b38baebc74e)) ### Performance Improvements * 邮箱设置改为系统设置,普通用户无需配置发件邮箱 ([4244569](https://github.com/certd/certd/commit/42445692117184a3293e63bef84a74cbb5984b0e)) ## [1.26.1](https://github.com/certd/certd/compare/v1.26.0...v1.26.1) (2024-10-10) **Note:** Version bump only for package root # [1.26.0](https://github.com/certd/certd/compare/v1.25.9...v1.26.0) (2024-10-10) ### Bug Fixes * 修复管理员编辑其他用户流水线任务时归属userid也被修改的bug ([e85c477](https://github.com/certd/certd/commit/e85c47744cf740b4af3b93dca7c2f0ccc818ec2f)) * 修复历史记录根据流水线名称查询报错的bug ([ce9a986](https://github.com/certd/certd/commit/ce9a9862f122fce2186e7727eaa4b251b59e6032)) * 修复某些代理情况下 报 400 The plain HTTP request was sent to HTTPS port use proxy 的bug ([a13203f](https://github.com/certd/certd/commit/a13203fb3f48c427d0d81a504912248dcc07df1a)) ### Features * 域名验证方法支持CNAME间接方式,此方式支持所有域名注册商,且无需提供Access授权,但是需要手动添加cname解析 ([f3d3508](https://github.com/certd/certd/commit/f3d35084ed44f9f33845f7045e520be5c27eed93)) * 站点个性化设置 ([11a9fe9](https://github.com/certd/certd/commit/11a9fe9014d96cba929e5a066e78f2af7ae59d14)) ### Performance Improvements * 并行任务名称改成添加任务,取消并行,可以在同一个阶段获取上一个task的输出 ([c5e5877](https://github.com/certd/certd/commit/c5e58770d1c5edc19c6f9ea1618f44b68e091f35)) * 调整静态资源到static目录 ([0584b36](https://github.com/certd/certd/commit/0584b3672b40f9042a2ed87e5627022606d046cd)) * 调整全部静态资源到static目录 ([a218890](https://github.com/certd/certd/commit/a21889080d6c7ffdf0af526a3a21f0b2d1c77288)) * 检查cname是否正确配置 ([b5d8935](https://github.com/certd/certd/commit/b5d8935159374fbe7fc7d4c48ae0ed9396861bdd)) * 七牛云cdn支持配置多个域名 ([88d745e](https://github.com/certd/certd/commit/88d745e29063a089864fb9c6705be7b8d4c2669a)) * 上传到主机插件支持注入环境变量 ([81fac73](https://github.com/certd/certd/commit/81fac736f9ccc8d1cda7ef4178752239cec20849)) * 优化宝塔网站部署插件远程获取数据的提示 ([2a3ca9f](https://github.com/certd/certd/commit/2a3ca9f552d96594ec6690a1c4c91f598451b9a1)) * 优化缩短首页缓存时间 ([49395e8](https://github.com/certd/certd/commit/49395e8cb65f4b30c0145329ed5de48be4ef3842)) * 域名输入增加校验提示,避免输入错误的域名 ([0c8e83e](https://github.com/certd/certd/commit/0c8e83e1254a9ce4d5a4e7888eb1710394a4b77c)) * cname校验配置增加未校验通过提示 ([77cc3c4](https://github.com/certd/certd/commit/77cc3c4a5cbd81f8233a8e0bb33fab0621c0905f)) * google eab授权支持自动获取,不过要配置代理 ([592791d](https://github.com/certd/certd/commit/592791d1356fc252fbb70d7f168567aee9585507)) ## [1.25.9](https://github.com/certd/certd/compare/v1.25.8...v1.25.9) (2024-10-01) ### Bug Fixes * 修复西部数码账户级别apikey不可用的bug ([f8f3e8b](https://github.com/certd/certd/commit/f8f3e8b43fd5d815887bcb53b95f46dc96424b79)) ### Performance Improvements * 增加等待插件 ([3ef0541](https://github.com/certd/certd/commit/3ef0541cc85ab6abf698ead3b258ae1ac156ef98)) ## [1.25.8](https://github.com/certd/certd/compare/v1.25.7...v1.25.8) (2024-09-30) ### Bug Fixes * 修复pfxPassword无效的bug ([251e450](https://github.com/certd/certd/commit/251e450fabfe62405bac13e39f2153736c081ef0)) ### Performance Improvements * 群晖获取deviceid优化 ([8d42273](https://github.com/certd/certd/commit/8d4227366548eb70f6bc04303829e6933168f906)) ## [1.25.7](https://github.com/certd/certd/compare/v1.25.6...v1.25.7) (2024-09-29) ### Bug Fixes * 修复某些地区被屏蔽无法激活专业版的bug ([7532a96](https://github.com/certd/certd/commit/7532a960851b84d4f2cc3dba02353c5235e1a364)) ### Performance Improvements * 上传到主机,支持socks代理 ([d91026d](https://github.com/certd/certd/commit/d91026dc4fbfe5fedc4ee8e43dc0d08f1cf88356)) * 支持上传到七牛云oss ([bf024bd](https://github.com/certd/certd/commit/bf024bdda8bc2a463475be5761acf0da7317a08a)) ## [1.25.6](https://github.com/certd/certd/compare/v1.25.5...v1.25.6) (2024-09-29) ### Bug Fixes * 修复中间证书复制错误的bug ([76e86ea](https://github.com/certd/certd/commit/76e86ea283ecbe4ec76cdc92b98457d0fef544ac)) ### Performance Improvements * 部署支持1Panel ([d047234](https://github.com/certd/certd/commit/d047234d98d31504f2e5a472b66e1b75806af26e)) * 增加使用教程 ([9d9c021](https://github.com/certd/certd/commit/9d9c0218195af5b9896cce7109b26a433480571d)) ## [1.25.5](https://github.com/certd/certd/compare/v1.25.4...v1.25.5) (2024-09-26) **Note:** Version bump only for package root ## [1.25.4](https://github.com/certd/certd/compare/v1.25.3...v1.25.4) (2024-09-25) ### Bug Fixes * 修复启动报授权验证失败的bug ([3460d3d](https://github.com/certd/certd/commit/3460d3ddca222ea702816ab805909d489eff957f)) ## [1.25.3](https://github.com/certd/certd/compare/v1.25.2...v1.25.3) (2024-09-24) ### Bug Fixes * 修复upload to host trim错误 ([0f0ddb9](https://github.com/certd/certd/commit/0f0ddb9c5963fd643d6d203334efac471c43ec3b)) ## [1.25.2](https://github.com/certd/certd/compare/v1.25.1...v1.25.2) (2024-09-24) **Note:** Version bump only for package root ## [1.25.1](https://github.com/certd/certd/compare/v1.25.0...v1.25.1) (2024-09-24) **Note:** Version bump only for package root # [1.25.0](https://github.com/certd/certd/compare/v1.24.4...v1.25.0) (2024-09-24) ### Bug Fixes * 修复首次创建任务运行时不自动设置当前运行情况的bug ([ecd83ee](https://github.com/certd/certd/commit/ecd83ee136abdd3df9ed2f21ec2ff0f24c0ed9d9)) ### Features * 账号绑定 ([e046640](https://github.com/certd/certd/commit/e0466409d0c021bb415abd94df448c8a0d4799e9)) * 支持中间证书 ([e86756e](https://github.com/certd/certd/commit/e86756e4c65a53dd23106d7ecbfe2fa987cc13f3)) * 支持vip转移 ([361e8fe](https://github.com/certd/certd/commit/361e8fe7ae5877e23fd5de31bc919bedd09c57f5)) ### Performance Improvements * 群晖支持OTP双重验证登录 ([8b8039f](https://github.com/certd/certd/commit/8b8039f42bbce10a4d0e737cdeeeef9bb17bee5a)) * 任务支持禁用 ([8ed16b3](https://github.com/certd/certd/commit/8ed16b3ea2dfe847357863a0bfa614e4fa5fc041)) * 优化收件邮箱输入 ([22ef28f](https://github.com/certd/certd/commit/22ef28f6338a78465bd52ccbad13e66e80263b2f)) * 优化主机登录失败提示 ([9de77b3](https://github.com/certd/certd/commit/9de77b327d39cff5ed6660ec53b58ba0eea18e5a)) * 增加重启certd插件 ([48238d9](https://github.com/certd/certd/commit/48238d929e6c4afa1d428e4d35b9159d37a47ae0)) * 证书支持旧版RSA,pkcs1 ([3d9c3ec](https://github.com/certd/certd/commit/3d9c3ecb3eb604b2458154f608bde0f01915d116)) * 支持阿里云ACK证书部署 ([d331fea](https://github.com/certd/certd/commit/d331fea47789122650e057ec7c9e85ee8e66f09b)) * 支持七牛云 ([8ecc2f9](https://github.com/certd/certd/commit/8ecc2f9446a9ebd11b9bfbffbb6cf7812a043495)) * 支持k8s ingress secret ([e5a5d0a](https://github.com/certd/certd/commit/e5a5d0a607bb6b4e1a1f7a1a419bada5f2dee59f)) * http请求增加默认超时时间 ([664bd86](https://github.com/certd/certd/commit/664bd863e5b4895aabe2384277c0c65f5902fdb2)) * plugins增加图标 ([a8da658](https://github.com/certd/certd/commit/a8da658a9723342b4f43a579f7805bfef0648efb)) ## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09) ### Bug Fixes * 修复腾讯云cdn证书部署后会自动关闭hsts,http2.0等配置的bug ([7908ab7](https://github.com/certd/certd/commit/7908ab79da624c94fa05849925b15e480e3317c4)) * 修复腾讯云tke证书部署报错的bug ([653f409](https://github.com/certd/certd/commit/653f409d91a441850d6381f89a8dd390831f0d5e)) ### Performance Improvements * 插件选择支持搜索 ([d1498a7](https://github.com/certd/certd/commit/d1498a71601b74d38343b1d070eadd03705dd9d5)) * 前置任务步骤增加错误提示 ([ae3daa9](https://github.com/certd/certd/commit/ae3daa9bcf4fc363825aad9b77f5d3879aeeff70)) * 群晖部署教程 ([0f0af2f](https://github.com/certd/certd/commit/0f0af2f309390f388e7a272cea3a1dd30c01977d)) * 支持群晖 ([5c270b6](https://github.com/certd/certd/commit/5c270b6b9d45a2152f9fdb3c07bd98b7c803cb8e)) ## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06) ### Performance Improvements * 支持多吉云cdn证书部署 ([65ef685](https://github.com/certd/certd/commit/65ef6857296784ca765926e09eafcb6fc8b6ecde)) ## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06) ### Bug Fixes * 修复复制流水线出现的各种问题 ([6314e8d](https://github.com/certd/certd/commit/6314e8d7eb58cd52e2a7bd3b5ffb9112b0b69577)) * 修复windows下无法执行第二条命令的bug ([71ac8aa](https://github.com/certd/certd/commit/71ac8aae4aa694e1a23761e9761c9fba30b43a21)) ### Performance Improvements * 阶段、任务、步骤全面支持拖动排序 ([bd73a16](https://github.com/certd/certd/commit/bd73a163cd0497f062bd424ddc6bc9bbc95f81ea)) * 任务配置不需要的字段可以自动隐藏 ([192d9dc](https://github.com/certd/certd/commit/192d9dc7e36737d684c769f255f407c28b1152ac)) * 任务支持拖动排序 ([1e9b563](https://github.com/certd/certd/commit/1e9b5638aa36a8ce70019a9c750230ba41938327)) * 西部数据支持用户级的apikey ([1c17b41](https://github.com/certd/certd/commit/1c17b41e160944b073e1849e6f9467c3659a4bfc)) * 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/certd/certd/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5)) * 优化跳过处理逻辑 ([b80210f](https://github.com/certd/certd/commit/b80210f24bf5db1c958d06ab27c9e5d3db452eda)) * 支持阿里云oss ([87a2673](https://github.com/certd/certd/commit/87a2673e8c33dff6eda1b836d92ecc121564ed78)) * 支持西部数码DNS ([c59cab1](https://github.com/certd/certd/commit/c59cab1aaeb19f86df8e3e0d8127cbd0a9ef77f3)) * 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723)) * client 请求超时时间延长为10s ([ff46771](https://github.com/certd/certd/commit/ff46771d8dd43e71c1ca70e3ba783945750342cc)) ## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02) ### Bug Fixes * 激活仅限管理员 ([1c17970](https://github.com/certd/certd/commit/1c17970b981f0987c506744ee6b2283fd5e40493)) * 修复在没有勾选使用代理的情况下,仍然会使用代理的bug ([0f66794](https://github.com/certd/certd/commit/0f6679425f6a736bb0128527dd99c085fac17d84)) ### Performance Improvements * 部署插件支持宝塔、易盾云等 ([ee61709](https://github.com/certd/certd/commit/ee617095efa1171548cf52fd45f0f98a368555a3)) * 授权配置支持加密 ([42a56b5](https://github.com/certd/certd/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb)) * 优化内存占用 ([db61033](https://github.com/certd/certd/commit/db6103363364440b650bc10bb334834e4a9470c7)) * 支持阿里云 DCDN ([98b77f8](https://github.com/certd/certd/commit/98b77f80843834616fb26f83b4c42245326abd06)) * 支持已跳过的步骤重新运行 ([ea775ad](https://github.com/certd/certd/commit/ea775adae18d57a04470cfba6b9460d761d74035)) * 支持cdnfly ([724a850](https://github.com/certd/certd/commit/724a85028b4a7146c9e3b4df4497dcf2a7bf7c67)) * 支持ftp上传 ([b9bddbf](https://github.com/certd/certd/commit/b9bddbfabb5664365f1232e9432532187c98006c)) # [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25) ### Bug Fixes * 部署到腾讯云cdn选择证书任务步骤限制只能选证书 ([3345c14](https://github.com/certd/certd/commit/3345c145b802170f75a098a35d0c4b8312efcd17)) * 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/certd/certd/commit/37eb762afe25c5896b75dee25f32809f8426e7b7)) * 修复创建流水线后立即运行时报no id错误的bug ([17ead54](https://github.com/certd/certd/commit/17ead547aab25333603980304aa3aad3db1f73d5)) * 修复使用代理的情况下申请证书失败的bug ([95122e2](https://github.com/certd/certd/commit/95122e28609333f4df55c266e5434897954c0fb3)) * 修复执行日志没有清理的bug ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5)) * 修复重置密码参数配置后无效的bug ([e358a88](https://github.com/certd/certd/commit/e358a8869696578687306e4cd0dcda53f898fe13)) * 修复ssh无法连接成功,无法执行命令的bug ([41b9837](https://github.com/certd/certd/commit/41b9837582323fb400ef8525ce65e8b37ad4b36f)) ### Features * 支持ECC类型 ([a7424e0](https://github.com/certd/certd/commit/a7424e02f5c7e02ac1688791040785920ce67473)) * 支持google证书申请(需要使用代理) ([a593056](https://github.com/certd/certd/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe)) ### Performance Improvements * 更新k8s底层api库 ([746bb9d](https://github.com/certd/certd/commit/746bb9d385e2f397daef4976eca1d4782a2f5ebd)) * 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b)) * 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345)) * 优化dnspod的token id 说明 ([790bf11](https://github.com/certd/certd/commit/790bf11af06d6264ef74bc1bb919661f0354239a)) * email proxy ([453f1ba](https://github.com/certd/certd/commit/453f1baa0b9eb0f648aa1b71ccf5a95b202ce13f)) ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06) ### Bug Fixes * 修复模糊查询无效的bug ([9355917](https://github.com/certd/certd/commit/93559174c780173f0daec7cdbd1f72f8d5c504d5)) ### Performance Improvements * 优化插件字段的default value ([24c7be2](https://github.com/certd/certd/commit/24c7be2c9cb39c14f7a97b674127c88033280b02)) * 优化默认值设置 ([1af19f0](https://github.com/certd/certd/commit/1af19f0ac053fe109782882964533636b5969d6b)) # [1.23.0](https://github.com/certd/certd/compare/v1.22.9...v1.23.0) (2024-08-05) ### Bug Fixes * 修复环境变量多个下划线不生效的bug ([7ec2218](https://github.com/certd/certd/commit/7ec2218c9fee5bee2bf0aa31f3e3a4301575f247)) ### Features * use node 20 ([e8ed972](https://github.com/certd/certd/commit/e8ed97206bf28e83f942db2ef4ea07fa76fd3567)) ## [1.22.9](https://github.com/certd/certd/compare/v1.22.8...v1.22.9) (2024-08-05) ### Performance Improvements * 优化定时任务 ([87e440e](https://github.com/certd/certd/commit/87e440ee2a8b10dc571ce619f28bc83c1e5eb147)) ## [1.22.8](https://github.com/certd/certd/compare/v1.22.7...v1.22.8) (2024-08-05) ### Performance Improvements * 修复删除历史记录没有删除log的bug,新增history管理页面,演示站点启动时不自动启动非管理员用户的定时任务 ([f78ae93](https://github.com/certd/certd/commit/f78ae93eedfe214008c3d071ca3d77c962137a64)) * 优化pipeline删除时,删除其他history ([b425203](https://github.com/certd/certd/commit/b4252033d56a9ad950f3e204ff021497c3978015)) ## [1.22.7](https://github.com/certd/certd/compare/v1.22.6...v1.22.7) (2024-08-04) ### Bug Fixes * 修复保存配置报id不能为空的bug ([367f807](https://github.com/certd/certd/commit/367f80731396003416665c22853dfbc09c2c03a0)) ## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03) ### Bug Fixes * 修复在相同的cron时偶尔无法触发定时任务的bug ([680941a](https://github.com/certd/certd/commit/680941af119619006b592e3ab6fb112cb5556a8b)) * 修复pg下pipeline title 类型问题 ([a9717b9](https://github.com/certd/certd/commit/a9717b9a0df7b5a64d4fe03314fecad4f59774cc)) ### Performance Improvements * 流水线支持名称模糊查询 ([59897c4](https://github.com/certd/certd/commit/59897c4ceae992ebe2972ca9e8f9196616ffdfd7)) * 腾讯云clb支持更多大区选择 ([e4f4570](https://github.com/certd/certd/commit/e4f4570b29f26c60f1ee9660a4c507cbeaba3d7e)) * 优化前置任务输出为空的提示 ([6ed1e18](https://github.com/certd/certd/commit/6ed1e18c7d9c46d964ecc6abc90f3908297b7632)) ## [1.22.5](https://github.com/certd/certd/compare/v1.22.4...v1.22.5) (2024-07-26) ### Bug Fixes * 修复用户管理无法添加用户的bug ([e7e89b8](https://github.com/certd/certd/commit/e7e89b8de7386e84c0d6b8e217e2034909657d68)) ## [1.22.4](https://github.com/certd/certd/compare/v1.22.3...v1.22.4) (2024-07-26) ### Performance Improvements * 证书申请支持反向代理,letsencrypt无法访问时的备用方案 ([b7b5df0](https://github.com/certd/certd/commit/b7b5df0587e0f7ea288c1b2af6f87211f207395f)) * 支持arm64 ([fa14f87](https://github.com/certd/certd/commit/fa14f87a8093ef3addc5e5f3315ce1bfc9982782)) ## [1.22.3](https://github.com/certd/certd/compare/v1.22.2...v1.22.3) (2024-07-25) ### Bug Fixes * lege 无执行权限问题 ([338eb3b](https://github.com/certd/certd/commit/338eb3bdfeb461e9b3bc7eee97b97a59f5642ffe)) ## [1.22.2](https://github.com/certd/certd/compare/v1.22.1...v1.22.2) (2024-07-23) ### Bug Fixes * 修复创建流水线时,无法根据dns类型默认正确的dns授权的bug ([a2c43b5](https://github.com/certd/certd/commit/a2c43b50a6069ed48958fd142844a8568c2af452)) ## [1.22.1](https://github.com/certd/certd/compare/v1.22.0...v1.22.1) (2024-07-20) ### Performance Improvements * 创建证书任务可以选择lege插件 ([affef13](https://github.com/certd/certd/commit/affef130378030c517250c58a4e787b0fc85d7d1)) * 创建证书任务增加定时任务和邮件通知输入 ([427620d](https://github.com/certd/certd/commit/427620d34f3b8ad6933005faf1878908441a2453)) * 支持配置启动后自动触发一次任务 ([a5a0c1f](https://github.com/certd/certd/commit/a5a0c1f6e7a3f05e581005e491d5b102ee854412)) # [1.22.0](https://github.com/certd/certd/compare/v1.21.2...v1.22.0) (2024-07-19) ### Features * 升级midway,支持esm ([485e603](https://github.com/certd/certd/commit/485e603b5165c28bc08694997726eaf2a585ebe7)) * 支持lego,海量DNS提供商 ([0bc6d0a](https://github.com/certd/certd/commit/0bc6d0a211920fb0084d705e1db67ee1e7262c44)) * 支持postgresql ([3b19bfb](https://github.com/certd/certd/commit/3b19bfb4291e89064b3b407a80dae092d54747d5)) ### Performance Improvements * 优化一些小细节 ([b168852](https://github.com/certd/certd/commit/b1688525dbbbfd67e0ab1cf5b4ddfbe9d394f370)) * 增加备案号设置 ([bd3d959](https://github.com/certd/certd/commit/bd3d959944db63a5690b55ee150e1007133868b9)) * 自动生成jwtkey,无需手动配置 ([390e485](https://github.com/certd/certd/commit/390e4853a570390a97df6a3b3882579f9547eeb4)) ## [1.21.2](https://github.com/certd/certd/compare/v1.21.1...v1.21.2) (2024-07-08) ### Performance Improvements * 申请证书时可以选择跳过本地dns校验 ([fe91d94](https://github.com/certd/certd/commit/fe91d94090d22ed0a3ea753ba74dfaa1bf057c17)) ## [1.21.1](https://github.com/certd/certd/compare/v1.21.0...v1.21.1) (2024-07-08) ### Performance Improvements * 上传到主机,支持设置不mkdirs ([5ba9831](https://github.com/certd/certd/commit/5ba9831ed1aa6ec6057df246f1035b36b9c41d2e)) * 说明优化,默认值优化 ([970c7fd](https://github.com/certd/certd/commit/970c7fd8a0f557770e973d8462ee5684ef742810)) # [1.21.0](https://github.com/certd/certd/compare/v1.20.17...v1.21.0) (2024-07-03) ### Features * 支持zero ssl ([eade2c2](https://github.com/certd/certd/commit/eade2c2b681569f03e9cd466e7d5bcd6703ed492)) ## [1.20.17](https://github.com/certd/certd/compare/v1.20.16...v1.20.17) (2024-07-03) ### Performance Improvements * 创建dns解析后,强制等待60s ([f47b35f](https://github.com/certd/certd/commit/f47b35f6d5bd7d675005c3e286b7e9a029201f8b)) * 文件上传提示由cert.crt改为cert.pem ([a09b0e4](https://github.com/certd/certd/commit/a09b0e48c176f3ed763791bd50322c29729f7c1c)) * 优化cname verify ([eba333d](https://github.com/certd/certd/commit/eba333de7a5b5ef4b0b7eaa904f578720102fa61)) ## [1.20.16](https://github.com/certd/certd/compare/v1.20.15...v1.20.16) (2024-07-01) ### Bug Fixes * 修复配置了cdn cname后申请失败的bug ([4a5fa76](https://github.com/certd/certd/commit/4a5fa767edc347d03d29a467e86c9a4d70b0220c)) ## [1.20.15](https://github.com/certd/certd/compare/v1.20.14...v1.20.15) (2024-06-28) ### Bug Fixes * 修复无法强制取消任务的bug ([9cc01db](https://github.com/certd/certd/commit/9cc01db1d569a5c45bb3e731f35d85df324a8e62)) ### Performance Improvements * 腾讯云dns provider 支持腾讯云的accessId ([e0eb3a4](https://github.com/certd/certd/commit/e0eb3a441384d474fe2923c69b25318264bdc9df)) * 支持windows文件上传 ([7f61cab](https://github.com/certd/certd/commit/7f61cab101fa13b4e88234e9ad47434e6130fed2)) ## [1.20.14](https://github.com/certd/certd/compare/v1.20.13...v1.20.14) (2024-06-23) ### Bug Fixes * 修复修改密码功能异常问题 ([f740ff5](https://github.com/certd/certd/commit/f740ff517f521dce361284c2c54bccc68aee0ea2)) ## [1.20.13](https://github.com/certd/certd/compare/v1.20.12...v1.20.13) (2024-06-18) ### Bug Fixes * 日志高度越界 ([c4c9adb](https://github.com/certd/certd/commit/c4c9adb8bfd513f57252e523794e3799a9b220f8)) * 修复邮箱设置页面SMTP拼写错误的问题 ([b98f1c0](https://github.com/certd/certd/commit/b98f1c0dd0bc6c6b4f814c578692afdf6d90b88d)) * 修复logo问题 ([7e483e6](https://github.com/certd/certd/commit/7e483e60913d509b113148c735fe13ba1d72dddf)) ### Performance Improvements * 增加警告,修复一些样式错乱问题 ([fd54c2f](https://github.com/certd/certd/commit/fd54c2ffac492222e85ff2f5f49a9ee5cfc73588)) * ssh登录支持openssh格式私钥、支持私钥密码 ([5c2c508](https://github.com/certd/certd/commit/5c2c50839a9076004f9034d754ac6deb531acdfb)) ## [1.20.12](https://github.com/certd/certd/compare/v1.20.10...v1.20.12) (2024-06-17) ### Bug Fixes * 修复aliyun域名超过100个找不到域名的bug ([5b1494b](https://github.com/certd/certd/commit/5b1494b3ce93d1026dc56ee741342fbb8bf7be24)) ### Performance Improvements * 增加系统设置,可以关闭自助注册功能 ([20feace](https://github.com/certd/certd/commit/20feacea12d43386540db6a600f391d786be4014)) * 增加cloudflare access token说明 ([934e6e2](https://github.com/certd/certd/commit/934e6e2bd05387cd50ffab95f230933543954098)) * 支持重置管理员密码,忘记密码的补救方案 ([732cbc5](https://github.com/certd/certd/commit/732cbc5e927b526850724594830392b2f10c6705)) * 支持cloudflare域名 ([fbb9a47](https://github.com/certd/certd/commit/fbb9a47e8f7bb805289b9ee64bd46ffee0f01c06)) ## [1.20.10](https://github.com/certd/certd/compare/v1.20.9...v1.20.10) (2024-05-30) ### Bug Fixes * 增加权限相关helper说明 ([83e4083](https://github.com/certd/certd/commit/83e40836ebff10bec60efe8933183e1ba1c22bf9)) * 增加权限相关helper说明 ([4304c94](https://github.com/certd/certd/commit/4304c9443ad9248f63dd6d8c512d8d6f32f90d37)) ### Performance Improvements * 上传到主机插件支持复制到本机路径 ([92446c3](https://github.com/certd/certd/commit/92446c339936f98f08f654b8971a7393d8435224)) * 优化文件下载包名 ([d9eb927](https://github.com/certd/certd/commit/d9eb927b0a1445feab08b1958aa9ea80637a5ae6)) * 增加任务复制功能 ([39ad759](https://github.com/certd/certd/commit/39ad7597fa0e19cc1f7631bbd6fea0a9e05a62c9)) ## [1.20.9](https://github.com/certd/certd/compare/v1.20.8...v1.20.9) (2024-03-22) **Note:** Version bump only for package root ## [1.20.8](https://github.com/certd/certd/compare/v1.20.7...v1.20.8) (2024-03-22) **Note:** Version bump only for package root ## [1.20.7](https://github.com/certd/certd/compare/v1.20.6...v1.20.7) (2024-03-22) **Note:** Version bump only for package root ## [1.20.6](https://github.com/certd/certd/compare/v1.20.5...v1.20.6) (2024-03-21) ### Bug Fixes * 调整按钮图标到居中位置 ([836d18f](https://github.com/certd/certd/commit/836d18f07e22d00faf2f213bc3301a6672b5bafc)) ### Performance Improvements * 插件贡献文档及示例 ([72fb20a](https://github.com/certd/certd/commit/72fb20abf3ba5bdd862575d2907703a52fd7eb17)) ## [1.20.5](https://github.com/certd/certd/compare/v1.20.2...v1.20.5) (2024-03-11) ### Bug Fixes * 修复腾讯云cdn部署无法选择端点的bug ([154409b](https://github.com/certd/certd/commit/154409b1dfee3ea1caae740ad9c1f99a6e7a9814)) ## [1.20.2](https://github.com/certd/certd/compare/v1.2.1...v1.20.2) (2024-02-28) ### Bug Fixes * 临时修复阿里云domainlist接口返回域名列表不全的问题,后续还需要增加翻页查询 ([849c145](https://github.com/certd/certd/commit/849c145926984762bd9dbec87bd91cd047fc0855)) ## [1.2.1](https://github.com/certd/certd/compare/v1.2.0...v1.2.1) (2023-12-12) ### Bug Fixes * 修复邮箱设置无效的bug ([aaa3224](https://github.com/certd/certd/commit/aaa322464d0f65e924d1850995540d396ee24d25)) **Note:** Version bump only for package root # [1.2.0](https://github.com/certd/certd/compare/v1.1.6...v1.2.0) (2023-10-27) * 🔱: [client] sync upgrade with 2 commits [trident-sync] ([aa3207f](https://github.com/certd/certd/commit/aa3207fca5f15f7c3da789989d99c8ae7d1c4551)) ### BREAKING CHANGES * search支持自定义布局,search.layout、search.collapse转移到 search.container之下。如果想使用原来的search组件,请配置search.is=fs-search-v1 ## [1.1.6](https://github.com/certd/certd/compare/v1.1.5...v1.1.6) (2023-07-10) ### Bug Fixes * 修复上传证书到腾讯云失败的bug ([e950322](https://github.com/certd/certd/commit/e950322232e19d1263b8552eefa5b0150fd7864e)) ## [1.1.5](https://github.com/certd/certd/compare/v1.1.4...v1.1.5) (2023-07-03) **Note:** Version bump only for package root ## [1.1.4](https://github.com/certd/certd/compare/v1.1.3...v1.1.4) (2023-07-03) ### Bug Fixes * 成功图标转动的问题 ([f87eee3](https://github.com/certd/certd/commit/f87eee3b9ff1ef9874e79a81fe0ed7104cb9ee8c)) ### Performance Improvements * cancel task ([bc65c0a](https://github.com/certd/certd/commit/bc65c0a786360c087fe95cad93ec6a87804cc5ee)) * flush log ([891a43a](https://github.com/certd/certd/commit/891a43ae6716ff98ed06643f7da2e35199ee195c)) * flush logger ([91be682](https://github.com/certd/certd/commit/91be6826b902e0f302b1a6cbdb1d24e15914c18d)) * timeout ([3eeb1f7](https://github.com/certd/certd/commit/3eeb1f77aa2922f3545f3d2067f561d95621d54f)) ## [1.1.3](https://github.com/certd/certd/compare/v1.1.2...v1.1.3) (2023-07-03) **Note:** Version bump only for package root ## [1.1.2](https://github.com/certd/certd/compare/v1.1.1...v1.1.2) (2023-07-03) **Note:** Version bump only for package root ## [1.1.1](https://github.com/certd/certd/compare/v1.1.0...v1.1.1) (2023-06-28) **Note:** Version bump only for package root # [1.1.0](https://github.com/certd/certd/compare/v1.0.6...v1.1.0) (2023-06-28) ### Bug Fixes * 修复access选择类型trigger ([2851a33](https://github.com/certd/certd/commit/2851a33eb2510f038fadb55da29512597a4ba512)) ### Features * 权限控制 ([27a4c81](https://github.com/certd/certd/commit/27a4c81c6d70e70abb3892c3ea58d4719988808a)) * 邮件通知 ([937e3fa](https://github.com/certd/certd/commit/937e3fac19cd03b8aa91db8ba03fda7fcfbacea2)) * cert download ([5a51c14](https://github.com/certd/certd/commit/5a51c14de521cb8075a80d2ae41a16e6d5281259)) * config merge ([fdc25dc](https://github.com/certd/certd/commit/fdc25dc0d795555cffacc4572648ec158988fbbb)) * save files ([99522fb](https://github.com/certd/certd/commit/99522fb49adb42c1dfdf7bec3dd52d641158285b)) * save files ([671d273](https://github.com/certd/certd/commit/671d273e2f9136d16896536b0ca127cf372f1619)) ## [1.0.6](https://github.com/certd/certd/compare/v1.0.5...v1.0.6) (2023-05-25) **Note:** Version bump only for package root ## [1.0.5](https://github.com/certd/certd/compare/v1.0.4...v1.0.5) (2023-05-25) **Note:** Version bump only for package root ## [1.0.4](https://github.com/certd/certd/compare/v1.0.3...v1.0.4) (2023-05-25) **Note:** Version bump only for package root ## [1.0.3](https://github.com/certd/certd/compare/v1.0.2...v1.0.3) (2023-05-25) **Note:** Version bump only for package root ## [1.0.2](https://github.com/certd/certd/compare/v1.0.1...v1.0.2) (2023-05-24) **Note:** Version bump only for package root ## [1.0.1](https://github.com/certd/certd/compare/v1.0.0...v1.0.1) (2023-05-24) **Note:** Version bump only for package root ================================================ FILE: docs/guide/contact/index.md ================================================ # 联系我们 ## 1. 交流群 如有疑问,欢迎加入群聊(请备注certd) 如有疑问,欢迎加入群聊(请备注certd) | 加群 | 微信群 | QQ群 | |---------|-------|-------| | 二维码 | | | ## 2. 加作者好友 | 加作者好友 | 微信 QQ | |---------|-------------------------------------------------------------| | 二维码 | | ================================================ FILE: docs/guide/development/demo/access.md ================================================ # 授权插件Demo ```ts import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline'; import { isDev } from '../../utils/env.js'; /** * 这个注解将注册一个授权配置 * 在certd的后台管理系统中,用户可以选择添加此类型的授权 */ @IsAccess({ name: 'demo', title: '授权插件示例', icon: 'clarity:plugin-line', desc: '', }) export class DemoAccess extends BaseAccess { /** * 授权属性配置 */ @AccessInput({ title: '密钥Id', component: { placeholder: 'demoKeyId', }, required: true, }) demoKeyId = ''; /** * 授权属性配置 */ @AccessInput({ //标题 title: '密钥串', component: { //input组件的placeholder placeholder: 'demoKeySecret', }, //是否必填 required: true, //改属性是否需要加密 encrypt: true, }) //属性名称 demoKeySecret = ''; } new DemoAccess(); ``` # 阿里云授权 ```ts import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline"; @IsAccess({ name: "aliyun", title: "阿里云授权", desc: "", icon: "ant-design:aliyun-outlined", order: 0, }) export class AliyunAccess extends BaseAccess { @AccessInput({ title: "accessKeyId", component: { placeholder: "accessKeyId", }, helper: "登录阿里云控制台->AccessKey管理页面获取。", required: true, }) accessKeyId = ""; @AccessInput({ title: "accessKeySecret", component: { placeholder: "accessKeySecret", }, required: true, encrypt: true, helper: "注意:证书申请需要dns解析权限;其他阿里云插件,需要对应的权限,比如证书上传需要证书管理权限;嫌麻烦就用主账号的全量权限的accessKey", }) accessKeySecret = ""; } new AliyunAccess(); ``` ================================================ FILE: docs/guide/development/index.md ================================================ # 本地开发 欢迎贡献插件 建议nodejs版本 `20.x` 及以上 ## 一、本地调试运行 ### 克隆代码 ```shell # 克隆代码 git clone https://github.com/certd/certd --depth=1 #进入项目目录 cd certd ``` ### 修改pnpm-workspace.yaml文件 重要:否则无法正确加载专业版的access和plugin ```yaml # pnpm-workspace.yaml packages: - 'packages/**' # <--------------注释掉这一行,PR时不要提交此修改 - 'packages/ui/**' ``` ### 安装依赖和初始化: ```shell # 安装pnpm,如果提示npm命令不存在,就需要先安装nodejs npm install -g pnpm--registry=https://registry.npmmirror.com # 使用国内镜像源,如果有代理,就不需要 pnpm config set registry https://registry.npmmirror.com # 安装依赖 pnpm install # 初始化构建 pnpm init ``` ### 启动 server: ```shell cd packages/ui/certd-server pnpm dev ``` ### 启动 client: ```shell cd packages/ui/certd-client pnpm dev # 会自动打开浏览器,确认正常运行 ``` ## 二、开发插件 进入 `packages/ui/certd-server/src/plugins` ### 1.复制`plugin-demo`目录作为你的插件目录 比如你想做`cloudflare`的插件,那么你可以复制`plugin-demo`目录,将其命名成`plugin-cloudflare`。 以下均以`plugin-cloudflare`为例进行说明,你需要将其替换成你的插件名称 ### 2. access授权 如果这是一个新的平台,它应该有授权方式,比如accessKey accessSecret之类的 参考`plugin-cloudflare/access.ts` 修改为你要做的平台的`access` 这样用户就可以在`certd`后台中创建这种授权凭证了 ### 3. dns-provider 如果域名是这个平台进行解析的,那么你需要实现dns-provider,(申请证书需要) 参考`plugin-cloudflare/dns-provider.ts` 修改为你要做的平台的`dns-provider` ### 4. plugin-deploy 如果这个平台有需要部署证书的地方 参考`plugin-cloudflare/plugins/plugin-deploy-to-xx.ts` 修改为你要做的平台的`plugin-deploy-to-xx` ### 5. 增加导入 在`plugin-cloudflare/index.ts`中增加你的插件的`import` ```ts export * from './dns-provider' export * from './access' export * from './plugins/plugin-deploy-to-xx' ```` 在`./src/plugins/index.ts`中增加`import` ```ts export * from "./plugin-cloudflare.js" ``` ### 6. 重启服务进行调试 刷新浏览器,检查你的插件是否工作正常, 确保能够正常进行证书申请和部署 ## 三、提交PR 我们将尽快审核PR ## 四、 注意事项 ### 1. 如何让任务报错停止 ```js // 抛出异常即可使任务停止,否则会判定为成功 throw new Error("错误信息") ``` ## 五、贡献插件送激活码 - PR要求,插件功能完整,代码规范 - PR通过后,联系我们,送您一个半年期专业版激活码 ================================================ FILE: docs/guide/donate/index.md ================================================ # 捐赠 ************************ 支持开源,为爱发电,我已入驻爱发电 https://afdian.com/a/greper ## 发电权益: 1. 可加入发电专属群,可以获得作者一对一技术支持 2. 您的需求我们将优先实现,并且将作为专业版功能提供 3. 一年期专业版激活码 ## 专业版特权对比 | 功能 | 免费版 | 专业版 | |---------|------------------------|-----------------------------| | 免费证书申请 | 免费无限制 | 免费无限制 | | 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署等 | 支持群晖、宝塔、1Panel等,持续开发中 | | 证书流水线条数 | 无限制 | 无限制 | | 站点证书监控 | 限制1条 | 无限制 | | 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、飞书、anpush、server酱等 | ## 专业版激活方式 ![](./images/plus.png) 发电后,在私信中获取激活码 ************************ ================================================ FILE: docs/guide/feature/cname/index.md ================================================ # CNAME代理校验方式 通过CNAME代理校验方式,可以给`Certd`不支持的域名服务商的域名申请证书。 ## 1. 前言 * 申请证书是需要`校验域名所有权`的。 * `DNS校验方式`需要开发适配DNS服务商的接口 * 目前`Certd`已实现`主流域名注册商`的接口(阿里云、腾讯云、华为云、Cloudflare、西数) * 如果域名不在这几家,`DNS校验方式`就行不通 * 那么就只能通过`CNAME代理校验方式`来实现`证书自动申请` ## 2. 原理 * 假设你要申请证书的域名叫:`cert.com` ,它是在`Certd`不支持的服务商注册的 * 假设我们还有另外一个域名叫:`proxy.com`,它是在`Certd`支持的服务商注册的。 * 当我们按照如下进行配置时 ``` CNAME记录(手动、固定) TXT记录(自动、随机) _acme-challenge.cert.com ---> xxxxx.cname.proxy.com ----> txt-record-abcdefg ``` * 证书颁发机构就可以从`_acme-challenge.cert.com`查到TXT记录 `txt-record-abcdefg`,从而完成域名所有权校验。 * 以上可以看出 `xxxxx.cname.proxy.com ----> txt-record-abcdefg` 这一段`Certd` 是可以自动添加的。 * 剩下的只需要在你的`cert.com`域名中手动添加一条固定的`CNAME解析`即可 ## 3. Certd CNAME使用步骤 1. 创建证书流水线,输入你要申请证书的域名,假设就是`cert.com`,然后选择`CNAME`校验方式 2. 此时需要配置验证计划,Certd会生成一个随机的CNAME记录模版,例如:`_acme-challenge`->`xxxxxx.cname.proxy.com` ![](./images/cname2.png) 3. 您需要手动在你的`cert.com`域名中添加CNAME解析,点击验证,校验成功后就可以开始申请证书了 (此操作每个域名只需要做一次,后续可以重复使用,注意不要删除添加的CNAME记录) ![](./images/cname3.png) ![](./images/cname4.png) 4. 申请过程中,Certd会在`xxxxxx.cname.proxy.com`下自动添加TXT记录。 5. 到此即可自动化申请证书了 ================================================ FILE: docs/guide/feature/safe/hidden/index.md ================================================ # 站点隐藏 * 一般来说Certd设置好之后,很少需要访问。 * 所以我们`平时`可以把`站点访问关闭`,需要的时候再打开,减少站点被攻击的风险 ## 1、开启站点隐藏 `系统管理->系统设置->安全设置->站点隐藏 ` ![](./images/hidden1.png) :::warning 注意保存好`解除地址`和`解除密码` ::: ## 2、临时关闭站点隐藏 访问上面的`解除地址`,输入`解除密码`,`临时解除`站点隐藏 ![](./images/hidden2.png) ## 3、忘记解除地址和解除密码怎么办 登录服务器,在数据库平级的目录下创建`.unhidden`命名的空白文件,即可临时解除站点隐藏 临时解除后会自动删除`.unhidden`文件,请尽快设置好新的`解除地址`和`解除密码`,并记住 ================================================ FILE: docs/guide/feature/safe/index.md ================================================ # 安全特性 Certd 存储了证书以及授权等敏感数据,所以需要严格保障安全。 我们提供了以下安全特性,以及安全生产建议(请遵照建议进行生产部署以保障数据安全) ## 一、站点安全特性 ### 1、 授权数据加密存储【默认开启】 * 所有的授权敏感字段会加密后存储 * 每个用户独立维护授权数据,连管理员都无权查看 ![星号部分为加密数据](./images/access.png) 星号部分为加密数据 ### 2、 密码防爆破【默认开启】 * 登录失败次数过多,账号将被锁定,最高24小时(重启服务可解除锁定) * 用户登录密码加密hash后存储,无法计算出密码明文 ![](./images/login.png) ### 3、站点隐藏【建议开启】 * 一般来说Certd设置好之后,后续很少需要访问修改。 * 所以我们平时可以把站点访问关闭,需要的时候再打开,减少站点被攻击的风险 * 请前往 `系统管理->系统设置->安全设置->开启站点隐藏` ![](./images/hidden.png) 点击查看 [站点隐藏功能详细使用说明](./hidden/) ### 4、登录双重验证 支持2FA双重认证 ![](./images/2fa.png) ### 5、数据库自动备份【建议开启】 * [自动备份设置说明](../../use/backup/) ## 二、安全生产建议 尽管`Cert`本身实现了很多安全特性,但`外部环境的安全`仍需要您来确保。 请`务必`遵循如下建议做好安全防护 * 请`务必`使用`HTTPS协议`访问本应用,避免被中间人攻击 * 请`务必`使用`web应用防火墙`防护本应用,防止XSS、SQL注入等攻击 * 请`务必`做好`服务器本身`的安全防护,防止数据库泄露 * 请`务必`做好[`数据备份`](../../use/backup/),避免数据丢失 * 请`务必`修改管理员账号用户名,且建议将admin注册为普通用户,且设置为禁用。 * 建议开启[`站点隐藏`](./hidden/)功能 ================================================ FILE: docs/guide/image.md ================================================ # 镜像说明 ## 国内镜像地址: * `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` * `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7`、`[version]-armv7` ## DockerHub地址: * `https://hub.docker.com/r/greper/certd` * `greper/certd:latest` * `greper/certd:armv7`、`greper/certd:[version]-armv7` ## GitHub Packages地址: * `ghcr.io/certd/certd:latest` * `ghcr.io/certd/certd:armv7`、`ghcr.io/certd/certd:[version]-armv7` * ## 镜像构建公开 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用 * [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml) ![](../images/action/action-build.jpg) ================================================ FILE: docs/guide/index.md ================================================ # Certd Certd 是一款开源、免费、全自动申请和部署更新SSL证书的工具。 后缀d取自linux守护进程的命名风格,意为证书守护进程。 关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具 ## 1、关于证书续期 >* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。 >* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。 >* 免费证书过期时间90天,以后可能还会缩短,所以自动化部署必不可少 ## 2、项目特性 本项目不仅支持证书申请过程自动化,还可以自动化部署更新证书,让你的证书永不过期。 * 全自动申请证书(支持所有注册商注册的域名,支持DNS-01、HTTP-01、CNAME代理等多种域名验证方式) * 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等70+部署插件) * 支持通配符域名/泛域名,支持多个域名打到一个证书上,支持pem、pfx、der、jks等多种证书格式 * 邮件通知、webhook通知、企微、钉钉、飞书、anpush等多种通知方式 * 私有化部署,数据保存本地,安装升级非常简单快捷 * 镜像由Github Actions构建,过程公开透明 * 授权加密,站点隐藏,2FA,密码防爆破等多重安全保障 * 支持SQLite,PostgreSQL、MySQL多种数据库 * 开放接口支持 * 站点证书监控 * 多用户管理 ![](../images/intro/intro.svg) ================================================ FILE: docs/guide/install/1panel/index.md ================================================ # 部署到1Panel面板 ## 一、安装1Panel https://1panel.cn/docs/installation/online_installation/ ## 二、部署certd 1. 打开`docker-compose.yaml`,整个内容复制下来 https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml 2. 然后到 `1Panel->容器->编排->新建编排` 输入名称,粘贴`docker-compose.yaml`原文内容 ![](./images/1.png) 3. 点击确定,启动容器 ![](./images/2.png) > 默认使用sqlite数据库,数据保存在`/data/certd`目录下,您可以手动备份该目录 > certd还支持`mysql`和`postgresql`数据库,[点我了解如何切换其他数据库](../database) 3. 访问测试 http://ip:7001 https://ip:7002 默认账号密码 admin/123456 登录后请及时修改密码 ## 三、升级 1. 找到容器,点击更多->升级 ![](./images/upgrade-1.png) 2. 选择强制拉取镜像,点击确认即可 ![img.png](./images/upgrade-2.png) ## 四、数据备份 > 默认数据保存在`/data/certd`目录下,可以手动备份 > 建议配置一条 [数据库备份流水线](../../use/backup/),自动备份 ## 五、备份恢复 将备份的`db.sqlite`及同目录下的其他文件一起覆盖到原来的位置,重启certd即可 ================================================ FILE: docs/guide/install/baota/index.md ================================================ # 部署到宝塔面板 ## 一、安装 宝塔面板支持两种方式安装Certd,请选择其中一种方式 ### 1、安装宝塔面板 * 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/u/CL3JHS) 官网,选择`9.2.0`以上正式版的脚本下载安装 * 登录宝塔面板,在菜单栏中点击 Docker,首次进入会提示安装Docker服务,点击立即安装,按提示完成安装 ### 2、部署certd 以下两种方式人选一种: #### 2.1 应用商店方式一键部署【推荐】 * 在宝塔Docker应用商店中找到`certd`(要先点右上角更新应用) * 点击安装,配置域名等基本信息即可完成安装 > 需要宝塔9.2.0及以上版本才支持 #### 2.2 容器编排方式部署 1. 打开`docker-compose.yaml`,整个内容复制下来 https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml 然后到宝塔里面进到docker->容器编排->添加容器编排 ![](./images/1.png) 点击确定,等待启动完成 ![](./images/2.png) > certd默认使用sqlite数据库,另外支持`mysql`和`postgresql`数据库,[点我了解如何切换其他数据库](../database) ## 二、访问应用 http://ip:7001 https://ip:7002 默认账号密码 admin/123456 登录后请及时修改密码 ## 三、如何升级 宝塔升级certd非常简单 打开容器页面: `docker`->`容器编排`->`左侧选择Certd`->`更新镜像` ![img.png](./images/upgrade.png) ## 四、数据备份 部署方式不同,数据保存位置不同 ### 4.1 应用商店部署方式 点击进入安装路径,数据保存在`./data`目录下,可以手动备份 ![](./images/app.png) ![](./images/db_path.png) ### 4.2 容器编排部署方式 数据默认保存在`/data/certd`目录下,可以手动备份 ### 4.3 自动备份 > 建议配置一条 [数据库备份流水线](../../use/backup/),自动备份 ## 五、备份恢复 将备份的`db.sqlite`及同目录下的其他文件一起覆盖到原来的位置,重启certd即可 ## 六、宝塔部署相关问题排查 ### 1. 无法访问Certd 1. 确认服务器的安全规则,是否放开了对应端口 2. 确认宝塔防火墙是否放开对应端口 3. 尝试将Certd容器加入宝塔的`bridge`网络 ![](./images/network.png) ================================================ FILE: docs/guide/install/database.md ================================================ # 切换数据库 certd支持如下几种数据库: 1. sqlite3 (默认) 2. mysql 3. postgresql 您可以按如下两种方式切换数据库 ## 一、全新安装 ::: tip 以下按照`docker-compose`安装方式介绍如何使用mysql或postgresql数据库 如果您使用其他方式部署,请自行修改对应的环境变量即可。 ::: ### 1.1、使用mysql数据库 1. 安装mysql,创建数据库 `(注意:charset=utf8mb4, collation=utf8mb4_bin)` 2. 下载最新的docker-compose.yaml 3. 修改环境变量配置 ```yaml services: certd: environment: # 使用mysql数据库,需要提前创建数据库 charset=utf8mb4, collation=utf8mb4_bin - certd_flyway_scriptDir=./db/migration-mysql # 升级脚本目录 【照抄】 - certd_typeorm_dataSource_default_type=mysql # 数据库类型, 或者 mariadb - certd_typeorm_dataSource_default_host=localhost # 数据库地址 - certd_typeorm_dataSource_default_port=3306 # 数据库端口 - certd_typeorm_dataSource_default_username=root # 用户名 - certd_typeorm_dataSource_default_password=yourpasswd # 密码 - certd_typeorm_dataSource_default_database=certd # 数据库名 ``` 4. 启动certd ```shell docker-compose up -d ``` ### 1.2、使用Postgresql数据库 1. 安装postgresql,创建数据库 2. 下载最新的docker-compose.yaml 3. 修改环境变量配置 ```yaml services: certd: environment: # 使用postgresql数据库,需要提前创建数据库 - certd_flyway_scriptDir=./db/migration-pg # 升级脚本目录 【照抄】 - certd_typeorm_dataSource_default_type=postgres # 数据库类型 【照抄】 - certd_typeorm_dataSource_default_host=localhost # 数据库地址 - certd_typeorm_dataSource_default_port=5433 # 数据库端口 - certd_typeorm_dataSource_default_username=postgres # 用户名 - certd_typeorm_dataSource_default_password=yourpasswd # 密码 - certd_typeorm_dataSource_default_database=certd # 数据库名 ``` 4. 启动certd ```shell docker-compose up -d ``` ## 二、从旧版的sqlite切换数据库 1. 先将`旧certd`升级到最新版 (`建议:备份sqlite数据库` ) 2. 按照上面全新安装方式部署一套`新的certd` (`注意:新旧版本的certd要一致`) 3. 使用数据库工具将数据从sqlite导入到mysql或postgresql (`注意:flyway_history数据表不要导入`) 4. 重启新certd 5. 确认没有问题之后,删除旧版certd ================================================ FILE: docs/guide/install/docker/index.md ================================================ # Docker方式部署 ## 一、安装 ### 1. 环境准备 1.1 准备一台云服务器 * 【阿里云】云服务器2核2G,新老用户同享,99元/年,续费同价!【 [立即购买](https://www.aliyun.com/benefit?scm=20140722.M_10244282._.V_1&source=5176.11533457&userCode=qya11txb )】 * 【腾讯云】云服务器2核2G,新老用户同享,99元/年,续费同价!【 [立即购买](https://cloud.tencent.com/act/cps/redirect?redirect=6094&cps_key=b3ef73330335d7a6efa4a4bbeeb6b2c9&from=console)】 1.2 安装docker、docker-compose https://docs.docker.com/engine/install/ 选择对应的操作系统,按照官方文档执行命令即可 ### 2. 部署certd容器 ```bash # 随便创建一个目录 mkdir certd # 进入目录 cd certd # 下载docker-compose.yaml文件,或者手动下载放到certd目录下 wget https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml # 可以根据需要修改里面的配置 # 1.修改镜像版本号【可选】 # 2.配置数据保存路径【可选】 # 3.修改端口号【可选】 vi docker-compose.yaml # 【可选】 # 启动certd docker compose up -d ``` > [手动下载docker-compose.yaml ](https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml) > 当前版本号: ![](https://img.shields.io/npm/v/%40certd%2Fpipeline) > 如果提示 没有docker compose命令,请安装docker-compose > https://docs.docker.com/compose/install/linux/ > certd默认使用sqlite数据库,另外还支持`mysql`和`postgresql`数据库,[点我了解如何切换其他数据库](../database) ### 3. 访问测试 http://your_server_ip:7001 https://your_server_ip:7002 默认账号密码:admin/123456 记得修改密码 ## 二、升级 ::: warning 如果您是第一次升级certd版本,切记切记先备份一下数据 ::: ### 如果使用固定版本号 1. 修改`docker-compose.yaml`中的镜像版本号 2. 运行`docker compose up -d` 即可 ### 如果使用`latest`版本 ```shell #重新拉取镜像 docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest # 重新启动容器 docker compose down docker compose up -d ``` ## 三、数据备份 > 数据默认存在`/data/certd`目录下,不用担心数据丢失 > 建议配置一条[数据库备份流水线](../../use/backup/) 自动备份 ## 四、备份恢复 将备份的`db.sqlite`及同目录下的其他文件一起覆盖到原来的位置,重启certd即可 ================================================ FILE: docs/guide/install/source/index.md ================================================ # 源码部署 如果没有开发基础、没有运维基础、没有`git`和`nodejs`基础,强烈不推荐此方式 ## 一、源码安装 ### 环境要求 - nodejs 20 及以上 ### 源码启动 ```shell # 克隆代码 git clone https://github.com/certd/certd --depth=1 # git checkout v1.x.x # 当v2主干分支代码无法正常启动时,可以尝试此命令,1.x.x换成最新版本号 cd certd # 启动服务 ./start.sh ``` >如果是windows,请先安装`git for windows` ,然后右键,选择`open git bash here`打开终端,再执行`./start.sh`命令 > 数据默认保存在 `./packages/ui/certd-server/data` 目录下,注意数据备份 ### 访问测试 http://your_server_ip:7001 https://your_server_ip:7002 默认账号密码:admin/123456 记得修改密码 ## 二、升级 ```shell cd certd # 确保数据安全,备份一下数据 cp -rf ./packages/ui/certd-server/data ../certd-data-backup git pull # 如果提示pull失败,可以尝试强制更新 # git checkout v2 -f && git pull # 先停止旧的服务,7001是certd的默认端口 kill -9 $(lsof -t -i:7001) # 重新编译启动 ./start.sh ``` ::: warning 升级certd版本前,切记切记先备份一下数据 ::: ## 三、数据备份 > 数据默认保存在 `./packages/ui/certd-server/data` 目录下 > 建议配置一条[数据库备份流水线](../../use/backup/) 自动备份 ## 四、备份恢复 将备份的`db.sqlite`及同目录下的其他文件覆盖到原来的位置,重启certd即可 ================================================ FILE: docs/guide/install/upgrade.md ================================================ # 版本升级 ## 升级方法 根据不同部署方式查看升级方法 1. [Docker方式部署升级](./docker/#二、升级) 2. [宝塔面板方式部署升级](./baota/#三、如何升级) 3. [1Panel面板方式部署升级](./1panel/#三、升级) 4. [源码方式部署](./source/#二、升级) ::: warning 如果您是第一次升级certd版本,切记切记先备份一下数据 ::: ## 升级日志 可以查看最新版本号,以及所有版本的更新日志 [CHANGELOG](../changelogs/CHANGELOG.md) ## 自动升级配置 ### 1. 方法一:使用watchtower监控 修改docker-compose.yaml文件增加如下配置, 使用watchtower监控自动升级 ```yaml services: certd: ... labels: com.centurylinklabs.watchtower.enable: "true" # ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest certd-updater: # 添加 Watchtower 服务 image: containrrr/watchtower:latest container_name: certd-updater restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock # 配置 自动更新 environment: - WATCHTOWER_CLEANUP=true # 自动清理旧版本容器 - WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器 - WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新 - WATCHTOWER_POLL_INTERVAL=600 # 每 10 分钟检查一次更新 ``` ### 2. 方法二:使用Certd版本监控功能 选择Github-检查Release版本插件 ![](./images/github-release.png) 按如下图填写配置 ![](./images/github-release-2.png) 检测到新版本后执行宿主机升级命令: ```shell # 拉取最新镜像 docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest # 升级容器命令, 替换成你自己的certd更新命令 export RESTART_CERT='sleep 10; cd ~/deploy/certd/ ; docker compose down; docker compose up -d' # 构造一个脚本10s后在后台执行,避免容器销毁时执行太快,导致流水线任务无法结束 nohup sh -c '$RESTART_CERT' >/dev/null 2>&1 & echo '10秒后重启' && exit ``` ================================================ FILE: docs/guide/license/index.md ================================================ # 开源协议 * 本项目遵循 GNU Affero General Public License(AGPL)开源协议。 * 允许个人和公司使用、复制、修改和分发本项目,禁止任何形式的商业用途 * 未获得商业授权情况下,禁止任何对logo、版权信息及授权许可相关代码的修改。 * 如需商业授权,请联系作者。 ================================================ FILE: docs/guide/link/index.md ================================================ # 我的其他项目 | 项目名称 | stars | 项目描述 | |---------------------------------------------------------|-------------------------------------------------------------------------------------------------------|-----------------------------------| | [袖手AI](https://ai.handsfree.work/) | | 袖手GPT,国内可用,无需FQ,每日免费额度 | | [fast-crud](https://gitee.com/fast-crud/fast-crud/) | GitHub stars | 基于vue3的crud快速开发框架 | | [dev-sidecar](https://github.com/docmirror/dev-sidecar/) | GitHub stars | 直连访问github工具,无需FQ,解决github无法访问的问题 | ================================================ FILE: docs/guide/open/index.md ================================================ # 开放接口 被动方式对第三方提供证书, 支持根据域名或证书id获取证书。 ## 获取keyId和KeySecret ![](./images/1.png) ## 接口文档 https://apifox.com/apidoc/shared-2e76f8c4-7c58-413b-a32d-a1316529af44/254949529e0 ## Token生成方法 header中传入x-certd-token即可调用开放接口 1、首先从OpenKey页面生成keyId,keySecret; 2、准备一个content( json字符串): content={"keyId":keyId, t:时间戳秒数, encrypt:false, signType:"md5"} `// encrypt返回结果是否加密` 3、将content加上keySecret进行签名: sign = md5(content + keySecret) 4、然后将content和sign分别base64后用.号连接: x-certd-token = base64(content) +"."+base64(sign) ## SDK 待开发 ## 客户端工具 ### SSL-Assistant `SSL Assistant` 是一个基于 Go 语言开发的跨平台证书部署管理助手。 支持自动扫描主机`Nginx`配置,然后从Certd拉取证书并部署。 在不想暴露ssh主机密码情况下,该工具非常好用。 开源地址: https://github.com/Youngxj/SSL-Assistant ================================================ FILE: docs/guide/plugins/access.md ================================================ # 授权列表 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **阿里云授权** | | | 2.| **EAB授权** | ZeroSSL证书申请需要EAB授权 | | 3.| **google cloud** | 谷歌云授权 | | 4.| **主机登录授权** | | | 5.| **SFTP授权** | | | 6.| **阿里云OSS授权** | 包含地域和Bucket | | 7.| **FTP授权** | | | 8.| **腾讯云** | | | 9.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 | | 10.| **七牛云授权** | | | 11.| **七牛OSS授权** | | | 12.| **天翼云授权** | | | 13.| **s3/minio授权** | S3/minio oss授权 | | 14.| **baota授权** | | | 15.| **易盾DCDN授权** | https://user.yiduncdn.com | | 16.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) | | 17.| **易发云短信** | sms.yfyidc.cn/ | | 18.| **cdnfly授权** | | | 19.| **群晖登录授权** | | | 20.| **k8s授权** | | | 21.| **1panel授权** | 账号和密码 | | 22.| **百度云授权** | | | 23.| **LeCDN授权** | | | 24.| **白山云授权** | | | 25.| **plesk授权** | | | 26.| **易支付** | | | 27.| **支付宝** | | | 28.| **微信支付** | | | 29.| **长亭雷池授权** | | | 30.| **lucky** | | | 31.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) | | 32.| **uniCloud** | unicloud授权 | | 33.| **华为云授权** | | | 34.| **西部数码授权** | | | 35.| **多吉云** | | | 36.| **我爱云授权** | 我爱云CDN | | 37.| **CacheFly** | CacheFly | | 38.| **Gcore** | Gcore | | 39.| **亚马逊云aws授权** | | | 40.| **dns.la授权** | | | 41.| **又拍云** | | | 42.| **火山引擎** | | | 43.| **京东云** | | | 44.| **51dns授权** | | ================================================ FILE: docs/guide/plugins/deploy.md ================================================ # 任务插件 共 `70` 款任务插件 ## 1. 证书申请 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **证书申请(JS版)** | 免费通配符域名证书申请,支持多个域名打到同一个证书上 | | 2.| **证书申请(Lego)** | 支持海量DNS解析提供商,推荐使用,一样的免费通配符域名证书申请,支持多个域名打到同一个证书上 | | 3.| **商用证书托管** | 手动上传自定义证书后,自动部署(每次证书有更新,都需要手动上传一次) | ## 2. 主机 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **FTP-上传证书到FTP** | 将证书上传到FTP服务器 | | 2.| **IIS-部署到IIS站点** | | | 3.| **主机-执行远程主机脚本命令** | 可以执行重启nginx等操作让证书生效 | | 4.| **主机-部署证书到SSH主机** | SFTP上传证书到主机,然后SSH执行部署脚本命令 | ## 3. CDN | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ | | 2.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) | | 3.| **cdnfly-部署证书到cdnfly** | cdnfly | | 4.| **百度云-部署证书到CDN** | 部署到百度云CDN | | 5.| **LeCDN-更新证书** | | | 6.| **LeCDN-更新证书V2** | 支持新版本LeCDN | | 7.| **白山云-更新证书** | | | 8.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 | | 9.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) | | 10.| **多吉云-部署到多吉云CDN** | | | 11.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN | | 12.| **CacheFly-部署证书到CacheFly** | 部署证书到 CacheFly | | 13.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn | | 14.| **Gcore-刷新Gcore证书** | 刷新现有的证书 | | 15.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS | ## 4. 面板 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **宝塔-面板证书部署** | 部署宝塔面板本身的ssl证书 | | 2.| **宝塔-网站证书部署** | 部署宝塔管理的站点的ssl证书,目前支持网站站点、docker站点等 | | 3.| **群晖-部署证书到群晖面板** | Synology,支持6.x以上版本 | | 4.| **K8S-部署证书到Secret** | 部署证书到k8s的secret | | 5.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress | | 6.| **1Panel-部署证书到1Panel** | 更新1Panel的证书 | | 7.| **Plesk-部署Plesk网站证书** | | | 8.| **雷池-更新证书** | 更新长亭雷池WAF的证书 | | 9.| **lucky-更新Lucky证书** | | | 10.| **uniCloud-部署到服务空间** | 部署到服务空间 | | 11.| **威联通-部署证书到威联通** | 部署证书到qnap | ## 5. 阿里云 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **阿里云-部署到Ack** | 部署到阿里云Ack集群Ingress等通过Secret管理证书的应用 | | 2.| **阿里云-部署至任意云资源** | 【不建议使用】需要消耗阿里云自动部署次数,支持SLB、LIVE、webHosting、VOD、CR、DCDN、DDoS、CDN、ALB、APIGateway、FC、GA、MSE、NLB、OSS、SAE、WAF等云产品 | | 3.| **阿里云-部署证书至CDN** | 自动部署域名证书至阿里云CDN | | 4.| **阿里云-部署证书至DCDN** | 依赖证书申请前置任务,自动部署域名证书至阿里云DCDN | | 5.| **阿里云-部署证书至OSS** | 自动部署域名证书至阿里云OSS | | 6.| **阿里云-上传证书到阿里云** | 如果不想在阿里云上同一份证书上传多次,可以把此任务作为前置任务,其他阿里云任务证书那一项选择此任务的输出 | | 7.| **阿里云-部署至阿里云WAF** | 部署证书到阿里云WAF | | 8.| **阿里云-部署至ALB(应用负载均衡)** | ALB,更新监听器的默认证书 | | 9.| **阿里云-部署至NLB(网络负载均衡)** | NLB,网络负载均衡,更新监听器的默认证书 | | 10.| **阿里云-部署至SLB(传统负载均衡)** | 部署证书到阿里云SLB(传统负载均衡) | | 11.| **阿里云-部署至阿里云FC(3.0)** | 部署证书到阿里云函数计算(FC3.0),【注意】证书的加密算法必须选择【pkcs1旧版】 | ## 6. 华为云 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **华为云-部署证书至CDN** | | ## 7. 腾讯云 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **腾讯云-部署证书到任意云资源** | 支持负载均衡、CDN、DDoS、直播、点播、Web应用防火墙、API网关、TEO、容器服务、对象存储、轻应用服务器、云原生微服务、云开发 | | 2.| **腾讯云-部署到CLB** | 暂时只支持单向认证证书,暂时只支持通用负载均衡 | | 3.| **腾讯云-部署到CDN(废弃)** | 已废弃,请使用v2版 | | 4.| **腾讯云-部署到CDN-v2** | 推荐使用 | | 5.| **腾讯云-上传证书到腾讯云** | 上传成功后输出:tencentCertId | | 6.| **腾讯云-部署证书到COS** | 部署到腾讯云COS源站域名证书【注意:很不稳定,需要重试很多次偶尔才能成功一次】 | | 7.| **腾讯云-部署到腾讯云EO** | 腾讯云边缘安全加速平台EO,必须配置上传证书到腾讯云任务 | | 8.| **腾讯云-删除即将过期证书** | 仅删除未使用的证书 | | 9.| **腾讯云-部署到TKE-ingress** | serverless集群请使用K8S部署插件;Qcloud类型需要【上传到腾讯云】作为前置任务;ApiServer未开启外网访问则需要做域名的内网IP映射 | ## 8. 火山引擎 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **火山引擎-部署证书至CDN** | 支持网页,文件下载,音视频点播 | | 2.| **火山引擎-部署证书至CLB** | 部署至火山引擎负载均衡 | | 3.| **火山引擎-上传证书至证书中心** | 上传证书至火山引擎证书中心 | | 4.| **火山引擎-部署证书至ALB** | 部署至火山引擎应用负载均衡 | | 5.| **火山引擎-部署证书至Live** | 部署至火山引擎视频直播 | ## 9. 京东云 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **京东云-部署证书至CDN** | 京东云内容分发网络 | | 2.| **京东云-更新已有证书** | 更新SSL数字证书中的证书 | | 3.| **京东云-上传新证书** | 上传证书到SSL数字证书中心 | ## 10. 七牛云 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **七牛云-部署证书至OSS** | 自动部署域名证书至七牛云KODO,注意是自定义源站域名,不是CDN域名 | | 2.| **七牛云-部署证书至CDN** | 自动部署域名证书至七牛云CDN | ## 11. 亚马逊云 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **AWS-部署证书到CloudFront** | 部署证书到 AWS CloudFront | ## 12. 其他 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **Demo-测试插件** | | | 2.| **重启 Certd** | 【仅管理员可用】 重启 certd的https服务,用于更新 Certd 的 ssl 证书 | | 3.| **自定义js脚本** | 【仅管理员】运行自定义js脚本执行 | | 4.| **等待** | 等待一段时间 | | 5.| **数据库备份** | 仅支持备份SQLite数据库 | ================================================ FILE: docs/guide/plugins/dns-provider.md ================================================ # DNS提供商 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **阿里云** | 阿里云DNS解析提供商 | | 2.| **腾讯云** | 腾讯云域名DNS解析提供者 | | 3.| **华为云** | 华为云DNS解析提供商 | | 4.| **西部数码** | west dns provider | | 5.| **dns.la** | dns.la | | 6.| **火山引擎** | 火山引擎DNS解析提供商 | | 7.| **京东云** | 京东云DNS解析提供商 | | 8.| **51dns** | 51DNS | ================================================ FILE: docs/guide/plugins/notification.md ================================================ # 通知插件 | 序号 | 名称 | 说明 | |-----|-----|-----| | 1.| **企业微信通知** | 企业微信群聊机器人通知 | | 2.| **电子邮件** | 电子邮件通知 | | 3.| **爱语飞飞微信通知(iyuu)** | https://iyuu.cn/ | | 4.| **自定义webhook** | 根据模版自定义http请求 | | 5.| **Server酱ᵀ** | https://sct.ftqq.com/ | | 6.| **Server酱³** | https://doc.sc3.ft07.com/serverchan3 | | 7.| **AnPush** | https://anpush.com | | 8.| **Telegram通知** | Telegram Bot推送通知 | | 9.| **Discord 通知** | Discord 机器人通知 | | 10.| **Slack通知** | Slack消息推送通知 | | 11.| **Bark 通知** | Bark 推送通知插件 | | 12.| **飞书通知** | 飞书群聊webhook通知 | ================================================ FILE: docs/guide/qa/index.md ================================================ # 常见报错解决 ## 1. getaddrinfo ENOTFOUND错误 如果出现`getaddrinfo ENOTFOUND`/`getaddrinfo EAI_AGAIN`错误,可以尝试在`docker-compose.yaml`中设置dns ```yaml version: '3.3' # 兼容旧版docker-compose services: certd: #↓↓↓↓ ------------ # 如果出现getaddrinfo ENOTFOUND 或 EAI_AGAIN错误,可以尝试设置dns dns: - 223.5.5.5 # 阿里云公共dns - 223.6.6.6 # # ↓↓↓↓ ------- # 如果你服务器在腾讯云,可以用这个替换上面阿里云的公共dns # - 119.29.29.29 # 腾讯云公共dns # - 182.254.116.116 # # ↓↓↓↓ ------- # 如果你服务器部署在国外,可以用这个替换上面阿里云的公共dns # - 8.8.8.8 # 谷歌公共dns # - 8.8.4.4 ``` 如果仍然有问题,按如下步骤检查是否能够ping通域名 ```shell docker exec -it certd /bin/sh ping www.baidu.com ping gg.px.certd.handfree.work ping app.handfree.work ``` 如果您是宝塔部署的 可以试试将容器网络加入brige网络,看是否解决问题 ![img.png](images/baota-net.png) 如果还是不行,请联系我们 ## 2. 连接IPv6超时 docker-compose 需要放开IPv6网络的配置 ```yaml services: certd: networks: - ip6net # ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络,还需要把上面networks的注释放开 networks: ip6net: enable_ipv6: true ipam: config: - subnet: 2001:db8::/64 ``` ## 3. SSL_CERT_NOT_MATCH_DOMAIN_ERROR 部署证书任务报类似 `SSL_CERT_NOT_MATCH_DOMAIN_ERROR`错误 这是由于当前流水线的证书域名与要部署的目标站点的域名不匹配导致的,在申请证书任务中,增加目标站点域名,重新运行流水线即可 ## 4. 没有服务器配置文件,请检查是否开启了外网映射! 宝塔网站证书部署报错:`Error: 没有服务器配置文件,请检查是否开启了外网映射!` 解决方案:先手动在宝塔网站中设置一次证书 ## 5. 如何查看容器日志 ```shell docker logs -f --tail 200 certd ``` ================================================ FILE: docs/guide/qa/use.md ================================================ # 使用问题 ## 1. 是否支持IP证书 因为ACME协议不支持IP证书,所以certd目前也不支持IP证书 ## 2. 建议设置多长时间运行一次流水线 建议每天运行一次,检查证书过期时间 当证书没过期时,自动跳过部署 当证书到期前35天(创建流水线时可以修改),将会自动重新申请证书,自动部署 ================================================ FILE: docs/guide/start.md ================================================ # 快速开始 本章节介绍如何快速开始使用`Certd` ## 一、 demo在线体验 官方DEMO地址,自助注册后体验 https://certd.handsfree.work/ > 注意数据将不定期清理,不定期停止定时任务,生产使用请自行部署 > 包含敏感信息,务必自己本地部署进行生产使用 ## 二、私有化部署 由于证书、授权信息等属于高度敏感数据,请务必私有化部署,保障数据安全 ### 1. 部署方式 1. [宝塔面板方式部署](./install/baota/) 2. [1Panel面板方式部署](./install/1panel/) 2. [Docker方式部署](./install/docker/) 3. [源码方式部署](./install/source/) ### 2. 访问测试 http://your_server_ip:7001 https://your_server_ip:7002 默认账号密码:admin/123456 记得修改密码 ================================================ FILE: docs/guide/tutorial.md ================================================ # 演示教程 教程演示从创建证书申请任务到自动部署证书全流程 `申请证书->部署证书->设置定时执行->设置邮件通知` 可以从如下两处查看演示流程 ## 1. 系统顶部使用教程菜单 点击`使用教程`可以学习如何自动申请和部署证书 ![img.png](../images/start/tt.png) ## 2. 图文教程链接 如果不方便登录系统,您还可以直接查看 [图文教程](https://gitee.com/certd/certd/blob/v2/step.md) ================================================ FILE: docs/guide/use/ESXi/index.md ================================================ # 部署证书到ESXi 使用`部署证书到主机插件`即可 ## 开启ssh 登陆ESXi Web后台,点击 主机 -> 操作 -> 服务 -> 启用 Secure Shell(SSH)打开SSH ## 添加部署到主机任务 ![img.png](./images/ssh.png) ## 配置重启脚本 ```bash /etc/init.d/hostd restart /etc/init.d/vpxa restart ``` ================================================ FILE: docs/guide/use/aliyun/index.md ================================================ # 阿里云相关 ## 阿里云客户端请求超时配置 配置环境变量 ```shell ALIYUN_CLIENT_CONNECT_TIMEOUT=10000 # 连接超时,单位毫秒 ALIYUN_CLIENT_READ_TIMEOUT=10000 #读取数据超时,单位毫秒 ``` ## 阿里云Access权限设置 * 申请证书 :`AliyunDNSFullAccess` * 上传证书到阿里云: `AliyunYundunCertFullAccess` * 部署证书到OSS: `AliyunYundunCertFullAccess`、`AliyunOSSFullAccess` * 部署证书到CDN: `AliyunYundunCertFullAccess`、`AliyunCDNFullAccess` * 部署证书到DCDN: `AliyunYundunCertFullAccess`、`AliyunDCDNFullAccess` ================================================ FILE: docs/guide/use/backup/index.md ================================================ # 数据库备份 * 两种备份方法: 1、手动备份 2、自动备份 * 本文仅限sqlite数据库。 ## 一、手动备份 数据库文件根据不同的部署方式保存的位置不一样,您可以手动复制出来进行备份 * docker: 默认保存在`/data/certd/db.sqlite` * 源码: 默认保存在 `./packages/ui/certd-server/data/db.sqlite` * 宝塔: [手动数据备份位置](https://certd.docmirror.cn/guide/install/baota/#%E5%9B%9B%E3%80%81%E6%95%B0%E6%8D%AE%E5%A4%87%E4%BB%BD) * 1panel: 默认保存在`/data/certd/db.sqlite` ## 二、自动备份 通过配置数据库自动备份流水线实现数据备份 ## 1. 创建自动备份流水线 ![](./images/1.png) ## 2. 添加备份任务 ![](./images/2.png) ## 3. 选择备份方法 ![](./images/3.png) ## 4. 配置定时和失败通知 ![](./images/4.png) ## 三、备份恢复 将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 ================================================ FILE: docs/guide/use/cert/index.md ================================================ # 证书申请失败情况 ## DNS记录问题 1. DNS 不要设置CAA记录,删除即可 2. DNSSEC相关报错,DNSSEC管理中删除即可 3. DNS 有其他平台申请过的_acme-challenge记录,删除即可 ================================================ FILE: docs/guide/use/cf/cf.md ================================================ # Cloudflare ## CF Token申请 ### 申请地址: https://dash.cloudflare.com/profile/api-tokens ### 权限设置: 需要设置权限和资源范围 权限包括:Zone.Zone.edit, Zone.DNS.edit 资源范围:要包含对应域名,推荐直接设置为All Zones 最终效果如下,可以切换语言为英文对比如下图检查 ![](./cf_token.png) ================================================ FILE: docs/guide/use/comm/index.md ================================================ # 商业版文档 ![](./images/index.png) ## 支付方式配置 * [支付宝支付配置](./payments/alipay.md) * [微信支付配置](./payments/wxpay.md) * [彩虹易支付配置](./payments/yizhifu.md) ================================================ FILE: docs/guide/use/comm/payments/alipay.md ================================================ # 支付宝配置 ## 配置步骤 1. 创建应用,获取APPID * 登录支付宝开放平台,进入开发者中心,创建网页应用,获取应用的AppId(左上角复制) * 开发者中心:https://open.alipay.com/develop/manage 2. 进入应用详情,选择开发设置,配置接口加签方式 (选择密钥类型) * 参考文档:https://opendocs.alipay.com/common/02kdnc?pathHash=fb0c752a * 此步骤完成后,可以获取应用的私钥、支付宝公钥。 * 注意:支付宝不会保存应用的私钥,你需要自己保管好私钥。 3. 在Certd后台配置支付宝 * 进入“系统”->"设置"->“支付设置” * 启用支付宝,选择“支付宝配置”,点击添加 * 填写支付宝AppId、应用私钥、支付宝公钥等信息即可。 ================================================ FILE: docs/guide/use/comm/payments/wxpay.md ================================================ # 微信支付配置 ## 配置步骤 1. 开通Native支付 * 登录微信支付平台 * 进入产品中心: https://pay.weixin.qq.com/index.php/extend/product/lists?tid=3 * 选择开通Native支付 2. 申请证书 * 进入“账户中心”->“API安全”->“商户API证书”->“管理证书” * 根据指引生成证书 * 得到私钥和公钥 3. 填写APIv3密钥 * 进入“账户中心”->“API安全”->“解密回调” * 填写APIv3密钥 * 参考文档 https://kf.qq.com/faq/180830E36vyQ180830AZFZvu.html 4. 在Certd后台配置微信支付 * 进入“系统”->"设置"->“支付设置” * 启用微信支付,选择“微信支付配置”,点击添加 * 填写微信支付商户号、证书私钥、证书公钥、APIv3密钥即可。 ================================================ FILE: docs/guide/use/comm/payments/yizhifu.md ================================================ # 彩虹易支付配置 彩虹易支付是一款非常流行的php聚合支付系统。 ## 配置步骤 1. 获取商户ID、商户密钥 * 登录彩虹易支付平台 * 进入用户中心:https://xxxxxx.com/user/userinfo.php?mod=api * 点击API信息 * 可以复制:接口地址、商户ID、商户密钥(key) * 点击查看文档,了解支持的签名类型,一般为MD5 2. 进入Certd后台配置彩虹易支付 * 进入“系统”->"设置"->“支付设置” * 启用彩虹易支付,选择“彩虹易支付配置”,点击添加 * 填写接口地址、商户ID、商户密钥、签名方式等信息即可。 ================================================ FILE: docs/guide/use/custom-script/index.md ================================================ # 自定义脚本插件 ## 1. 介绍 自定义脚本插件是一个通用的插件,可以通过编写脚本来实现各种功能,例如:调用第三方API、执行系统命令、发送邮件等。 ## 2. 使用示例 ```js // 如果需要引用第三方库,必须使用import语法 // const thirdSdk = await import("third-sdk-name") const certPem = ctx.self.cert.crt const certKey = ctx.self.cert.key //axios发起http请求上传证书 const res = await ctx.http.request({ url:"your_cert_deploy_url", method:"post", data:{ crt : certPem, key : certKey } }) if(!res || res.code !== 0){ //抛异常才能让任务失败 throw new Error("上传失败") } //不能用console.log,需要用ctx.logger 才能把日志打印在ui上 ctx.logger.info("上传成功",res.data) ``` ## 3. API 下面是`ctx`对象的`typescript`类型定义 ```ts type ctx = { CertReader: typeof CertReader; self: CustomScriptPlugin; //流水线定义 pipeline: Pipeline; //步骤定义 step: Step; //日志 logger: Logger; //当前步骤输入参数跟上一次执行比较是否有变化 inputChanged: boolean; //授权获取服务 accessService: IAccessService; //邮件服务 emailService: IEmailService; //cname记录服务 cnameProxyService: ICnameProxyService; //插件配置服务 pluginConfigService: IPluginConfigService; //流水线上下文 pipelineContext: IContext; //用户上下文 userContext: IContext; //http请求客户端 http: HttpClient; // http.request(AxiosConfig) //文件存储 fileStore: FileStore; //上一次执行结果状态 lastStatus?: Runnable; //用户取消信号 signal: AbortSignal; //工具类 utils: typeof utils; //用户信息 user: UserInfo; } type CertInfo = { crt:string; //fullchain证书,即 cert.pem, cert.crt key:string; // 私钥 ic: string; //中间证书 pfx: string;//PFX证书,base64编码 der: string;//DER证书,base64编码 } type CustomScriptPlugin = { //可以获取证书 cert: CertInfo } ``` ================================================ FILE: docs/guide/use/email/index.md ================================================ # 邮箱配置 ## 腾讯企业邮箱配置 1. 开启smtp ![](./images/qq-3.png) 2. 获取授权码作为密码 ![](./images/qq-1.png) ![](./images/qq-2.png) 3. 填写域名、端口和密码 ![](./images/qq-0.png) ## QQ邮箱配置 1. smtp配置 ```yaml smtp域名: smtp.qq.com smtp端口: 465 密码: 授权码,获取方式见下方 是否SSL: 是 ``` 2. 获取授权码 ![](./images/qq-11.png) ================================================ FILE: docs/guide/use/forgotpasswd/index.md ================================================ # 忘记管理员密码 解决方法如下: ## 1. 修改环境变量 修改docker-compose.yaml文件,将环境变量`certd_system_resetAdminPasswd`改为`true` ```yaml services: certd: environment: # 环境变量 - certd_system_resetAdminPasswd=false ``` ## 2. 重启容器 ```shell docker compose up -d docker logs -f --tail 500 certd # 观察日志,当日志中输出“重置1号管理员用户的密码完成”,即可操作下一步 ``` ## 3. 恢复环境变量 修改docker-compose.yaml,将`certd_system_resetAdminPasswd`改回`false` ## 4. 再次重启容器 ```shell docker compose up -d ``` ## 5. 默认密码登录 使用`admin/123456`登录系统,请及时修改管理员密码 ================================================ FILE: docs/guide/use/google/index.md ================================================ # google证书申请教程 ## 1、启用API 打开如下链接,启用 API https://console.cloud.google.com/apis/library/publicca.googleapis.com 打开该链接后点击“启用”,随后等待右侧出现“API已启用”则可以关闭该页。 ## 2、 获取授权 以下两种方式任选其一 ### 2.1 直接获取EAB 【推荐】 1. 打开“Google Cloud Shell”(在右上角点击激活CloudShell图标)。 等待分配完成后在 Shell 窗口内输入如下命令: ```shell gcloud beta publicca external-account-keys create ``` 2. 此时会弹出“为 Cloud Shell 提供授权”,点击授权即可。 执行完成后会返回类似如下输出;注意不要在没有收到 Google 的邮件时执行该命令,会返回命令不存在。 ```shell Created an external account key [b64MacKey: xxxxxxxxxxxxxxxx keyId: xxxxxxxxxxxxx] ``` 3. 到Certd中,创建一条EAB授权记录,填写keyId(=kid) 和 b64MacKey 信息 注意:keyId没有`]`结尾,不要把`]`也复制了 注意:EAB授权使用过一次之后,会绑定邮箱,后续再次使用时,要使用相同的邮箱 否则会报错 `Unknown external account binding (EAB) key. This may be due to the EAB key expiring which occurs 7 days after creation` ### 2.2 通过服务账号获取EAB 此方式可以自动EAB,需要配置代理 1. 创建服务账号 https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account&hl=zh-cn#step_index=1 2. 选择一个项目,进入创建服务账号页面 3. 给服务账号起一个名字,点击`创建并继续` 4. 向此服务账号授予对项目的访问权限: `选择角色`->`基本`->`Owner` 5. 点击完成 6. 点击服务账号,进入服务账号详情页面 7. 点击`添加密钥`->`创建新密钥`->`JSON`,下载密钥文件 8. 将json文件内容粘贴到 certd中 Google服务授权输入框中 ## 3、 创建证书流水线 选择证书提供商为google, 选择EAB授权 或 服务账号授权 ## 4、 其他就跟正常申请证书一样了 ================================================ FILE: docs/guide/use/host/windows.md ================================================ # 连接windows主机 远程主机基于ssh协议,通过ssh连接远程主机,执行命令。 ## windows开启OpenSSH Server ### 1. 安装OpenSSH Server * 下载安装包安装: https://github.com/PowerShell/Win32-OpenSSH/releases OpenSSH-Win64-vxx.xx.x.msi * 前往Microsoft官方文档查看如何开启openSSH,以及其他设置 https://learn.microsoft.com/zh-cn/windows-server/administration/openssh/openssh_install_firstuse?tabs=gui#install-openssh-for-windows ### 2. 启动OpenSSH Server服务 ``` win+R 弹出运行对话框,输入 services.msc 打开服务管理器 找到 OpenSSH SSH Server 启动ssh server服务,并且设置为自动启动 ``` ### 3. 测试ssh登录 使用你常用的ssh客户端,连接你的windows主机,进行测试 ```cmd # 如何确定你用户名 C:\Users\xxxxx> ↑↑↑↑---------这个就是windows ssh的登录用户名 ``` ### 4. 切换默认shell终端 安装openssh后,默认终端是cmd,建议切换成powershell ```shell # powershell中执行如下命令切换 # 设置默认shell为powershell 【推荐】 New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force # 恢复默认shell为cmd 【不推荐】 New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe" -PropertyType String -Force ``` ================================================ FILE: docs/guide/use/https/index.md ================================================ # Certd本身的https证书配置 ## 一、启用https `Certd`默认启用https,监听7002端口 如果你想关闭https,或者修改端口,可以在环境变量中配置 ```shell CERTD_HTTPS_ENABLE=true CERTD_HTTPS_port=7002 ``` ## 二、自动更新Certd的https证书 ### 1、创建证书流水线 参考Certd顶部的创建证书流水线教程 ### 2、配置复制到本机任务 将证书复制到certd的证书安装位置 证书路径:`ssl/cert.crt` 私钥路径:`ssl/cert.key` ![](./images/1.png) ![](./images/2.png) ### 3、配置重启Certd任务 重启certd的https server,让证书生效 ![img.png](./images/3.png) ### 4、配置定时任务 每天定时执行,最终效果如下 ![](./images/ok.png) :::warning 建议将本流水线的触发时间与其他流水线时间错开,避免重启时影响其他流水线的执行 ::: ================================================ FILE: docs/guide/use/pretask/index.md ================================================ # 带输出的前置任务 前置任务输出可以在后续任务中使用 比如上传证书到阿里云,会返回阿里云的CertId,之后其他阿里云的部署任务可以选择复用这个证书 ## 复用证书 ![img.png](images/pretask1.png) 在后续任务中可以选择前置任务的输出 ![img.png](images/pretask2.png) ================================================ FILE: docs/guide/use/rerun/index.md ================================================ # 如何强制重新执行任务 ## 强制重新执行任务 ![](./images/rerun.png) ================================================ FILE: docs/guide/use/setting/ipv6.md ================================================ # IPv6支持 ## 启用IPv6 在`docker-compose.yaml`中启用IPv6支持,放开如下注释: ```yaml # #↓↓↓↓ ------------------------------------------------------------- 启用ipv6网络 networks: - ip6net networks: ip6net: enable_ipv6: true ipam: config: - subnet: 2001:db8::/64 ``` ## 设置双栈网络优先级 可根据实际情况设置 ![img.png](./images/ipv6.png) ================================================ FILE: docs/guide/use/synology/index.md ================================================ # 群晖部署和证书更新 支持群晖`6.x`、`7.x` ## 一、群晖部署Certd 以下是群晖`7.x`的部署`certd`步骤。 群晖`6.x`请参考[docker部署](./../../install/docker/) ### 1. 打开Container Manager ![](./images/1.png) ### 2. 新增项目 ![](./images/2.png) ### 3. 配置Certd项目 ![](./images/3.png) ### 4. 外网访问设置 ![](./images/4.png) ### 5. 确认项目信息 ![](./images/5.png) 点击完成安装,等待certd启动完成即可 ### 6. 门户配置向导【可选】 ![](./images/6.png) ## 二、更新群晖证书 证书部署插件支持群晖`6.x`、`7.x` ## 1. 前提条件 * 已经部署了certd * 群晖上已经设置好了证书(证书建议设置好描述,插件需要根据描述查找证书) ## 2. 在certd上配置自动更新群晖证书插件 ![](./images/deploy.png) ## 3. 配置任务参数 ![](./images/deploy1.png) ## 4. 创建授权 ![](./images/deploy2.png) > 注意群晖上要做两个设置 ![](./images/setting2.png) ![](./images/setting1.png) ## 5. 运行部署 点击手动运行即可 ![](./images/deploy3.png) ![](./images/deploy4.png) ## 6. 配置通知和自动运行 ![](./images/notify.png) ================================================ FILE: docs/guide/use/tencent/index.md ================================================ # 腾讯云 ## 腾讯云API密钥设置 腾讯云其他部署需要API密钥,需要在腾讯云控制台进行设置 打开 https://console.cloud.tencent.com/cam/capi 然后按如下方式获取腾讯云的API密钥 ![](./tencent-access.png) ## 如何避免收到腾讯云证书过期邮件 > 新版本已经自动将证书设置为免提醒,certd上传的证书后续都不会再提醒了。 腾讯云在证书有效期还剩28天时会发送过期通知邮件 您可以通过配置“腾讯云过期证书删除”任务来避免收到此类邮件。 ![](./images/delete.png) 注意点: > 1. 选择腾讯云授权,需授权`服务角色SSL_QCSLinkedRoleInReplaceLoadCertificate`权限 > 2. `1.26.14`版本之前Certd创建的证书流水线默认是到期前20天才更新证书,需要将之前创建的证书申请任务的更新天数修改为35天,保证删除之前就已经替换掉即将过期证书 ![](./images/delete2.png) ## TKE service 的 TCP_SSL Opaque类型证书授权 部署证书到腾讯云TKE,如果报以下错误: `is forbidden: User "xxxxxx-xxxxx" cannot get resource "secrets" in API group "" in the namespace "default"'` 则需要单独从授权管理侧再授权子用户的权限 ![](./images/tcpssl.png) ![](./images/opaque.png) ================================================ FILE: docs/index.md ================================================ --- # https://vitepress.dev/reference/default-theme-home-page layout: home hero: name: "Certd" text: "开源、免费、全自动的证书管理工具" tagline: 让你的网站证书永不过期 image: src: /static/logo/logo.svg alt: Certd actions: - theme: brand text: 快速开始 link: /guide/start.md - theme: alt text: 演示教程 link: /guide/tutorial.md - theme: alt text: demo体验 link: https://certd.handfree.work features: - title: 全自动申请证书 details: 支持所有注册商注册的域名 - title: 全自动部署证书 details: 支持部署到主机、阿里云、腾讯云等,目前已支持60+部署插件 - title: 多域名、泛域名打到一个证书上 details: 支持通配符域名/泛域名,支持多个域名打到一个证书上 - title: 多证书格式支持 details: 支持pem、pfx、der、jks等多种证书格式,支持Google、Letsencrypt、ZeroSSL证书颁发机构 - title: 支持私有化部署 details: 授权数据加密存储,保障数据安全 - title: 多数据库支持 details: 支持SQLite、Postgresql、MySQL数据库 --- ================================================ FILE: docs/public/robots.txt ================================================ User-agent: * Allow: / ================================================ FILE: index.ts ================================================ ================================================ FILE: init.sh ================================================ current_pwd=$(pwd) echo "开始设置git配置" read -p "请输入username:" username git config user.name $username read -p "请输入email:" email git config user.email $email git config credential.helper "store --file=$current_pwd/.git/credential.store" echo "已设置记住git账号密码" git config core.autocrlf input echo "已设置auto crlf = input" git config core.filemode false echo "已设置忽略文件模式变化" echo "git配置完成" ================================================ FILE: lerna.json ================================================ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, "command": { "bootstrap": { "npmClientArgs": [ "--no-package-lock" ] } }, "npmClient": "pnpm", "version": "1.36.10" } ================================================ FILE: package.json ================================================ { "name": "root", "version": "1.20.4", "private": true, "type": "module", "devDependencies": { "@lerna-lite/cli": "^3.9.3", "@lerna-lite/publish": "^3.9.3", "@lerna-lite/run": "^3.9.3", "@lerna-lite/version": "^3.9.3", "medium-zoom": "^1.1.0", "vitepress": "^2.0.0-alpha.4", "vitepress-plugin-lightbox": "^1.0.2" }, "scripts": { "start": "lerna bootstrap --hoist", "devb": "lerna run dev-build", "i-all": "lerna link && lerna exec npm install ", "publish": "npm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits --create-release github && npm run afterpublishOnly && npm run commitAll", "afterpublishOnly": "npm run copylogs && time /t >build.trigger && git add ./build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && git push", "transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js", "commitAll": "git add . && git commit -m \"build: publish\" && git push && npm run commitPro", "commitPro": "cd ./packages/pro/ && git add . && git commit -m \"build: publish\" && git push", "copylogs": "copyfiles \"CHANGELOG.md\" ./docs/guide/changelogs/", "prepublishOnly1": "npm run check && lerna run build ", "prepublishOnly2": "npm run check && npm run before-build && lerna run build ", "before-build": "npm run transform-sql && cd ./packages/core/basic && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"", "deploy1": "node --experimental-json-modules deploy.js ", "check": "node --experimental-json-modules publish-check.js", "init": "lerna run build", "init:dev": "lerna run build", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "pub": "echo 1" }, "license": "AGPL-3.0", "dependencies": { "@certd/ui-server": "link:packages/ui/certd-server", "axios": "^1.7.7", "copyfiles": "^2.4.1", "lodash-es": "^4.17.21", "typescript": "^5.4.2" }, "workspaces": [ "packages/**" ], "pnpm": { "neverBuiltDependencies": [] } } ================================================ FILE: packages/core/acme-client/.editorconfig ================================================ # # http://editorconfig.org # root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true [{*.yml,*.yaml}] indent_size = 2 ================================================ FILE: packages/core/acme-client/.eslintrc ================================================ { "extends": [ "plugin:prettier/recommended", "prettier" ], "plugins": [ "eslint-plugin-import" ], "env": { "mocha": true }, "rules": { "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-empty-function": "off", // "no-unused-expressions": "off", "max-len": [ 0, 160, 2, { "ignoreUrls": true } ] } } ================================================ FILE: packages/core/acme-client/.github/scripts/tests-install-coredns.sh ================================================ #!/bin/bash # # Install CoreDNS for testing. # set -euo pipefail # Download and install wget -nv "https://github.com/coredns/coredns/releases/download/v${COREDNS_VERSION}/coredns_${COREDNS_VERSION}_linux_amd64.tgz" -O /tmp/coredns.tgz tar zxvf /tmp/coredns.tgz -C /usr/local/bin chown root:root /usr/local/bin/coredns chmod 0755 /usr/local/bin/coredns mkdir -p /etc/coredns # Zones tee /etc/coredns/db.example.com << EOF \$ORIGIN example.com. @ 3600 IN SOA ns.coredns.invalid. master.coredns.invalid. ( 2017042745 ; serial 7200 ; refresh 3600 ; retry 1209600 ; expire 3600 ; minimum ) 3600 IN NS ns1.example.com. 3600 IN NS ns2.example.com. ns1 3600 IN A 127.0.0.1 ns2 3600 IN A 127.0.0.1 @ 3600 IN A 127.0.0.1 www 3600 IN CNAME example.com. EOF # Config tee /etc/coredns/Corefile << EOF example.com { errors log bind 127.53.53.53 file /etc/coredns/db.example.com } test.example.com { errors log bind 127.53.53.53 forward . 127.0.0.1:${PEBBLECTS_DNS_PORT} } . { errors log bind 127.53.53.53 forward . 8.8.8.8 } EOF exit 0 ================================================ FILE: packages/core/acme-client/.github/scripts/tests-install-cts.sh ================================================ #!/bin/bash # # Install Pebble Challenge Test Server for testing. # set -euo pipefail # Download and install wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLECTS_VERSION}/pebble-challtestsrv-linux-amd64.tar.gz" -O /tmp/pebble-challtestsrv.tar.gz tar zxvf /tmp/pebble-challtestsrv.tar.gz -C /tmp mv /tmp/pebble-challtestsrv-linux-amd64/linux/amd64/pebble-challtestsrv /usr/local/bin/pebble-challtestsrv chown root:root /usr/local/bin/pebble-challtestsrv chmod 0755 /usr/local/bin/pebble-challtestsrv exit 0 ================================================ FILE: packages/core/acme-client/.github/scripts/tests-install-pebble.sh ================================================ #!/bin/bash # # Install Pebble for testing. # set -euo pipefail CONFIG_NAME="pebble-config.json" # Use Pebble EAB config if enabled set +u if [[ -n $ACME_CAP_EAB_ENABLED ]] && [[ $ACME_CAP_EAB_ENABLED -eq 1 ]]; then CONFIG_NAME="pebble-config-external-account-bindings.json" fi set -u # Download certs and config mkdir -p /etc/pebble wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/certs/pebble.minica.pem" -O /etc/pebble/ca.cert.pem wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/certs/localhost/cert.pem" -O /etc/pebble/cert.pem wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/certs/localhost/key.pem" -O /etc/pebble/key.pem wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/config/${CONFIG_NAME}" -O /etc/pebble/pebble.json # Download and install Pebble wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLE_VERSION}/pebble-linux-amd64.tar.gz" -O /tmp/pebble.tar.gz tar zxvf /tmp/pebble.tar.gz -C /tmp mv /tmp/pebble-linux-amd64/linux/amd64/pebble /usr/local/bin/pebble chown root:root /usr/local/bin/pebble chmod 0755 /usr/local/bin/pebble # Config sed -i 's#test/certs/localhost#/etc/pebble#' /etc/pebble/pebble.json exit 0 ================================================ FILE: packages/core/acme-client/.github/scripts/tests-wait-for-ca.sh ================================================ #!/bin/bash # # Wait for ACME server to accept connections. # set -euo pipefail MAX_ATTEMPTS=15 ATTEMPT=0 # Loop until ready while ! curl --cacert "${ACME_CA_CERT_PATH}" -s -D - "${ACME_DIRECTORY_URL}" | grep '^HTTP.*200' > /dev/null 2>&1; do ATTEMPT=$((ATTEMPT + 1)) # Max attempts if [[ $ATTEMPT -gt $MAX_ATTEMPTS ]]; then echo "[!] Waited ${ATTEMPT} attempts for server to become ready, exit 1" exit 1 fi # Retry echo "[-] Waiting 1 second for server to become ready, attempt: ${ATTEMPT}/${MAX_ATTEMPTS}, check: ${ACME_DIRECTORY_URL}, cert: ${ACME_CA_CERT_PATH}" sleep 1 done # Ready echo "[+] Server ready!" exit 0 ================================================ FILE: packages/core/acme-client/.github/workflows/tests.yml ================================================ name: test on: [push, pull_request] jobs: test: name: node=${{matrix.node}} eab=${{matrix.eab}} runs-on: ubuntu-latest strategy: matrix: node: [16, 18, 20] eab: [0, 1] # # Environment # env: FORCE_COLOR: 1 NPM_CONFIG_COLOR: always PEBBLE_VERSION: 2.6.0 PEBBLE_ALTERNATE_ROOTS: 2 PEBBLECTS_VERSION: 2.6.0 PEBBLECTS_DNS_PORT: 8053 COREDNS_VERSION: 1.11.1 NODE_EXTRA_CA_CERTS: /etc/pebble/ca.cert.pem ACME_CA_CERT_PATH: /etc/pebble/ca.cert.pem ACME_DIRECTORY_URL: https://127.0.0.1:14000/dir ACME_CHALLTESTSRV_URL: http://127.0.0.1:8055 ACME_PEBBLE_MANAGEMENT_URL: https://127.0.0.1:15000 ACME_DOMAIN_NAME: test.example.com ACME_CAP_EAB_ENABLED: ${{matrix.eab}} ACME_TLSALPN_PORT: 5001 ACME_HTTP_PORT: 5002 ACME_HTTPS_PORT: 5003 # # Pipeline # steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{matrix.node}} # Pebble Challenge Test Server - name: Install Pebble Challenge Test Server run: sudo -E /bin/bash ./.github/scripts/tests-install-cts.sh - name: Start Pebble Challenge Test Server run: |- nohup bash -c "pebble-challtestsrv \ -dns01 :${PEBBLECTS_DNS_PORT} \ -tlsalpn01 :${ACME_TLSALPN_PORT} \ -http01 :${ACME_HTTP_PORT} \ -https01 :${ACME_HTTPS_PORT} \ -defaultIPv4 127.0.0.1 \ -defaultIPv6 \"\" &" # Pebble - name: Install Pebble run: sudo -E /bin/bash ./.github/scripts/tests-install-pebble.sh - name: Start Pebble run: nohup bash -c "pebble -strict -config /etc/pebble/pebble.json -dnsserver 127.53.53.53:53 &" - name: Wait for Pebble run: /bin/bash ./.github/scripts/tests-wait-for-ca.sh # CoreDNS - name: Install CoreDNS run: sudo -E /bin/bash ./.github/scripts/tests-install-coredns.sh - name: Start CoreDNS run: nohup bash -c "sudo coredns -p 53 -conf /etc/coredns/Corefile &" - name: Use CoreDNS for DNS resolution run: echo "nameserver 127.53.53.53" | sudo tee /etc/resolv.conf # Run tests - run: npm i - run: npm run lint - run: npm run lint-types - run: npm run build-docs - run: npm run test ================================================ FILE: packages/core/acme-client/.gitignore ================================================ .actrc .vscode/ node_modules/ npm-debug.log package-lock.json /.idea/ ================================================ FILE: packages/core/acme-client/.prettierrc ================================================ { "printWidth": 220, "bracketSpacing": true, "singleQuote": false, "trailingComma": "es5", "arrowParens": "avoid" } ================================================ FILE: packages/core/acme-client/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [1.36.10](https://github.com/publishlab/node-acme-client/compare/v1.36.9...v1.36.10) (2025-07-18) **Note:** Version bump only for package @certd/acme-client ## [1.36.9](https://github.com/publishlab/node-acme-client/compare/v1.36.7...v1.36.9) (2025-07-15) **Note:** Version bump only for package @certd/acme-client ## [1.36.7](https://github.com/publishlab/node-acme-client/compare/v1.36.6...v1.36.7) (2025-07-15) **Note:** Version bump only for package @certd/acme-client ## [1.36.6](https://github.com/publishlab/node-acme-client/compare/v1.36.5...v1.36.6) (2025-07-14) **Note:** Version bump only for package @certd/acme-client ## [1.36.5](https://github.com/publishlab/node-acme-client/compare/v1.36.4...v1.36.5) (2025-07-11) **Note:** Version bump only for package @certd/acme-client ## [1.36.4](https://github.com/publishlab/node-acme-client/compare/v1.36.3...v1.36.4) (2025-07-10) **Note:** Version bump only for package @certd/acme-client ## [1.36.3](https://github.com/publishlab/node-acme-client/compare/v1.36.2...v1.36.3) (2025-07-07) **Note:** Version bump only for package @certd/acme-client ## [1.36.2](https://github.com/publishlab/node-acme-client/compare/v1.36.1...v1.36.2) (2025-07-06) **Note:** Version bump only for package @certd/acme-client ## [1.36.1](https://github.com/publishlab/node-acme-client/compare/v1.36.0...v1.36.1) (2025-07-02) **Note:** Version bump only for package @certd/acme-client # [1.36.0](https://github.com/publishlab/node-acme-client/compare/v1.35.5...v1.36.0) (2025-07-01) **Note:** Version bump only for package @certd/acme-client ## [1.35.5](https://github.com/publishlab/node-acme-client/compare/v1.35.4...v1.35.5) (2025-06-20) **Note:** Version bump only for package @certd/acme-client ## [1.35.4](https://github.com/publishlab/node-acme-client/compare/v1.35.3...v1.35.4) (2025-06-13) **Note:** Version bump only for package @certd/acme-client ## [1.35.3](https://github.com/publishlab/node-acme-client/compare/v1.35.2...v1.35.3) (2025-06-12) **Note:** Version bump only for package @certd/acme-client ## [1.35.2](https://github.com/publishlab/node-acme-client/compare/v1.35.1...v1.35.2) (2025-06-09) **Note:** Version bump only for package @certd/acme-client ## [1.35.1](https://github.com/publishlab/node-acme-client/compare/v1.35.0...v1.35.1) (2025-06-07) ### Performance Improvements * 证书申请支持letencrypt profile选项 ([2eb0e54](https://github.com/publishlab/node-acme-client/commit/2eb0e54909d8ad36708e07c12fd598998159bc43)) # [1.35.0](https://github.com/publishlab/node-acme-client/compare/v1.34.11...v1.35.0) (2025-06-05) **Note:** Version bump only for package @certd/acme-client ## [1.34.11](https://github.com/publishlab/node-acme-client/compare/v1.34.10...v1.34.11) (2025-06-05) ### Bug Fixes * 修复中文域名使用cname方式校验无法通过的问题 ([f7d5baa](https://github.com/publishlab/node-acme-client/commit/f7d5baa6d04cb83c572b06e62f885890cfa0143a)) ### Performance Improvements * 优化cname检查,当有冲突的cname记录时,给出提示 ([e639a8f](https://github.com/publishlab/node-acme-client/commit/e639a8f9f12640ffcca69f1a6a0324459924afbd)) ## [1.34.10](https://github.com/publishlab/node-acme-client/compare/v1.34.9...v1.34.10) (2025-06-03) **Note:** Version bump only for package @certd/acme-client ## [1.34.9](https://github.com/publishlab/node-acme-client/compare/v1.34.8...v1.34.9) (2025-05-30) **Note:** Version bump only for package @certd/acme-client ## [1.34.8](https://github.com/publishlab/node-acme-client/compare/v1.34.7...v1.34.8) (2025-05-28) **Note:** Version bump only for package @certd/acme-client ## [1.34.7](https://github.com/publishlab/node-acme-client/compare/v1.34.6...v1.34.7) (2025-05-26) **Note:** Version bump only for package @certd/acme-client ## [1.34.6](https://github.com/publishlab/node-acme-client/compare/v1.34.5...v1.34.6) (2025-05-25) **Note:** Version bump only for package @certd/acme-client ## [1.34.5](https://github.com/publishlab/node-acme-client/compare/v1.34.4...v1.34.5) (2025-05-19) **Note:** Version bump only for package @certd/acme-client ## [1.34.4](https://github.com/publishlab/node-acme-client/compare/v1.34.3...v1.34.4) (2025-05-16) **Note:** Version bump only for package @certd/acme-client ## [1.34.3](https://github.com/publishlab/node-acme-client/compare/v1.34.2...v1.34.3) (2025-05-15) **Note:** Version bump only for package @certd/acme-client ## [1.34.2](https://github.com/publishlab/node-acme-client/compare/v1.34.1...v1.34.2) (2025-05-11) ### Performance Improvements * http方式支持校验443端口 ([d75fcb7](https://github.com/publishlab/node-acme-client/commit/d75fcb7fec421a9a638eaa27fe9378c84b5e0f19)) ## [1.34.1](https://github.com/publishlab/node-acme-client/compare/v1.34.0...v1.34.1) (2025-05-05) ### Bug Fixes * 根据SOA记录判断子域名托管有缺陷,改回手动配置子域名托管记录的方式 ([1b280a2](https://github.com/publishlab/node-acme-client/commit/1b280a2940f9e2d919b0bf23b89cc185be1fa498)) # [1.34.0](https://github.com/publishlab/node-acme-client/compare/v1.33.8...v1.34.0) (2025-04-28) **Note:** Version bump only for package @certd/acme-client ## [1.33.8](https://github.com/publishlab/node-acme-client/compare/v1.33.7...v1.33.8) (2025-04-26) ### Bug Fixes * 修复http上传方式无法清除记录文件的bug ([72a7b51](https://github.com/publishlab/node-acme-client/commit/72a7b51d479602b2c54c6c3ac8d8a0dcb9664e73)) ### Performance Improvements * 从域名的soa获取主域名,子域名托管无需额外配置 ([a586a92](https://github.com/publishlab/node-acme-client/commit/a586a92d5e32ea846ac37be52a7ad8c328d89966)) * 七牛oss支持删除过期备份 ([b7113bd](https://github.com/publishlab/node-acme-client/commit/b7113bda2378116d6c116dc583f563cce7cf9f00)) ## [1.33.7](https://github.com/publishlab/node-acme-client/compare/v1.33.6...v1.33.7) (2025-04-22) **Note:** Version bump only for package @certd/acme-client ## [1.33.6](https://github.com/publishlab/node-acme-client/compare/v1.33.5...v1.33.6) (2025-04-20) **Note:** Version bump only for package @certd/acme-client ## [1.33.5](https://github.com/publishlab/node-acme-client/compare/v1.33.4...v1.33.5) (2025-04-17) **Note:** Version bump only for package @certd/acme-client ## [1.33.4](https://github.com/publishlab/node-acme-client/compare/v1.33.3...v1.33.4) (2025-04-15) **Note:** Version bump only for package @certd/acme-client ## [1.33.3](https://github.com/publishlab/node-acme-client/compare/v1.33.2...v1.33.3) (2025-04-14) **Note:** Version bump only for package @certd/acme-client ## [1.33.2](https://github.com/publishlab/node-acme-client/compare/v1.33.1...v1.33.2) (2025-04-12) **Note:** Version bump only for package @certd/acme-client ## [1.33.1](https://github.com/publishlab/node-acme-client/compare/v1.33.0...v1.33.1) (2025-04-12) **Note:** Version bump only for package @certd/acme-client # [1.33.0](https://github.com/publishlab/node-acme-client/compare/v1.32.0...v1.33.0) (2025-04-11) **Note:** Version bump only for package @certd/acme-client # [1.32.0](https://github.com/publishlab/node-acme-client/compare/v1.31.11...v1.32.0) (2025-04-04) ### Bug Fixes * 修复从本地dns获取记录报错的bug ([c39b1bf](https://github.com/publishlab/node-acme-client/commit/c39b1bf823ddc6216bed2049e4c87e6107def08a)) ### Features * 优化证书申请速度,修复某些情况下letsencrypt 校验失败的问题 ([857589b](https://github.com/publishlab/node-acme-client/commit/857589b365c6f709e0ae67914d2f50ce182e6dd6)) ### Performance Improvements * 优化华为dns解析记录创建和删除问题 ([0948c5b](https://github.com/publishlab/node-acme-client/commit/0948c5bc691d2ee6eb47c72a85da1b7453361878)) ## [1.31.11](https://github.com/publishlab/node-acme-client/compare/v1.31.10...v1.31.11) (2025-04-02) **Note:** Version bump only for package @certd/acme-client ## [1.31.10](https://github.com/publishlab/node-acme-client/compare/v1.31.9...v1.31.10) (2025-03-29) **Note:** Version bump only for package @certd/acme-client ## [1.31.9](https://github.com/publishlab/node-acme-client/compare/v1.31.8...v1.31.9) (2025-03-28) ### Performance Improvements * dns支持火山引擎 ([99ff879](https://github.com/publishlab/node-acme-client/commit/99ff879d93658c29ea493a4bde7e9e3f85996d64)) ## [1.31.8](https://github.com/publishlab/node-acme-client/compare/v1.31.7...v1.31.8) (2025-03-26) ### Performance Improvements * 优化txt本地校验效率 ([fd507f2](https://github.com/publishlab/node-acme-client/commit/fd507f269253607e68c5c099c99e0de11636f229)) ## [1.31.7](https://github.com/publishlab/node-acme-client/compare/v1.31.6...v1.31.7) (2025-03-24) **Note:** Version bump only for package @certd/acme-client ## [1.31.6](https://github.com/publishlab/node-acme-client/compare/v1.31.5...v1.31.6) (2025-03-24) **Note:** Version bump only for package @certd/acme-client ## [1.31.5](https://github.com/publishlab/node-acme-client/compare/v1.31.4...v1.31.5) (2025-03-22) **Note:** Version bump only for package @certd/acme-client ## [1.31.4](https://github.com/publishlab/node-acme-client/compare/v1.31.3...v1.31.4) (2025-03-21) **Note:** Version bump only for package @certd/acme-client ## [1.31.3](https://github.com/publishlab/node-acme-client/compare/v1.31.2...v1.31.3) (2025-03-13) **Note:** Version bump only for package @certd/acme-client ## [1.31.2](https://github.com/publishlab/node-acme-client/compare/v1.31.1...v1.31.2) (2025-03-12) **Note:** Version bump only for package @certd/acme-client ## [1.31.1](https://github.com/publishlab/node-acme-client/compare/v1.31.0...v1.31.1) (2025-03-11) **Note:** Version bump only for package @certd/acme-client # [1.31.0](https://github.com/publishlab/node-acme-client/compare/v1.30.6...v1.31.0) (2025-03-10) **Note:** Version bump only for package @certd/acme-client ## [1.30.6](https://github.com/publishlab/node-acme-client/compare/v1.30.5...v1.30.6) (2025-02-24) **Note:** Version bump only for package @certd/acme-client ## [1.30.5](https://github.com/publishlab/node-acme-client/compare/v1.30.4...v1.30.5) (2025-02-14) **Note:** Version bump only for package @certd/acme-client ## [1.30.4](https://github.com/publishlab/node-acme-client/compare/v1.30.3...v1.30.4) (2025-02-14) **Note:** Version bump only for package @certd/acme-client ## [1.30.3](https://github.com/publishlab/node-acme-client/compare/v1.30.2...v1.30.3) (2025-02-13) **Note:** Version bump only for package @certd/acme-client ## [1.30.2](https://github.com/publishlab/node-acme-client/compare/v1.30.1...v1.30.2) (2025-02-09) **Note:** Version bump only for package @certd/acme-client ## [1.30.1](https://github.com/publishlab/node-acme-client/compare/v1.30.0...v1.30.1) (2025-01-20) **Note:** Version bump only for package @certd/acme-client # [1.30.0](https://github.com/publishlab/node-acme-client/compare/v1.29.5...v1.30.0) (2025-01-19) ### Bug Fixes * 修复查看任务日志偶发性无法自动滚动底部的bug ([7e482f7](https://github.com/publishlab/node-acme-client/commit/7e482f798c0142bce1866f84676cb40210f9638a)) ## [1.29.5](https://github.com/publishlab/node-acme-client/compare/v1.29.4...v1.29.5) (2025-01-07) **Note:** Version bump only for package @certd/acme-client ## [1.29.4](https://github.com/publishlab/node-acme-client/compare/v1.29.3...v1.29.4) (2025-01-06) **Note:** Version bump only for package @certd/acme-client ## [1.29.3](https://github.com/publishlab/node-acme-client/compare/v1.29.2...v1.29.3) (2025-01-04) ### Performance Improvements * 优化acme sdk ([54db744](https://github.com/publishlab/node-acme-client/commit/54db74428259de64d12230c2ab7353ae11197bbc)) ## [1.29.2](https://github.com/publishlab/node-acme-client/compare/v1.29.1...v1.29.2) (2024-12-25) **Note:** Version bump only for package @certd/acme-client ## [1.29.1](https://github.com/publishlab/node-acme-client/compare/v1.29.0...v1.29.1) (2024-12-25) **Note:** Version bump only for package @certd/acme-client # [1.29.0](https://github.com/publishlab/node-acme-client/compare/v1.28.4...v1.29.0) (2024-12-24) **Note:** Version bump only for package @certd/acme-client ## [1.28.4](https://github.com/publishlab/node-acme-client/compare/v1.28.3...v1.28.4) (2024-12-12) **Note:** Version bump only for package @certd/acme-client ## [1.28.3](https://github.com/publishlab/node-acme-client/compare/v1.28.2...v1.28.3) (2024-12-12) **Note:** Version bump only for package @certd/acme-client ## [1.28.2](https://github.com/publishlab/node-acme-client/compare/v1.28.1...v1.28.2) (2024-12-09) ### Performance Improvements * 支持mysql ([7cde1fd](https://github.com/publishlab/node-acme-client/commit/7cde1fdc4a9ed851900d231a5460c8dbfbcd148e)) ## [1.28.1](https://github.com/publishlab/node-acme-client/compare/v1.28.0...v1.28.1) (2024-12-08) **Note:** Version bump only for package @certd/acme-client # [1.28.0](https://github.com/publishlab/node-acme-client/compare/v1.27.9...v1.28.0) (2024-11-30) **Note:** Version bump only for package @certd/acme-client ## [1.27.9](https://github.com/publishlab/node-acme-client/compare/v1.27.8...v1.27.9) (2024-11-26) **Note:** Version bump only for package @certd/acme-client ## [1.27.8](https://github.com/publishlab/node-acme-client/compare/v1.27.7...v1.27.8) (2024-11-25) **Note:** Version bump only for package @certd/acme-client ## [1.27.7](https://github.com/publishlab/node-acme-client/compare/v1.27.6...v1.27.7) (2024-11-25) **Note:** Version bump only for package @certd/acme-client ## [1.27.6](https://github.com/publishlab/node-acme-client/compare/v1.27.5...v1.27.6) (2024-11-19) **Note:** Version bump only for package @certd/acme-client ## [1.27.5](https://github.com/publishlab/node-acme-client/compare/v1.27.4...v1.27.5) (2024-11-18) **Note:** Version bump only for package @certd/acme-client ## [1.27.4](https://github.com/publishlab/node-acme-client/compare/v1.27.3...v1.27.4) (2024-11-14) **Note:** Version bump only for package @certd/acme-client ## [1.27.3](https://github.com/publishlab/node-acme-client/compare/v1.27.2...v1.27.3) (2024-11-13) **Note:** Version bump only for package @certd/acme-client ## [1.27.2](https://github.com/publishlab/node-acme-client/compare/v1.27.1...v1.27.2) (2024-11-08) **Note:** Version bump only for package @certd/acme-client ## [1.27.1](https://github.com/publishlab/node-acme-client/compare/v1.27.0...v1.27.1) (2024-11-04) **Note:** Version bump only for package @certd/acme-client # [1.27.0](https://github.com/publishlab/node-acme-client/compare/v1.26.16...v1.27.0) (2024-10-31) **Note:** Version bump only for package @certd/acme-client ## [1.26.16](https://github.com/publishlab/node-acme-client/compare/v1.26.15...v1.26.16) (2024-10-30) **Note:** Version bump only for package @certd/acme-client ## [1.26.15](https://github.com/publishlab/node-acme-client/compare/v1.26.14...v1.26.15) (2024-10-28) **Note:** Version bump only for package @certd/acme-client ## [1.26.14](https://github.com/publishlab/node-acme-client/compare/v1.26.13...v1.26.14) (2024-10-26) ### Bug Fixes * 修复启动时自签证书无法保存的bug ([526c484](https://github.com/publishlab/node-acme-client/commit/526c48450bcd37b3ccded9b448f17de8140bdc6e)) ## [1.26.13](https://github.com/publishlab/node-acme-client/compare/v1.26.12...v1.26.13) (2024-10-26) **Note:** Version bump only for package @certd/acme-client ## [1.26.12](https://github.com/publishlab/node-acme-client/compare/v1.26.11...v1.26.12) (2024-10-25) **Note:** Version bump only for package @certd/acme-client ## [1.26.11](https://github.com/publishlab/node-acme-client/compare/v1.26.10...v1.26.11) (2024-10-23) ### Bug Fixes * 申请证书没有使用到系统设置的http代理的bug ([3db216f](https://github.com/publishlab/node-acme-client/commit/3db216f515ba404cb4330fdab452971b22a50f08)) * 修复google证书*.xx.com与xx.com同时申请时报错的bug ([f8b99b8](https://github.com/publishlab/node-acme-client/commit/f8b99b81a23e7e9fd5e05ebd5caf355c41d67a90)) ### Performance Improvements * 优化证书申请速度和成功率,反代地址优化,google基本可以稳定请求。增加请求重试。 ([41d9c3a](https://github.com/publishlab/node-acme-client/commit/41d9c3ac8398def541e65351cbe920d4a927182d)) ## [1.26.10](https://github.com/publishlab/node-acme-client/compare/v1.26.9...v1.26.10) (2024-10-20) **Note:** Version bump only for package @certd/acme-client ## [1.26.9](https://github.com/publishlab/node-acme-client/compare/v1.26.8...v1.26.9) (2024-10-19) **Note:** Version bump only for package @certd/acme-client ## [1.26.8](https://github.com/publishlab/node-acme-client/compare/v1.26.7...v1.26.8) (2024-10-15) **Note:** Version bump only for package @certd/acme-client ## [1.26.7](https://github.com/publishlab/node-acme-client/compare/v1.26.6...v1.26.7) (2024-10-14) **Note:** Version bump only for package @certd/acme-client ## [1.26.6](https://github.com/publishlab/node-acme-client/compare/v1.26.5...v1.26.6) (2024-10-14) **Note:** Version bump only for package @certd/acme-client ## [1.26.5](https://github.com/publishlab/node-acme-client/compare/v1.26.4...v1.26.5) (2024-10-14) **Note:** Version bump only for package @certd/acme-client ## [1.26.4](https://github.com/publishlab/node-acme-client/compare/v1.26.3...v1.26.4) (2024-10-14) **Note:** Version bump only for package @certd/acme-client ## [1.26.3](https://github.com/publishlab/node-acme-client/compare/v1.26.2...v1.26.3) (2024-10-12) **Note:** Version bump only for package @certd/acme-client ## [1.26.2](https://github.com/publishlab/node-acme-client/compare/v1.26.1...v1.26.2) (2024-10-11) **Note:** Version bump only for package @certd/acme-client ## [1.26.1](https://github.com/publishlab/node-acme-client/compare/v1.26.0...v1.26.1) (2024-10-10) **Note:** Version bump only for package @certd/acme-client # [1.26.0](https://github.com/publishlab/node-acme-client/compare/v1.25.9...v1.26.0) (2024-10-10) ### Features * 域名验证方法支持CNAME间接方式,此方式支持所有域名注册商,且无需提供Access授权,但是需要手动添加cname解析 ([f3d3508](https://github.com/publishlab/node-acme-client/commit/f3d35084ed44f9f33845f7045e520be5c27eed93)) ## [1.25.9](https://github.com/publishlab/node-acme-client/compare/v1.25.8...v1.25.9) (2024-10-01) **Note:** Version bump only for package @certd/acme-client ## [1.25.8](https://github.com/publishlab/node-acme-client/compare/v1.25.7...v1.25.8) (2024-09-30) **Note:** Version bump only for package @certd/acme-client ## [1.25.7](https://github.com/publishlab/node-acme-client/compare/v1.25.6...v1.25.7) (2024-09-29) **Note:** Version bump only for package @certd/acme-client ## [1.25.6](https://github.com/publishlab/node-acme-client/compare/v1.25.5...v1.25.6) (2024-09-29) ### Performance Improvements * 部署支持1Panel ([d047234](https://github.com/publishlab/node-acme-client/commit/d047234d98d31504f2e5a472b66e1b75806af26e)) ## [1.25.5](https://github.com/publishlab/node-acme-client/compare/v1.25.4...v1.25.5) (2024-09-26) **Note:** Version bump only for package @certd/acme-client ## [1.25.4](https://github.com/publishlab/node-acme-client/compare/v1.25.3...v1.25.4) (2024-09-25) **Note:** Version bump only for package @certd/acme-client ## [1.25.3](https://github.com/publishlab/node-acme-client/compare/v1.25.2...v1.25.3) (2024-09-24) **Note:** Version bump only for package @certd/acme-client ## [1.25.2](https://github.com/publishlab/node-acme-client/compare/v1.25.1...v1.25.2) (2024-09-24) **Note:** Version bump only for package @certd/acme-client ## [1.25.1](https://github.com/publishlab/node-acme-client/compare/v1.25.0...v1.25.1) (2024-09-24) **Note:** Version bump only for package @certd/acme-client # [1.25.0](https://github.com/publishlab/node-acme-client/compare/v1.24.4...v1.25.0) (2024-09-24) ### Performance Improvements * 证书支持旧版RSA,pkcs1 ([3d9c3ec](https://github.com/publishlab/node-acme-client/commit/3d9c3ecb3eb604b2458154f608bde0f01915d116)) * 支持七牛云 ([8ecc2f9](https://github.com/publishlab/node-acme-client/commit/8ecc2f9446a9ebd11b9bfbffbb6cf7812a043495)) ## [1.24.4](https://github.com/publishlab/node-acme-client/compare/v1.24.3...v1.24.4) (2024-09-09) **Note:** Version bump only for package @certd/acme-client ## [1.24.3](https://github.com/publishlab/node-acme-client/compare/v1.24.2...v1.24.3) (2024-09-06) **Note:** Version bump only for package @certd/acme-client ## [1.24.2](https://github.com/publishlab/node-acme-client/compare/v1.24.1...v1.24.2) (2024-09-06) ### Performance Improvements * 修复windows下无法执行第二条命令的bug ([d5bfcdb](https://github.com/publishlab/node-acme-client/commit/d5bfcdb6de1dcc1702155442e2e00237d0bbb6e5)) ## [1.24.1](https://github.com/publishlab/node-acme-client/compare/v1.24.0...v1.24.1) (2024-09-02) ### Bug Fixes * 修复在没有勾选使用代理的情况下,仍然会使用代理的bug ([0f66794](https://github.com/publishlab/node-acme-client/commit/0f6679425f6a736bb0128527dd99c085fac17d84)) ### Performance Improvements * 部署插件支持宝塔、易盾云等 ([ee61709](https://github.com/publishlab/node-acme-client/commit/ee617095efa1171548cf52fd45f0f98a368555a3)) * 授权配置支持加密 ([42a56b5](https://github.com/publishlab/node-acme-client/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb)) # [1.24.0](https://github.com/publishlab/node-acme-client/compare/v1.23.1...v1.24.0) (2024-08-25) ### Bug Fixes * 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/publishlab/node-acme-client/commit/37eb762afe25c5896b75dee25f32809f8426e7b7)) * 修复创建流水线后立即运行时报no id错误的bug ([17ead54](https://github.com/publishlab/node-acme-client/commit/17ead547aab25333603980304aa3aad3db1f73d5)) * 修复使用代理的情况下申请证书失败的bug ([95122e2](https://github.com/publishlab/node-acme-client/commit/95122e28609333f4df55c266e5434897954c0fb3)) ### Features * 支持google证书申请(需要使用代理) ([a593056](https://github.com/publishlab/node-acme-client/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe)) ### Performance Improvements * 优化证书申请成功率 ([968c469](https://github.com/publishlab/node-acme-client/commit/968c4690a07f69c08dcb3d3a494da4e319627345)) ## [1.22.6](https://github.com/publishlab/node-acme-client/compare/v1.22.5...v1.22.6) (2024-08-03) **Note:** Version bump only for package @certd/acme-client ## [1.22.4](https://github.com/publishlab/node-acme-client/compare/v1.22.3...v1.22.4) (2024-07-26) ### Performance Improvements * 证书申请支持反向代理,letsencrypt无法访问时的备用方案 ([b7b5df0](https://github.com/publishlab/node-acme-client/commit/b7b5df0587e0f7ea288c1b2af6f87211f207395f)) ## [1.22.3](https://github.com/publishlab/node-acme-client/compare/v1.22.2...v1.22.3) (2024-07-25) **Note:** Version bump only for package @certd/acme-client ## [1.22.2](https://github.com/publishlab/node-acme-client/compare/v1.22.1...v1.22.2) (2024-07-23) **Note:** Version bump only for package @certd/acme-client ## [1.22.1](https://github.com/publishlab/node-acme-client/compare/v1.22.0...v1.22.1) (2024-07-20) **Note:** Version bump only for package @certd/acme-client # [1.22.0](https://github.com/publishlab/node-acme-client/compare/v1.21.2...v1.22.0) (2024-07-19) ### Features * 升级midway,支持esm ([485e603](https://github.com/publishlab/node-acme-client/commit/485e603b5165c28bc08694997726eaf2a585ebe7)) ## [1.21.2](https://github.com/publishlab/node-acme-client/compare/v1.21.1...v1.21.2) (2024-07-08) **Note:** Version bump only for package @certd/acme-client ## [1.21.1](https://github.com/publishlab/node-acme-client/compare/v1.21.0...v1.21.1) (2024-07-08) **Note:** Version bump only for package @certd/acme-client # [1.21.0](https://github.com/publishlab/node-acme-client/compare/v1.20.17...v1.21.0) (2024-07-03) ### Features * 支持zero ssl ([eade2c2](https://github.com/publishlab/node-acme-client/commit/eade2c2b681569f03e9cd466e7d5bcd6703ed492)) ## [1.20.17](https://github.com/publishlab/node-acme-client/compare/v1.20.16...v1.20.17) (2024-07-03) ### Performance Improvements * 创建dns解析后,强制等待60s ([f47b35f](https://github.com/publishlab/node-acme-client/commit/f47b35f6d5bd7d675005c3e286b7e9a029201f8b)) * 优化cname verify ([eba333d](https://github.com/publishlab/node-acme-client/commit/eba333de7a5b5ef4b0b7eaa904f578720102fa61)) ## [1.20.16](https://github.com/publishlab/node-acme-client/compare/v1.20.15...v1.20.16) (2024-07-01) ### Bug Fixes * 修复配置了cdn cname后申请失败的bug ([4a5fa76](https://github.com/publishlab/node-acme-client/commit/4a5fa767edc347d03d29a467e86c9a4d70b0220c)) ## [1.20.15](https://github.com/publishlab/node-acme-client/compare/v1.20.14...v1.20.15) (2024-06-28) ### Performance Improvements * 腾讯云dns provider 支持腾讯云的accessId ([e0eb3a4](https://github.com/publishlab/node-acme-client/commit/e0eb3a441384d474fe2923c69b25318264bdc9df)) ## [1.20.14](https://github.com/publishlab/node-acme-client/compare/v1.20.13...v1.20.14) (2024-06-23) **Note:** Version bump only for package @certd/acme-client ## [1.20.13](https://github.com/publishlab/node-acme-client/compare/v1.20.12...v1.20.13) (2024-06-18) **Note:** Version bump only for package @certd/acme-client ## [1.20.12](https://github.com/publishlab/node-acme-client/compare/v1.20.10...v1.20.12) (2024-06-17) ### Bug Fixes * 修复aliyun域名超过100个找不到域名的bug ([5b1494b](https://github.com/publishlab/node-acme-client/commit/5b1494b3ce93d1026dc56ee741342fbb8bf7be24)) ### Performance Improvements * 支持cloudflare域名 ([fbb9a47](https://github.com/publishlab/node-acme-client/commit/fbb9a47e8f7bb805289b9ee64bd46ffee0f01c06)) ## [1.20.10](https://github.com/publishlab/node-acme-client/compare/v1.20.9...v1.20.10) (2024-05-30) **Note:** Version bump only for package @certd/acme-client ## [1.20.9](https://github.com/publishlab/node-acme-client/compare/v1.20.8...v1.20.9) (2024-03-22) **Note:** Version bump only for package @certd/acme-client ## [1.20.8](https://github.com/publishlab/node-acme-client/compare/v1.20.7...v1.20.8) (2024-03-22) **Note:** Version bump only for package @certd/acme-client ## [1.20.7](https://github.com/publishlab/node-acme-client/compare/v1.20.6...v1.20.7) (2024-03-22) **Note:** Version bump only for package @certd/acme-client ## [1.20.6](https://github.com/publishlab/node-acme-client/compare/v1.20.5...v1.20.6) (2024-03-21) **Note:** Version bump only for package @certd/acme-client ## [1.20.5](https://github.com/publishlab/node-acme-client/compare/v1.20.2...v1.20.5) (2024-03-11) ### Bug Fixes * 修复腾讯云cdn部署无法选择端点的bug ([154409b](https://github.com/publishlab/node-acme-client/commit/154409b1dfee3ea1caae740ad9c1f99a6e7a9814)) # Changelog ## v5.4.0 (2024-07-16) * `added` Directory URLs for [Google](https://cloud.google.com/certificate-manager/docs/overview) ACME provider * `fixed` Invalidate ACME provider directory cache after 24 hours * `fixed` Retry HTTP requests on server errors or when rate limited - [#89](https://github.com/publishlab/node-acme-client/issues/89) ## v5.3.1 (2024-05-22) * `fixed` Allow `client.auto()` being called with an empty CSR common name * `fixed` Bug when calling `updateAccountKey()` with external account binding ## v5.3.0 (2024-02-05) * `added` Support and tests for satisfying `tls-alpn-01` challenges * `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR handling * `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge * Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously * This change is not considered breaking since the previous behavior was incorrect ## v5.2.0 (2024-01-22) * `fixed` Allow self-signed or invalid certs when validating `http-01` challenges that redirect to HTTPS - [#65](https://github.com/publishlab/node-acme-client/issues/65) * `fixed` Wait for all challenge promises to settle before rejecting `client.auto()` - [#75](https://github.com/publishlab/node-acme-client/issues/75) ## v5.1.0 (2024-01-20) * `fixed` Upgrade `jsrsasign@11.0.0` - [GHSA-rh63-9qcf-83gf](https://github.com/kjur/jsrsasign/security/advisories/GHSA-rh63-9qcf-83gf) * `fixed` Upgrade `axios@1.6.5` - [CVE-2023-45857](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2023-45857) ## v5.0.0 (2022-07-28) * [Upgrade guide here](docs/upgrade-v5.md) * `added` New native crypto interface, ECC/ECDSA support * `breaking` Remove support for Node v10, v12 and v14 * `breaking` Prioritize issuer closest to root during preferred chain selection - [#46](https://github.com/publishlab/node-acme-client/issues/46) * `changed` Replace `bluebird` dependency with native promise APIs * `changed` Replace `backo2` dependency with internal utility ## v4.2.5 (2022-03-21) * `fixed` Upgrade `axios@0.26.1` * `fixed` Upgrade `node-forge@1.3.0` - [CVE-2022-24771](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24771), [CVE-2022-24772](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24772), [CVE-2022-24773](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24773) ## v4.2.4 (2022-03-19) * `fixed` Use SHA-256 when signing CSRs ## v3.3.2 (2022-03-19) * `backport` Use SHA-256 when signing CSRs ## v4.2.3 (2022-01-11) * `added` Directory URLs for ACME providers [Buypass](https://www.buypass.com) and [ZeroSSL](https://zerossl.com) * `fixed` Skip already valid authorizations when using `client.auto()` ## v4.2.2 (2022-01-10) * `fixed` Upgrade `node-forge@1.2.0` ## v4.2.1 (2022-01-10) * `fixed` ZeroSSL `duplicate_domains_in_array` error when using `client.auto()` ## v4.2.0 (2022-01-06) * `added` Support for external account binding - [RFC 8555 Section 7.3.4](https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4) * `added` Ability to pass through custom logger function * `changed` Increase default `backoffAttempts` to 10 * `fixed` Deactivate authorizations where challenges can not be completed * `fixed` Attempt authoritative name servers when verifying `dns-01` challenges * `fixed` Error verbosity when failing to read ACME directory * `fixed` Correctly recognize `ready` and `processing` states - [RFC 8555 Section 7.1.6](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.6) ## v4.1.4 (2021-12-23) * `fixed` Upgrade `axios@0.21.4` - [CVE-2021-3749](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3749) ## v4.1.3 (2021-02-22) * `fixed` Upgrade `axios@0.21.1` - [CVE-2020-28168](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28168) ## v4.1.2 (2020-11-16) * `fixed` Bug when encoding PEM payloads, potentially causing malformed requests ## v4.1.1 (2020-11-13) * `fixed` Missing TypeScript definitions ## v4.1.0 (2020-11-12) * `added` Option `preferredChain` added to `client.getCertificate()` and `client.auto()` to indicate which certificate chain is preferred if a CA offers multiple * Related: [https://community.letsencrypt.org/t/transition-to-isrgs-root-delayed-until-jan-11-2021/125516](https://community.letsencrypt.org/t/transition-to-isrgs-root-delayed-until-jan-11-2021/125516) * `added` Method `client.getOrder()` to refresh order from CA * `fixed` Upgrade `axios@0.21.0` * `fixed` Error when attempting to revoke a certificate chain * `fixed` Missing URL augmentation in `client.finalizeOrder()` and `client.deactivateAuthorization()` * `fixed` Add certificate issuer to response from `forge.readCertificateInfo()` ## v4.0.2 (2020-10-09) * `fixed` Explicitly set default `axios` HTTP adapter - [axios/axios#1180](https://github.com/axios/axios/issues/1180) ## v4.0.1 (2020-09-15) * `fixed` Upgrade `node-forge@0.10.0` - [CVE-2020-7720](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-7720) ## v4.0.0 (2020-05-29) * `breaking` Remove support for Node v8 * `breaking` Remove deprecated `openssl` crypto module * `fixed` Incorrect TypeScript `CertificateInfo` definitions * `fixed` Allow trailing whitespace character in `http-01` challenge response ## v3.3.1 (2020-01-07) * `fixed` Improvements to TypeScript definitions ## v3.3.0 (2019-12-19) * `added` TypeScript definitions * `fixed` Allow missing ACME directory meta field - [RFC 8555 Section 7.1.1](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1) ## v3.2.1 (2019-11-14) * `added` New option `skipChallengeVerification` added to `client.auto()` to bypass internal challenge verification ## v3.2.0 (2019-08-26) * `added` More extensive testing using [letsencrypt/pebble](https://github.com/letsencrypt/pebble) * `changed` When creating a CSR, `commonName` no longer defaults to `'localhost'` * This change is not considered breaking since `commonName: 'localhost'` will result in an error when ordering a certificate * `fixed` Retry signed API requests on `urn:ietf:params:acme:error:badNonce` - [RFC 8555 Section 6.5](https://datatracker.ietf.org/doc/html/rfc8555#section-6.5) * `fixed` Minor bugs related to `POST-as-GET` when calling `updateAccount()` * `fixed` Ensure subject common name is present in SAN when creating a CSR - [CAB v1.2.3 Section 9.2.2](https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf) * `fixed` Send empty JSON body when responding to challenges - [RFC 8555 Section 7.5.1](https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1) ## v2.3.1 (2019-08-26) * `backport` Minor bugs related to `POST-as-GET` when calling `client.updateAccount()` * `backport` Send empty JSON body when responding to challenges ## v3.1.0 (2019-08-21) * `added` UTF-8 support when generating a CSR subject using forge - [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280) * `fixed` Implement `POST-as-GET` for all ACME API requests - [RFC 8555 Section 6.3](https://datatracker.ietf.org/doc/html/rfc8555#section-6.3) ## v2.3.0 (2019-08-21) * `backport` Implement `POST-as-GET` for all ACME API requests ## v3.0.0 (2019-07-13) * `added` Expose `axios` instance to allow manipulating HTTP client defaults * `breaking` Remove support for Node v4 and v6 * `breaking` Remove Babel transpilation ## v2.2.3 (2019-01-25) * `added` DNS CNAME detection when verifying `dns-01` challenges ## v2.2.2 (2019-01-07) * `added` Support for `tls-alpn-01` challenge key authorization ## v2.2.1 (2019-01-04) * `fixed` Handle and throw errors from OpenSSL process ## v2.2.0 (2018-11-06) * `added` New [node-forge](https://www.npmjs.com/package/node-forge) crypto interface, removes OpenSSL CLI dependency * `added` Support native `crypto.generateKeyPair()` API when generating key pairs ## v2.1.0 (2018-10-21) * `added` Ability to set and get current account URL * `fixed` Replace HTTP client `request` with `axios` * `fixed` Auto-mode no longer tries to create account when account URL exists ## v2.0.1 (2018-08-17) * `fixed` Key rollover in compliance with [draft-ietf-acme-13](https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-13) ## v2.0.0 (2018-04-02) * `breaking` ACMEv2 * `breaking` API changes * `breaking` Rewrite to ES6 * `breaking` Promises instead of callbacks ## v1.0.0 (2017-10-20) * API stable ## v0.2.1 (2017-09-27) * `fixed` Bug causing invalid anti-replay nonce ## v0.2.0 (2017-09-21) * `breaking` OpenSSL method `readCsrDomains` and `readCertificateInfo` now return domains as an object * `fixed` Added and fixed some tests ## v0.1.0 (2017-09-14) * `acme-client` released ================================================ FILE: packages/core/acme-client/LICENSE ================================================ MIT License Copyright (c) 2017-2024 Labrador CMS AS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/core/acme-client/README.md ================================================ # acme-client [![test](https://github.com/publishlab/node-acme-client/actions/workflows/tests.yml/badge.svg)](https://github.com/publishlab/node-acme-client/actions/workflows/tests.yml) *A simple and unopinionated ACME client.* This module is written to handle communication with a Boulder/Let's Encrypt-style ACME API. * RFC 8555 - Automatic Certificate Management Environment (ACME): [https://datatracker.ietf.org/doc/html/rfc8555](https://datatracker.ietf.org/doc/html/rfc8555) * Boulder divergences from ACME: [https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md) ## Compatibility | acme-client | Node.js | | | ----------- | ------- | ----------------------------------------- | | v5.x | >= v16 | [Upgrade guide](docs/upgrade-v5.md) | | v4.x | >= v10 | [Changelog](CHANGELOG.md#v400-2020-05-29) | | v3.x | >= v8 | [Changelog](CHANGELOG.md#v300-2019-07-13) | | v2.x | >= v4 | [Changelog](CHANGELOG.md#v200-2018-04-02) | | v1.x | >= v4 | [Changelog](CHANGELOG.md#v100-2017-10-20) | ## Table of contents * [Installation](#installation) * [Usage](#usage) * [Directory URLs](#directory-urls) * [External account binding](#external-account-binding) * [Specifying the account URL](#specifying-the-account-url) * [Cryptography](#cryptography) * [Legacy .forge interface](#legacy-forge-interface) * [Auto mode](#auto-mode) * [Challenge priority](#challenge-priority) * [Internal challenge verification](#internal-challenge-verification) * [API](#api) * [HTTP client defaults](#http-client-defaults) * [Debugging](#debugging) * [License](#license) ## Installation ```bash $ npm install acme-client ``` ## Usage ```js const acme = require('acme-client'); const accountPrivateKey = ''; const client = new acme.Client({ directoryUrl: acme.directory.letsencrypt.staging, accountKey: accountPrivateKey, }); ``` ### Directory URLs ```js acme.directory.buypass.staging; acme.directory.buypass.production; acme.directory.google.staging; acme.directory.google.production; acme.directory.letsencrypt.staging; acme.directory.letsencrypt.production; acme.directory.zerossl.production; ``` ### External account binding To enable [external account binding](https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4) when creating your ACME account, provide your KID and HMAC key to the client constructor. ```js const client = new acme.Client({ directoryUrl: 'https://acme-provider.example.com/directory-url', accountKey: accountPrivateKey, externalAccountBinding: { kid: 'YOUR-EAB-KID', hmacKey: 'YOUR-EAB-HMAC-KEY', }, }); ``` ### Specifying the account URL During the ACME account creation process, the server will check the supplied account key and either create a new account if the key is unused, or return the existing ACME account bound to that key. In some cases, for example with some EAB providers, this account creation step may be prohibited and might require you to manually specify the account URL beforehand. This can be done through `accountUrl` in the client constructor. ```js const client = new acme.Client({ directoryUrl: acme.directory.letsencrypt.staging, accountKey: accountPrivateKey, accountUrl: 'https://acme-v02.api.letsencrypt.org/acme/acct/12345678', }); ``` You can fetch the clients current account URL, either after creating an account or supplying it through the constructor, using `getAccountUrl()`: ```js const myAccountUrl = client.getAccountUrl(); ``` ## Cryptography For key pairs `acme-client` utilizes native Node.js cryptography APIs, supporting signing and generation of both RSA and ECDSA keys. The module [@peculiar/x509](https://www.npmjs.com/package/@peculiar/x509) is used to generate and parse Certificate Signing Requests. These utility methods are exposed through `.crypto`. * **Documentation: [docs/crypto.md](docs/crypto.md)** ```js const privateRsaKey = await acme.crypto.createPrivateRsaKey(); const privateEcdsaKey = await acme.crypto.createPrivateEcdsaKey(); const [certificateKey, certificateCsr] = await acme.crypto.createCsr({ altNames: ['example.com', '*.example.com'], }); ``` ### Legacy `.forge` interface The legacy `node-forge` crypto interface is still available for backward compatibility, however this interface is now considered deprecated and will be removed in a future major version of `acme-client`. You should consider migrating to the new `.crypto` API at your earliest convenience. More details can be found in the [acme-client v5 upgrade guide](docs/upgrade-v5.md). * **Documentation: [docs/forge.md](docs/forge.md)** ## Auto mode For convenience an `auto()` method is included in the client that takes a single config object. This method will handle the entire process of getting a certificate for one or multiple domains. * **Documentation: [docs/client.md#AcmeClient+auto](docs/client.md#AcmeClient+auto)** * **Full example: [examples/auto.js](examples/auto.js)** ```js const autoOpts = { csr: '', email: 'test@example.com', termsOfServiceAgreed: true, challengeCreateFn: async (authz, challenge, keyAuthorization) => {}, challengeRemoveFn: async (authz, challenge, keyAuthorization) => {}, }; const certificate = await client.auto(autoOpts); ``` ### Challenge priority When ordering a certificate using auto mode, `acme-client` uses a priority list when selecting challenges to respond to. Its default value is `['http-01', 'dns-01']` which translates to "use `http-01` if any challenges exist, otherwise fall back to `dns-01`". While most challenges can be validated using the method of your choosing, please note that **wildcard certificates can only be validated through `dns-01`**. More information regarding Let's Encrypt challenge types [can be found here](https://letsencrypt.org/docs/challenge-types/). To modify challenge priority, provide a list of challenge types in `challengePriority`: ```js await client.auto({ ..., challengePriority: ['http-01', 'dns-01'], }); ``` ### Internal challenge verification When using auto mode, `acme-client` will first validate that challenges are satisfied internally before completing the challenge at the ACME provider. In some cases (firewalls, etc) this internal challenge verification might not be possible to complete. If internal challenge validation needs to travel through an HTTP proxy, see [HTTP client defaults](#http-client-defaults). To completely disable `acme-client`s internal challenge verification, enable `skipChallengeVerification`: ```js await client.auto({ ..., skipChallengeVerification: true, }); ``` ## API For more fine-grained control you can interact with the ACME API using the methods documented below. * **Documentation: [docs/client.md](docs/client.md)** * **Full example: [examples/api.js](examples/api.js)** ```js const account = await client.createAccount({ termsOfServiceAgreed: true, contact: ['mailto:test@example.com'], }); const order = await client.createOrder({ identifiers: [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' }, ], }); ``` ## HTTP client defaults This module uses [axios](https://github.com/axios/axios) when communicating with the ACME HTTP API, and exposes the client instance through `.axios`. For example, should you need to change the default axios configuration to route requests through an HTTP proxy, this can be achieved as follows: ```js const acme = require('acme-client'); acme.axios.defaults.proxy = { host: '127.0.0.1', port: 9000, }; ``` A complete list of axios options and documentation can be found at: * [https://github.com/axios/axios#request-config](https://github.com/axios/axios#request-config) * [https://github.com/axios/axios#custom-instance-defaults](https://github.com/axios/axios#custom-instance-defaults) ## Debugging To get a better grasp of what `acme-client` is doing behind the scenes, you can either pass it a logger function, or enable debugging through an environment variable. Setting a logger function may for example be useful for passing messages on to another logging system, or just dumping them to the console. ```js acme.setLogger((message) => { console.log(message); }); ``` Debugging to the console can also be enabled through [debug](https://www.npmjs.com/package/debug) by setting an environment variable. ```bash DEBUG=acme-client node index.js ``` ## License [MIT](LICENSE) ================================================ FILE: packages/core/acme-client/build.md ================================================ 20:55 ================================================ FILE: packages/core/acme-client/docs/client.md ================================================ ## Classes
AcmeClient

AcmeClient

## Objects
Client : object

ACME client

## AcmeClient AcmeClient **Kind**: global class * [AcmeClient](#AcmeClient) * [new AcmeClient(opts)](#new_AcmeClient_new) * [.getTermsOfServiceUrl()](#AcmeClient+getTermsOfServiceUrl) ⇒ Promise.<(string\|null)> * [.getAccountUrl()](#AcmeClient+getAccountUrl) ⇒ string * [.createAccount([data])](#AcmeClient+createAccount) ⇒ Promise.<object> * [.updateAccount([data])](#AcmeClient+updateAccount) ⇒ Promise.<object> * [.updateAccountKey(newAccountKey, [data])](#AcmeClient+updateAccountKey) ⇒ Promise.<object> * [.createOrder(data)](#AcmeClient+createOrder) ⇒ Promise.<object> * [.getOrder(order)](#AcmeClient+getOrder) ⇒ Promise.<object> * [.finalizeOrder(order, csr)](#AcmeClient+finalizeOrder) ⇒ Promise.<object> * [.getAuthorizations(order)](#AcmeClient+getAuthorizations) ⇒ Promise.<Array.<object>> * [.deactivateAuthorization(authz)](#AcmeClient+deactivateAuthorization) ⇒ Promise.<object> * [.getChallengeKeyAuthorization(challenge)](#AcmeClient+getChallengeKeyAuthorization) ⇒ Promise.<string> * [.verifyChallenge(authz, challenge)](#AcmeClient+verifyChallenge) ⇒ Promise * [.completeChallenge(challenge)](#AcmeClient+completeChallenge) ⇒ Promise.<object> * [.waitForValidStatus(item)](#AcmeClient+waitForValidStatus) ⇒ Promise.<object> * [.getCertificate(order, [preferredChain])](#AcmeClient+getCertificate) ⇒ Promise.<string> * [.revokeCertificate(cert, [data])](#AcmeClient+revokeCertificate) ⇒ Promise * [.auto(opts)](#AcmeClient+auto) ⇒ Promise.<string> ### new AcmeClient(opts) | Param | Type | Description | | --- | --- | --- | | opts | object | | | opts.directoryUrl | string | ACME directory URL | | opts.accountKey | buffer \| string | PEM encoded account private key | | [opts.accountUrl] | string | Account URL, default: `null` | | [opts.externalAccountBinding] | object | | | [opts.externalAccountBinding.kid] | string | External account binding KID | | [opts.externalAccountBinding.hmacKey] | string | External account binding HMAC key | | [opts.backoffAttempts] | number | Maximum number of backoff attempts, default: `10` | | [opts.backoffMin] | number | Minimum backoff attempt delay in milliseconds, default: `5000` | | [opts.backoffMax] | number | Maximum backoff attempt delay in milliseconds, default: `30000` | **Example** Create ACME client instance ```js const client = new acme.Client({ directoryUrl: acme.directory.letsencrypt.staging, accountKey: 'Private key goes here', }); ``` **Example** Create ACME client instance ```js const client = new acme.Client({ directoryUrl: acme.directory.letsencrypt.staging, accountKey: 'Private key goes here', accountUrl: 'Optional account URL goes here', backoffAttempts: 10, backoffMin: 5000, backoffMax: 30000, }); ``` **Example** Create ACME client with external account binding ```js const client = new acme.Client({ directoryUrl: 'https://acme-provider.example.com/directory-url', accountKey: 'Private key goes here', externalAccountBinding: { kid: 'YOUR-EAB-KID', hmacKey: 'YOUR-EAB-HMAC-KEY', }, }); ``` ### acmeClient.getTermsOfServiceUrl() ⇒ Promise.<(string\|null)> Get Terms of Service URL if available **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<(string\|null)> - ToS URL **Example** Get Terms of Service URL ```js const termsOfService = client.getTermsOfServiceUrl(); if (!termsOfService) { // CA did not provide Terms of Service } ``` ### acmeClient.getAccountUrl() ⇒ string Get current account URL **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: string - Account URL **Throws**: - Error No account URL found **Example** Get current account URL ```js try { const accountUrl = client.getAccountUrl(); } catch (e) { // No account URL exists, need to create account first } ``` ### acmeClient.createAccount([data]) ⇒ Promise.<object> Create a new account https://datatracker.ietf.org/doc/html/rfc8555#section-7.3 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<object> - Account | Param | Type | Description | | --- | --- | --- | | [data] | object | Request data | **Example** Create a new account ```js const account = await client.createAccount({ termsOfServiceAgreed: true, }); ``` **Example** Create a new account with contact info ```js const account = await client.createAccount({ termsOfServiceAgreed: true, contact: ['mailto:test@example.com'], }); ``` ### acmeClient.updateAccount([data]) ⇒ Promise.<object> Update existing account https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<object> - Account | Param | Type | Description | | --- | --- | --- | | [data] | object | Request data | **Example** Update existing account ```js const account = await client.updateAccount({ contact: ['mailto:foo@example.com'], }); ``` ### acmeClient.updateAccountKey(newAccountKey, [data]) ⇒ Promise.<object> Update account private key https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<object> - Account | Param | Type | Description | | --- | --- | --- | | newAccountKey | buffer \| string | New PEM encoded private key | | [data] | object | Additional request data | **Example** Update account private key ```js const newAccountKey = 'New private key goes here'; const result = await client.updateAccountKey(newAccountKey); ``` ### acmeClient.createOrder(data) ⇒ Promise.<object> Create a new order https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<object> - Order | Param | Type | Description | | --- | --- | --- | | data | object | Request data | **Example** Create a new order ```js const order = await client.createOrder({ identifiers: [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: 'test.example.com' }, ], }); ``` ### acmeClient.getOrder(order) ⇒ Promise.<object> Refresh order object from CA https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<object> - Order | Param | Type | Description | | --- | --- | --- | | order | object | Order object | **Example** ```js const order = { ... }; // Previously created order object const result = await client.getOrder(order); ``` ### acmeClient.finalizeOrder(order, csr) ⇒ Promise.<object> Finalize order https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<object> - Order | Param | Type | Description | | --- | --- | --- | | order | object | Order object | | csr | buffer \| string | PEM encoded Certificate Signing Request | **Example** Finalize order ```js const order = { ... }; // Previously created order object const csr = { ... }; // Previously created Certificate Signing Request const result = await client.finalizeOrder(order, csr); ``` ### acmeClient.getAuthorizations(order) ⇒ Promise.<Array.<object>> Get identifier authorizations from order https://datatracker.ietf.org/doc/html/rfc8555#section-7.5 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<Array.<object>> - Authorizations | Param | Type | Description | | --- | --- | --- | | order | object | Order | **Example** Get identifier authorizations ```js const order = { ... }; // Previously created order object const authorizations = await client.getAuthorizations(order); authorizations.forEach((authz) => { const { challenges } = authz; }); ``` ### acmeClient.deactivateAuthorization(authz) ⇒ Promise.<object> Deactivate identifier authorization https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<object> - Authorization | Param | Type | Description | | --- | --- | --- | | authz | object | Identifier authorization | **Example** Deactivate identifier authorization ```js const authz = { ... }; // Identifier authorization resolved from previously created order const result = await client.deactivateAuthorization(authz); ``` ### acmeClient.getChallengeKeyAuthorization(challenge) ⇒ Promise.<string> Get key authorization for ACME challenge https://datatracker.ietf.org/doc/html/rfc8555#section-8.1 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<string> - Key authorization | Param | Type | Description | | --- | --- | --- | | challenge | object | Challenge object returned by API | **Example** Get challenge key authorization ```js const challenge = { ... }; // Challenge from previously resolved identifier authorization const key = await client.getChallengeKeyAuthorization(challenge); // Write key somewhere to satisfy challenge ``` ### acmeClient.verifyChallenge(authz, challenge) ⇒ Promise Verify that ACME challenge is satisfied **Kind**: instance method of [AcmeClient](#AcmeClient) | Param | Type | Description | | --- | --- | --- | | authz | object | Identifier authorization | | challenge | object | Authorization challenge | **Example** Verify satisfied ACME challenge ```js const authz = { ... }; // Identifier authorization const challenge = { ... }; // Satisfied challenge await client.verifyChallenge(authz, challenge); ``` ### acmeClient.completeChallenge(challenge) ⇒ Promise.<object> Notify CA that challenge has been completed https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<object> - Challenge | Param | Type | Description | | --- | --- | --- | | challenge | object | Challenge object returned by API | **Example** Notify CA that challenge has been completed ```js const challenge = { ... }; // Satisfied challenge const result = await client.completeChallenge(challenge); ``` ### acmeClient.waitForValidStatus(item) ⇒ Promise.<object> Wait for ACME provider to verify status on a order, authorization or challenge https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<object> - Valid order, authorization or challenge | Param | Type | Description | | --- | --- | --- | | item | object | An order, authorization or challenge object | **Example** Wait for valid challenge status ```js const challenge = { ... }; await client.waitForValidStatus(challenge); ``` **Example** Wait for valid authorization status ```js const authz = { ... }; await client.waitForValidStatus(authz); ``` **Example** Wait for valid order status ```js const order = { ... }; await client.waitForValidStatus(order); ``` ### acmeClient.getCertificate(order, [preferredChain]) ⇒ Promise.<string> Get certificate from ACME order https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2 **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<string> - Certificate | Param | Type | Default | Description | | --- | --- | --- | --- | | order | object | | Order object | | [preferredChain] | string | null | Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` | **Example** Get certificate ```js const order = { ... }; // Previously created order const certificate = await client.getCertificate(order); ``` **Example** Get certificate with preferred chain ```js const order = { ... }; // Previously created order const certificate = await client.getCertificate(order, 'DST Root CA X3'); ``` ### acmeClient.revokeCertificate(cert, [data]) ⇒ Promise Revoke certificate https://datatracker.ietf.org/doc/html/rfc8555#section-7.6 **Kind**: instance method of [AcmeClient](#AcmeClient) | Param | Type | Description | | --- | --- | --- | | cert | buffer \| string | PEM encoded certificate | | [data] | object | Additional request data | **Example** Revoke certificate ```js const certificate = { ... }; // Previously created certificate const result = await client.revokeCertificate(certificate); ``` **Example** Revoke certificate with reason ```js const certificate = { ... }; // Previously created certificate const result = await client.revokeCertificate(certificate, { reason: 4, }); ``` ### acmeClient.auto(opts) ⇒ Promise.<string> Auto mode **Kind**: instance method of [AcmeClient](#AcmeClient) **Returns**: Promise.<string> - Certificate | Param | Type | Description | | --- | --- | --- | | opts | object | | | opts.csr | buffer \| string | Certificate Signing Request | | opts.challengeCreateFn | function | Function returning Promise triggered before completing ACME challenge | | opts.challengeRemoveFn | function | Function returning Promise triggered after completing ACME challenge | | [opts.email] | string | Account email address | | [opts.termsOfServiceAgreed] | boolean | Agree to Terms of Service, default: `false` | | [opts.skipChallengeVerification] | boolean | Skip internal challenge verification before notifying ACME provider, default: `false` | | [opts.challengePriority] | Array.<string> | Array defining challenge type priority, default: `['http-01', 'dns-01']` | | [opts.preferredChain] | string | Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` | **Example** Order a certificate using auto mode ```js const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ altNames: ['test.example.com'], }); const certificate = await client.auto({ csr: certificateRequest, email: 'test@example.com', termsOfServiceAgreed: true, challengeCreateFn: async (authz, challenge, keyAuthorization) => { // Satisfy challenge here }, challengeRemoveFn: async (authz, challenge, keyAuthorization) => { // Clean up challenge here }, }); ``` **Example** Order a certificate using auto mode with preferred chain ```js const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ altNames: ['test.example.com'], }); const certificate = await client.auto({ csr: certificateRequest, email: 'test@example.com', termsOfServiceAgreed: true, preferredChain: 'DST Root CA X3', challengeCreateFn: async () => {}, challengeRemoveFn: async () => {}, }); ``` ## Client : object ACME client **Kind**: global namespace ================================================ FILE: packages/core/acme-client/docs/crypto.md ================================================ ## Objects
crypto : object

Native Node.js crypto interface

## Constants
createPrivateEcdsaKeyPromise.<buffer>

Generate a private ECDSA key

getPublicKeybuffer

Get a public key derived from a RSA or ECDSA key

getPemBodyAsB64ustring

Parse body of PEM encoded object and return a Base64URL string If multiple objects are chained, the first body will be returned

readCsrDomainsobject

Read domains from a Certificate Signing Request

readCertificateInfoobject

Read information from a certificate If multiple certificates are chained, the first will be read

createCsrPromise.<Array.<buffer>>

Create a Certificate Signing Request

createAlpnCertificatePromise.<Array.<buffer>>

Create a self-signed ALPN certificate for TLS-ALPN-01 challenges

https://datatracker.ietf.org/doc/html/rfc8737

isAlpnCertificateAuthorizationValidboolean

Validate that a ALPN certificate contains the expected key authorization

## Functions
createPrivateRsaKey([modulusLength])Promise.<buffer>

Generate a private RSA key

createPrivateKey()

Alias of createPrivateRsaKey()

getJwk(keyPem)object

Get a JSON Web Key derived from a RSA or ECDSA key

https://datatracker.ietf.org/doc/html/rfc7517

splitPemChain(chainPem)Array.<string>

Split chain of PEM encoded objects from string into array

## crypto : object Native Node.js crypto interface **Kind**: global namespace ## createPrivateEcdsaKey ⇒ Promise.<buffer> Generate a private ECDSA key **Kind**: global constant **Returns**: Promise.<buffer> - PEM encoded private ECDSA key | Param | Type | Description | | --- | --- | --- | | [namedCurve] | string | ECDSA curve name (P-256, P-384 or P-521), default `P-256` | **Example** Generate private ECDSA key ```js const privateKey = await acme.crypto.createPrivateEcdsaKey(); ``` **Example** Private ECDSA key using P-384 curve ```js const privateKey = await acme.crypto.createPrivateEcdsaKey('P-384'); ``` ## getPublicKey ⇒ buffer Get a public key derived from a RSA or ECDSA key **Kind**: global constant **Returns**: buffer - PEM encoded public key | Param | Type | Description | | --- | --- | --- | | keyPem | buffer \| string | PEM encoded private or public key | **Example** Get public key ```js const publicKey = acme.crypto.getPublicKey(privateKey); ``` ## getPemBodyAsB64u ⇒ string Parse body of PEM encoded object and return a Base64URL string If multiple objects are chained, the first body will be returned **Kind**: global constant **Returns**: string - Base64URL-encoded body | Param | Type | Description | | --- | --- | --- | | pem | buffer \| string | PEM encoded chain or object | ## readCsrDomains ⇒ object Read domains from a Certificate Signing Request **Kind**: global constant **Returns**: object - {commonName, altNames} | Param | Type | Description | | --- | --- | --- | | csrPem | buffer \| string | PEM encoded Certificate Signing Request | **Example** Read Certificate Signing Request domains ```js const { commonName, altNames } = acme.crypto.readCsrDomains(certificateRequest); console.log(`Common name: ${commonName}`); console.log(`Alt names: ${altNames.join(', ')}`); ``` ## readCertificateInfo ⇒ object Read information from a certificate If multiple certificates are chained, the first will be read **Kind**: global constant **Returns**: object - Certificate info | Param | Type | Description | | --- | --- | --- | | certPem | buffer \| string | PEM encoded certificate or chain | **Example** Read certificate information ```js const info = acme.crypto.readCertificateInfo(certificate); const { commonName, altNames } = info.domains; console.log(`Not after: ${info.notAfter}`); console.log(`Not before: ${info.notBefore}`); console.log(`Common name: ${commonName}`); console.log(`Alt names: ${altNames.join(', ')}`); ``` ## createCsr ⇒ Promise.<Array.<buffer>> Create a Certificate Signing Request **Kind**: global constant **Returns**: Promise.<Array.<buffer>> - [privateKey, certificateSigningRequest] | Param | Type | Description | | --- | --- | --- | | data | object | | | [data.keySize] | number | Size of newly created RSA private key modulus in bits, default: `2048` | | [data.commonName] | string | FQDN of your server | | [data.altNames] | Array.<string> | SAN (Subject Alternative Names), default: `[]` | | [data.country] | string | 2 letter country code | | [data.state] | string | State or province | | [data.locality] | string | City | | [data.organization] | string | Organization name | | [data.organizationUnit] | string | Organizational unit name | | [data.emailAddress] | string | Email address | | [keyPem] | buffer \| string | PEM encoded CSR private key | **Example** Create a Certificate Signing Request ```js const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ altNames: ['test.example.com'], }); ``` **Example** Certificate Signing Request with both common and alternative names > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf). ```js const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ keySize: 4096, commonName: 'test.example.com', altNames: ['foo.example.com', 'bar.example.com'], }); ``` **Example** Certificate Signing Request with additional information ```js const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ altNames: ['test.example.com'], country: 'US', state: 'California', locality: 'Los Angeles', organization: 'The Company Inc.', organizationUnit: 'IT Department', emailAddress: 'contact@example.com', }); ``` **Example** Certificate Signing Request with ECDSA private key ```js const certificateKey = await acme.crypto.createPrivateEcdsaKey(); const [, certificateRequest] = await acme.crypto.createCsr({ altNames: ['test.example.com'], }, certificateKey); ``` ## createAlpnCertificate ⇒ Promise.<Array.<buffer>> Create a self-signed ALPN certificate for TLS-ALPN-01 challenges https://datatracker.ietf.org/doc/html/rfc8737 **Kind**: global constant **Returns**: Promise.<Array.<buffer>> - [privateKey, certificate] | Param | Type | Description | | --- | --- | --- | | authz | object | Identifier authorization | | keyAuthorization | string | Challenge key authorization | | [keyPem] | buffer \| string | PEM encoded CSR private key | **Example** Create a ALPN certificate ```js const [alpnKey, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization); ``` **Example** Create a ALPN certificate with ECDSA private key ```js const alpnKey = await acme.crypto.createPrivateEcdsaKey(); const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey); ``` ## isAlpnCertificateAuthorizationValid ⇒ boolean Validate that a ALPN certificate contains the expected key authorization **Kind**: global constant **Returns**: boolean - True when valid | Param | Type | Description | | --- | --- | --- | | certPem | buffer \| string | PEM encoded certificate | | keyAuthorization | string | Expected challenge key authorization | ## createPrivateRsaKey([modulusLength]) ⇒ Promise.<buffer> Generate a private RSA key **Kind**: global function **Returns**: Promise.<buffer> - PEM encoded private RSA key | Param | Type | Description | | --- | --- | --- | | [modulusLength] | number | Size of the keys modulus in bits, default: `2048` | **Example** Generate private RSA key ```js const privateKey = await acme.crypto.createPrivateRsaKey(); ``` **Example** Private RSA key with modulus size 4096 ```js const privateKey = await acme.crypto.createPrivateRsaKey(4096); ``` ## createPrivateKey() Alias of `createPrivateRsaKey()` **Kind**: global function ## getJwk(keyPem) ⇒ object Get a JSON Web Key derived from a RSA or ECDSA key https://datatracker.ietf.org/doc/html/rfc7517 **Kind**: global function **Returns**: object - JSON Web Key | Param | Type | Description | | --- | --- | --- | | keyPem | buffer \| string | PEM encoded private or public key | **Example** Get JWK ```js const jwk = acme.crypto.getJwk(privateKey); ``` ## splitPemChain(chainPem) ⇒ Array.<string> Split chain of PEM encoded objects from string into array **Kind**: global function **Returns**: Array.<string> - Array of PEM objects including headers | Param | Type | Description | | --- | --- | --- | | chainPem | buffer \| string | PEM encoded object chain | ================================================ FILE: packages/core/acme-client/docs/forge.md ================================================ ## Objects
forge : object

Legacy node-forge crypto interface

DEPRECATION WARNING: This crypto interface is deprecated and will be removed from acme-client in a future major release. Please migrate to the new acme.crypto interface at your earliest convenience.

## Constants
createPublicKeyPromise.<buffer>

Create public key from a private RSA key

getPemBodystring

Parse body of PEM encoded object from buffer or string If multiple objects are chained, the first body will be returned

splitPemChainArray.<string>

Split chain of PEM encoded objects from buffer or string into array

getModulusPromise.<buffer>

Get modulus

getPublicExponentPromise.<buffer>

Get public exponent

readCsrDomainsPromise.<object>

Read domains from a Certificate Signing Request

readCertificateInfoPromise.<object>

Read information from a certificate

createCsrPromise.<Array.<buffer>>

Create a Certificate Signing Request

## Functions
createPrivateKey([size])Promise.<buffer>

Generate a private RSA key

## forge : object Legacy node-forge crypto interface DEPRECATION WARNING: This crypto interface is deprecated and will be removed from acme-client in a future major release. Please migrate to the new `acme.crypto` interface at your earliest convenience. **Kind**: global namespace ## createPublicKey ⇒ Promise.<buffer> Create public key from a private RSA key **Kind**: global constant **Returns**: Promise.<buffer> - PEM encoded public RSA key | Param | Type | Description | | --- | --- | --- | | key | buffer \| string | PEM encoded private RSA key | **Example** Create public key ```js const publicKey = await acme.forge.createPublicKey(privateKey); ``` ## getPemBody ⇒ string Parse body of PEM encoded object from buffer or string If multiple objects are chained, the first body will be returned **Kind**: global constant **Returns**: string - PEM body | Param | Type | Description | | --- | --- | --- | | str | buffer \| string | PEM encoded buffer or string | ## splitPemChain ⇒ Array.<string> Split chain of PEM encoded objects from buffer or string into array **Kind**: global constant **Returns**: Array.<string> - Array of PEM bodies | Param | Type | Description | | --- | --- | --- | | str | buffer \| string | PEM encoded buffer or string | ## getModulus ⇒ Promise.<buffer> Get modulus **Kind**: global constant **Returns**: Promise.<buffer> - Modulus | Param | Type | Description | | --- | --- | --- | | input | buffer \| string | PEM encoded private key, certificate or CSR | **Example** Get modulus ```js const m1 = await acme.forge.getModulus(privateKey); const m2 = await acme.forge.getModulus(certificate); const m3 = await acme.forge.getModulus(certificateRequest); ``` ## getPublicExponent ⇒ Promise.<buffer> Get public exponent **Kind**: global constant **Returns**: Promise.<buffer> - Exponent | Param | Type | Description | | --- | --- | --- | | input | buffer \| string | PEM encoded private key, certificate or CSR | **Example** Get public exponent ```js const e1 = await acme.forge.getPublicExponent(privateKey); const e2 = await acme.forge.getPublicExponent(certificate); const e3 = await acme.forge.getPublicExponent(certificateRequest); ``` ## readCsrDomains ⇒ Promise.<object> Read domains from a Certificate Signing Request **Kind**: global constant **Returns**: Promise.<object> - {commonName, altNames} | Param | Type | Description | | --- | --- | --- | | csr | buffer \| string | PEM encoded Certificate Signing Request | **Example** Read Certificate Signing Request domains ```js const { commonName, altNames } = await acme.forge.readCsrDomains(certificateRequest); console.log(`Common name: ${commonName}`); console.log(`Alt names: ${altNames.join(', ')}`); ``` ## readCertificateInfo ⇒ Promise.<object> Read information from a certificate **Kind**: global constant **Returns**: Promise.<object> - Certificate info | Param | Type | Description | | --- | --- | --- | | cert | buffer \| string | PEM encoded certificate | **Example** Read certificate information ```js const info = await acme.forge.readCertificateInfo(certificate); const { commonName, altNames } = info.domains; console.log(`Not after: ${info.notAfter}`); console.log(`Not before: ${info.notBefore}`); console.log(`Common name: ${commonName}`); console.log(`Alt names: ${altNames.join(', ')}`); ``` ## createCsr ⇒ Promise.<Array.<buffer>> Create a Certificate Signing Request **Kind**: global constant **Returns**: Promise.<Array.<buffer>> - [privateKey, certificateSigningRequest] | Param | Type | Description | | --- | --- | --- | | data | object | | | [data.keySize] | number | Size of newly created private key, default: `2048` | | [data.commonName] | string | | | [data.altNames] | Array.<string> | default: `[]` | | [data.country] | string | | | [data.state] | string | | | [data.locality] | string | | | [data.organization] | string | | | [data.organizationUnit] | string | | | [data.emailAddress] | string | | | [key] | buffer \| string | CSR private key | **Example** Create a Certificate Signing Request ```js const [certificateKey, certificateRequest] = await acme.forge.createCsr({ altNames: ['test.example.com'], }); ``` **Example** Certificate Signing Request with both common and alternative names > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf). ```js const [certificateKey, certificateRequest] = await acme.forge.createCsr({ keySize: 4096, commonName: 'test.example.com', altNames: ['foo.example.com', 'bar.example.com'], }); ``` **Example** Certificate Signing Request with additional information ```js const [certificateKey, certificateRequest] = await acme.forge.createCsr({ altNames: ['test.example.com'], country: 'US', state: 'California', locality: 'Los Angeles', organization: 'The Company Inc.', organizationUnit: 'IT Department', emailAddress: 'contact@example.com', }); ``` **Example** Certificate Signing Request with predefined private key ```js const certificateKey = await acme.forge.createPrivateKey(); const [, certificateRequest] = await acme.forge.createCsr({ altNames: ['test.example.com'], }, certificateKey); ## createPrivateKey([size]) ⇒ Promise.<buffer> Generate a private RSA key **Kind**: global function **Returns**: Promise.<buffer> - PEM encoded private RSA key | Param | Type | Description | | --- | --- | --- | | [size] | number | Size of the key, default: `2048` | **Example** Generate private RSA key ```js const privateKey = await acme.forge.createPrivateKey(); ``` **Example** Private RSA key with defined size ```js const privateKey = await acme.forge.createPrivateKey(4096); ``` ================================================ FILE: packages/core/acme-client/docs/upgrade-v5.md ================================================ # Upgrading to v5 of `acme-client` This document outlines the breaking changes introduced in v5 of `acme-client`, why they were introduced and what you should look out for when upgrading your application. First off this release drops support for Node LTS v10, v12 and v14, and the reason for that is a new native crypto interface - more on that below. Since Node v14 is still currently in maintenance mode, `acme-client` v4 will continue to receive security updates and bugfixes until (at least) Node v14 reaches its end-of-line. ## New native crypto interface A new crypto interface has been introduced with v5, which you can find under `acme.crypto`. It uses native Node.js cryptography APIs to generate private keys, JSON Web Keys and signatures, and finally enables support for ECC/ECDSA (P-256, P384 and P521), both for account private keys and certificates. The [@peculiar/x509](https://www.npmjs.com/package/@peculiar/x509) module is used to handle generation and parsing of Certificate Signing Requests. Full documentation of `acme.crypto` can be [found here](crypto.md). Since the release of `acme-client` v1.0.0 the crypto interface API has remained mostly unaltered. Back then an OpenSSL CLI wrapper was used to generate keys, and very much has changed since. This has naturally resulted in a buildup of technical debt and slight API inconsistencies over time. The introduction of a new interface was a good opportunity to finally clean up these APIs. Below you will find a table summarizing the current `acme.forge` methods, and their new `acme.crypto` replacements. A summary of the changes for each method, including examples on how to migrate, can be found following the table. *Note: The now deprecated `acme.forge` interface is still available for use in v5, and will not be removed until a future major version, most likely v6. Should you not wish to change to the new interface right away, the following breaking changes will not immediately affect you.* * :green_circle: = API functionality unchanged between `acme.forge` and `acme.crypto` * :orange_circle: = Slight API changes, like depromising or renaming, action may be required * :red_circle: = Breaking API changes or removal, action required if using these methods | Deprecated `.forge` API | New `.crypto` API | State | | ----------------------------- | ----------------------------- | --------------------- | | `await createPrivateKey()` | `await createPrivateKey()` | :green_circle: | | `await createPublicKey()` | `getPublicKey()` | :orange_circle: (1) | | `getPemBody()` | `getPemBodyAsB64u()` | :red_circle: (2) | | `splitPemChain()` | `splitPemChain()` | :green_circle: | | `await getModulus()` | `getJwk()` | :red_circle: (3) | | `await getPublicExponent()` | `getJwk()` | :red_circle: (3) | | `await readCsrDomains()` | `readCsrDomains()` | :orange_circle: (4) | | `await readCertificateInfo()` | `readCertificateInfo()` | :orange_circle: (4) | | `await createCsr()` | `await createCsr()` | :green_circle: | ### 1. `createPublicKey` renamed and depromised * The method `createPublicKey()` has been renamed to `getPublicKey()` * No longer returns a promise, but the resulting public key directly * This is non-breaking if called with `await`, since `await` does not require its operand to be a promise * :orange_circle: **This is a breaking change if used with `.then()` or `.catch()`** ```js // Before const publicKey = await acme.forge.createPublicKey(privateKey); // After const publicKey = acme.crypto.getPublicKey(privateKey); ``` ### 2. `getPemBody` renamed, now returns Base64URL * Method `getPemBody()` has been renamed to `getPemBodyAsB64u()` * Instead of a Base64-encoded PEM body, now returns a Base64URL-encoded PEM body * :red_circle: **This is a breaking change** ```js // Before const body = acme.forge.getPemBody(pem); // After const body = acme.crypto.getPemBodyAsB64u(pem); ``` ### 3. `getModulus` and `getPublicExponent` merged into `getJwk` * Methods `getModulus()` and `getPublicExponent()` have been removed * Replaced by new method `getJwk()` * :red_circle: **This is a breaking change** ```js // Before const mod = await acme.forge.getModulus(key); const exp = await acme.forge.getPublicExponent(key); // After const { e, n } = acme.crypto.getJwk(key); ``` ### 4. `readCsrDomains` and `readCertificateInfo` depromised * Methods `readCsrDomains()` and `readCertificateInfo()` no longer return promises, but their resulting payloads directly * This is non-breaking if called with `await`, since `await` does not require its operand to be a promise * :orange_circle: **This is a breaking change if used with `.then()` or `.catch()`** ```js // Before const domains = await acme.forge.readCsrDomains(csr); const info = await acme.forge.readCertificateInfo(certificate); // After const domains = acme.crypto.readCsrDomains(csr); const info = acme.crypto.readCertificateInfo(certificate); ``` ================================================ FILE: packages/core/acme-client/examples/README.md ================================================ # Disclaimer These examples should not be used as is for any production environment, as they are just proof of concepts meant for testing and to get you started. The examples are naively written and purposefully avoids important topics since they will be specific to your application and how you choose to use `acme-client`, like for example: 1. **Concurrency control** * If implementing on-demand certificate generation * What happens when multiple requests hit your domain at the same time? * Ensure your application does not place multiple cert orders for the same domain at the same time by implementing some sort of exclusive lock 2. **Domain allow lists** * If implementing on-demand certificate generation * What happens when someone manipulates the `ServerName` or `Host` header to your service? * Ensure your application is unable to place certificate orders for domains you do not intend, as this can quickly rate limit your account and cause a DoS 3. **Clustering** * If using `acme-client` across a cluster of servers * Ensure challenge responses are known to all servers in your cluster, perhaps using a database or shared storage 4. **Certificate and key storage** * Where and how should the account key be stored and read? * Where and how should certificates and cert keys be stored and read? * How and when should they be renewed? ================================================ FILE: packages/core/acme-client/examples/api.js ================================================ /** * Example of acme.Client API */ const acme = require('./../'); function log(m) { process.stdout.write(`${m}\n`); } /** * Function used to satisfy an ACME challenge * * @param {object} authz Authorization object * @param {object} challenge Selected challenge * @param {string} keyAuthorization Authorization key * @returns {Promise} */ async function challengeCreateFn(authz, challenge, keyAuthorization) { /* Do something here */ log(JSON.stringify(authz)); log(JSON.stringify(challenge)); log(keyAuthorization); } /** * Function used to remove an ACME challenge response * * @param {object} authz Authorization object * @param {object} challenge Selected challenge * @returns {Promise} */ async function challengeRemoveFn(authz, challenge, keyAuthorization) { /* Do something here */ log(JSON.stringify(authz)); log(JSON.stringify(challenge)); log(keyAuthorization); } /** * Main */ module.exports = async () => { /* Init client */ const client = new acme.Client({ directoryUrl: acme.directory.letsencrypt.staging, accountKey: await acme.crypto.createPrivateKey(), }); /* Register account */ await client.createAccount({ termsOfServiceAgreed: true, contact: ['mailto:test@example.com'], }); /* Place new order */ const order = await client.createOrder({ identifiers: [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' }, ], }); /** * authorizations / client.getAuthorizations(order); * An array with one item per DNS name in the certificate order. * All items require at least one satisfied challenge before order can be completed. */ const authorizations = await client.getAuthorizations(order); const promises = authorizations.map(async (authz) => { let challengeCompleted = false; try { /** * challenges / authz.challenges * An array of all available challenge types for a single DNS name. * One of these challenges needs to be satisfied. */ const { challenges } = authz; /* Just select any challenge */ const challenge = challenges.pop(); const keyAuthorization = await client.getChallengeKeyAuthorization(challenge); try { /* Satisfy challenge */ await challengeCreateFn(authz, challenge, keyAuthorization); /* Verify that challenge is satisfied */ await client.verifyChallenge(authz, challenge); /* Notify ACME provider that challenge is satisfied */ await client.completeChallenge(challenge); challengeCompleted = true; /* Wait for ACME provider to respond with valid status */ await client.waitForValidStatus(challenge); } finally { /* Clean up challenge response */ try { await challengeRemoveFn(authz, challenge, keyAuthorization); } catch (e) { /** * Catch errors thrown by challengeRemoveFn() so the order can * be finalized, even though something went wrong during cleanup */ } } } catch (e) { /* Deactivate pending authz when unable to complete challenge */ if (!challengeCompleted) { try { await client.deactivateAuthorization(authz); } catch (f) { /* Catch and suppress deactivateAuthorization() errors */ } } throw e; } }); /* Wait for challenges to complete */ await Promise.all(promises); /* Finalize order */ const [key, csr] = await acme.crypto.createCsr({ altNames: ['example.com', '*.example.com'], }); const finalized = await client.finalizeOrder(order, csr); const cert = await client.getCertificate(finalized); /* Done */ log(`CSR:\n${csr.toString()}`); log(`Private key:\n${key.toString()}`); log(`Certificate:\n${cert.toString()}`); }; ================================================ FILE: packages/core/acme-client/examples/auto.js ================================================ /** * Example of acme.Client.auto() */ // const fs = require('fs').promises; const acme = require('./../'); function log(m) { process.stdout.write(`${m}\n`); } /** * Function used to satisfy an ACME challenge * * @param {object} authz Authorization object * @param {object} challenge Selected challenge * @param {string} keyAuthorization Authorization key * @returns {Promise} */ async function challengeCreateFn(authz, challenge, keyAuthorization) { log('Triggered challengeCreateFn()'); /* http-01 */ if (challenge.type === 'http-01') { const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`; const fileContents = keyAuthorization; log(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`); /* Replace this */ log(`Would write "${fileContents}" to path "${filePath}"`); // await fs.writeFile(filePath, fileContents); } /* dns-01 */ else if (challenge.type === 'dns-01') { const dnsRecord = `_acme-challenge.${authz.identifier.value}`; const recordValue = keyAuthorization; log(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`); /* Replace this */ log(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`); // await dnsProvider.createRecord(dnsRecord, 'TXT', recordValue); } } /** * Function used to remove an ACME challenge response * * @param {object} authz Authorization object * @param {object} challenge Selected challenge * @param {string} keyAuthorization Authorization key * @returns {Promise} */ async function challengeRemoveFn(authz, challenge, keyAuthorization) { log('Triggered challengeRemoveFn()'); /* http-01 */ if (challenge.type === 'http-01') { const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`; log(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`); /* Replace this */ log(`Would remove file on path "${filePath}"`); // await fs.unlink(filePath); } /* dns-01 */ else if (challenge.type === 'dns-01') { const dnsRecord = `_acme-challenge.${authz.identifier.value}`; const recordValue = keyAuthorization; log(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`); /* Replace this */ log(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`); // await dnsProvider.removeRecord(dnsRecord, 'TXT', recordValue); } } /** * Main */ module.exports = async () => { /* Init client */ const client = new acme.Client({ directoryUrl: acme.directory.letsencrypt.staging, accountKey: await acme.crypto.createPrivateKey(), }); /* Create CSR */ const [key, csr] = await acme.crypto.createCsr({ altNames: ['example.com'], }); /* Certificate */ const cert = await client.auto({ csr, email: 'test@example.com', termsOfServiceAgreed: true, challengeCreateFn, challengeRemoveFn, }); /* Done */ log(`CSR:\n${csr.toString()}`); log(`Private key:\n${key.toString()}`); log(`Certificate:\n${cert.toString()}`); }; ================================================ FILE: packages/core/acme-client/examples/dns-01/README.md ================================================ # dns-01 The greatest benefit of `dns-01` is that it is the only challenge type that can be used to issue ACME wildcard certificates, however it also has a few downsides. Your DNS provider needs to offer some sort of API you can use to automate adding and removing the required `TXT` DNS records. Additionally, solving DNS challenges will be much slower than the other challenge types because of DNS propagation delays. ## How it works When solving `dns-01` challenges, you prove ownership of a domain by serving a specific payload within a specific DNS `TXT` record from the domains authoritative nameservers. The ACME authority provides the client with a token that, along with a thumbprint of your account key, is used to generate a `base64url` encoded `SHA256` digest. This payload is then placed as a `TXT` record under DNS name `_acme-challenge.$YOUR_DOMAIN`. Once the order is finalized, the ACME authority will lookup your domains DNS record to verify that the payload is correct. `CNAME` and `NS` records are followed, should you wish to delegate challenge response to another DNS zone or record. ## Pros and cons * Only challenge type that can be used to issue wildcard certificates * Your DNS provider needs to supply an API that can be used * DNS propagation time may be slow * Useful in instances where both port 80 and 443 are unavailable ## External links * [https://letsencrypt.org/docs/challenge-types/#dns-01-challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) * [https://datatracker.ietf.org/doc/html/rfc8555#section-8.4](https://datatracker.ietf.org/doc/html/rfc8555#section-8.4) ================================================ FILE: packages/core/acme-client/examples/dns-01/dns-01.js ================================================ /** * Example using dns-01 challenge to generate certificates * * NOTE: This example is incomplete as the DNS challenge response implementation * will be specific to your DNS providers API. * * NOTE: This example does not order certificates on-demand, as solving dns-01 * will likely be too slow for it to make sense. Instead, it orders a wildcard * certificate on init before starting the HTTPS server as a demonstration. */ const https = require('https'); const acme = require('./../../'); const HTTPS_SERVER_PORT = 443; const WILDCARD_DOMAIN = 'example.com'; function log(m) { process.stdout.write(`${(new Date()).toISOString()} ${m}\n`); } /** * Main */ (async () => { try { /** * Initialize ACME client */ log('Initializing ACME client'); const client = new acme.Client({ directoryUrl: acme.directory.letsencrypt.staging, accountKey: await acme.crypto.createPrivateKey(), }); /** * Order wildcard certificate */ log(`Creating CSR for ${WILDCARD_DOMAIN}`); const [key, csr] = await acme.crypto.createCsr({ altNames: [WILDCARD_DOMAIN, `*.${WILDCARD_DOMAIN}`], }); log(`Ordering certificate for ${WILDCARD_DOMAIN}`); const cert = await client.auto({ csr, email: 'test@example.com', termsOfServiceAgreed: true, challengePriority: ['dns-01'], challengeCreateFn: (authz, challenge, keyAuthorization) => { /* TODO: Implement this */ log(`[TODO] Add TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`); }, challengeRemoveFn: (authz, challenge, keyAuthorization) => { /* TODO: Implement this */ log(`[TODO] Remove TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`); }, }); log(`Certificate for ${WILDCARD_DOMAIN} created successfully`); /** * HTTPS server */ const requestListener = (req, res) => { log(`HTTP 200 ${req.headers.host}${req.url}`); res.writeHead(200); res.end('Hello world\n'); }; const httpsServer = https.createServer({ key, cert, }, requestListener); httpsServer.listen(HTTPS_SERVER_PORT, () => { log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`); }); } catch (e) { log(`[FATAL] ${e.message}`); process.exit(1); } })(); ================================================ FILE: packages/core/acme-client/examples/fallback.crt ================================================ -----BEGIN CERTIFICATE----- MIIDCTCCAfGgAwIBAgIUGwI6ZLE3HN7oRZ9BvWLde0Tsu7EwDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDgwMTAwNTMzMVoXDTIyMDgz MTAwNTMzMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEA4c7zSiY6OEp9xYZHY42FUfOLREm03NstZhd9IxFFePwe CTTirJjmi5teKQwzBmEok0SJkanJUaMsMlOHjEykWSc4SBO4QjD349Q60044i9WS 7KHzeSqpWTG+V9jF3HOJPw843VG9hXy3ulXKcysTXzumTVQwfatCODBNkpWqMju2 N33biLgmpqwLbDSfKXS3uSVTfoHAKGT/oRepko7/0Hwr5oEmjXEbpRWRhU09KYjH 7jokRaiQRn0h216a0r4AKzSNGihNQtKJZIuwJvLFPMQYafsu9qBaCLPqDBXCwQWG aYh6Cm3kTkADKzG1LVPB/7/Uh2d4Fck/ejR9qXRK3QIDAQABo1MwUTAdBgNVHQ4E FgQUvyceAVDMPbW7wHwNF9px5dWfgd4wHwYDVR0jBBgwFoAUvyceAVDMPbW7wHwN F9px5dWfgd4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAaYkz AOHrRirPwfkwjb+uMliGHfANrmak8r5VDQA73RLTQLRhMpf1yrb1uhH7p/CUYKap x1C8RGQAXujoQbQOslyZA7cVLA9ASSZS6Noq7NerfGBiqxeuye+x3lIIk1EOL/rH aBu9rrYGmlU49PlGAQSfFHkwzXti2Mp1VQv8eMOBLR49ezZIXHiPE8S3gjNymZ0G UA13wzZCT7SG1BLmQ/cBVASG2wvhlC8IG/4vF0Xe+boSOb1vGWUtHS+MnvvRK4n5 TMUtrnxSQ/LA8AtobvzqgvQVKBSPLK6RzLE7I+Q9pWsbKTBqfyStuQrQFqafBOqN eYfPUgiID9uvfrxLvA== -----END CERTIFICATE----- ================================================ FILE: packages/core/acme-client/examples/fallback.key ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDhzvNKJjo4Sn3F hkdjjYVR84tESbTc2y1mF30jEUV4/B4JNOKsmOaLm14pDDMGYSiTRImRqclRoywy U4eMTKRZJzhIE7hCMPfj1DrTTjiL1ZLsofN5KqlZMb5X2MXcc4k/DzjdUb2FfLe6 VcpzKxNfO6ZNVDB9q0I4ME2SlaoyO7Y3fduIuCamrAtsNJ8pdLe5JVN+gcAoZP+h F6mSjv/QfCvmgSaNcRulFZGFTT0piMfuOiRFqJBGfSHbXprSvgArNI0aKE1C0olk i7Am8sU8xBhp+y72oFoIs+oMFcLBBYZpiHoKbeROQAMrMbUtU8H/v9SHZ3gVyT96 NH2pdErdAgMBAAECggEBAImI0FxQblOM45AkmmTDdPmWWjPspNGEWeF92wU55tOq 0+yNnqa7tmg/6JkdyhJPqTQRoazr+ifUN/4rLDtDDzMSFVCpWihOxR2qTW4YjY52 NjgU6EPbvSwLhUDiUplUcbrL3bnHqKSecxV2XYnKKdFudntRFPvmDL5GhWkL6Y8P 9KiQaYuPf4av8PR0NlWBMiZs+CBjLlnSTMAWRYj5mRSyFSEOMT7+Lvr3TqrO2/nh 0H30LXxrXXXuCbQXnVy3oSNf7TrathT2ADIrUUTdRHsLscvkEA35VtFQtWdJLtEg sso1J7viV9YDU4niPSdHPj3ubBjAExej4qCOzatsIQ0CgYEA8L5S3ojy89g7q6vB QuusIrjGkyM1yebDWqhEnjvlMpfrU1hCS90BM1ozZ28bjz/7PBimKL+A8BO+W0m4 2s9YbZP5aGwo18Iq86XEdtDgWtQ3NXbYkb8F8LNtyevC/UlAI/xyIRr7hDYlr/1v jJg16DXiNLyk+uj4Q3EuwzNl8n8CgYEA8B5UUkOiufPtm+ZOq9AlBpIa+NYaahZM h52jzMTKsFB18xsZU/ufvpKvXEu1sTeCDRo3JAHmiA6AG292Zc7W+uWRtMtlmQWE wnoZ6hKvEkFnArLCY6Nm5Qqm1wipLwDVO3dD/CDL86siHrXK4wU7Q+bp6xbt8lDi itz5F7p7HKMCgYAoj8iimexlTU9wczXSsqaECyHZ9JrBc9ICWkuFZY4OYi5SEpLI +WmUX2Q9zyiTkDIiQ/zq7KkqygjOlLNCmqDJhZ8GCwMupxZZitp5MmQ6qXrL1URT +h1kGrcqyEBIMKlP5t7L2SH7eqwK5OaAh7y9bSa5v/cEF3CM3GsGlIhevQKBgBGU RtwW84zlnNmzDMNrY6qNe8gH9LsbktLC6cEOD0DFQz1fGIWbgGB1YL1DFbQ5uh23 c54BPZ1sYlif2m0trXOE5xvzYCbJzqRmSAto/sQ5YY9DAxREXD4cf4ZyreAxEWtf Ge0VgZj/SGozKP1h3qrj9vAtJ5J79XnxH5NrJaQ9AoGBAM2rQrt8H2kizg4wMGRZ 0G3709W7xxlbPdm+i/jFVDayJswCr0+eMm4gGyyZL3135D0fcijxytKgg3/OpOJF jC9vsHsE2K1ATp6eYvYjrhqJHI1m44aq/h46SfajytZQjwMT/jaApULDP2/fCBm5 6eS2WCyHyrYJyrgoYQF56nsT -----END PRIVATE KEY----- ================================================ FILE: packages/core/acme-client/examples/http-01/README.md ================================================ # http-01 The `http-01` challenge type is the simplest to implement and should likely be your default choice, unless you either require wildcard certificates or if port 80 is unavailable for use. ## How it works When solving `http-01` challenges, you prove ownership of a domain name by serving a specific payload from a specific URL. The ACME authority provides the client with a token that is used to generate the URL and file contents. The file must exist at `http://$YOUR_DOMAIN/.well-known/acme-challenge/$TOKEN` and contain the token and a thumbprint of your account key. Once the order is finalized, the ACME authority will verify that the URL responds with the correct payload by sending HTTP requests before the challenge is valid. HTTP redirects are followed, and Let's Encrypt allows redirecting to HTTPS although this diverges from the ACME spec. ## Pros and cons * Challenge must be satisfied using port 80 (HTTP) * The simplest challenge type to implement * Can not be used to issue wildcard certificates * If using multiple web servers, all of them need to respond with the correct token ## External links * [https://letsencrypt.org/docs/challenge-types/#http-01-challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) * [https://datatracker.ietf.org/doc/html/rfc8555#section-8.3](https://datatracker.ietf.org/doc/html/rfc8555#section-8.3) ================================================ FILE: packages/core/acme-client/examples/http-01/http-01.js ================================================ /** * Example using http-01 challenge to generate certificates on-demand */ const fs = require('fs'); const path = require('path'); const http = require('http'); const https = require('https'); const tls = require('tls'); const acme = require('./../../'); const HTTP_SERVER_PORT = 80; const HTTPS_SERVER_PORT = 443; const VALID_DOMAINS = ['example.com', 'example.org']; const FALLBACK_KEY = fs.readFileSync(path.join(__dirname, '..', 'fallback.key')); const FALLBACK_CERT = fs.readFileSync(path.join(__dirname, '..', 'fallback.crt')); const pendingDomains = {}; const challengeResponses = {}; const certificateStore = {}; function log(m) { process.stdout.write(`${(new Date()).toISOString()} ${m}\n`); } /** * On-demand certificate generation using http-01 */ async function getCertOnDemand(client, servername, attempt = 0) { /* Invalid domain */ if (!VALID_DOMAINS.includes(servername)) { throw new Error(`Invalid domain: ${servername}`); } /* Certificate exists */ if (servername in certificateStore) { return certificateStore[servername]; } /* Waiting on certificate order to go through */ if (servername in pendingDomains) { if (attempt >= 10) { throw new Error(`Gave up waiting on certificate for ${servername}`); } await new Promise((resolve) => { setTimeout(resolve, 1000); }); return getCertOnDemand(client, servername, (attempt + 1)); } /* Create CSR */ log(`Creating CSR for ${servername}`); const [key, csr] = await acme.crypto.createCsr({ altNames: [servername], }); /* Order certificate */ log(`Ordering certificate for ${servername}`); const cert = await client.auto({ csr, email: 'test@example.com', termsOfServiceAgreed: true, challengePriority: ['http-01'], challengeCreateFn: (authz, challenge, keyAuthorization) => { challengeResponses[challenge.token] = keyAuthorization; }, challengeRemoveFn: (authz, challenge) => { delete challengeResponses[challenge.token]; }, }); /* Done, store certificate */ log(`Certificate for ${servername} created successfully`); certificateStore[servername] = [key, cert]; delete pendingDomains[servername]; return certificateStore[servername]; } /** * Main */ (async () => { try { /** * Initialize ACME client */ log('Initializing ACME client'); const client = new acme.Client({ directoryUrl: acme.directory.letsencrypt.staging, accountKey: await acme.crypto.createPrivateKey(), }); /** * HTTP server */ const httpServer = http.createServer((req, res) => { if (req.url.match(/\/\.well-known\/acme-challenge\/.+/)) { const token = req.url.split('/').pop(); log(`Received challenge request for token=${token}`); /* ACME challenge response */ if (token in challengeResponses) { log(`Serving challenge response HTTP 200 token=${token}`); res.writeHead(200); res.end(challengeResponses[token]); return; } /* Challenge response not found */ log(`Oops, challenge response not found for token=${token}`); res.writeHead(404); res.end(); return; } /* HTTP 302 redirect */ log(`HTTP 302 ${req.headers.host}${req.url}`); res.writeHead(302, { Location: `https://${req.headers.host}${req.url}` }); res.end(); }); httpServer.listen(HTTP_SERVER_PORT, () => { log(`HTTP server listening on port ${HTTP_SERVER_PORT}`); }); /** * HTTPS server */ const requestListener = (req, res) => { log(`HTTP 200 ${req.headers.host}${req.url}`); res.writeHead(200); res.end('Hello world\n'); }; const httpsServer = https.createServer({ /* Fallback certificate */ key: FALLBACK_KEY, cert: FALLBACK_CERT, /* Serve certificate based on servername */ SNICallback: async (servername, cb) => { try { log(`Handling SNI request for ${servername}`); const [key, cert] = await getCertOnDemand(client, servername); log(`Found certificate for ${servername}, serving secure context`); cb(null, tls.createSecureContext({ key, cert })); } catch (e) { log(`[ERROR] ${e.message}`); cb(e.message); } }, }, requestListener); httpsServer.listen(HTTPS_SERVER_PORT, () => { log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`); }); } catch (e) { log(`[FATAL] ${e.message}`); process.exit(1); } })(); ================================================ FILE: packages/core/acme-client/examples/tls-alpn-01/README.md ================================================ # tls-alpn-01 Responding to `tls-alpn-01` challenges using Node.js is a bit more involved than the other two challenge types, and requires a proxy (f.ex. [Nginx](https://nginx.org) or [HAProxy](https://www.haproxy.org)) in front of the Node.js service. The reason for this is that `tls-alpn-01` is solved by responding to the ACME challenge using self-signed certificates with an ALPN extension containing the challenge response. Since we don't want users of our application to be served with these self-signed certificates, we need to split the HTTPS traffic into two different Node.js backends - one that only serves ALPN certificates for challenge responses, and the other for actual end-user traffic that serves certificates retrieved from the ACME provider. As far as I *(library author)* know, routing HTTPS traffic based on ALPN protocol can not be done purely using Node.js. The end result should look something like this: ```text Nginx or HAProxy (0.0.0.0:443) *inspect requests SSL ALPN protocol* If ALPN == acme-tls/1 -> Node.js ALPN responder (127.0.0.1:4444) Else -> Node.js HTTPS server (127.0.0.1:4443) ``` Example proxy configuration: * [haproxy.cfg](haproxy.cfg) *(requires HAProxy >= v1.9.1)* * [nginx.conf](nginx.conf) *(requires [ngx_stream_ssl_preread_module](https://nginx.org/en/docs/stream/ngx_stream_ssl_preread_module.html))* Big thanks to [acme.sh](https://github.com/acmesh-official/acme.sh) and [dehydrated](https://github.com/dehydrated-io/dehydrated) for doing the legwork and providing Nginx and HAProxy config examples. ## How it works When solving `tls-alpn-01` challenges, you prove ownership of a domain name by serving a specially crafted certificate over HTTPS. The ACME authority provides the client with a token that is placed into the certificates `id-pe-acmeIdentifier` extension along with a thumbprint of your account key. Once the order is finalized, the ACME authority will verify by sending HTTPS requests to your domain with the `acme-tls/1` ALPN protocol, indicating to the server that it should serve the challenge response certificate. If the `id-pe-acmeIdentifier` extension contains the correct payload, the challenge is valid. ## Pros and cons * Challenge must be satisfied using port 443 (HTTPS) * Useful in instances where port 80 is unavailable * Can not be used to issue wildcard certificates * More complex than `http-01`, can not be solved purely using Node.js * If using multiple web servers, all of them need to respond with the correct certificate ## External links * [https://letsencrypt.org/docs/challenge-types/#tls-alpn-01](https://letsencrypt.org/docs/challenge-types/#tls-alpn-01) * [https://github.com/dehydrated-io/dehydrated/blob/master/docs/tls-alpn.md](https://github.com/dehydrated-io/dehydrated/blob/master/docs/tls-alpn.md) * [https://github.com/acmesh-official/acme.sh/wiki/TLS-ALPN-without-downtime](https://github.com/acmesh-official/acme.sh/wiki/TLS-ALPN-without-downtime) * [https://datatracker.ietf.org/doc/html/rfc8737](https://datatracker.ietf.org/doc/html/rfc8737) ================================================ FILE: packages/core/acme-client/examples/tls-alpn-01/haproxy.cfg ================================================ ## # HTTPS listener # - Send to ALPN responder port 4444 if protocol is acme-tls/1 # - Default to HTTPS backend port 4443 ## frontend https mode tcp bind :443 tcp-request inspect-delay 5s tcp-request content accept if { req_ssl_hello_type 1 } use_backend alpnresp if { req.ssl_alpn acme-tls/1 } default_backend https # Default HTTPS backend backend https mode tcp server https 127.0.0.1:4443 # ACME tls-alpn-01 responder backend backend alpnresp mode tcp server acmesh 127.0.0.1:4444 ================================================ FILE: packages/core/acme-client/examples/tls-alpn-01/nginx.conf ================================================ ## # HTTPS server # - Send to ALPN responder port 4444 if protocol is acme-tls/1 # - Default to HTTPS backend port 4443 ## stream { map $ssl_preread_alpn_protocols $tls_port { ~\bacme-tls/1\b 4444; default 4443; } server { listen 443; listen [::]:443; proxy_pass 127.0.0.1:$tls_port; ssl_preread on; } } ================================================ FILE: packages/core/acme-client/examples/tls-alpn-01/tls-alpn-01.js ================================================ /** * Example using tls-alpn-01 challenge to generate certificates on-demand */ const fs = require('fs'); const path = require('path'); const https = require('https'); const tls = require('tls'); const acme = require('./../../'); const HTTPS_SERVER_PORT = 4443; const ALPN_RESPONDER_PORT = 4444; const VALID_DOMAINS = ['example.com', 'example.org']; const FALLBACK_KEY = fs.readFileSync(path.join(__dirname, '..', 'fallback.key')); const FALLBACK_CERT = fs.readFileSync(path.join(__dirname, '..', 'fallback.crt')); const pendingDomains = {}; const alpnResponses = {}; const certificateStore = {}; function log(m) { process.stdout.write(`${(new Date()).toISOString()} ${m}\n`); } /** * On-demand certificate generation using tls-alpn-01 */ async function getCertOnDemand(client, servername, attempt = 0) { /* Invalid domain */ if (!VALID_DOMAINS.includes(servername)) { throw new Error(`Invalid domain: ${servername}`); } /* Certificate exists */ if (servername in certificateStore) { return certificateStore[servername]; } /* Waiting on certificate order to go through */ if (servername in pendingDomains) { if (attempt >= 10) { throw new Error(`Gave up waiting on certificate for ${servername}`); } await new Promise((resolve) => { setTimeout(resolve, 1000); }); return getCertOnDemand(client, servername, (attempt + 1)); } /* Create CSR */ log(`Creating CSR for ${servername}`); const [key, csr] = await acme.crypto.createCsr({ altNames: [servername], }); /* Order certificate */ log(`Ordering certificate for ${servername}`); const cert = await client.auto({ csr, email: 'test@example.com', termsOfServiceAgreed: true, challengePriority: ['tls-alpn-01'], challengeCreateFn: async (authz, challenge, keyAuthorization) => { alpnResponses[authz.identifier.value] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization); }, challengeRemoveFn: (authz) => { delete alpnResponses[authz.identifier.value]; }, }); /* Done, store certificate */ log(`Certificate for ${servername} created successfully`); certificateStore[servername] = [key, cert]; delete pendingDomains[servername]; return certificateStore[servername]; } /** * Main */ (async () => { try { /** * Initialize ACME client */ log('Initializing ACME client'); const client = new acme.Client({ directoryUrl: acme.directory.letsencrypt.staging, accountKey: await acme.crypto.createPrivateKey(), }); /** * ALPN responder */ const alpnResponder = https.createServer({ /* Fallback cert */ key: FALLBACK_KEY, cert: FALLBACK_CERT, /* Allow acme-tls/1 ALPN protocol */ ALPNProtocols: ['acme-tls/1'], /* Serve ALPN certificate based on servername */ SNICallback: async (servername, cb) => { try { log(`Handling ALPN SNI request for ${servername}`); if (!Object.keys(alpnResponses).includes(servername)) { throw new Error(`No ALPN certificate found for ${servername}`); } /* Serve ALPN challenge response */ log(`Found ALPN certificate for ${servername}, serving secure context`); cb(null, tls.createSecureContext({ key: alpnResponses[servername][0], cert: alpnResponses[servername][1], })); } catch (e) { log(`[ERROR] ${e.message}`); cb(e.message); } }, }); /* Terminate once TLS handshake has been established */ alpnResponder.on('secureConnection', (socket) => { socket.end(); }); alpnResponder.listen(ALPN_RESPONDER_PORT, () => { log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`); }); /** * HTTPS server */ const requestListener = (req, res) => { log(`HTTP 200 ${req.headers.host}${req.url}`); res.writeHead(200); res.end('Hello world\n'); }; const httpsServer = https.createServer({ /* Fallback cert */ key: FALLBACK_KEY, cert: FALLBACK_CERT, /* Serve certificate based on servername */ SNICallback: async (servername, cb) => { try { log(`Handling SNI request for ${servername}`); const [key, cert] = await getCertOnDemand(client, servername); log(`Found certificate for ${servername}, serving secure context`); cb(null, tls.createSecureContext({ key, cert })); } catch (e) { log(`[ERROR] ${e.message}`); cb(e.message); } }, }, requestListener); httpsServer.listen(HTTPS_SERVER_PORT, () => { log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`); }); } catch (e) { log(`[FATAL] ${e.message}`); process.exit(1); } })(); ================================================ FILE: packages/core/acme-client/package.json ================================================ { "name": "@certd/acme-client", "description": "Simple and unopinionated ACME client", "private": false, "author": "nmorsman", "version": "1.36.10", "type": "module", "module": "scr/index.js", "main": "src/index.js", "types": "types/index.d.ts", "license": "MIT", "homepage": "https://github.com/publishlab/node-acme-client", "engines": { "node": ">= 18" }, "files": [ "src", "types" ], "dependencies": { "@certd/basic": "^1.36.10", "@peculiar/x509": "^1.11.0", "asn1js": "^3.0.5", "axios": "^1.7.2", "debug": "^4.3.5", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "lodash-es": "^4.17.21", "node-forge": "^1.3.1", "punycode.js": "^2.3.1" }, "devDependencies": { "@types/node": "^20.14.10", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "chai": "^4.4.1", "chai-as-promised": "^7.1.2", "eslint": "^8.57.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^4.2.1", "jsdoc-to-markdown": "^8.0.1", "mocha": "^10.6.0", "nock": "^13.5.4", "prettier": "^2.8.8", "tsd": "^0.31.1", "typescript": "^5.4.2" }, "scripts": { "build-docs": "jsdoc2md src/client.js > docs/client.md && jsdoc2md src/crypto/index.js > docs/crypto.md && jsdoc2md src/crypto/forge.js > docs/forge.md", "lint": "eslint .", "lint-types": "tsd", "prepublishOnly": "npm run build-docs", "test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"", "pub": "npm publish" }, "repository": { "type": "git", "url": "https://github.com/publishlab/node-acme-client" }, "keywords": [ "acme", "client", "lets", "encrypt", "acmev2", "boulder" ], "bugs": { "url": "https://github.com/publishlab/node-acme-client/issues" }, "gitHead": "085bdf5cfa9140903611aa12cdd2542d05aba321" } ================================================ FILE: packages/core/acme-client/src/api.js ================================================ /** * ACME API client */ import * as util from './util.js'; /** * AcmeApi * * @class * @param {HttpClient} httpClient */ class AcmeApi { constructor(httpClient, accountUrl = null) { this.http = httpClient; this.accountUrl = accountUrl; } getLocationFromHeader(resp) { let locationUrl = resp.headers.location; const mapping = this.http.urlMapping; if (mapping.mappings) { // eslint-disable-next-line guard-for-in,no-restricted-syntax for (const key in mapping.mappings) { const url = mapping.mappings[key]; if (locationUrl.indexOf(url) > -1) { locationUrl = locationUrl.replace(url, key); } } } console.log(locationUrl, mapping); return locationUrl; } /** * Get account URL * * @private * @returns {string} Account URL */ getAccountUrl() { if (!this.accountUrl) { throw new Error('No account URL found, register account first'); } return this.accountUrl; } /** * ACME API request * * @private * @param {string} url Request URL * @param {object} [payload] Request payload, default: `null` * @param {number[]} [validStatusCodes] Array of valid HTTP response status codes, default: `[]` * @param {object} [opts] * @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true` * @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false` * @returns {Promise} HTTP response */ async apiRequest(url, payload = null, validStatusCodes = [], { includeJwsKid = true, includeExternalAccountBinding = false } = {}) { const kid = includeJwsKid ? this.getAccountUrl() : null; const resp = await this.http.signedRequest(url, payload, { kid, includeExternalAccountBinding }); if (validStatusCodes.length && (validStatusCodes.indexOf(resp.status) === -1)) { throw new Error(util.formatResponseError(resp)); } return resp; } /** * ACME API request by resource name helper * * @private * @param {string} resource Request resource name * @param {object} [payload] Request payload, default: `null` * @param {number[]} [validStatusCodes] Array of valid HTTP response status codes, default: `[]` * @param {object} [opts] * @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true` * @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false` * @returns {Promise} HTTP response */ async apiResourceRequest(resource, payload = null, validStatusCodes = [], { includeJwsKid = true, includeExternalAccountBinding = false } = {}) { const resourceUrl = await this.http.getResourceUrl(resource); return this.apiRequest(resourceUrl, payload, validStatusCodes, { includeJwsKid, includeExternalAccountBinding }); } /** * Get Terms of Service URL if available * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1 * * @returns {Promise} ToS URL */ async getTermsOfServiceUrl() { return this.http.getMetaField('termsOfService'); } /** * Create new account * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3 * * @param {object} data Request payload * @returns {Promise} HTTP response */ async createAccount(data) { const resp = await this.apiResourceRequest('newAccount', data, [200, 201], { includeJwsKid: false, includeExternalAccountBinding: (data.onlyReturnExisting !== true), }); /* Set account URL */ if (resp.headers.location) { this.accountUrl = this.getLocationFromHeader(resp); } return resp; } /** * Update account * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2 * * @param {object} data Request payload * @returns {Promise} HTTP response */ updateAccount(data) { return this.apiRequest(this.getAccountUrl(), data, [200, 202]); } /** * Update account key * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5 * * @param {object} data Request payload * @returns {Promise} HTTP response */ updateAccountKey(data) { return this.apiResourceRequest('keyChange', data, [200]); } /** * Create new order * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 * * @param {object} data Request payload * @returns {Promise} HTTP response */ createOrder(data) { return this.apiResourceRequest('newOrder', data, [201]); } /** * Get order * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 * * @param {string} url Order URL * @returns {Promise} HTTP response */ getOrder(url) { return this.apiRequest(url, null, [200]); } /** * Finalize order * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 * * @param {string} url Finalization URL * @param {object} data Request payload * @returns {Promise} HTTP response */ finalizeOrder(url, data) { return this.apiRequest(url, data, [200]); } /** * Get identifier authorization * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5 * * @param {string} url Authorization URL * @returns {Promise} HTTP response */ getAuthorization(url) { return this.apiRequest(url, null, [200]); } /** * Update identifier authorization * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2 * * @param {string} url Authorization URL * @param {object} data Request payload * @returns {Promise} HTTP response */ updateAuthorization(url, data) { return this.apiRequest(url, data, [200]); } /** * Complete challenge * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1 * * @param {string} url Challenge URL * @param {object} data Request payload * @returns {Promise} HTTP response */ completeChallenge(url, data) { return this.apiRequest(url, data, [200]); } /** * Revoke certificate * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.6 * * @param {object} data Request payload * @returns {Promise} HTTP response */ revokeCert(data) { return this.apiResourceRequest('revokeCert', data, [200]); } } /* Export API */ export default AcmeApi; ================================================ FILE: packages/core/acme-client/src/auto.js ================================================ /** * ACME auto helper */ import { readCsrDomains } from "./crypto/index.js"; import { log } from "./logger.js"; import { wait } from "./wait.js"; import { CancelError } from "./error.js"; const defaultOpts = { csr: null, email: null, preferredChain: null, termsOfServiceAgreed: false, skipChallengeVerification: false, challengePriority: ["http-01", "dns-01"], challengeCreateFn: async () => { throw new Error("Missing challengeCreateFn()"); }, challengeRemoveFn: async () => { throw new Error("Missing challengeRemoveFn()"); } }; /** * ACME client auto mode * * @param {AcmeClient} client ACME client * @param {object} userOpts Options * @returns {Promise} Certificate */ export default async (client, userOpts) => { const opts = { ...defaultOpts, ...userOpts }; const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed }; if (!Buffer.isBuffer(opts.csr)) { opts.csr = Buffer.from(opts.csr); } if (opts.email) { accountPayload.contact = [`mailto:${opts.email}`]; } if (opts.externalAccountBinding) { accountPayload.externalAccountBinding = opts.externalAccountBinding; } /** * Register account */ log("[auto] Checking account"); try { client.getAccountUrl(); log("[auto] Account URL already exists, skipping account registration( 证书申请账户已存在,跳过注册 )"); } catch (e) { log("[auto] Registering account (注册证书申请账户)"); await client.createAccount(accountPayload); } /** * Parse domains from CSR */ log("[auto] Parsing domains from Certificate Signing Request "); const { commonName, altNames } = readCsrDomains(opts.csr); const uniqueDomains = Array.from(new Set([commonName].concat(altNames).filter((d) => d))); log(`[auto] Resolved ${uniqueDomains.length} unique domains from parsing the Certificate Signing Request`); /** * Place order */ log("[auto] Placing new certificate order with ACME provider"); const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type: "dns", value: d })) }; if (opts.profile && client.sslProvider === 'letsencrypt' ){ orderPayload.profile = opts.profile; } const order = await client.createOrder(orderPayload); const authorizations = await client.getAuthorizations(order); log(`[auto] Placed certificate order successfully, received ${authorizations.length} identity authorizations`); /** * Resolve and satisfy challenges */ log("[auto] Resolving and satisfying authorization challenges"); const clearTasks = []; const localVerifyTasks = []; const completeChallengeTasks = []; const challengeFunc = async (authz) => { const d = authz.identifier.value; let challengeCompleted = false; /* Skip authz that already has valid status */ if (authz.status === "valid") { log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`); return; } const keyAuthorizationGetter = async (challenge) => { return await client.getChallengeKeyAuthorization(challenge); }; async function deactivateAuth(e) { log(`[auto] [${d}] Unable to complete challenge: ${e.message}`); try { log(`[auto] [${d}] Deactivating failed authorization`); await client.deactivateAuthorization(authz); } catch (f) { /* Suppress deactivateAuthorization() errors */ log(`[auto] [${d}] Authorization deactivation threw error: ${f.message}`); } } log(`[auto] [${d}] Trigger challengeCreateFn()`); try { const { recordReq, recordRes, dnsProvider, challenge, keyAuthorization ,httpUploader} = await opts.challengeCreateFn(authz, keyAuthorizationGetter); clearTasks.push(async () => { /* Trigger challengeRemoveFn(), suppress errors */ log(`[auto] [${d}] Trigger challengeRemoveFn()`); try { await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider,httpUploader); } catch (e) { log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`); } }); localVerifyTasks.push(async () => { /* Challenge verification */ log(`[auto] [${d}] 开始本地验证, type = ${challenge.type}`); try { await client.verifyChallenge(authz, challenge); } catch (e) { log(`[auto] [${d}] 本地验证失败,尝试请求ACME提供商获取状态: ${e.message}`); } }); completeChallengeTasks.push(async () => { /* Complete challenge and wait for valid status */ log(`[auto] [${d}] 请求ACME提供商完成验证`); try{ await client.completeChallenge(challenge); }catch (e) { await deactivateAuth(e); throw e; } challengeCompleted = true; log(`[auto] [${d}] 等待返回valid状态`); await client.waitForValidStatus(challenge,d); }); } catch (e) { log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`); await deactivateAuth(e); throw e; } }; const domainSets = []; authorizations.forEach((authz) => { const d = authz.identifier.value; log(`authorization:domain = ${d}, value = ${JSON.stringify(authz)}`); if (authz.status === "valid") { log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`); return; } let setd = false; // eslint-disable-next-line no-restricted-syntax for (const group of domainSets) { if (!group[d]) { group[d] = authz; setd = true; break; } } if (!setd) { const group = {}; group[d] = authz; domainSets.push(group); } }); // log(`domainSets:${JSON.stringify(domainSets)}`); const allChallengePromises = []; // eslint-disable-next-line no-restricted-syntax const challengePromises = []; allChallengePromises.push(challengePromises); for (const domainSet of domainSets) { // eslint-disable-next-line guard-for-in,no-restricted-syntax for (const domain in domainSet) { const authz = domainSet[domain]; challengePromises.push(async () => { log(`[auto] [${domain}] Starting challenge`); await challengeFunc(authz); }); } } log(`[auto] challengeGroups:${allChallengePromises.length}`); async function runAllPromise(tasks) { let promise = Promise.resolve(); tasks.forEach((task) => { promise = promise.then(task); }); return promise; } async function runPromisePa(tasks, waitTime = 8000) { const results = []; let j = 0 // eslint-disable-next-line no-await-in-loop,no-restricted-syntax for (const task of tasks) { j++ log(`开始第${j}个任务`); results.push(task()); // eslint-disable-next-line no-await-in-loop log(`wait ${Math.floor(waitTime/1000)}s`) await wait(waitTime); } return Promise.all(results); } log(`开始challenge,共${allChallengePromises.length}组`); let i = 0; // eslint-disable-next-line no-restricted-syntax for (const challengePromises of allChallengePromises) { i += 1; log(`开始第${i}组`); if (opts.signal && opts.signal.aborted) { throw new CancelError("用户取消"); } const waitDnsDiffuseTime = opts.waitDnsDiffuseTime || 30; try { // eslint-disable-next-line no-await-in-loop await runPromisePa(challengePromises); if (opts.skipChallengeVerification === true) { log(`跳过本地验证(skipChallengeVerification=true),等待 60s`); await wait(60 * 1000); } else { log("开始本地校验") await runPromisePa(localVerifyTasks, 1000); log(`本地校验完成,等待${waitDnsDiffuseTime}s`) await wait(waitDnsDiffuseTime * 1000) } log("开始向提供商请求挑战验证"); await runPromisePa(completeChallengeTasks, 1000); } catch (e) { log(`证书申请失败${e.message}`); throw e; } finally { // letsencrypt 如果同时检出两个TXT记录,会以第一个为准,就会校验失败,所以需要提前删除 // zerossl 此方式测试无问题 log(`清理challenge痕迹,length:${clearTasks.length}`); try { // eslint-disable-next-line no-await-in-loop await runAllPromise(clearTasks); } catch (e) { log("清理challenge失败"); log(e); } } } log("challenge结束"); // log('[auto] Waiting for challenge valid status'); // await Promise.all(challengePromises); /** * Finalize order and download certificate */ log("[auto] Finalizing order and downloading certificate"); const finalized = await client.finalizeOrder(order, opts.csr); const res = await client.getCertificate(finalized, opts.preferredChain); return res; // try { // await Promise.allSettled(challengePromises); // } // finally { // log('清理challenge'); // await Promise.allSettled(clearTasks); // } }; ================================================ FILE: packages/core/acme-client/src/axios.js ================================================ /** * Axios instance */ import axios from 'axios'; import { parseRetryAfterHeader } from './util.js'; import { log } from './logger.js'; const { AxiosError } = axios; import {getGlobalAgents, HttpError} from '@certd/basic' /** * Defaults */ const instance = axios.create(); /* Default User-Agent */ instance.defaults.headers.common['User-Agent'] = `@certd/acme-client`; /* Default ACME settings */ instance.defaults.acmeSettings = { httpChallengePort: 80, httpsChallengePort: 443, tlsAlpnChallengePort: 443, retryMaxAttempts: 3, retryDefaultDelay: 3, }; // instance.defaults.proxy = { // host: '192.168.34.139', // port: 10811 // }; /** * Explicitly set Node as default HTTP adapter * * https://github.com/axios/axios/issues/1180 * https://stackoverflow.com/questions/42677387 */ instance.defaults.adapter = 'http'; /** * Retry requests on server errors or when rate limited * * https://datatracker.ietf.org/doc/html/rfc8555#section-6.6 */ function isRetryableError(error) { return (error.code !== 'ECONNABORTED') && (error.code !== 'ERR_NOCK_NO_MATCH') && (!error.response || (error.response.status === 429) || ((error.response.status >= 500) && (error.response.status <= 599))); } /* https://github.com/axios/axios/blob/main/lib/core/settle.js */ function validateStatus(response) { if (!response) { return new Error('Response is undefined'); } let validator = null; if (response.config) { validator = response.config.retryValidateStatus; } if (!response.status || !validator || validator(response.status)) { return response; } const err = new AxiosError( `Request failed with status code ${response.status}`, (Math.floor(response.status / 100) === 4) ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE, response.config, response.request, response, ); throw new HttpError(err); } /* Pass all responses through the error interceptor */ instance.interceptors.request.use((config) => { if (!('retryValidateStatus' in config)) { config.retryValidateStatus = config.validateStatus; } config.validateStatus = () => false; const agents = getGlobalAgents(); // if (config.skipSslVerify) { // logger.info('跳过SSL验证'); // agents = createAgent({ rejectUnauthorized: false } as any); // } // delete config.skipSslVerify; config.httpsAgent = agents.httpsAgent; config.httpAgent = agents.httpAgent; config.proxy = false; // 必须 否则还会走一层代理, return config; }); /* Handle request retries if applicable */ instance.interceptors.response.use(null, async (error) => { const { config, response } = error; if (!config) { return Promise.reject(new HttpError(error)); } /* Pick up errors we want to retry */ if (isRetryableError(error)) { const { retryMaxAttempts, retryDefaultDelay } = instance.defaults.acmeSettings; config.retryAttempt = ('retryAttempt' in config) ? (config.retryAttempt + 1) : 1; if (config.retryAttempt <= retryMaxAttempts) { const code = response ? `HTTP ${response.status}` : error.code; log(`Caught ${code}, retry attempt ${config.retryAttempt}/${retryMaxAttempts} to URL ${config.url}`); const retryAfter = (retryDefaultDelay * config.retryAttempt); /* Attempt to parse Retry-After header, fallback to default delay */ const headerRetryAfter = response ? parseRetryAfterHeader(response.headers['retry-after']) : 0; if (headerRetryAfter > 0) { const waitMinutes = (headerRetryAfter / 60).toFixed(1); log(`Found retry-after response header with value: ${response.headers['retry-after']}, waiting ${waitMinutes} minutes`); log(JSON.stringify(response.data)); return Promise.reject(new HttpError(error)); } log(`waiting ${retryAfter} seconds`); /* Wait and retry the request */ await new Promise((resolve) => { setTimeout(resolve, (retryAfter * 1000)); }); log(`Retrying request to URL ${config.url}`); return instance(config); } } if (!response) { return Promise.reject(new HttpError(error)); } /* Validate and return response */ return validateStatus(response); }); /** * Export instance */ export default instance; ================================================ FILE: packages/core/acme-client/src/client.js ================================================ /** * ACME client * * @namespace Client */ import { createHash } from 'crypto'; import { getPemBodyAsB64u } from './crypto/index.js'; import { log } from './logger.js'; import HttpClient from './http.js'; import AcmeApi from './api.js'; import verify from './verify.js'; import * as util from './util.js'; import auto from './auto.js'; import { CancelError } from './error.js'; /** * ACME states * * @private */ const validStates = ['ready', 'valid']; const pendingStates = ['pending', 'processing']; const invalidStates = ['invalid']; /** * Default options * * @private */ const defaultOpts = { directoryUrl: undefined, accountKey: undefined, accountUrl: null, externalAccountBinding: {}, backoffAttempts: 10, backoffMin: 5000, backoffMax: 30000, }; /** * AcmeClient * * @class * @param {object} opts * @param {string} opts.directoryUrl ACME directory URL * @param {buffer|string} opts.accountKey PEM encoded account private key * @param {string} [opts.accountUrl] Account URL, default: `null` * @param {object} [opts.externalAccountBinding] * @param {string} [opts.externalAccountBinding.kid] External account binding KID * @param {string} [opts.externalAccountBinding.hmacKey] External account binding HMAC key * @param {number} [opts.backoffAttempts] Maximum number of backoff attempts, default: `10` * @param {number} [opts.backoffMin] Minimum backoff attempt delay in milliseconds, default: `5000` * @param {number} [opts.backoffMax] Maximum backoff attempt delay in milliseconds, default: `30000` * * @example Create ACME client instance * ```js * const client = new acme.Client({ * directoryUrl: acme.directory.letsencrypt.staging, * accountKey: 'Private key goes here', * }); * ``` * * @example Create ACME client instance * ```js * const client = new acme.Client({ * directoryUrl: acme.directory.letsencrypt.staging, * accountKey: 'Private key goes here', * accountUrl: 'Optional account URL goes here', * backoffAttempts: 10, * backoffMin: 5000, * backoffMax: 30000, * }); * ``` * * @example Create ACME client with external account binding * ```js * const client = new acme.Client({ * directoryUrl: 'https://acme-provider.example.com/directory-url', * accountKey: 'Private key goes here', * externalAccountBinding: { * kid: 'YOUR-EAB-KID', * hmacKey: 'YOUR-EAB-HMAC-KEY', * }, * }); * ``` */ class AcmeClient { sslProvider constructor(opts) { if (!Buffer.isBuffer(opts.accountKey)) { opts.accountKey = Buffer.from(opts.accountKey); } this.sslProvider = opts.sslProvider; this.opts = { ...defaultOpts, ...opts }; this.backoffOpts = { attempts: this.opts.backoffAttempts, min: this.opts.backoffMin, max: this.opts.backoffMax, }; this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey, this.opts.externalAccountBinding, this.opts.urlMapping); this.api = new AcmeApi(this.http, this.opts.accountUrl); } /** * Get Terms of Service URL if available * * @returns {Promise} ToS URL * * @example Get Terms of Service URL * ```js * const termsOfService = client.getTermsOfServiceUrl(); * * if (!termsOfService) { * // CA did not provide Terms of Service * } * ``` */ getTermsOfServiceUrl() { return this.api.getTermsOfServiceUrl(); } /** * Get current account URL * * @returns {string} Account URL * @throws {Error} No account URL found * * @example Get current account URL * ```js * try { * const accountUrl = client.getAccountUrl(); * } * catch (e) { * // No account URL exists, need to create account first * } * ``` */ getAccountUrl() { return this.api.getAccountUrl(); } /** * Create a new account * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3 * * @param {object} [data] Request data * @returns {Promise} Account * * @example Create a new account * ```js * const account = await client.createAccount({ * termsOfServiceAgreed: true, * }); * ``` * * @example Create a new account with contact info * ```js * const account = await client.createAccount({ * termsOfServiceAgreed: true, * contact: ['mailto:test@example.com'], * }); * ``` */ async createAccount(data = {}) { try { this.getAccountUrl(); /* Account URL exists */ log('Account URL exists, returning updateAccount()'); return this.updateAccount(data); } catch (e) { const resp = await this.api.createAccount(data); /* HTTP 200: Account exists */ if (resp.status === 200) { log('Account already exists (HTTP 200), returning updateAccount()'); return this.updateAccount(data); } return resp.data; } } /** * Update existing account * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2 * * @param {object} [data] Request data * @returns {Promise} Account * * @example Update existing account * ```js * const account = await client.updateAccount({ * contact: ['mailto:foo@example.com'], * }); * ``` */ async updateAccount(data = {}) { try { this.api.getAccountUrl(); } catch (e) { log('No account URL found, returning createAccount()'); return this.createAccount(data); } /* Remove data only applicable to createAccount() */ if ('onlyReturnExisting' in data) { delete data.onlyReturnExisting; } /* POST-as-GET */ if (Object.keys(data).length === 0) { data = null; } const resp = await this.api.updateAccount(data); return resp.data; } /** * Update account private key * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5 * * @param {buffer|string} newAccountKey New PEM encoded private key * @param {object} [data] Additional request data * @returns {Promise} Account * * @example Update account private key * ```js * const newAccountKey = 'New private key goes here'; * const result = await client.updateAccountKey(newAccountKey); * ``` */ async updateAccountKey(newAccountKey, data = {}) { if (!Buffer.isBuffer(newAccountKey)) { newAccountKey = Buffer.from(newAccountKey); } const accountUrl = this.api.getAccountUrl(); /* Create new HTTP and API clients using new key */ const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey, this.opts.externalAccountBinding); const newApiClient = new AcmeApi(newHttpClient, accountUrl); /* Get old JWK */ data.account = accountUrl; data.oldKey = this.http.getJwk(); /* Get signed request body from new client */ const url = await newHttpClient.getResourceUrl('keyChange'); const body = newHttpClient.createSignedBody(url, data); /* Change key using old client */ const resp = await this.api.updateAccountKey(body); /* Replace existing HTTP and API client */ this.http = newHttpClient; this.api = newApiClient; return resp.data; } /** * Create a new order * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 * * @param {object} data Request data * @returns {Promise} Order * * @example Create a new order * ```js * const order = await client.createOrder({ * identifiers: [ * { type: 'dns', value: 'example.com' }, * { type: 'dns', value: 'test.example.com' }, * ], * }); * ``` */ async createOrder(data) { const resp = await this.api.createOrder(data); if (!resp.headers.location) { throw new Error('Creating a new order did not return an order link'); } /* Add URL to response */ resp.data.url = this.api.getLocationFromHeader(resp); return resp.data; } /** * Refresh order object from CA * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 * * @param {object} order Order object * @returns {Promise} Order * * @example * ```js * const order = { ... }; // Previously created order object * const result = await client.getOrder(order); * ``` */ async getOrder(order) { if (!order.url) { throw new Error('Unable to get order, URL not found'); } const resp = await this.api.getOrder(order.url); /* Add URL to response */ resp.data.url = order.url; return resp.data; } /** * Finalize order * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 * * @param {object} order Order object * @param {buffer|string} csr PEM encoded Certificate Signing Request * @returns {Promise} Order * * @example Finalize order * ```js * const order = { ... }; // Previously created order object * const csr = { ... }; // Previously created Certificate Signing Request * const result = await client.finalizeOrder(order, csr); * ``` */ async finalizeOrder(order, csr) { if (!order.finalize) { throw new Error('Unable to finalize order, URL not found'); } if (!Buffer.isBuffer(csr)) { csr = Buffer.from(csr); } const data = { csr: getPemBodyAsB64u(csr) }; const resp = await this.api.finalizeOrder(order.finalize, data); /* Add URL to response */ resp.data.url = order.url; return resp.data; } /** * Get identifier authorizations from order * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5 * * @param {object} order Order * @returns {Promise} Authorizations * * @example Get identifier authorizations * ```js * const order = { ... }; // Previously created order object * const authorizations = await client.getAuthorizations(order); * * authorizations.forEach((authz) => { * const { challenges } = authz; * }); * ``` */ async getAuthorizations(order) { return Promise.all((order.authorizations || []).map(async (url) => { const resp = await this.api.getAuthorization(url); /* Add URL to response */ resp.data.url = url; return resp.data; })); } /** * Deactivate identifier authorization * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2 * * @param {object} authz Identifier authorization * @returns {Promise} Authorization * * @example Deactivate identifier authorization * ```js * const authz = { ... }; // Identifier authorization resolved from previously created order * const result = await client.deactivateAuthorization(authz); * ``` */ async deactivateAuthorization(authz) { if (!authz.url) { throw new Error('Unable to deactivate identifier authorization, URL not found'); } const data = { status: 'deactivated' }; const resp = await this.api.updateAuthorization(authz.url, data); /* Add URL to response */ resp.data.url = authz.url; return resp.data; } /** * Get key authorization for ACME challenge * * https://datatracker.ietf.org/doc/html/rfc8555#section-8.1 * * @param {object} challenge Challenge object returned by API * @returns {Promise} Key authorization * * @example Get challenge key authorization * ```js * const challenge = { ... }; // Challenge from previously resolved identifier authorization * const key = await client.getChallengeKeyAuthorization(challenge); * * // Write key somewhere to satisfy challenge * ``` */ async getChallengeKeyAuthorization(challenge) { const jwk = this.http.getJwk(); const keysum = createHash('sha256').update(JSON.stringify(jwk)); const thumbprint = keysum.digest('base64url'); const result = `${challenge.token}.${thumbprint}`; /* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 */ if (challenge.type === 'http-01') { return result; } /* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4 */ if (challenge.type === 'dns-01') { return createHash('sha256').update(result).digest('base64url'); } /* https://datatracker.ietf.org/doc/html/rfc8737 */ if (challenge.type === 'tls-alpn-01') { return result; } throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`); } /** * Verify that ACME challenge is satisfied * * @param {object} authz Identifier authorization * @param {object} challenge Authorization challenge * @returns {Promise} * * @example Verify satisfied ACME challenge * ```js * const authz = { ... }; // Identifier authorization * const challenge = { ... }; // Satisfied challenge * await client.verifyChallenge(authz, challenge); * ``` */ async verifyChallenge(authz, challenge) { if (!authz.url || !challenge.url) { throw new Error('Unable to verify ACME challenge, URL not found'); } if (typeof verify[challenge.type] === 'undefined') { throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`); } const keyAuthorization = await this.getChallengeKeyAuthorization(challenge); const verifyFn = async (abort) => { if (this.opts.signal && this.opts.signal.aborted) { abort(); throw new CancelError('用户取消'); } await verify[challenge.type](authz, challenge, keyAuthorization); }; log('Waiting for ACME challenge verification(等待ACME挑战验证)', this.backoffOpts); return util.retry(verifyFn, this.backoffOpts); } /** * Notify CA that challenge has been completed * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1 * * @param {object} challenge Challenge object returned by API * @returns {Promise} Challenge * * @example Notify CA that challenge has been completed * ```js * const challenge = { ... }; // Satisfied challenge * const result = await client.completeChallenge(challenge); * ``` */ async completeChallenge(challenge) { if (this.opts.signal && this.opts.signal.aborted) { throw new CancelError('用户取消'); } const resp = await this.api.completeChallenge(challenge.url, {}); return resp.data; } /** * Wait for ACME provider to verify status on a order, authorization or challenge * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1 * * @param {object} item An order, authorization or challenge object * @returns {Promise} Valid order, authorization or challenge * * @example Wait for valid challenge status * ```js * const challenge = { ... }; * await client.waitForValidStatus(challenge); * ``` * * @example Wait for valid authorization status * ```js * const authz = { ... }; * await client.waitForValidStatus(authz); * ``` * * @example Wait for valid order status * ```js * const order = { ... }; * await client.waitForValidStatus(order); * ``` */ async waitForValidStatus(item,d) { if (!item.url) { throw new Error(`[${d}] Unable to verify status of item, URL not found`); } const verifyFn = async (abort) => { if (this.opts.signal && this.opts.signal.aborted) { abort(); throw new CancelError('用户取消'); } const resp = await this.api.apiRequest(item.url, null, [200]); /* Verify status */ log(`[${d}] Item has status(挑战状态): ${resp.data.status}`); if (invalidStates.includes(resp.data.status)) { abort(); throw new Error(util.formatResponseError(resp)); } else if (pendingStates.includes(resp.data.status)) { throw new Error(`[${d}] Operation is pending or processing(当前仍然在等待状态)`); } else if (validStates.includes(resp.data.status)) { return resp.data; } throw new Error(`[${d}] Unexpected item status: ${resp.data.status}`); }; log(`[${d}] Waiting for valid status (等待valid状态): ${item.url}`, this.backoffOpts); return util.retry(verifyFn, this.backoffOpts); } /** * Get certificate from ACME order * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2 * * @param {object} order Order object * @param {string} [preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` * @returns {Promise} Certificate * * @example Get certificate * ```js * const order = { ... }; // Previously created order * const certificate = await client.getCertificate(order); * ``` * * @example Get certificate with preferred chain * ```js * const order = { ... }; // Previously created order * const certificate = await client.getCertificate(order, 'DST Root CA X3'); * ``` */ async getCertificate(order, preferredChain = null) { if (!validStates.includes(order.status)) { order = await this.waitForValidStatus(order); } if (!order.certificate) { throw new Error('Unable to download certificate, URL not found'); } const resp = await this.api.apiRequest(order.certificate, null, [200]); /* Handle alternate certificate chains */ if (preferredChain && resp.headers.link) { const alternateLinks = util.parseLinkHeader(resp.headers.link); const alternates = await Promise.all(alternateLinks.map(async (link) => this.api.apiRequest(link, null, [200]))); const certificates = [resp].concat(alternates).map((c) => c.data); return util.findCertificateChainForIssuer(certificates, preferredChain); } /* Return default certificate chain */ return resp.data; } /** * Revoke certificate * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.6 * * @param {buffer|string} cert PEM encoded certificate * @param {object} [data] Additional request data * @returns {Promise} * * @example Revoke certificate * ```js * const certificate = { ... }; // Previously created certificate * const result = await client.revokeCertificate(certificate); * ``` * * @example Revoke certificate with reason * ```js * const certificate = { ... }; // Previously created certificate * const result = await client.revokeCertificate(certificate, { * reason: 4, * }); * ``` */ async revokeCertificate(cert, data = {}) { data.certificate = getPemBodyAsB64u(cert); const resp = await this.api.revokeCert(data); return resp.data; } /** * Auto mode * * @param {object} opts * @param {buffer|string} opts.csr Certificate Signing Request * @param {function} opts.challengeCreateFn Function returning Promise triggered before completing ACME challenge * @param {function} opts.challengeRemoveFn Function returning Promise triggered after completing ACME challenge * @param {string} [opts.email] Account email address * @param {boolean} [opts.termsOfServiceAgreed] Agree to Terms of Service, default: `false` * @param {boolean} [opts.skipChallengeVerification] Skip internal challenge verification before notifying ACME provider, default: `false` * @param {string[]} [opts.challengePriority] Array defining challenge type priority, default: `['http-01', 'dns-01']` * @param {string} [opts.preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` * @returns {Promise} Certificate * * @example Order a certificate using auto mode * ```js * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * altNames: ['test.example.com'], * }); * * const certificate = await client.auto({ * csr: certificateRequest, * email: 'test@example.com', * termsOfServiceAgreed: true, * challengeCreateFn: async (authz, challenge, keyAuthorization) => { * // Satisfy challenge here * }, * challengeRemoveFn: async (authz, challenge, keyAuthorization) => { * // Clean up challenge here * }, * }); * ``` * * @example Order a certificate using auto mode with preferred chain * ```js * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * altNames: ['test.example.com'], * }); * * const certificate = await client.auto({ * csr: certificateRequest, * email: 'test@example.com', * termsOfServiceAgreed: true, * preferredChain: 'DST Root CA X3', * challengeCreateFn: async () => {}, * challengeRemoveFn: async () => {}, * }); * ``` */ auto(opts) { return auto(this, opts); } } /* Export client */ export default AcmeClient; ================================================ FILE: packages/core/acme-client/src/crypto/forge.js ================================================ /** * Legacy node-forge crypto interface * * DEPRECATION WARNING: This crypto interface is deprecated and will be removed from acme-client in a future * major release. Please migrate to the new `acme.crypto` interface at your earliest convenience. * * @namespace forge */ import net from 'net'; import { promisify } from 'util'; import forge from 'node-forge'; import { createPrivateEcdsaKey } from './index.js'; const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair); /** * Attempt to parse forge object from PEM encoded string * * @private * @param {string} input PEM string * @return {object} */ function forgeObjectFromPem(input) { const msg = forge.pem.decode(input)[0]; let result; switch (msg.type) { case 'PRIVATE KEY': case 'RSA PRIVATE KEY': result = forge.pki.privateKeyFromPem(input); break; case 'PUBLIC KEY': case 'RSA PUBLIC KEY': result = forge.pki.publicKeyFromPem(input); break; case 'CERTIFICATE': case 'X509 CERTIFICATE': case 'TRUSTED CERTIFICATE': result = forge.pki.certificateFromPem(input).publicKey; break; case 'CERTIFICATE REQUEST': result = forge.pki.certificationRequestFromPem(input).publicKey; break; default: throw new Error('Unable to detect forge message type'); } return result; } /** * Parse domain names from a certificate or CSR * * @private * @param {object} obj Forge certificate or CSR * @returns {object} {commonName, altNames} */ function parseDomains(obj) { let commonName = null; let altNames = []; let altNamesDict = []; const commonNameObject = (obj.subject.attributes || []).find((a) => a.name === 'commonName'); const rootAltNames = (obj.extensions || []).find((e) => 'altNames' in e); const rootExtensions = (obj.attributes || []).find((a) => 'extensions' in a); if (rootAltNames && rootAltNames.altNames && rootAltNames.altNames.length) { altNamesDict = rootAltNames.altNames; } else if (rootExtensions && rootExtensions.extensions && rootExtensions.extensions.length) { const extAltNames = rootExtensions.extensions.find((e) => 'altNames' in e); if (extAltNames && extAltNames.altNames && extAltNames.altNames.length) { altNamesDict = extAltNames.altNames; } } if (commonNameObject) { commonName = commonNameObject.value; } if (altNamesDict) { altNames = altNamesDict.map((a) => a.value); } return { commonName, altNames, }; } /** * Generate a private RSA key * * @param {number} [size] Size of the key, default: `2048` * @returns {Promise} PEM encoded private RSA key * * @example Generate private RSA key * ```js * const privateKey = await acme.forge.createPrivateKey(); * ``` * * @example Private RSA key with defined size * ```js * const privateKey = await acme.forge.createPrivateKey(4096); * ``` */ export async function createPrivateKey(size = 2048) { const keyPair = await generateKeyPair({ bits: size }); const pemKey = forge.pki.privateKeyToPem(keyPair.privateKey); return Buffer.from(pemKey); } /** * Create public key from a private RSA key * * @param {buffer|string} key PEM encoded private RSA key * @returns {Promise} PEM encoded public RSA key * * @example Create public key * ```js * const publicKey = await acme.forge.createPublicKey(privateKey); * ``` */ export const createPublicKey = async (key) => { const privateKey = forge.pki.privateKeyFromPem(key); const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); const pemKey = forge.pki.publicKeyToPem(publicKey); return Buffer.from(pemKey); }; /** * Parse body of PEM encoded object from buffer or string * If multiple objects are chained, the first body will be returned * * @param {buffer|string} str PEM encoded buffer or string * @returns {string} PEM body */ export const getPemBody = (str) => { const msg = forge.pem.decode(str)[0]; return forge.util.encode64(msg.body); }; /** * Split chain of PEM encoded objects from buffer or string into array * * @param {buffer|string} str PEM encoded buffer or string * @returns {string[]} Array of PEM bodies */ export const splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode); /** * Get modulus * * @param {buffer|string} input PEM encoded private key, certificate or CSR * @returns {Promise} Modulus * * @example Get modulus * ```js * const m1 = await acme.forge.getModulus(privateKey); * const m2 = await acme.forge.getModulus(certificate); * const m3 = await acme.forge.getModulus(certificateRequest); * ``` */ export const getModulus = async (input) => { if (!Buffer.isBuffer(input)) { input = Buffer.from(input); } const obj = forgeObjectFromPem(input); return Buffer.from(forge.util.hexToBytes(obj.n.toString(16)), 'binary'); }; /** * Get public exponent * * @param {buffer|string} input PEM encoded private key, certificate or CSR * @returns {Promise} Exponent * * @example Get public exponent * ```js * const e1 = await acme.forge.getPublicExponent(privateKey); * const e2 = await acme.forge.getPublicExponent(certificate); * const e3 = await acme.forge.getPublicExponent(certificateRequest); * ``` */ export const getPublicExponent = async (input) => { if (!Buffer.isBuffer(input)) { input = Buffer.from(input); } const obj = forgeObjectFromPem(input); return Buffer.from(forge.util.hexToBytes(obj.e.toString(16)), 'binary'); }; /** * Read domains from a Certificate Signing Request * * @param {buffer|string} csr PEM encoded Certificate Signing Request * @returns {Promise} {commonName, altNames} * * @example Read Certificate Signing Request domains * ```js * const { commonName, altNames } = await acme.forge.readCsrDomains(certificateRequest); * * console.log(`Common name: ${commonName}`); * console.log(`Alt names: ${altNames.join(', ')}`); * ``` */ export const readCsrDomains = async (csr) => { if (!Buffer.isBuffer(csr)) { csr = Buffer.from(csr); } const obj = forge.pki.certificationRequestFromPem(csr); return parseDomains(obj); }; /** * Read information from a certificate * * @param {buffer|string} cert PEM encoded certificate * @returns {Promise} Certificate info * * @example Read certificate information * ```js * const info = await acme.forge.readCertificateInfo(certificate); * const { commonName, altNames } = info.domains; * * console.log(`Not after: ${info.notAfter}`); * console.log(`Not before: ${info.notBefore}`); * * console.log(`Common name: ${commonName}`); * console.log(`Alt names: ${altNames.join(', ')}`); * ``` */ export const readCertificateInfo = async (cert) => { if (!Buffer.isBuffer(cert)) { cert = Buffer.from(cert); } const obj = forge.pki.certificateFromPem(cert); const issuerCn = (obj.issuer.attributes || []).find((a) => a.name === 'commonName'); return { issuer: { commonName: issuerCn ? issuerCn.value : null, }, domains: parseDomains(obj), notAfter: obj.validity.notAfter, notBefore: obj.validity.notBefore, }; }; /** * Determine ASN.1 type for CSR subject short name * Note: https://datatracker.ietf.org/doc/html/rfc5280 * * @private * @param {string} shortName CSR subject short name * @returns {forge.asn1.Type} ASN.1 type */ function getCsrValueTagClass(shortName) { switch (shortName) { case 'C': return forge.asn1.Type.PRINTABLESTRING; case 'E': return forge.asn1.Type.IA5STRING; default: return forge.asn1.Type.UTF8; } } /** * Create array of short names and values for Certificate Signing Request subjects * * @private * @param {object} subjectObj Key-value of short names and values * @returns {object[]} Certificate Signing Request subject array */ function createCsrSubject(subjectObj) { return Object.entries(subjectObj).reduce((result, [shortName, value]) => { if (value) { const valueTagClass = getCsrValueTagClass(shortName); result.push({ shortName, value, valueTagClass }); } return result; }, []); } /** * Create array of alt names for Certificate Signing Requests * Note: https://github.com/digitalbazaar/forge/blob/dfdde475677a8a25c851e33e8f81dca60d90cfb9/lib/x509.js#L1444-L1454 * * @private * @param {string[]} altNames Alt names * @returns {object[]} Certificate Signing Request alt names array */ function formatCsrAltNames(altNames) { return altNames.map((value) => { const type = net.isIP(value) ? 7 : 2; return { type, value }; }); } /** * Create a Certificate Signing Request * * @param {object} data * @param {number} [data.keySize] Size of newly created private key, default: `2048` * @param {string} [data.commonName] * @param {string[]} [data.altNames] default: `[]` * @param {string} [data.country] * @param {string} [data.state] * @param {string} [data.locality] * @param {string} [data.organization] * @param {string} [data.organizationUnit] * @param {string} [data.emailAddress] * @param {buffer|string} [key] CSR private key * @returns {Promise} [privateKey, certificateSigningRequest] * * @example Create a Certificate Signing Request * ```js * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ * altNames: ['test.example.com'], * }); * ``` * * @example Certificate Signing Request with both common and alternative names * > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf). * ```js * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ * keySize: 4096, * commonName: 'test.example.com', * altNames: ['foo.example.com', 'bar.example.com'], * }); * ``` * * @example Certificate Signing Request with additional information * ```js * const [certificateKey, certificateRequest] = await acme.forge.createCsr({ * altNames: ['test.example.com'], * country: 'US', * state: 'California', * locality: 'Los Angeles', * organization: 'The Company Inc.', * organizationUnit: 'IT Department', * emailAddress: 'contact@example.com', * }); * ``` * * @example Certificate Signing Request with predefined private key * ```js * const certificateKey = await acme.forge.createPrivateKey(); * * const [, certificateRequest] = await acme.forge.createCsr({ * altNames: ['test.example.com'], * }, certificateKey); */ export const createCsr = async (data, keyType = null) => { let key = null; if (keyType === 'ec') { key = await createPrivateEcdsaKey(); } else { key = await createPrivateKey(data.keySize); } // else if (!Buffer.isBuffer(key)) { // key = Buffer.from(key); // } if (typeof data.altNames === 'undefined') { data.altNames = []; } const csr = forge.pki.createCertificationRequest(); /* Public key */ const privateKey = forge.pki.privateKeyFromPem(key); const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); csr.publicKey = publicKey; // const privateKey = key; // csr.publicKey = getPublicKey(key); /* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */ if (data.commonName && !data.altNames.includes(data.commonName)) { data.altNames.unshift(data.commonName); } /* Subject */ const subject = createCsrSubject({ CN: data.commonName, C: data.country, ST: data.state, L: data.locality, O: data.organization, OU: data.organizationUnit, E: data.emailAddress, }); csr.setSubject(subject); /* SAN extension */ if (data.altNames.length) { csr.setAttributes([{ name: 'extensionRequest', extensions: [{ name: 'subjectAltName', altNames: formatCsrAltNames(data.altNames), }], }]); } /* Sign CSR using SHA-256 */ csr.sign(privateKey, forge.md.sha256.create()); /* Done */ const pemCsr = forge.pki.certificationRequestToPem(csr); return [key, Buffer.from(pemCsr)]; }; ================================================ FILE: packages/core/acme-client/src/crypto/index.js ================================================ /** * Native Node.js crypto interface * * @namespace crypto */ import net from 'net'; import { promisify } from 'util'; import crypto from 'crypto'; import asn1js from 'asn1js'; import x509 from '@peculiar/x509'; const randomInt = promisify(crypto.randomInt); const generateKeyPair = promisify(crypto.generateKeyPair); /* Use Node.js Web Crypto API */ x509.cryptoProvider.set(crypto.webcrypto); /* id-ce-subjectAltName - https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */ const subjectAltNameOID = '2.5.29.17'; /* id-pe-acmeIdentifier - https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */ const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31'; /** * Determine key type and info by attempting to derive public key * * @private * @param {buffer|string} keyPem PEM encoded private or public key * @returns {object} */ function getKeyInfo(keyPem) { const result = { isRSA: false, isECDSA: false, publicKey: crypto.createPublicKey(keyPem), }; if (result.publicKey.asymmetricKeyType === 'rsa') { result.isRSA = true; } else if (result.publicKey.asymmetricKeyType === 'ec') { result.isECDSA = true; } else { throw new Error('Unable to parse key information, unknown format'); } return result; } /** * Generate a private RSA key * * @param {number} [modulusLength] Size of the keys modulus in bits, default: `2048` * @returns {Promise} PEM encoded private RSA key * * @example Generate private RSA key * ```js * const privateKey = await acme.crypto.createPrivateRsaKey(); * ``` * * @example Private RSA key with modulus size 4096 * ```js * const privateKey = await acme.crypto.createPrivateRsaKey(4096); * ``` */ export async function createPrivateRsaKey(modulusLength = 2048, encodingType = 'pkcs8') { const pair = await generateKeyPair('rsa', { modulusLength, privateKeyEncoding: { type: encodingType, format: 'pem', }, }); return Buffer.from(pair.privateKey); } /** * Alias of `createPrivateRsaKey()` * * @function */ export const createPrivateKey = createPrivateRsaKey; /** * Generate a private ECDSA key * * @param {string} [namedCurve] ECDSA curve name (P-256, P-384 or P-521), default `P-256` * @returns {Promise} PEM encoded private ECDSA key * * @example Generate private ECDSA key * ```js * const privateKey = await acme.crypto.createPrivateEcdsaKey(); * ``` * * @example Private ECDSA key using P-384 curve * ```js * const privateKey = await acme.crypto.createPrivateEcdsaKey('P-384'); * ``` */ export const createPrivateEcdsaKey = async (namedCurve = 'P-256', encodingType = 'pkcs8') => { const pair = await generateKeyPair('ec', { namedCurve, privateKeyEncoding: { type: encodingType, format: 'pem', }, }); return Buffer.from(pair.privateKey); }; /** * Get a public key derived from a RSA or ECDSA key * * @param {buffer|string} keyPem PEM encoded private or public key * @returns {buffer} PEM encoded public key * * @example Get public key * ```js * const publicKey = acme.crypto.getPublicKey(privateKey); * ``` */ export const getPublicKey = (keyPem) => { const info = getKeyInfo(keyPem); const publicKey = info.publicKey.export({ type: info.isECDSA ? 'spki' : 'pkcs1', format: 'pem', }); return Buffer.from(publicKey); }; /** * Get a JSON Web Key derived from a RSA or ECDSA key * * https://datatracker.ietf.org/doc/html/rfc7517 * * @param {buffer|string} keyPem PEM encoded private or public key * @returns {object} JSON Web Key * * @example Get JWK * ```js * const jwk = acme.crypto.getJwk(privateKey); * ``` */ export function getJwk(keyPem) { const jwk = crypto.createPublicKey(keyPem).export({ format: 'jwk', }); /* Sort keys */ return Object.keys(jwk).sort().reduce((result, k) => { result[k] = jwk[k]; return result; }, {}); } /** * Produce CryptoKeyPair and signing algorithm from a PEM encoded private key * * @private * @param {buffer|string} keyPem PEM encoded private key * @returns {Promise} [keyPair, signingAlgorithm] */ async function getWebCryptoKeyPair(keyPem) { const info = getKeyInfo(keyPem); const jwk = getJwk(keyPem); /* Signing algorithm */ const sigalg = { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' }, }; if (info.isECDSA) { sigalg.name = 'ECDSA'; sigalg.namedCurve = jwk.crv; if (jwk.crv === 'P-384') { sigalg.hash.name = 'SHA-384'; } if (jwk.crv === 'P-521') { sigalg.hash.name = 'SHA-512'; } } /* Decode PEM and import into CryptoKeyPair */ const privateKeyDec = x509.PemConverter.decodeFirst(keyPem.toString()); const privateKey = await crypto.webcrypto.subtle.importKey('pkcs8', privateKeyDec, sigalg, true, ['sign']); const publicKey = await crypto.webcrypto.subtle.importKey('jwk', jwk, sigalg, true, ['verify']); return [{ privateKey, publicKey }, sigalg]; } /** * Split chain of PEM encoded objects from string into array * * @param {buffer|string} chainPem PEM encoded object chain * @returns {string[]} Array of PEM objects including headers */ export function splitPemChain(chainPem) { if (Buffer.isBuffer(chainPem)) { chainPem = chainPem.toString(); } /* Decode into array and re-encode */ return x509.PemConverter.decodeWithHeaders(chainPem) .map((params) => x509.PemConverter.encode([params])); } /** * Parse body of PEM encoded object and return a Base64URL string * If multiple objects are chained, the first body will be returned * * @param {buffer|string} pem PEM encoded chain or object * @returns {string} Base64URL-encoded body */ export const getPemBodyAsB64u = (pem) => { const chain = splitPemChain(pem); if (!chain.length) { throw new Error('Unable to parse PEM body from string'); } /* Select first object, extract body and convert to b64u */ const dec = x509.PemConverter.decodeFirst(chain[0]); return Buffer.from(dec).toString('base64url'); }; /** * Parse domains from a certificate or CSR * * @private * @param {object} input x509.Certificate or x509.Pkcs10CertificateRequest * @returns {object} {commonName, altNames} */ function parseDomains(input) { const commonName = input.subjectName.getField('CN').pop() || null; const altNamesRaw = input.getExtension(subjectAltNameOID); let altNames = []; if (altNamesRaw) { const altNamesExt = new x509.SubjectAlternativeNameExtension(altNamesRaw.rawData); altNames = altNames.concat(altNamesExt.names.items.map((i) => i.value)); } return { commonName, altNames, }; } /** * Read domains from a Certificate Signing Request * * @param {buffer|string} csrPem PEM encoded Certificate Signing Request * @returns {object} {commonName, altNames} * * @example Read Certificate Signing Request domains * ```js * const { commonName, altNames } = acme.crypto.readCsrDomains(certificateRequest); * * console.log(`Common name: ${commonName}`); * console.log(`Alt names: ${altNames.join(', ')}`); * ``` */ export const readCsrDomains = (csrPem) => { if (Buffer.isBuffer(csrPem)) { csrPem = csrPem.toString(); } const dec = x509.PemConverter.decodeFirst(csrPem); const csr = new x509.Pkcs10CertificateRequest(dec); return parseDomains(csr); }; /** * Read information from a certificate * If multiple certificates are chained, the first will be read * * @param {buffer|string} certPem PEM encoded certificate or chain * @returns {object} Certificate info * * @example Read certificate information * ```js * const info = acme.crypto.readCertificateInfo(certificate); * const { commonName, altNames } = info.domains; * * console.log(`Not after: ${info.notAfter}`); * console.log(`Not before: ${info.notBefore}`); * * console.log(`Common name: ${commonName}`); * console.log(`Alt names: ${altNames.join(', ')}`); * ``` */ export const readCertificateInfo = (certPem) => { if (Buffer.isBuffer(certPem)) { certPem = certPem.toString(); } const dec = x509.PemConverter.decodeFirst(certPem); const cert = new x509.X509Certificate(dec); return { issuer: { commonName: cert.issuerName.getField('CN').pop() || null, }, domains: parseDomains(cert), notBefore: cert.notBefore, notAfter: cert.notAfter, }; }; /** * Determine ASN.1 character string type for CSR subject field name * * https://datatracker.ietf.org/doc/html/rfc5280 * https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71 * * @private * @param {string} field CSR subject field name * @returns {string} ASN.1 character string type */ function getCsrAsn1CharStringType(field) { switch (field) { case 'C': return 'printableString'; case 'E': return 'ia5String'; default: return 'utf8String'; } } /** * Create array of subject fields for a Certificate Signing Request * * https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71 * * @private * @param {object} input Key-value of subject fields * @returns {object[]} Certificate Signing Request subject array */ function createCsrSubject(input) { return Object.entries(input).reduce((result, [type, value]) => { if (value) { const ds = getCsrAsn1CharStringType(type); result.push({ [type]: [{ [ds]: value }] }); } return result; }, []); } /** * Create x509 subject alternate name extension * * https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/extensions/subject_alt_name.ts * * @private * @param {string[]} altNames Array of alt names * @returns {x509.SubjectAlternativeNameExtension} Subject alternate name extension */ function createSubjectAltNameExtension(altNames) { return new x509.SubjectAlternativeNameExtension(altNames.map((value) => { const type = net.isIP(value) ? 'ip' : 'dns'; return { type, value }; })); } /** * Create a Certificate Signing Request * * @param {object} data * @param {number} [data.keySize] Size of newly created RSA private key modulus in bits, default: `2048` * @param {string} [data.commonName] FQDN of your server * @param {string[]} [data.altNames] SAN (Subject Alternative Names), default: `[]` * @param {string} [data.country] 2 letter country code * @param {string} [data.state] State or province * @param {string} [data.locality] City * @param {string} [data.organization] Organization name * @param {string} [data.organizationUnit] Organizational unit name * @param {string} [data.emailAddress] Email address * @param {buffer|string} [keyPem] PEM encoded CSR private key * @returns {Promise} [privateKey, certificateSigningRequest] * * @example Create a Certificate Signing Request * ```js * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * altNames: ['test.example.com'], * }); * ``` * * @example Certificate Signing Request with both common and alternative names * > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf). * ```js * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * keySize: 4096, * commonName: 'test.example.com', * altNames: ['foo.example.com', 'bar.example.com'], * }); * ``` * * @example Certificate Signing Request with additional information * ```js * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * altNames: ['test.example.com'], * country: 'US', * state: 'California', * locality: 'Los Angeles', * organization: 'The Company Inc.', * organizationUnit: 'IT Department', * emailAddress: 'contact@example.com', * }); * ``` * * @example Certificate Signing Request with ECDSA private key * ```js * const certificateKey = await acme.crypto.createPrivateEcdsaKey(); * * const [, certificateRequest] = await acme.crypto.createCsr({ * altNames: ['test.example.com'], * }, certificateKey); * ``` */ export const createCsr = async (data, keyPem = null) => { if (!keyPem) { keyPem = await createPrivateRsaKey(data.keySize); } else if (!Buffer.isBuffer(keyPem)) { keyPem = Buffer.from(keyPem); } if (typeof data.altNames === 'undefined') { data.altNames = []; } /* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */ if (data.commonName && !data.altNames.includes(data.commonName)) { data.altNames.unshift(data.commonName); } /* CryptoKeyPair and signing algorithm from private key */ const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem); const extensions = [ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */ new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */ createSubjectAltNameExtension(data.altNames), ]; /* Create CSR */ const csr = await x509.Pkcs10CertificateRequestGenerator.create({ keys, extensions, signingAlgorithm, name: createCsrSubject({ CN: data.commonName, C: data.country, ST: data.state, L: data.locality, O: data.organization, OU: data.organizationUnit, E: data.emailAddress, }), }); /* Done */ const pem = csr.toString('pem'); return [keyPem, Buffer.from(pem)]; }; /** * Create a self-signed ALPN certificate for TLS-ALPN-01 challenges * * https://datatracker.ietf.org/doc/html/rfc8737 * * @param {object} authz Identifier authorization * @param {string} keyAuthorization Challenge key authorization * @param {buffer|string} [keyPem] PEM encoded CSR private key * @returns {Promise} [privateKey, certificate] * * @example Create a ALPN certificate * ```js * const [alpnKey, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization); * ``` * * @example Create a ALPN certificate with ECDSA private key * ```js * const alpnKey = await acme.crypto.createPrivateEcdsaKey(); * const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey); * ``` */ export const createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => { if (!keyPem) { keyPem = await createPrivateRsaKey(); } else if (!Buffer.isBuffer(keyPem)) { keyPem = Buffer.from(keyPem); } const now = new Date(); const commonName = authz.identifier.value; /* Pseudo-random serial - max 20 bytes, 11 for epoch (year 5138), 9 random */ const random = await randomInt(1, 999999999); const serialNumber = `${Math.floor(now.getTime() / 1000)}${random}`; /* CryptoKeyPair and signing algorithm from private key */ const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem); const extensions = [ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */ new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), // eslint-disable-line no-bitwise /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 */ new x509.BasicConstraintsExtension(true, 2, true), /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2 */ await x509.SubjectKeyIdentifierExtension.create(keys.publicKey), /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */ createSubjectAltNameExtension([commonName]), ]; /* ALPN extension */ const payload = crypto.createHash('sha256').update(keyAuthorization).digest('hex'); const octstr = new asn1js.OctetString({ valueHex: Buffer.from(payload, 'hex') }); extensions.push(new x509.Extension(alpnAcmeIdentifierOID, true, octstr.toBER())); /* Self-signed ALPN certificate */ const cert = await x509.X509CertificateGenerator.createSelfSigned({ keys, signingAlgorithm, extensions, serialNumber, notBefore: now, notAfter: now, name: createCsrSubject({ CN: commonName, }), }); /* Done */ const pem = cert.toString('pem'); return [keyPem, Buffer.from(pem)]; }; /** * Validate that a ALPN certificate contains the expected key authorization * * @param {buffer|string} certPem PEM encoded certificate * @param {string} keyAuthorization Expected challenge key authorization * @returns {boolean} True when valid */ export const isAlpnCertificateAuthorizationValid = (certPem, keyAuthorization) => { const expected = crypto.createHash('sha256').update(keyAuthorization).digest('hex'); /* Attempt to locate ALPN extension */ const cert = new x509.X509Certificate(certPem); const ext = cert.getExtension(alpnAcmeIdentifierOID); if (!ext) { throw new Error('Unable to locate ALPN extension within parsed certificate'); } /* Decode extension value */ const parsed = asn1js.fromBER(ext.value); const result = Buffer.from(parsed.result.valueBlock.valueHexView).toString('hex'); /* Return true if match */ return (result === expected); }; ================================================ FILE: packages/core/acme-client/src/error.js ================================================ export class CancelError extends Error { constructor(message) { super(message); this.name = 'CancelError'; } } ================================================ FILE: packages/core/acme-client/src/http.js ================================================ /** * ACME HTTP client */ import { createHmac, createSign, constants } from 'crypto'; const { RSA_PKCS1_PADDING } = constants; import axios from './axios.js'; import { log } from './logger.js'; import { getJwk } from './crypto/index.js'; /** * ACME HTTP client * * @class * @param {string} directoryUrl ACME directory URL * @param {buffer} accountKey PEM encoded account private key * @param {object} [opts.externalAccountBinding] * @param {string} [opts.externalAccountBinding.kid] External account binding KID * @param {string} [opts.externalAccountBinding.hmacKey] External account binding HMAC key */ class HttpClient { constructor(directoryUrl, accountKey, externalAccountBinding = {}, urlMapping = {}) { this.directoryUrl = directoryUrl; this.accountKey = accountKey; this.externalAccountBinding = externalAccountBinding; this.maxBadNonceRetries = 5; this.jwk = null; this.directoryCache = null; this.directoryMaxAge = 86400; this.directoryTimestamp = 0; this.urlMapping = urlMapping; } /** * HTTP request * * @param {string} url HTTP URL * @param {string} method HTTP method * @param {object} [opts] Request options * @returns {Promise} HTTP response */ async request(url, method, opts = {}) { if (this.urlMapping && this.urlMapping.enabled && this.urlMapping.mappings) { // eslint-disable-next-line no-restricted-syntax for (const key in this.urlMapping.mappings) { if (url.includes(key)) { const newUrl = url.replace(key, this.urlMapping.mappings[key]); log(`use reverse proxy: ${newUrl}`); url = newUrl; } } } opts.url = url; opts.method = method; opts.validateStatus = null; /* Headers */ if (typeof opts.headers === 'undefined') { opts.headers = {}; } opts.headers['Content-Type'] = 'application/jose+json'; /* Request */ log(`HTTP request: ${method} ${url}`); const resp = await axios.request(opts); log(`RESP ${resp.status} ${method} ${url}`); return resp; } /** * Get ACME provider directory * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1 * * @returns {Promise} ACME directory contents */ async getDirectory() { const now = Math.floor(Date.now() / 1000); const age = (now - this.directoryTimestamp); if (!this.directoryCache || (age > this.directoryMaxAge)) { log(`Refreshing ACME directory, age: ${age}`); const resp = await this.request(this.directoryUrl, 'get'); if (resp.status >= 400) { throw new Error(`Attempting to read ACME directory returned error ${resp.status}: ${this.directoryUrl}`); } if (!resp.data) { throw new Error('Attempting to read ACME directory returned no data'); } this.directoryCache = resp.data; this.directoryTimestamp = now; } return this.directoryCache; } /** * Get JSON Web Key * * @returns {object} JSON Web Key */ getJwk() { if (!this.jwk) { this.jwk = getJwk(this.accountKey); } return this.jwk; } /** * Get nonce from directory API endpoint * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.2 * * @returns {Promise} Nonce */ async getNonce() { const url = await this.getResourceUrl('newNonce'); const resp = await this.request(url, 'head'); if (!resp.headers['replay-nonce']) { throw new Error('Failed to get nonce from ACME provider'); } return resp.headers['replay-nonce']; } /** * Get URL for a directory resource * * @param {string} resource API resource name * @returns {Promise} URL */ async getResourceUrl(resource) { const dir = await this.getDirectory(); if (!dir[resource]) { throw new Error(`Unable to locate API resource URL in ACME directory: "${resource}"`); } return dir[resource]; } /** * Get directory meta field * * @param {string} field Meta field name * @returns {Promise} Meta field value */ async getMetaField(field) { const dir = await this.getDirectory(); if (('meta' in dir) && (field in dir.meta)) { return dir.meta[field]; } return null; } /** * Prepare HTTP request body for signature * * @param {string} alg JWS algorithm * @param {string} url Request URL * @param {object} [payload] Request payload * @param {object} [opts] * @param {string} [opts.nonce] JWS anti-replay nonce * @param {string} [opts.kid] JWS KID * @returns {object} Signed HTTP request body */ prepareSignedBody(alg, url, payload = null, { nonce = null, kid = null } = {}) { const header = { alg, url }; /* Nonce */ if (nonce) { log(`Using nonce: ${nonce}`); header.nonce = nonce; } /* KID or JWK */ if (kid) { header.kid = kid; } else { header.jwk = this.getJwk(); } /* Body */ return { payload: payload ? Buffer.from(JSON.stringify(payload)).toString('base64url') : '', protected: Buffer.from(JSON.stringify(header)).toString('base64url'), }; } /** * Create JWS HTTP request body using HMAC * * @param {string} hmacKey HMAC key * @param {string} url Request URL * @param {object} [payload] Request payload * @param {object} [opts] * @param {string} [opts.nonce] JWS anti-replay nonce * @param {string} [opts.kid] JWS KID * @returns {object} Signed HMAC request body */ createSignedHmacBody(hmacKey, url, payload = null, { nonce = null, kid = null } = {}) { const result = this.prepareSignedBody('HS256', url, payload, { nonce, kid }); /* Signature */ const signer = createHmac('SHA256', Buffer.from(hmacKey, 'base64')).update(`${result.protected}.${result.payload}`, 'utf8'); result.signature = signer.digest().toString('base64url'); return result; } /** * Create JWS HTTP request body using RSA or ECC * * https://datatracker.ietf.org/doc/html/rfc7515 * * @param {string} url Request URL * @param {object} [payload] Request payload * @param {object} [opts] * @param {string} [opts.nonce] JWS nonce * @param {string} [opts.kid] JWS KID * @returns {object} JWS request body */ createSignedBody(url, payload = null, { nonce = null, kid = null } = {}) { const jwk = this.getJwk(); let headerAlg = 'RS256'; let signerAlg = 'SHA256'; /* https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 */ if (jwk.crv && (jwk.kty === 'EC')) { headerAlg = 'ES256'; if (jwk.crv === 'P-384') { headerAlg = 'ES384'; signerAlg = 'SHA384'; } else if (jwk.crv === 'P-521') { headerAlg = 'ES512'; signerAlg = 'SHA512'; } } /* Prepare body and signer */ const result = this.prepareSignedBody(headerAlg, url, payload, { nonce, kid }); const signer = createSign(signerAlg).update(`${result.protected}.${result.payload}`, 'utf8'); /* Signature - https://stackoverflow.com/questions/39554165 */ result.signature = signer.sign({ key: this.accountKey, padding: RSA_PKCS1_PADDING, dsaEncoding: 'ieee-p1363', }, 'base64url'); return result; } /** * Signed HTTP request * * https://datatracker.ietf.org/doc/html/rfc8555#section-6.2 * * @param {string} url Request URL * @param {object} payload Request payload * @param {object} [opts] * @param {string} [opts.kid] JWS KID * @param {string} [opts.nonce] JWS anti-replay nonce * @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request * @param {number} [attempts] Request attempt counter * @returns {Promise} HTTP response */ async signedRequest(url, payload, { kid = null, nonce = null, includeExternalAccountBinding = false } = {}, attempts = 0) { if (!nonce) { nonce = await this.getNonce(); } /* External account binding */ if (includeExternalAccountBinding && this.externalAccountBinding) { if (this.externalAccountBinding.kid && this.externalAccountBinding.hmacKey) { const jwk = this.getJwk(); const eabKid = this.externalAccountBinding.kid; const eabHmacKey = this.externalAccountBinding.hmacKey; payload.externalAccountBinding = this.createSignedHmacBody(eabHmacKey, url, jwk, { kid: eabKid }); } } /* Sign body and send request */ const data = this.createSignedBody(url, payload, { nonce, kid }); const resp = await this.request(url, 'post', { data }); /* Retry on bad nonce - https://datatracker.ietf.org/doc/html/rfc8555#section-6.5 */ if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) { nonce = resp.headers['replay-nonce'] || null; attempts += 1; log(`Caught invalid nonce error, retrying (${attempts}/${this.maxBadNonceRetries}) signed request to: ${url}`); return this.signedRequest(url, payload, { kid, nonce, includeExternalAccountBinding }, attempts); } /* Return response */ return resp; } } /* Export client */ export default HttpClient; ================================================ FILE: packages/core/acme-client/src/index.js ================================================ /** * acme-client */ import AcmeClinet from './client.js' export const Client = AcmeClinet /** * Directory URLs */ export const directory = { buypass: { staging: 'https://api.test4.buypass.no/acme/directory', production: 'https://api.buypass.com/acme/directory', }, google: { staging: 'https://dv.acme-v02.test-api.pki.goog/directory', production: 'https://dv.acme-v02.api.pki.goog/directory', }, letsencrypt: { staging: 'https://acme-staging-v02.api.letsencrypt.org/directory', production: 'https://acme-v02.api.letsencrypt.org/directory', }, zerossl: { staging: 'https://acme.zerossl.com/v2/DV90', production: 'https://acme.zerossl.com/v2/DV90', }, }; /** * Crypto */ export * as crypto from './crypto/index.js' export * as forge from './crypto/forge.js' /** * Axios */ export * from './axios.js' /** * Logger */ export * from './logger.js' export * from './verify.js' export * from './error.js' export * from './util.js' ================================================ FILE: packages/core/acme-client/src/logger.js ================================================ /** * ACME logger */ import debugg from 'debug' const debug = debugg('acme-client'); let logger = () => {}; /** * Set logger function * * @param {function} fn Logger function */ export const setLogger = (fn) => { logger = fn; }; /** * Log message * * @param {string} msg Message */ export const log = (...msg) => { debug(...msg); logger(...msg); }; ================================================ FILE: packages/core/acme-client/src/util.js ================================================ /** * Utility methods */ import tls from 'tls'; import dnsSdk from 'dns'; import { readCertificateInfo, splitPemChain }from './crypto/index.js' import { log } from './logger.js' const dns = dnsSdk.promises; /** * Exponential backoff * * https://github.com/mokesmokes/backo * * @class * @param {object} [opts] * @param {number} [opts.min] Minimum backoff duration in ms * @param {number} [opts.max] Maximum backoff duration in ms */ class Backoff { constructor({ min = 100, max = 10000 } = {}) { this.min = min; this.max = max; this.attempts = 0; } /** * Get backoff duration * * @returns {number} Backoff duration in ms */ duration() { const ms = this.min * (2 ** this.attempts); this.attempts += 1; return Math.min(ms, this.max); } } /** * Retry promise * * @param {function} fn Function returning promise that should be retried * @param {number} attempts Maximum number of attempts * @param {Backoff} backoff Backoff instance * @returns {Promise} */ async function retryPromise(fn, attempts, backoff) { let aborted = false; try { const data = await fn(() => { aborted = true; }); return data; } catch (e) { if (aborted || ((backoff.attempts + 1) >= attempts)) { throw e; } log(`Promise rejected: ${e.message}`); const duration = backoff.duration(); log(`Promise rejected attempt #${backoff.attempts}, ${duration}ms 后重试: ${e.message}`); await new Promise((resolve) => { setTimeout(resolve, duration); }); return retryPromise(fn, attempts, backoff); } } /** * Retry promise * * @param {function} fn Function returning promise that should be retried * @param {object} [backoffOpts] Backoff options * @param {number} [backoffOpts.attempts] Maximum number of attempts, default: `5` * @param {number} [backoffOpts.min] Minimum attempt delay in milliseconds, default: `5000` * @param {number} [backoffOpts.max] Maximum attempt delay in milliseconds, default: `30000` * @returns {Promise} */ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) { const backoff = new Backoff({ min, max }); return retryPromise(fn, attempts, backoff); } /** * Parse URLs from Link header * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2 * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link * * @param {string} header Header contents * @param {string} rel Link relation, default: `alternate` * @returns {string[]} Array of URLs */ function parseLinkHeader(header, rel = 'alternate') { const relRe = new RegExp(`\\s*rel\\s*=\\s*"?${rel}"?`, 'i'); const results = (header || '').split(/,\s* { const [, linkUrl, linkParts] = link.match(/]*)>;(.*)/) || []; return (linkUrl && linkParts && linkParts.match(relRe)) ? linkUrl : null; }); return results.filter((r) => r); } /** * Parse date or duration from Retry-After header * * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After * * @param {string} header Header contents * @returns {number} Retry duration in seconds */ function parseRetryAfterHeader(header) { const sec = parseInt(header, 10); const date = new Date(header); /* Seconds into the future */ if (Number.isSafeInteger(sec) && (sec > 0)) { return sec; } /* Future date string */ if (date instanceof Date && !Number.isNaN(date)) { const now = new Date(); const diff = Math.ceil((date.getTime() - now.getTime()) / 1000); if (diff > 0) { return diff; } } return 0; } /** * Find certificate chain with preferred issuer common name * - If issuer is found in multiple chains, the closest to root wins * - If issuer can not be located, the first chain will be returned * * @param {string[]} certificates Array of PEM encoded certificate chains * @param {string} issuer Preferred certificate issuer * @returns {string} PEM encoded certificate chain */ function findCertificateChainForIssuer(chains, issuer) { log(`Attempting to find match for issuer="${issuer}" in ${chains.length} certificate chains`); let bestMatch = null; let bestDistance = null; chains.forEach((chain) => { /* Look up all issuers */ const certs = splitPemChain(chain); const infoCollection = certs.map((c) => readCertificateInfo(c)); const issuerCollection = infoCollection.map((i) => i.issuer.commonName); /* Found issuer match, get distance from root - lower is better */ if (issuerCollection.includes(issuer)) { const distance = (issuerCollection.length - issuerCollection.indexOf(issuer)); log(`Found matching chain for preferred issuer="${issuer}" distance=${distance} issuers=${JSON.stringify(issuerCollection)}`); /* Chain wins, use it */ if (!bestDistance || (distance < bestDistance)) { log(`Issuer is closer to root than previous match, using it (${distance} < ${bestDistance || 'undefined'})`); bestMatch = chain; bestDistance = distance; } } else { /* No match */ log(`Unable to match certificate for preferred issuer="${issuer}", issuers=${JSON.stringify(issuerCollection)}`); } }); /* Return found match */ if (bestMatch) { return bestMatch; } /* No chains matched, return default */ log(`Found no match in ${chains.length} certificate chains for preferred issuer="${issuer}", returning default certificate chain`); return chains[0]; } /** * Find and format error in response object * * @param {object} resp HTTP response * @returns {string} Error message */ function formatResponseError(resp) { let result; if (resp.data) { if (resp.data.error) { result = resp.data.error.detail || resp.data.error; } else { result = resp.data.detail || JSON.stringify(resp.data); } } return (result || '').replace(/\n/g, ''); } /** * Resolve root domain name by looking for SOA record * * @param {string} recordName DNS record name * @returns {Promise} Root domain name */ async function resolveDomainBySoaRecord(recordName) { try { await dns.resolveSoa(recordName); log(`找到${recordName}的SOA记录`); return recordName; } catch (e) { log(`找不到${recordName}的SOA记录,继续往主域名查找`); const parentRecordName = recordName.split('.').slice(1).join('.'); if (!parentRecordName.includes('.')) { throw new Error('SOA record查找失败'); } return resolveDomainBySoaRecord(parentRecordName); } } /** * Get DNS resolver using domains authoritative NS records * * @param {string} recordName DNS record name * @returns {Promise} DNS resolver */ async function getAuthoritativeDnsResolver(recordName) { log(`获取域名${recordName}的权威NS服务器: `); const resolver = new dns.Resolver(); try { /* Resolve root domain by SOA */ const domain = await resolveDomainBySoaRecord(recordName); /* Resolve authoritative NS addresses */ log(`获取到权威NS服务器name: ${domain}`); const nsRecords = await dns.resolveNs(domain); log(`域名权威NS服务器:${nsRecords}`); const nsAddrArray = await Promise.all(nsRecords.map(async (r) => dns.resolve4(r))); const nsAddresses = [].concat(...nsAddrArray).filter((a) => a); if (!nsAddresses.length) { throw new Error(`Unable to locate any valid authoritative NS addresses for domain(获取权威服务器IP失败): ${domain}`); } /* Authoritative NS success */ log(`Found ${nsAddresses.length} authoritative NS addresses for domain: ${domain}`); resolver.setServers(nsAddresses); } catch (e) { log(`Authoritative NS lookup error(获取权威NS服务器地址失败): ${e.message}`); } /* Return resolver */ const addresses = resolver.getServers(); log(`DNS resolver addresses(域名的权威NS服务器地址): ${addresses.join(', ')}`); return resolver; } /** * Attempt to retrieve TLS ALPN certificate from peer * * https://nodejs.org/api/tls.html#tlsconnectoptions-callback * * @param {string} host Host the TLS client should connect to * @param {number} port Port the client should connect to * @param {string} servername Server name for the SNI (Server Name Indication) * @returns {Promise} PEM encoded certificate */ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) { return new Promise((resolve, reject) => { let result; /* TLS connection */ const socket = tls.connect({ host, port, servername: host, rejectUnauthorized: false, ALPNProtocols: ['acme-tls/1'], }); socket.setTimeout(timeout); socket.setEncoding('utf-8'); /* Grab certificate once connected and close */ socket.on('secureConnect', () => { result = socket.getPeerX509Certificate(); socket.end(); }); /* Errors */ socket.on('error', (err) => { reject(err); }); socket.on('timeout', () => { socket.destroy(new Error('TLS ALPN certificate lookup request timed out')); }); /* Done, return cert as PEM if found */ socket.on('end', () => { if (result) { return resolve(result.toString()); } return reject(new Error('TLS ALPN lookup failed to retrieve certificate')); }); }); } /** * Export utils */ export { retry, parseLinkHeader, parseRetryAfterHeader, findCertificateChainForIssuer, formatResponseError, getAuthoritativeDnsResolver, retrieveTlsAlpnCertificate, resolveDomainBySoaRecord }; ================================================ FILE: packages/core/acme-client/src/verify.js ================================================ /** * ACME challenge verification */ import dnsSdk from "dns" import https from 'https' import {log} from './logger.js' import axios from './axios.js' import * as util from './util.js' import {isAlpnCertificateAuthorizationValid} from './crypto/index.js' const dns = dnsSdk.promises /** * Verify ACME HTTP challenge * * https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 * * @param {object} authz Identifier authorization * @param {object} challenge Authorization challenge * @param {string} keyAuthorization Challenge key authorization * @param {string} [suffix] URL suffix * @returns {Promise} */ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) { async function doQuery(challengeUrl){ log(`正在测试请求 ${challengeUrl} `) // const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443; // const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`; /* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */ const httpsAgent = new https.Agent({ rejectUnauthorized: false }); log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`); let data = "" try{ const resp = await axios.get(challengeUrl, { httpsAgent }); data = (resp.data || '').replace(/\s+$/, ''); }catch (e) { log(`[error] HTTP request error from ${authz.identifier.value}`,e.message); return false } if (!data || (data !== keyAuthorization)) { log(`[error] Authorization not found in HTTP response from ${authz.identifier.value}`); return false } return true } const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80; const challengeUrl = `http://${authz.identifier.value}:${httpPort}${suffix}`; if (!await doQuery(challengeUrl)) { const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443; const httpsChallengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`; const res = await doQuery(httpsChallengeUrl) if (!res) { throw new Error(`[error] 验证失败,请检查以上测试url是否可以正常访问`); } } log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`); return true; } /** * Walk DNS until TXT records are found */ async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) { let records = []; /* Resolve TXT records */ try { log(`检查域名 ${recordName} 的TXT记录`); const txtRecords = await resolver.resolveTxt(recordName); if (txtRecords && txtRecords.length) { log(`找到 ${txtRecords.length} 条 TXT记录( ${recordName})`); log(`TXT records: ${JSON.stringify(txtRecords)}`); records = records.concat(...txtRecords); } } catch (e) { log(`解析 TXT 记录出错, ${recordName} :${e.message}`); } /* Resolve CNAME record first */ try { log(`检查是否存在CNAME映射: ${recordName}`); const cnameRecords = await resolver.resolveCname(recordName); if (cnameRecords.length) { const cnameRecord = cnameRecords[0]; log(`已找到${recordName}的CNAME记录,将检查: ${cnameRecord}`); let res= await walkTxtRecord(cnameRecord,deep+1); if (res && res.length) { log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`); records = records.concat(...res); } }else{ log(`没有CNAME映射(${recordName})`); } } catch (e) { log(`检查CNAME出错(${recordName}) :${e.message}`); } return records } export async function walkTxtRecord(recordName,deep = 0) { if(deep >5){ log(`walkTxtRecord too deep (#${deep}) , skip walk`) return [] } const txtRecords = [] try { /* Default DNS resolver first */ log('从本地DNS服务器获取TXT解析记录'); const res = await walkDnsChallengeRecord(recordName,dns,deep); if (res && res.length > 0) { for (const item of res) { txtRecords.push(item) } } } catch (e) { log(`本地获取TXT解析记录失败:${e.message}`) } try{ /* Authoritative DNS resolver */ log(`从域名权威服务器获取TXT解析记录`); const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName); const res = await walkDnsChallengeRecord(recordName, authoritativeResolver,deep); if (res && res.length > 0) { for (const item of res) { txtRecords.push(item) } } }catch (e) { log(`权威服务器获取TXT解析记录失败:${e.message}`) } if (txtRecords.length === 0) { throw new Error(`没有找到TXT解析记录(${recordName})`); } return txtRecords; } /** * Verify ACME DNS challenge * * https://datatracker.ietf.org/doc/html/rfc8555#section-8.4 * * @param {object} authz Identifier authorization * @param {object} challenge Authorization challenge * @param {string} keyAuthorization Challenge key authorization * @param {string} [prefix] DNS prefix * @returns {Promise} */ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') { const recordName = `${prefix}${authz.identifier.value}`; log(`本地校验TXT记录): ${recordName}`); let recordValues = await walkTxtRecord(recordName); //去重 recordValues = [...new Set(recordValues)]; log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`); if (!recordValues.length || !recordValues.includes(keyAuthorization)) { throw new Error(`没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`); } log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`); return true; } /** * Verify ACME TLS ALPN challenge * * https://datatracker.ietf.org/doc/html/rfc8737 * * @param {object} authz Identifier authorization * @param {object} challenge Authorization challenge * @param {string} keyAuthorization Challenge key authorization * @returns {Promise} */ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) { const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443; const host = authz.identifier.value; log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`); const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort); log('Certificate received from server successfully, matching key authorization in ALPN'); if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) { throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`); } log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`); return true; } /** * Export API */ export default { 'http-01': verifyHttpChallenge, 'dns-01': verifyDnsChallenge, 'tls-alpn-01': verifyTlsAlpnChallenge, }; ================================================ FILE: packages/core/acme-client/src/wait.js ================================================ export async function wait(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } ================================================ FILE: packages/core/acme-client/test/00-pebble.spec.js ================================================ /** * Pebble Challenge Test Server tests */ const dns = require('dns').promises; const { randomUUID: uuid } = require('crypto'); const https = require('https'); const { assert } = require('chai'); const cts = require('./challtestsrv'); const axios = require('./../src/axios'); const { retrieveTlsAlpnCertificate } = require('./../src/util'); const { isAlpnCertificateAuthorizationValid } = require('./../src/crypto'); const domainName = process.env.ACME_DOMAIN_NAME || 'example.com'; const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80; const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443; const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443; describe('pebble', () => { const httpsAgent = new https.Agent({ rejectUnauthorized: false }); const testAHost = `${uuid()}.${domainName}`; const testARecords = ['1.1.1.1', '2.2.2.2']; const testCnameHost = `${uuid()}.${domainName}`; const testCnameRecord = `${uuid()}.${domainName}`; const testHttp01ChallengeHost = `${uuid()}.${domainName}`; const testHttp01ChallengeToken = uuid(); const testHttp01ChallengeContent = uuid(); const testHttps01ChallengeHost = `${uuid()}.${domainName}`; const testHttps01ChallengeToken = uuid(); const testHttps01ChallengeContent = uuid(); const testDns01ChallengeHost = `_acme-challenge.${uuid()}.${domainName}.`; const testDns01ChallengeValue = uuid(); const testTlsAlpn01ChallengeHost = `${uuid()}.${domainName}`; const testTlsAlpn01ChallengeValue = uuid(); /** * Pebble CTS required */ before(function () { if (!cts.isEnabled()) { this.skip(); } }); /** * DNS mocking */ describe('dns', () => { it('should not locate a records', async () => { const resp = await dns.resolve4(testAHost); assert.isArray(resp); assert.notDeepEqual(resp, testARecords); }); it('should add dns a records', async () => { const resp = await cts.addDnsARecord(testAHost, testARecords); assert.isTrue(resp); }); it('should locate a records', async () => { const resp = await dns.resolve4(testAHost); assert.isArray(resp); assert.deepStrictEqual(resp, testARecords); }); it('should not locate cname records', async () => { await assert.isRejected(dns.resolveCname(testCnameHost)); }); it('should set dns cname record', async () => { const resp = await cts.setDnsCnameRecord(testCnameHost, testCnameRecord); assert.isTrue(resp); }); it('should locate cname record', async () => { const resp = await dns.resolveCname(testCnameHost); assert.isArray(resp); assert.deepStrictEqual(resp, [testCnameRecord]); }); }); /** * HTTP-01 challenge response */ describe('http-01', () => { it('should not locate challenge response', async () => { const resp = await axios.get(`http://${testHttp01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttp01ChallengeToken}`); assert.isString(resp.data); assert.notEqual(resp.data, testHttp01ChallengeContent); }); it('should add challenge response', async () => { const resp = await cts.addHttp01ChallengeResponse(testHttp01ChallengeToken, testHttp01ChallengeContent); assert.isTrue(resp); }); it('should locate challenge response', async () => { const resp = await axios.get(`http://${testHttp01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttp01ChallengeToken}`); assert.isString(resp.data); assert.strictEqual(resp.data, testHttp01ChallengeContent); }); }); /** * HTTPS-01 challenge response */ describe('https-01', () => { it('should not locate challenge response', async () => { const r1 = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent }); const r2 = await axios.get(`https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent }); [r1, r2].forEach((resp) => { assert.isString(resp.data); assert.notEqual(resp.data, testHttps01ChallengeContent); }); }); it('should add challenge response', async () => { const resp = await cts.addHttps01ChallengeResponse(testHttps01ChallengeToken, testHttps01ChallengeContent, testHttps01ChallengeHost); assert.isTrue(resp); }); it('should 302 with self-signed cert', async () => { /* Assert HTTP 302 */ const resp = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { maxRedirects: 0, validateStatus: null, }); assert.strictEqual(resp.status, 302); assert.strictEqual(resp.headers.location, `https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`); /* Self-signed cert test */ await assert.isRejected(axios.get(`https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`)); await assert.isFulfilled(axios.get(`https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent })); }); it('should locate challenge response', async () => { const r1 = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent }); const r2 = await axios.get(`https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent }); [r1, r2].forEach((resp) => { assert.isString(resp.data); assert.strictEqual(resp.data, testHttps01ChallengeContent); }); }); }); /** * DNS-01 challenge response */ describe('dns-01', () => { it('should not locate challenge response', async () => { await assert.isRejected(dns.resolveTxt(testDns01ChallengeHost)); }); it('should add challenge response', async () => { const resp = await cts.addDns01ChallengeResponse(testDns01ChallengeHost, testDns01ChallengeValue); assert.isTrue(resp); }); it('should locate challenge response', async () => { const resp = await dns.resolveTxt(testDns01ChallengeHost); assert.isArray(resp); assert.deepStrictEqual(resp, [[testDns01ChallengeValue]]); }); }); /** * TLS-ALPN-01 challenge response */ describe('tls-alpn-01', () => { it('should not locate challenge response', async () => { await assert.isRejected(retrieveTlsAlpnCertificate(testTlsAlpn01ChallengeHost, tlsAlpnPort), /(failed to retrieve)|(ssl3_read_bytes:tlsv1 alert internal error)/); }); it('should timeout challenge response', async () => { await assert.isRejected(retrieveTlsAlpnCertificate('example.org', tlsAlpnPort, 500)); }); it('should add challenge response', async () => { const resp = await cts.addTlsAlpn01ChallengeResponse(testTlsAlpn01ChallengeHost, testTlsAlpn01ChallengeValue); assert.isTrue(resp); }); it('should locate challenge response', async () => { const resp = await retrieveTlsAlpnCertificate(testTlsAlpn01ChallengeHost, tlsAlpnPort); assert.isTrue(isAlpnCertificateAuthorizationValid(resp, testTlsAlpn01ChallengeValue)); }); }); }); ================================================ FILE: packages/core/acme-client/test/10-http.spec.js ================================================ /** * HTTP client tests */ const { randomUUID: uuid } = require('crypto'); const { assert } = require('chai'); const nock = require('nock'); const axios = require('./../src/axios'); const HttpClient = require('./../src/http'); const pkg = require('./../package.json'); describe('http', () => { let testClient; const endpoint = `http://${uuid()}.example.com`; const defaultUserAgent = `node-${pkg.name}/${pkg.version}`; const customUserAgent = 'custom-ua-123'; afterEach(() => { nock.cleanAll(); }); /** * Initialize */ it('should initialize clients', () => { testClient = new HttpClient(); }); /** * HTTP verbs */ it('should http get', async () => { nock(endpoint).get('/').reply(200, 'ok'); const resp = await testClient.request(endpoint, 'get'); assert.isObject(resp); assert.strictEqual(resp.status, 200); assert.strictEqual(resp.data, 'ok'); }); /** * User-Agent */ it('should request using default user-agent', async () => { nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok'); axios.defaults.headers.common['User-Agent'] = defaultUserAgent; const resp = await testClient.request(endpoint, 'get'); assert.isObject(resp); assert.strictEqual(resp.status, 200); assert.strictEqual(resp.data, 'ok'); }); it('should reject using custom user-agent', async () => { nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok'); axios.defaults.headers.common['User-Agent'] = customUserAgent; await assert.isRejected(testClient.request(endpoint, 'get')); }); it('should request using custom user-agent', async () => { nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok'); axios.defaults.headers.common['User-Agent'] = customUserAgent; const resp = await testClient.request(endpoint, 'get'); assert.isObject(resp); assert.strictEqual(resp.status, 200); assert.strictEqual(resp.data, 'ok'); }); it('should reject using default user-agent', async () => { nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok'); axios.defaults.headers.common['User-Agent'] = defaultUserAgent; await assert.isRejected(testClient.request(endpoint, 'get')); }); /** * Retry on HTTP errors */ it('should retry on 429 rate limit', async () => { let rateLimitCount = 0; nock(endpoint).persist().get('/').reply(() => { rateLimitCount += 1; if (rateLimitCount < 3) { return [429, 'Rate Limit Exceeded', { 'Retry-After': 1 }]; } return [200, 'ok']; }); assert.strictEqual(rateLimitCount, 0); const resp = await testClient.request(endpoint, 'get'); assert.isObject(resp); assert.strictEqual(resp.status, 200); assert.strictEqual(resp.data, 'ok'); assert.strictEqual(rateLimitCount, 3); }); it('should retry on 5xx server error', async () => { let serverErrorCount = 0; nock(endpoint).persist().get('/').reply(() => { serverErrorCount += 1; return [500, 'Internal Server Error', { 'Retry-After': 1 }]; }); assert.strictEqual(serverErrorCount, 0); const resp = await testClient.request(endpoint, 'get'); assert.isObject(resp); assert.strictEqual(resp.status, 500); assert.strictEqual(serverErrorCount, 4); }); }); ================================================ FILE: packages/core/acme-client/test/10-logger.spec.js ================================================ /** * Logger tests */ const { assert } = require('chai'); const logger = require('./../src/logger'); describe('logger', () => { let lastLogMessage = null; function customLoggerFn(msg) { lastLogMessage = msg; } /** * Logger */ it('should log without custom logger', () => { logger.log('something'); assert.isNull(lastLogMessage); }); it('should log with custom logger', () => { logger.setLogger(customLoggerFn); ['abc123', 'def456', 'ghi789'].forEach((m) => { logger.log(m); assert.strictEqual(lastLogMessage, m); }); }); }); ================================================ FILE: packages/core/acme-client/test/10-util.spec.js ================================================ /** * Utility method tests */ const dns = require('dns').promises; const fs = require('fs').promises; const path = require('path'); const { assert } = require('chai'); const util = require('./../src/util'); const { readCertificateInfo } = require('./../src/crypto'); describe('util', () => { const testCertPath1 = path.join(__dirname, 'fixtures', 'certificate.crt'); const testCertPath2 = path.join(__dirname, 'fixtures', 'letsencrypt.crt'); it('retry()', async () => { let attempts = 0; const backoffOpts = { min: 100, max: 500, }; await assert.isRejected(util.retry(() => { throw new Error('oops'); }, backoffOpts)); const r = await util.retry(() => { attempts += 1; if (attempts < 3) { throw new Error('oops'); } return 'abc'; }, backoffOpts); assert.strictEqual(r, 'abc'); assert.strictEqual(attempts, 3); }); it('parseLinkHeader()', () => { const r1 = util.parseLinkHeader(';rel="alternate"'); assert.isArray(r1); assert.strictEqual(r1.length, 1); assert.strictEqual(r1[0], 'https://example.com/a'); const r2 = util.parseLinkHeader(';rel="test"'); assert.isArray(r2); assert.strictEqual(r2.length, 0); const r3 = util.parseLinkHeader('; rel="test"', 'test'); assert.isArray(r3); assert.strictEqual(r3.length, 1); assert.strictEqual(r3[0], 'http://example.com/c'); const r4 = util.parseLinkHeader(`; rel="alternate", ; rel="nope", ;rel="alternate", ; rel="alternate"`); assert.isArray(r4); assert.strictEqual(r4.length, 3); assert.strictEqual(r4[0], 'https://example.com/a'); assert.strictEqual(r4[1], 'https://example.com/b'); assert.strictEqual(r4[2], 'https://example.com/c'); }); it('parseRetryAfterHeader()', () => { const r1 = util.parseRetryAfterHeader(''); assert.strictEqual(r1, 0); const r2 = util.parseRetryAfterHeader('abcdef'); assert.strictEqual(r2, 0); const r3 = util.parseRetryAfterHeader('123'); assert.strictEqual(r3, 123); const r4 = util.parseRetryAfterHeader('123.456'); assert.strictEqual(r4, 123); const r5 = util.parseRetryAfterHeader('-555'); assert.strictEqual(r5, 0); const r6 = util.parseRetryAfterHeader('Wed, 21 Oct 2015 07:28:00 GMT'); assert.strictEqual(r6, 0); const now = new Date(); const future = new Date(now.getTime() + 123000); const r7 = util.parseRetryAfterHeader(future.toUTCString()); assert.isTrue(r7 > 100); }); it('findCertificateChainForIssuer()', async () => { const certs = [ (await fs.readFile(testCertPath1)).toString(), (await fs.readFile(testCertPath2)).toString(), ]; const r1 = util.findCertificateChainForIssuer(certs, 'abc123'); const r2 = util.findCertificateChainForIssuer(certs, 'example.com'); const r3 = util.findCertificateChainForIssuer(certs, 'E6'); [r1, r2, r3].forEach((r) => { assert.isString(r); assert.isNotEmpty(r); }); assert.strictEqual(readCertificateInfo(r1).issuer.commonName, 'example.com'); assert.strictEqual(readCertificateInfo(r2).issuer.commonName, 'example.com'); assert.strictEqual(readCertificateInfo(r3).issuer.commonName, 'E6'); }); it('formatResponseError()', () => { const e1 = util.formatResponseError({ data: { error: 'aaa' } }); assert.strictEqual(e1, 'aaa'); const e2 = util.formatResponseError({ data: { error: { detail: 'bbb' } } }); assert.strictEqual(e2, 'bbb'); const e3 = util.formatResponseError({ data: { detail: 'ccc' } }); assert.strictEqual(e3, 'ccc'); const e4 = util.formatResponseError({ data: { a: 123 } }); assert.strictEqual(e4, '{"a":123}'); const e5 = util.formatResponseError({}); assert.isString(e5); assert.isEmpty(e5); }); it('getAuthoritativeDnsResolver()', async () => { /* valid domain - should not use global default */ const r1 = await util.getAuthoritativeDnsResolver('example.com'); assert.instanceOf(r1, dns.Resolver); assert.isNotEmpty(r1.getServers()); assert.notDeepEqual(r1.getServers(), dns.getServers()); /* invalid domain - fallback to global default */ const r2 = await util.getAuthoritativeDnsResolver('invalid.xtldx'); assert.instanceOf(r2, dns.Resolver); assert.deepStrictEqual(r2.getServers(), dns.getServers()); }); /* TODO: Figure out how to test this */ it('retrieveTlsAlpnCertificate()'); }); ================================================ FILE: packages/core/acme-client/test/10-verify.spec.js ================================================ /** * Challenge verification tests */ const { randomUUID: uuid } = require('crypto'); const { assert } = require('chai'); const cts = require('./challtestsrv'); const verify = require('./../src/verify'); const domainName = process.env.ACME_DOMAIN_NAME || 'example.com'; describe('verify', () => { const challengeTypes = ['http-01', 'dns-01']; const testHttp01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } }; const testHttp01Challenge = { type: 'http-01', status: 'pending', token: uuid() }; const testHttp01Key = uuid(); const testHttps01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } }; const testHttps01Challenge = { type: 'http-01', status: 'pending', token: uuid() }; const testHttps01Key = uuid(); const testDns01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } }; const testDns01Challenge = { type: 'dns-01', status: 'pending', token: uuid() }; const testDns01Key = uuid(); const testDns01Cname = `${uuid()}.${domainName}`; const testTlsAlpn01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } }; const testTlsAlpn01Challenge = { type: 'dns-01', status: 'pending', token: uuid() }; const testTlsAlpn01Key = uuid(); /** * Pebble CTS required */ before(function () { if (!cts.isEnabled()) { this.skip(); } }); /** * API */ it('should expose verification api', async () => { assert.containsAllKeys(verify, challengeTypes); }); /** * http-01 */ describe('http-01', () => { it('should reject challenge', async () => { await assert.isRejected(verify['http-01'](testHttp01Authz, testHttp01Challenge, testHttp01Key)); }); it('should mock challenge response', async () => { const resp = await cts.addHttp01ChallengeResponse(testHttp01Challenge.token, testHttp01Key); assert.isTrue(resp); }); it('should verify challenge', async () => { const resp = await verify['http-01'](testHttp01Authz, testHttp01Challenge, testHttp01Key); assert.isTrue(resp); }); it('should mock challenge response with trailing newline', async () => { const resp = await cts.addHttp01ChallengeResponse(testHttp01Challenge.token, `${testHttp01Key}\n`); assert.isTrue(resp); }); it('should verify challenge with trailing newline', async () => { const resp = await verify['http-01'](testHttp01Authz, testHttp01Challenge, testHttp01Key); assert.isTrue(resp); }); }); /** * https-01 */ describe('https-01', () => { it('should reject challenge', async () => { await assert.isRejected(verify['http-01'](testHttps01Authz, testHttps01Challenge, testHttps01Key)); }); it('should mock challenge response', async () => { const resp = await cts.addHttps01ChallengeResponse(testHttps01Challenge.token, testHttps01Key, testHttps01Authz.identifier.value); assert.isTrue(resp); }); it('should verify challenge', async () => { const resp = await verify['http-01'](testHttps01Authz, testHttps01Challenge, testHttps01Key); assert.isTrue(resp); }); }); /** * dns-01 */ describe('dns-01', () => { it('should reject challenge', async () => { await assert.isRejected(verify['dns-01'](testDns01Authz, testDns01Challenge, testDns01Key)); }); it('should mock challenge response', async () => { const resp = await cts.addDns01ChallengeResponse(`_acme-challenge.${testDns01Authz.identifier.value}.`, testDns01Key); assert.isTrue(resp); }); it('should add cname to challenge response', async () => { const resp = await cts.setDnsCnameRecord(testDns01Cname, `_acme-challenge.${testDns01Authz.identifier.value}.`); assert.isTrue(resp); }); it('should verify challenge', async () => { const resp = await verify['dns-01'](testDns01Authz, testDns01Challenge, testDns01Key); assert.isTrue(resp); }); it('should verify challenge using cname', async () => { const resp = await verify['dns-01'](testDns01Authz, testDns01Challenge, testDns01Key); assert.isTrue(resp); }); }); /** * tls-alpn-01 */ describe('tls-alpn-01', () => { it('should reject challenge', async () => { await assert.isRejected(verify['tls-alpn-01'](testTlsAlpn01Authz, testTlsAlpn01Challenge, testTlsAlpn01Key)); }); it('should mock challenge response', async () => { const resp = await cts.addTlsAlpn01ChallengeResponse(testTlsAlpn01Authz.identifier.value, testTlsAlpn01Key); assert.isTrue(resp); }); it('should verify challenge', async () => { const resp = await verify['tls-alpn-01'](testTlsAlpn01Authz, testTlsAlpn01Challenge, testTlsAlpn01Key); assert.isTrue(resp); }); }); }); ================================================ FILE: packages/core/acme-client/test/20-crypto-legacy.spec.js ================================================ /** * Legacy crypto tests */ const fs = require('fs').promises; const path = require('path'); const { assert } = require('chai'); const spec = require('./spec'); const forge = require('./../src/crypto/forge'); const cryptoEngines = { forge, }; describe('crypto-legacy', () => { let testPemKey; let testCert; let testSanCert; const modulusStore = []; const exponentStore = []; const publicKeyStore = []; const testCsrDomain = 'example.com'; const testSanCsrDomains = ['example.com', 'test.example.com', 'abc.example.com']; const testKeyPath = path.join(__dirname, 'fixtures', 'private.key'); const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt'); const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt'); /** * Fixtures */ describe('fixtures', () => { it('should read private key fixture', async () => { testPemKey = await fs.readFile(testKeyPath); assert.isTrue(Buffer.isBuffer(testPemKey)); }); it('should read certificate fixture', async () => { testCert = await fs.readFile(testCertPath); assert.isTrue(Buffer.isBuffer(testCert)); }); it('should read san certificate fixture', async () => { testSanCert = await fs.readFile(testSanCertPath); assert.isTrue(Buffer.isBuffer(testSanCert)); }); }); /** * Engines */ Object.entries(cryptoEngines).forEach(([name, engine]) => { describe(`engine/${name}`, () => { let testCsr; let testSanCsr; let testNonCnCsr; let testNonAsciiCsr; /** * Key generation */ it('should generate a private key', async () => { const key = await engine.createPrivateKey(); assert.isTrue(Buffer.isBuffer(key)); }); it('should generate a private key with size=1024', async () => { const key = await engine.createPrivateKey(1024); assert.isTrue(Buffer.isBuffer(key)); }); it('should generate a public key', async () => { const key = await engine.createPublicKey(testPemKey); assert.isTrue(Buffer.isBuffer(key)); publicKeyStore.push(key.toString().replace(/[\r\n]/gm, '')); }); /** * Certificate Signing Request */ it('should generate a csr', async () => { const [key, csr] = await engine.createCsr({ commonName: testCsrDomain, }); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); testCsr = csr; }); it('should generate a san csr', async () => { const [key, csr] = await engine.createCsr({ commonName: testSanCsrDomains[0], altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length), }); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); testSanCsr = csr; }); it('should generate a csr without common name', async () => { const [key, csr] = await engine.createCsr({ altNames: testSanCsrDomains, }); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); testNonCnCsr = csr; }); it('should generate a non-ascii csr', async () => { const [key, csr] = await engine.createCsr({ commonName: testCsrDomain, organization: '大安區', organizationUnit: '中文部門', }); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); testNonAsciiCsr = csr; }); it('should resolve domains from csr', async () => { const result = await engine.readCsrDomains(testCsr); spec.crypto.csrDomains(result); assert.strictEqual(result.commonName, testCsrDomain); assert.deepStrictEqual(result.altNames, [testCsrDomain]); }); it('should resolve domains from san csr', async () => { const result = await engine.readCsrDomains(testSanCsr); spec.crypto.csrDomains(result); assert.strictEqual(result.commonName, testSanCsrDomains[0]); assert.deepStrictEqual(result.altNames, testSanCsrDomains); }); it('should resolve domains from san without common name', async () => { const result = await engine.readCsrDomains(testNonCnCsr); spec.crypto.csrDomains(result); assert.isNull(result.commonName); assert.deepStrictEqual(result.altNames, testSanCsrDomains); }); it('should resolve domains from non-ascii csr', async () => { const result = await engine.readCsrDomains(testNonAsciiCsr); spec.crypto.csrDomains(result); assert.strictEqual(result.commonName, testCsrDomain); assert.deepStrictEqual(result.altNames, [testCsrDomain]); }); /** * Certificate */ it('should read info from certificate', async () => { const info = await engine.readCertificateInfo(testCert); spec.crypto.certificateInfo(info); assert.strictEqual(info.domains.commonName, testCsrDomain); assert.strictEqual(info.domains.altNames.length, 0); }); it('should read info from san certificate', async () => { const info = await engine.readCertificateInfo(testSanCert); spec.crypto.certificateInfo(info); assert.strictEqual(info.domains.commonName, testSanCsrDomains[0]); assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length)); }); /** * PEM utils */ it('should get pem body', () => { [testPemKey, testCert, testSanCert].forEach((pem) => { const body = engine.getPemBody(pem); assert.isString(body); assert.notInclude(body, '\r'); assert.notInclude(body, '\n'); assert.notInclude(body, '\r\n'); }); }); it('should split pem chain', () => { [testPemKey, testCert, testSanCert].forEach((pem) => { const chain = engine.splitPemChain(pem); assert.isArray(chain); assert.isNotEmpty(chain); chain.forEach((c) => assert.isString(c)); }); }); /** * Modulus and exponent */ it('should get modulus', async () => { const result = await Promise.all([testPemKey, testCert, testSanCert].map(async (item) => { const mod = await engine.getModulus(item); assert.isTrue(Buffer.isBuffer(mod)); return mod; })); modulusStore.push(result); }); it('should get public exponent', async () => { const result = await Promise.all([testPemKey, testCert, testSanCert].map(async (item) => { const exp = await engine.getPublicExponent(item); assert.isTrue(Buffer.isBuffer(exp)); const b64exp = exp.toString('base64'); assert.strictEqual(b64exp, 'AQAB'); return b64exp; })); exponentStore.push(result); }); }); }); /** * Verify identical results */ describe('verification', () => { it('should have identical public keys', () => { if (publicKeyStore.length > 1) { const reference = publicKeyStore.shift(); publicKeyStore.forEach((item) => assert.strictEqual(reference, item)); } }); it('should have identical moduli', () => { if (modulusStore.length > 1) { const reference = modulusStore.shift(); modulusStore.forEach((item) => assert.deepStrictEqual(reference, item)); } }); it('should have identical public exponents', () => { if (exponentStore.length > 1) { const reference = exponentStore.shift(); exponentStore.forEach((item) => assert.deepStrictEqual(reference, item)); } }); }); }); ================================================ FILE: packages/core/acme-client/test/20-crypto.spec.js ================================================ /** * Crypto tests */ const fs = require('fs').promises; const path = require('path'); const { assert } = require('chai'); const spec = require('./spec'); const { crypto } = require('./../'); const emptyBodyChain1 = ` -----BEGIN TEST----- dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw== -----END TEST----- -----BEGIN TEST----- dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw== -----END TEST----- -----BEGIN TEST----- -----END TEST----- -----BEGIN TEST----- dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw== -----END TEST----- `; const emptyBodyChain2 = ` -----BEGIN TEST----- -----END TEST----- -----BEGIN TEST----- -----END TEST----- -----BEGIN TEST----- dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw== -----END TEST----- -----BEGIN TEST----- dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw== -----END TEST----- -----BEGIN TEST----- dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw== -----END TEST----- `; describe('crypto', () => { const testCsrDomain = 'example.com'; const testSanCsrDomains = ['example.com', 'test.example.com', 'abc.example.com']; const testKeyPath = path.join(__dirname, 'fixtures', 'private.key'); const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt'); const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt'); /** * Key types */ Object.entries({ rsa: { createKeyFns: { s1024: () => crypto.createPrivateRsaKey(1024), s2048: () => crypto.createPrivateRsaKey(), s4096: () => crypto.createPrivateRsaKey(4096), }, jwkSpecFn: spec.jwk.rsa, }, ecdsa: { createKeyFns: { p256: () => crypto.createPrivateEcdsaKey(), p384: () => crypto.createPrivateEcdsaKey('P-384'), p521: () => crypto.createPrivateEcdsaKey('P-521'), }, jwkSpecFn: spec.jwk.ecdsa, }, }).forEach(([name, { createKeyFns, jwkSpecFn }]) => { describe(name, () => { const testPrivateKeys = {}; const testPublicKeys = {}; /** * Iterate through all generator variations */ Object.entries(createKeyFns).forEach(([n, createFn]) => { let testCsr; let testSanCsr; let testNonCnCsr; let testNonAsciiCsr; let testAlpnCertificate; /** * Keys and JWK */ it(`${n}/should generate private key`, async () => { testPrivateKeys[n] = await createFn(); assert.isTrue(Buffer.isBuffer(testPrivateKeys[n])); }); it(`${n}/should get public key`, () => { testPublicKeys[n] = crypto.getPublicKey(testPrivateKeys[n]); assert.isTrue(Buffer.isBuffer(testPublicKeys[n])); }); it(`${n}/should get public key from string`, () => { testPublicKeys[n] = crypto.getPublicKey(testPrivateKeys[n].toString()); assert.isTrue(Buffer.isBuffer(testPublicKeys[n])); }); it(`${n}/should get jwk from private key`, () => { const jwk = crypto.getJwk(testPrivateKeys[n]); jwkSpecFn(jwk); }); it(`${n}/should get jwk from public key`, () => { const jwk = crypto.getJwk(testPublicKeys[n]); jwkSpecFn(jwk); }); it(`${n}/should get jwk from string`, () => { const jwk = crypto.getJwk(testPrivateKeys[n].toString()); jwkSpecFn(jwk); }); /** * Certificate Signing Request */ it(`${n}/should generate a csr`, async () => { const [key, csr] = await crypto.createCsr({ commonName: testCsrDomain, }, testPrivateKeys[n]); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); testCsr = csr; }); it(`${n}/should generate a san csr`, async () => { const [key, csr] = await crypto.createCsr({ commonName: testSanCsrDomains[0], altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length), }, testPrivateKeys[n]); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); testSanCsr = csr; }); it(`${n}/should generate a csr without common name`, async () => { const [key, csr] = await crypto.createCsr({ altNames: testSanCsrDomains, }, testPrivateKeys[n]); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); testNonCnCsr = csr; }); it(`${n}/should generate a non-ascii csr`, async () => { const [key, csr] = await crypto.createCsr({ commonName: testCsrDomain, organization: '大安區', organizationUnit: '中文部門', }, testPrivateKeys[n]); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); testNonAsciiCsr = csr; }); it(`${n}/should generate a csr with key as string`, async () => { const [key, csr] = await crypto.createCsr({ commonName: testCsrDomain, }, testPrivateKeys[n].toString()); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); }); it(`${n}/should throw with invalid key`, async () => { await assert.isRejected(crypto.createCsr({ commonName: testCsrDomain, }, testPublicKeys[n])); }); /** * Domain and info resolver */ it(`${n}/should resolve domains from csr`, () => { const result = crypto.readCsrDomains(testCsr); spec.crypto.csrDomains(result); assert.strictEqual(result.commonName, testCsrDomain); assert.deepStrictEqual(result.altNames, [testCsrDomain]); }); it(`${n}/should resolve domains from san csr`, () => { const result = crypto.readCsrDomains(testSanCsr); spec.crypto.csrDomains(result); assert.strictEqual(result.commonName, testSanCsrDomains[0]); assert.deepStrictEqual(result.altNames, testSanCsrDomains); }); it(`${n}/should resolve domains from csr without common name`, () => { const result = crypto.readCsrDomains(testNonCnCsr); spec.crypto.csrDomains(result); assert.isNull(result.commonName); assert.deepStrictEqual(result.altNames, testSanCsrDomains); }); it(`${n}/should resolve domains from non-ascii csr`, () => { const result = crypto.readCsrDomains(testNonAsciiCsr); spec.crypto.csrDomains(result); assert.strictEqual(result.commonName, testCsrDomain); assert.deepStrictEqual(result.altNames, [testCsrDomain]); }); it(`${n}/should resolve domains from csr string`, () => { [testCsr, testSanCsr, testNonCnCsr, testNonAsciiCsr].forEach((csr) => { const result = crypto.readCsrDomains(csr.toString()); spec.crypto.csrDomains(result); }); }); /** * ALPN */ it(`${n}/should generate alpn certificate`, async () => { const authz = { identifier: { value: 'test.example.com' } }; const [key, cert] = await crypto.createAlpnCertificate(authz, 'super-secret.12345', await createFn()); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(cert)); testAlpnCertificate = cert; }); it(`${n}/should generate alpn certificate with key as string`, async () => { const k = await createFn(); const authz = { identifier: { value: 'test.example.com' } }; const [key, cert] = await crypto.createAlpnCertificate(authz, 'super-secret.12345', k.toString()); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(cert)); }); it(`${n}/should not validate invalid alpn certificate key authorization`, () => { assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'aaaaaaa')); assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'bbbbbbb')); assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'ccccccc')); }); it(`${n}/should validate valid alpn certificate key authorization`, () => { assert.isTrue(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'super-secret.12345')); }); it(`${n}/should validate valid alpn certificate with cert as string`, () => { assert.isTrue(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate.toString(), 'super-secret.12345')); }); }); }); }); /** * Common functionality */ describe('common', () => { let testPemKey; let testCert; let testSanCert; it('should read private key fixture', async () => { testPemKey = await fs.readFile(testKeyPath); assert.isTrue(Buffer.isBuffer(testPemKey)); }); it('should read certificate fixture', async () => { testCert = await fs.readFile(testCertPath); assert.isTrue(Buffer.isBuffer(testCert)); }); it('should read san certificate fixture', async () => { testSanCert = await fs.readFile(testSanCertPath); assert.isTrue(Buffer.isBuffer(testSanCert)); }); /** * CSR with auto-generated key */ it('should generate a csr with default key', async () => { const [key, csr] = await crypto.createCsr({ commonName: testCsrDomain, }); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(csr)); }); /** * Certificate */ it('should read certificate info', () => { const info = crypto.readCertificateInfo(testCert); spec.crypto.certificateInfo(info); assert.strictEqual(info.domains.commonName, testCsrDomain); assert.strictEqual(info.domains.altNames.length, 0); }); it('should read certificate info with san', () => { const info = crypto.readCertificateInfo(testSanCert); spec.crypto.certificateInfo(info); assert.strictEqual(info.domains.commonName, testSanCsrDomains[0]); assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length)); }); it('should read certificate info from string', () => { [testCert, testSanCert].forEach((cert) => { const info = crypto.readCertificateInfo(cert.toString()); spec.crypto.certificateInfo(info); }); }); /** * ALPN */ it('should generate alpn certificate with default key', async () => { const authz = { identifier: { value: 'test.example.com' } }; const [key, cert] = await crypto.createAlpnCertificate(authz, 'abc123'); assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(cert)); }); /** * PEM utils */ it('should get pem body as b64u', () => { [testPemKey, testCert, testSanCert].forEach((pem) => { const body = crypto.getPemBodyAsB64u(pem); assert.isString(body); assert.notInclude(body, '\r'); assert.notInclude(body, '\n'); assert.notInclude(body, '\r\n'); }); }); it('should get pem body as b64u from string', () => { [testPemKey, testCert, testSanCert].forEach((pem) => { const body = crypto.getPemBodyAsB64u(pem.toString()); assert.isString(body); assert.notInclude(body, '\r'); assert.notInclude(body, '\n'); assert.notInclude(body, '\r\n'); }); }); it('should split pem chain', () => { [testPemKey, testCert, testSanCert].forEach((pem) => { const chain = crypto.splitPemChain(pem); assert.isArray(chain); assert.isNotEmpty(chain); chain.forEach((c) => assert.isString(c)); }); }); it('should split pem chain from string', () => { [testPemKey, testCert, testSanCert].forEach((pem) => { const chain = crypto.splitPemChain(pem.toString()); assert.isArray(chain); assert.isNotEmpty(chain); chain.forEach((c) => assert.isString(c)); }); }); it('should split pem chain with empty bodies', () => { const c1 = crypto.splitPemChain(emptyBodyChain1); const c2 = crypto.splitPemChain(emptyBodyChain2); assert.strictEqual(c1.length, 3); assert.strictEqual(c2.length, 3); }); }); }); ================================================ FILE: packages/core/acme-client/test/50-client.spec.js ================================================ /** * ACME client tests */ const { randomUUID: uuid } = require('crypto'); const { assert } = require('chai'); const cts = require('./challtestsrv'); const getCertIssuers = require('./get-cert-issuers'); const spec = require('./spec'); const acme = require('./../'); const domainName = process.env.ACME_DOMAIN_NAME || 'example.com'; const directoryUrl = process.env.ACME_DIRECTORY_URL || acme.directory.letsencrypt.staging; const capEabEnabled = (('ACME_CAP_EAB_ENABLED' in process.env) && (process.env.ACME_CAP_EAB_ENABLED === '1')); const capMetaTosField = !(('ACME_CAP_META_TOS_FIELD' in process.env) && (process.env.ACME_CAP_META_TOS_FIELD === '0')); const capUpdateAccountKey = !(('ACME_CAP_UPDATE_ACCOUNT_KEY' in process.env) && (process.env.ACME_CAP_UPDATE_ACCOUNT_KEY === '0')); const capAlternateCertRoots = !(('ACME_CAP_ALTERNATE_CERT_ROOTS' in process.env) && (process.env.ACME_CAP_ALTERNATE_CERT_ROOTS === '0')); const clientOpts = { directoryUrl, backoffAttempts: 5, backoffMin: 1000, backoffMax: 5000, }; if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) { clientOpts.externalAccountBinding = { kid: process.env.ACME_EAB_KID, hmacKey: process.env.ACME_EAB_HMAC_KEY, }; } describe('client', () => { const testDomain = `${uuid()}.${domainName}`; const testDomainAlpn = `${uuid()}.${domainName}`; const testDomainWildcard = `*.${testDomain}`; const testContact = `mailto:test-${uuid()}@nope.com`; /** * Pebble CTS required */ before(function () { if (!cts.isEnabled()) { this.skip(); } }); /** * Key types */ Object.entries({ rsa: { createKeyFn: () => acme.crypto.createPrivateRsaKey(), createKeyAltFns: { s1024: () => acme.crypto.createPrivateRsaKey(1024), s4096: () => acme.crypto.createPrivateRsaKey(4096), }, jwkSpecFn: spec.jwk.rsa, }, ecdsa: { createKeyFn: () => acme.crypto.createPrivateEcdsaKey(), createKeyAltFns: { p384: () => acme.crypto.createPrivateEcdsaKey('P-384'), p521: () => acme.crypto.createPrivateEcdsaKey('P-521'), }, jwkSpecFn: spec.jwk.ecdsa, }, }).forEach(([name, { createKeyFn, createKeyAltFns, jwkSpecFn }]) => { describe(name, () => { let testIssuers; let testAccountKey; let testAccountSecondaryKey; let testClient; let testAccount; let testAccountUrl; let testOrder; let testOrderAlpn; let testOrderWildcard; let testAuthz; let testAuthzAlpn; let testAuthzWildcard; let testChallenge; let testChallengeAlpn; let testChallengeWildcard; let testKeyAuthorization; let testKeyAuthorizationAlpn; let testKeyAuthorizationWildcard; let testCsr; let testCsrAlpn; let testCsrWildcard; let testCertificate; let testCertificateAlpn; let testCertificateWildcard; /** * Fixtures */ it('should generate a private key', async () => { testAccountKey = await createKeyFn(); assert.isTrue(Buffer.isBuffer(testAccountKey)); }); it('should create a second private key', async () => { testAccountSecondaryKey = await createKeyFn(); assert.isTrue(Buffer.isBuffer(testAccountSecondaryKey)); }); it('should generate certificate signing request', async () => { [, testCsr] = await acme.crypto.createCsr({ commonName: testDomain }, await createKeyFn()); [, testCsrAlpn] = await acme.crypto.createCsr({ altNames: [testDomainAlpn] }, await createKeyFn()); [, testCsrWildcard] = await acme.crypto.createCsr({ altNames: [testDomainWildcard] }, await createKeyFn()); }); it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () { if (!capAlternateCertRoots) { this.skip(); } testIssuers = await getCertIssuers(); assert.isArray(testIssuers); assert.isTrue(testIssuers.length > 1); testIssuers.forEach((i) => { assert.isString(i); assert.strictEqual(1, testIssuers.filter((c) => (c === i)).length); }); }); /** * Initialize clients */ it('should initialize client', () => { testClient = new acme.Client({ ...clientOpts, accountKey: testAccountKey, }); }); it('should produce a valid jwk', () => { const jwk = testClient.http.getJwk(); jwkSpecFn(jwk); }); /** * Terms of Service */ it('should produce tos url [ACME_CAP_META_TOS_FIELD]', async function () { if (!capMetaTosField) { this.skip(); } const tos = await testClient.getTermsOfServiceUrl(); assert.isString(tos); }); it('should not produce tos url [!ACME_CAP_META_TOS_FIELD]', async function () { if (capMetaTosField) { this.skip(); } const tos = await testClient.getTermsOfServiceUrl(); assert.isNull(tos); }); /** * Create account */ it('should refuse account creation without tos [ACME_CAP_META_TOS_FIELD]', async function () { if (!capMetaTosField) { this.skip(); } await assert.isRejected(testClient.createAccount()); }); it('should refuse account creation without eab [ACME_CAP_EAB_ENABLED]', async function () { if (!capEabEnabled) { this.skip(); } const client = new acme.Client({ ...clientOpts, accountKey: testAccountKey, externalAccountBinding: null, }); await assert.isRejected(client.createAccount({ termsOfServiceAgreed: true, })); }); it('should create an account', async () => { testAccount = await testClient.createAccount({ termsOfServiceAgreed: true, }); spec.rfc8555.account(testAccount); assert.strictEqual(testAccount.status, 'valid'); }); it('should produce an account url', () => { testAccountUrl = testClient.getAccountUrl(); assert.isString(testAccountUrl); }); /** * Create account with alternate key sizes */ Object.entries(createKeyAltFns).forEach(([k, altKeyFn]) => { it(`should create account with key=${k}`, async () => { const client = new acme.Client({ ...clientOpts, accountKey: await altKeyFn(), }); const account = await client.createAccount({ termsOfServiceAgreed: true, }); spec.rfc8555.account(account); assert.strictEqual(account.status, 'valid'); }); }); /** * Find existing account using secondary client */ it('should throw when trying to find account using invalid account key', async () => { const client = new acme.Client({ ...clientOpts, accountKey: testAccountSecondaryKey, }); await assert.isRejected(client.createAccount({ onlyReturnExisting: true, })); }); it('should find existing account using account key', async () => { const client = new acme.Client({ ...clientOpts, accountKey: testAccountKey, }); const account = await client.createAccount({ onlyReturnExisting: true, }); spec.rfc8555.account(account); assert.strictEqual(account.status, 'valid'); assert.deepStrictEqual(account.key, testAccount.key); }); /** * Account URL */ it('should refuse invalid account url', async () => { const client = new acme.Client({ ...clientOpts, accountKey: testAccountKey, accountUrl: 'https://acme-staging-v02.api.letsencrypt.org/acme/acct/1', }); await assert.isRejected(client.updateAccount()); }); it('should find existing account using account url', async () => { const client = new acme.Client({ ...clientOpts, accountKey: testAccountKey, accountUrl: testAccountUrl, }); const account = await client.createAccount({ onlyReturnExisting: true, }); spec.rfc8555.account(account); assert.strictEqual(account.status, 'valid'); assert.deepStrictEqual(account.key, testAccount.key); }); /** * Update account contact info */ it('should update account contact info', async () => { const data = { contact: [testContact] }; const account = await testClient.updateAccount(data); spec.rfc8555.account(account); assert.strictEqual(account.status, 'valid'); assert.deepStrictEqual(account.key, testAccount.key); assert.isArray(account.contact); assert.include(account.contact, testContact); }); /** * Change account private key */ it('should change account private key [ACME_CAP_UPDATE_ACCOUNT_KEY]', async function () { if (!capUpdateAccountKey) { this.skip(); } await testClient.updateAccountKey(testAccountSecondaryKey); const account = await testClient.createAccount({ onlyReturnExisting: true, }); spec.rfc8555.account(account); assert.strictEqual(account.status, 'valid'); assert.notDeepEqual(account.key, testAccount.key); }); /** * Create new certificate order */ it('should create new order', async () => { const data1 = { identifiers: [{ type: 'dns', value: testDomain }] }; const data2 = { identifiers: [{ type: 'dns', value: testDomainAlpn }] }; const data3 = { identifiers: [{ type: 'dns', value: testDomainWildcard }] }; testOrder = await testClient.createOrder(data1); testOrderAlpn = await testClient.createOrder(data2); testOrderWildcard = await testClient.createOrder(data3); [testOrder, testOrderAlpn, testOrderWildcard].forEach((item) => { spec.rfc8555.order(item); assert.strictEqual(item.status, 'pending'); }); }); /** * Get status of existing certificate order */ it('should get existing order', async () => { await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (existing) => { const result = await testClient.getOrder(existing); spec.rfc8555.order(result); assert.deepStrictEqual(existing, result); })); }); /** * Get identifier authorization */ it('should get identifier authorization', async () => { const orderAuthzCollection = await testClient.getAuthorizations(testOrder); const alpnAuthzCollection = await testClient.getAuthorizations(testOrderAlpn); const wildcardAuthzCollection = await testClient.getAuthorizations(testOrderWildcard); [orderAuthzCollection, alpnAuthzCollection, wildcardAuthzCollection].forEach((collection) => { assert.isArray(collection); assert.isNotEmpty(collection); collection.forEach((authz) => { spec.rfc8555.authorization(authz); assert.strictEqual(authz.status, 'pending'); }); }); testAuthz = orderAuthzCollection.pop(); testAuthzAlpn = alpnAuthzCollection.pop(); testAuthzWildcard = wildcardAuthzCollection.pop(); testAuthz.challenges.concat(testAuthzAlpn.challenges).concat(testAuthzWildcard.challenges).forEach((item) => { spec.rfc8555.challenge(item); assert.strictEqual(item.status, 'pending'); }); }); /** * Generate challenge key authorization */ it('should get challenge key authorization', async () => { testChallenge = testAuthz.challenges.find((c) => (c.type === 'http-01')); testChallengeAlpn = testAuthzAlpn.challenges.find((c) => (c.type === 'tls-alpn-01')); testChallengeWildcard = testAuthzWildcard.challenges.find((c) => (c.type === 'dns-01')); testKeyAuthorization = await testClient.getChallengeKeyAuthorization(testChallenge); testKeyAuthorizationAlpn = await testClient.getChallengeKeyAuthorization(testChallengeAlpn); testKeyAuthorizationWildcard = await testClient.getChallengeKeyAuthorization(testChallengeWildcard); [testKeyAuthorization, testKeyAuthorizationAlpn, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k)); }); /** * Deactivate identifier authorization */ it('should deactivate identifier authorization', async () => { const order = await testClient.createOrder({ identifiers: [ { type: 'dns', value: `${uuid()}.${domainName}` }, { type: 'dns', value: `${uuid()}.${domainName}` }, ], }); const authzCollection = await testClient.getAuthorizations(order); const results = await Promise.all(authzCollection.map(async (authz) => { spec.rfc8555.authorization(authz); assert.strictEqual(authz.status, 'pending'); return testClient.deactivateAuthorization(authz); })); results.forEach((authz) => { spec.rfc8555.authorization(authz); assert.strictEqual(authz.status, 'deactivated'); }); }); /** * Verify satisfied challenge */ it('should verify challenge', async () => { await cts.assertHttpChallengeCreateFn(testAuthz, testChallenge, testKeyAuthorization); await cts.assertTlsAlpnChallengeCreateFn(testAuthzAlpn, testChallengeAlpn, testKeyAuthorizationAlpn); await cts.assertDnsChallengeCreateFn(testAuthzWildcard, testChallengeWildcard, testKeyAuthorizationWildcard); await testClient.verifyChallenge(testAuthz, testChallenge); await testClient.verifyChallenge(testAuthzAlpn, testChallengeAlpn); await testClient.verifyChallenge(testAuthzWildcard, testChallengeWildcard); }); /** * Complete challenge */ it('should complete challenge', async () => { await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (challenge) => { const result = await testClient.completeChallenge(challenge); spec.rfc8555.challenge(result); assert.strictEqual(challenge.url, result.url); })); }); /** * Wait for valid challenge */ it('should wait for valid challenge status', async () => { await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c))); }); /** * Finalize order */ it('should finalize order', async () => { const finalize = await testClient.finalizeOrder(testOrder, testCsr); const finalizeAlpn = await testClient.finalizeOrder(testOrderAlpn, testCsrAlpn); const finalizeWildcard = await testClient.finalizeOrder(testOrderWildcard, testCsrWildcard); [finalize, finalizeAlpn, finalizeWildcard].forEach((f) => spec.rfc8555.order(f)); assert.strictEqual(testOrder.url, finalize.url); assert.strictEqual(testOrderAlpn.url, finalizeAlpn.url); assert.strictEqual(testOrderWildcard.url, finalizeWildcard.url); }); /** * Wait for valid order */ it('should wait for valid order status', async () => { await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o))); }); /** * Get certificate */ it('should get certificate', async () => { testCertificate = await testClient.getCertificate(testOrder); testCertificateAlpn = await testClient.getCertificate(testOrderAlpn); testCertificateWildcard = await testClient.getCertificate(testOrderWildcard); [testCertificate, testCertificateAlpn, testCertificateWildcard].forEach((cert) => { assert.isString(cert); acme.crypto.readCertificateInfo(cert); }); }); it('should get alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () { if (!capAlternateCertRoots) { this.skip(); } await Promise.all(testIssuers.map(async (issuer) => { const cert = await testClient.getCertificate(testOrder, issuer); const rootCert = acme.crypto.splitPemChain(cert).pop(); const info = acme.crypto.readCertificateInfo(rootCert); assert.strictEqual(issuer, info.issuer.commonName); })); }); it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () { if (!capAlternateCertRoots) { this.skip(); } const cert = await testClient.getCertificate(testOrder, uuid()); const rootCert = acme.crypto.splitPemChain(cert).pop(); const info = acme.crypto.readCertificateInfo(rootCert); assert.strictEqual(testIssuers[0], info.issuer.commonName); }); /** * Revoke certificate */ it('should revoke certificate', async () => { await testClient.revokeCertificate(testCertificate); await testClient.revokeCertificate(testCertificateAlpn, { reason: 0 }); await testClient.revokeCertificate(testCertificateWildcard, { reason: 4 }); }); it('should not allow getting revoked certificate', async () => { await assert.isRejected(testClient.getCertificate(testOrder)); await assert.isRejected(testClient.getCertificate(testOrderAlpn)); await assert.isRejected(testClient.getCertificate(testOrderWildcard)); }); /** * Deactivate account */ it('should deactivate account', async () => { const data = { status: 'deactivated' }; const account = await testClient.updateAccount(data); spec.rfc8555.account(account); assert.strictEqual(account.status, 'deactivated'); }); /** * Verify that no new orders can be made */ it('should not allow new orders from deactivated account', async () => { const data = { identifiers: [{ type: 'dns', value: 'nope.com' }] }; await assert.isRejected(testClient.createOrder(data)); }); }); }); }); ================================================ FILE: packages/core/acme-client/test/70-auto.spec.js ================================================ /** * ACME client.auto tests */ const { randomUUID: uuid } = require('crypto'); const { assert } = require('chai'); const cts = require('./challtestsrv'); const getCertIssuers = require('./get-cert-issuers'); const spec = require('./spec'); const acme = require('./../'); const domainName = process.env.ACME_DOMAIN_NAME || 'example.com'; const directoryUrl = process.env.ACME_DIRECTORY_URL || acme.directory.letsencrypt.staging; const capEabEnabled = (('ACME_CAP_EAB_ENABLED' in process.env) && (process.env.ACME_CAP_EAB_ENABLED === '1')); const capAlternateCertRoots = !(('ACME_CAP_ALTERNATE_CERT_ROOTS' in process.env) && (process.env.ACME_CAP_ALTERNATE_CERT_ROOTS === '0')); const clientOpts = { directoryUrl, backoffAttempts: 5, backoffMin: 1000, backoffMax: 5000, }; if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) { clientOpts.externalAccountBinding = { kid: process.env.ACME_EAB_KID, hmacKey: process.env.ACME_EAB_HMAC_KEY, }; } describe('client.auto', () => { const testDomain = `${uuid()}.${domainName}`; const testHttpDomain = `${uuid()}.${domainName}`; const testHttpsDomain = `${uuid()}.${domainName}`; const testDnsDomain = `${uuid()}.${domainName}`; const testAlpnDomain = `${uuid()}.${domainName}`; const testWildcardDomain = `${uuid()}.${domainName}`; const testSanDomains = [ `${uuid()}.${domainName}`, `${uuid()}.${domainName}`, `${uuid()}.${domainName}`, ]; /** * Pebble CTS required */ before(function () { if (!cts.isEnabled()) { this.skip(); } }); /** * Key types */ Object.entries({ rsa: { createKeyFn: () => acme.crypto.createPrivateRsaKey(), createKeyAltFns: { s1024: () => acme.crypto.createPrivateRsaKey(1024), s4096: () => acme.crypto.createPrivateRsaKey(4096), }, }, ecdsa: { createKeyFn: () => acme.crypto.createPrivateEcdsaKey(), createKeyAltFns: { p384: () => acme.crypto.createPrivateEcdsaKey('P-384'), p521: () => acme.crypto.createPrivateEcdsaKey('P-521'), }, }, }).forEach(([name, { createKeyFn, createKeyAltFns }]) => { describe(name, () => { let testIssuers; let testClient; let testCertificate; let testSanCertificate; let testWildcardCertificate; /** * Fixtures */ it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () { if (!capAlternateCertRoots) { this.skip(); } testIssuers = await getCertIssuers(); assert.isArray(testIssuers); assert.isTrue(testIssuers.length > 1); testIssuers.forEach((i) => { assert.isString(i); assert.strictEqual(1, testIssuers.filter((c) => (c === i)).length); }); }); /** * Initialize client */ it('should initialize client', async () => { testClient = new acme.Client({ ...clientOpts, accountKey: await createKeyFn(), }); }); /** * Invalid challenge response */ it('should throw on invalid challenge response', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: `${uuid()}.${domainName}`, }, await createKeyFn()); await assert.isRejected(testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.challengeNoopFn, challengeRemoveFn: cts.challengeNoopFn, }), /^authorization not found/i); }); it('should throw on invalid challenge response with opts.skipChallengeVerification=true', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: `${uuid()}.${domainName}`, }, await createKeyFn()); await assert.isRejected(testClient.auto({ csr, termsOfServiceAgreed: true, skipChallengeVerification: true, challengeCreateFn: cts.challengeNoopFn, challengeRemoveFn: cts.challengeNoopFn, })); }); /** * Challenge function exceptions */ it('should throw on challengeCreate exception', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: `${uuid()}.${domainName}`, }, await createKeyFn()); await assert.isRejected(testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.challengeThrowFn, challengeRemoveFn: cts.challengeNoopFn, }), /^oops$/); }); it('should not throw on challengeRemove exception', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: `${uuid()}.${domainName}`, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.challengeCreateFn, challengeRemoveFn: cts.challengeThrowFn, }); assert.isString(cert); }); it('should settle all challenges before rejecting', async () => { const results = []; const [, csr] = await acme.crypto.createCsr({ commonName: `${uuid()}.${domainName}`, altNames: [ `${uuid()}.${domainName}`, `${uuid()}.${domainName}`, `${uuid()}.${domainName}`, `${uuid()}.${domainName}`, ], }, await createKeyFn()); await assert.isRejected(testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: async (...args) => { if ([0, 1, 2].includes(results.length)) { results.push(false); throw new Error('oops'); } await new Promise((resolve) => { setTimeout(resolve, 500); }); results.push(true); return cts.challengeCreateFn(...args); }, challengeRemoveFn: cts.challengeRemoveFn, })); assert.strictEqual(results.length, 5); assert.deepStrictEqual(results, [false, false, false, true, true]); }); /** * Order certificates */ it('should order certificate', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: testDomain, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.challengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, }); assert.isString(cert); testCertificate = cert; }); it('should order certificate using http-01', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: testHttpDomain, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.assertHttpChallengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, challengePriority: ['http-01'], }); assert.isString(cert); }); it('should order certificate using https-01', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: testHttpsDomain, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.assertHttpsChallengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, challengePriority: ['http-01'], }); assert.isString(cert); }); it('should order certificate using dns-01', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: testDnsDomain, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.assertDnsChallengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, challengePriority: ['dns-01'], }); assert.isString(cert); }); it('should order certificate using tls-alpn-01', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: testAlpnDomain, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.assertTlsAlpnChallengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, challengePriority: ['tls-alpn-01'], }); assert.isString(cert); }); it('should order san certificate', async () => { const [, csr] = await acme.crypto.createCsr({ altNames: testSanDomains, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.challengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, }); assert.isString(cert); testSanCertificate = cert; }); it('should order wildcard certificate', async () => { const [, csr] = await acme.crypto.createCsr({ altNames: [testWildcardDomain, `*.${testWildcardDomain}`], }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.challengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, }); assert.isString(cert); testWildcardCertificate = cert; }); it('should order certificate with opts.skipChallengeVerification=true', async () => { const [, csr] = await acme.crypto.createCsr({ commonName: `${uuid()}.${domainName}`, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, skipChallengeVerification: true, challengeCreateFn: cts.challengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, }); assert.isString(cert); }); it('should order alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () { if (!capAlternateCertRoots) { this.skip(); } await Promise.all(testIssuers.map(async (issuer) => { const [, csr] = await acme.crypto.createCsr({ commonName: `${uuid()}.${domainName}`, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, preferredChain: issuer, challengeCreateFn: cts.challengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, }); const rootCert = acme.crypto.splitPemChain(cert).pop(); const info = acme.crypto.readCertificateInfo(rootCert); assert.strictEqual(issuer, info.issuer.commonName); })); }); it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () { if (!capAlternateCertRoots) { this.skip(); } const [, csr] = await acme.crypto.createCsr({ commonName: `${uuid()}.${domainName}`, }, await createKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, preferredChain: uuid(), challengeCreateFn: cts.challengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, }); const rootCert = acme.crypto.splitPemChain(cert).pop(); const info = acme.crypto.readCertificateInfo(rootCert); assert.strictEqual(testIssuers[0], info.issuer.commonName); }); /** * Order certificate with alternate key sizes */ Object.entries(createKeyAltFns).forEach(([k, altKeyFn]) => { it(`should order certificate with key=${k}`, async () => { const [, csr] = await acme.crypto.createCsr({ commonName: testDomain, }, await altKeyFn()); const cert = await testClient.auto({ csr, termsOfServiceAgreed: true, challengeCreateFn: cts.challengeCreateFn, challengeRemoveFn: cts.challengeRemoveFn, }); assert.isString(cert); }); }); /** * Read certificates */ it('should read certificate info', () => { const info = acme.crypto.readCertificateInfo(testCertificate); spec.crypto.certificateInfo(info); assert.isNull(info.domains.commonName); assert.deepStrictEqual(info.domains.altNames, [testDomain]); }); it('should read san certificate info', () => { const info = acme.crypto.readCertificateInfo(testSanCertificate); spec.crypto.certificateInfo(info); assert.isNull(info.domains.commonName); assert.deepStrictEqual(info.domains.altNames, testSanDomains); }); it('should read wildcard certificate info', () => { const info = acme.crypto.readCertificateInfo(testWildcardCertificate); spec.crypto.certificateInfo(info); assert.isNull(info.domains.commonName); assert.deepStrictEqual(info.domains.altNames, [testWildcardDomain, `*.${testWildcardDomain}`]); }); }); }); }); ================================================ FILE: packages/core/acme-client/test/challtestsrv.js ================================================ /** * Pebble Challenge Test Server integration */ const { assert } = require('chai'); const axios = require('./../src/axios'); const apiBaseUrl = process.env.ACME_CHALLTESTSRV_URL || null; const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443; /** * Send request */ async function request(apiPath, data = {}) { if (!apiBaseUrl) { throw new Error('No Pebble Challenge Test Server URL found'); } await axios.request({ url: `${apiBaseUrl}/${apiPath}`, method: 'post', data, }); return true; } /** * State */ exports.isEnabled = () => !!apiBaseUrl; /** * DNS */ exports.addDnsARecord = async (host, addresses) => request('add-a', { host, addresses }); exports.setDnsCnameRecord = async (host, target) => request('set-cname', { host, target }); /** * Challenge response */ async function addHttp01ChallengeResponse(token, content) { return request('add-http01', { token, content }); } async function addHttps01ChallengeResponse(token, content, targetHostname) { await addHttp01ChallengeResponse(token, content); return request('add-redirect', { path: `/.well-known/acme-challenge/${token}`, targetURL: `https://${targetHostname}:${httpsPort}/.well-known/acme-challenge/${token}`, }); } async function addDns01ChallengeResponse(host, value) { return request('set-txt', { host, value }); } async function addTlsAlpn01ChallengeResponse(host, content) { return request('add-tlsalpn01', { host, content }); } exports.addHttp01ChallengeResponse = addHttp01ChallengeResponse; exports.addHttps01ChallengeResponse = addHttps01ChallengeResponse; exports.addDns01ChallengeResponse = addDns01ChallengeResponse; exports.addTlsAlpn01ChallengeResponse = addTlsAlpn01ChallengeResponse; /** * Challenge response mock functions */ async function assertHttpChallengeCreateFn(authz, challenge, keyAuthorization) { assert.strictEqual(challenge.type, 'http-01'); return addHttp01ChallengeResponse(challenge.token, keyAuthorization); } async function assertHttpsChallengeCreateFn(authz, challenge, keyAuthorization) { assert.strictEqual(challenge.type, 'http-01'); return addHttps01ChallengeResponse(challenge.token, keyAuthorization, authz.identifier.value); } async function assertDnsChallengeCreateFn(authz, challenge, keyAuthorization) { assert.strictEqual(challenge.type, 'dns-01'); return addDns01ChallengeResponse(`_acme-challenge.${authz.identifier.value}.`, keyAuthorization); } async function assertTlsAlpnChallengeCreateFn(authz, challenge, keyAuthorization) { assert.strictEqual(challenge.type, 'tls-alpn-01'); return addTlsAlpn01ChallengeResponse(authz.identifier.value, keyAuthorization); } async function challengeCreateFn(authz, challenge, keyAuthorization) { if (challenge.type === 'http-01') { return assertHttpChallengeCreateFn(authz, challenge, keyAuthorization); } if (challenge.type === 'dns-01') { return assertDnsChallengeCreateFn(authz, challenge, keyAuthorization); } if (challenge.type === 'tls-alpn-01') { return assertTlsAlpnChallengeCreateFn(authz, challenge, keyAuthorization); } throw new Error(`Unsupported challenge type ${challenge.type}`); } exports.challengeRemoveFn = async () => true; exports.challengeNoopFn = async () => true; exports.challengeThrowFn = async () => { throw new Error('oops'); }; exports.assertHttpChallengeCreateFn = assertHttpChallengeCreateFn; exports.assertHttpsChallengeCreateFn = assertHttpsChallengeCreateFn; exports.assertDnsChallengeCreateFn = assertDnsChallengeCreateFn; exports.assertTlsAlpnChallengeCreateFn = assertTlsAlpnChallengeCreateFn; exports.challengeCreateFn = challengeCreateFn; ================================================ FILE: packages/core/acme-client/test/fixtures/certificate.crt ================================================ -----BEGIN CERTIFICATE----- MIIFMjCCAxoCCQCVordquLnq8TANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQGEwJB VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 cyBQdHkgTHRkMRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNzA5MTQxNDMzMTRa Fw0xODA5MTQxNDMzMTRaMFsxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFDASBgNVBAMT C2V4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwi2P YBNGl1n78niRGDKgcsWK03TcTeVbQ1HztA57Rr1iDHAZNx3Mv4E/Sha8VKbKoshc mUcOS3AlmbIZX+7+9c7lL2oD+vtUZF1YUR/69fWuO72wk6fKj/eofxH9Ud5KFje8 qrYZdJWKkPMdWlYgjD6qpA5wl60NiuxmUr44ADZDytqHzNThN3wrFruz74PcMfak cSUMxkh98LuNeGtqHpEAw+wliko3oDD4PanvDvp5mRgiQVKHEGT7dm85Up+W1iJK J65fkc/j940MaLbdISZYYCT5dtPgCGKCHgVuVrY+OXFJrD3TTm94ILsR/BkS/VSK NigGVPXg3q8tgIS++k13CzLUO0PNRMuod1RD9j5NEc2CVic9rcH06ugZyHlOcuVv vRsPGd52BPn+Jf1aePKPPQHxT9i5GOs80CJw0eduZCDZB32biRYNwUtjFkHbu8ii 2IGkvhnWonjd4w5wOldG+RPr+XoFCIaHp5TszQ+HnUTLIXKtBgzzCKjK4eZqrck7 xpo5B5m5V7EUxBze2LYVky+GsDsqL8CggQqJL4ZKuZVoxgPwhnDy5nMs057NCU9E nXcauMW9UEqEHu5NXnmGJrCvQ56wjYN3lgvCHEtmIpsRjCCWaBJYiawu1J5ZAf1y GTVNh8pEvO//zL9ImUxrSfOGUeFiN1tzSFlTfbcCAwEAATANBgkqhkiG9w0BAQUF AAOCAgEAdZZpgWv79CgF5ny6HmMaYgsXJKJyQE9RhJ1cmzDY8KAF+nzT7q4Pgt3W bA9bpdji7C0WqKjX7hLipqhgFnqb8qZcodEKhX788qBj4X45+4nT6QipyJlz5x6K cCn/v9gQNKks7U+dBlqquiVfbXaa1EAKMeGtqinf+Y51nR/fBcr/P9TBnSJqH61K DO3qrE5KGTwHQ9VXoeKyeppGt5sYf8G0vwoHhtPTOO8TuLEIlFcXtzbC3zAtmQj6 Su//fI5yjuYTkiayxMx8nCGrQhQSXdC8gYpYd0os7UY01DVu4BTCXEvf0GYXtiGJ eG8lQT/eu7WdK83uJ93U/BMYzoq4lSVcqY4LNxlfAQXKhaAbioA5XyT7co7FQ0g+ s2CGBUKa11wPDe8M2GVLPsxT2bXDQap5DQyVIuTwjtgL0tykGxPJPAnL2zuUy6T3 /YzrWaJ9Os+6mUCVdLnXtDgZ10Ujel7mq6wo9Ns+u07grXZkXpmJYnJXBrwOsY8K Za5vFwgJrDXhWe+Fmgt1EP5VIqRCQAxH2iYvAaELi8udbN/ZiUU3K9t79MP/M3U/ tEWAubHXsaAv03jRy43X0VjlZHmagU/4dU7RBWfyuwRarYIXLNT2FCd2z4kd3fsL 3rB5iI+RH0uoNuOa1+UApfFCv0O65TYkp5jEWSlU8PhKYD43nXA= -----END CERTIFICATE----- ================================================ FILE: packages/core/acme-client/test/fixtures/letsencrypt.crt ================================================ -----BEGIN CERTIFICATE----- MIIDzzCCA1WgAwIBAgISA0ghDoSv5DpT3Pd3lqwjbVDDMAoGCCqGSM49BAMDMDIx CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF NjAeFw0yNDA2MTAxNzEyMjZaFw0yNDA5MDgxNzEyMjVaMBQxEjAQBgNVBAMTCWxl bmNyLm9yZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHJ3DjN7pYV3mftHzaP V/WI0RhOJnSI5AIFEPFHDi8UowOINRGIfm9FHGIDqrb4Rmyvr9JrrqBdFGDen8BW 6OGjggJnMIICYzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEG CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFIdCTnxqmpOELDyzPaEM seB36lUOMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMFUGCCsGAQUF BwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL2U2Lm8ubGVuY3Iub3JnMCIGCCsG AQUFBzAChhZodHRwOi8vZTYuaS5sZW5jci5vcmcvMG8GA1UdEQRoMGaCCWxlbmNy Lm9yZ4IPbGV0c2VuY3J5cHQuY29tgg9sZXRzZW5jcnlwdC5vcmeCDXd3dy5sZW5j ci5vcmeCE3d3dy5sZXRzZW5jcnlwdC5jb22CE3d3dy5sZXRzZW5jcnlwdC5vcmcw EwYDVR0gBAwwCjAIBgZngQwBAgEwggEFBgorBgEEAdZ5AgQCBIH2BIHzAPEAdgA/ F0tP1yJHWJQdZRyEvg0S7ZA3fx+FauvBvyiF7PhkbgAAAZADWfneAAAEAwBHMEUC IGlp+dPU2hLT2suTMYkYMlt/xbzSnKLZDA/wYSsPACP7AiEAxbAzx6mkzn0cs0hh ti6sLf0pcbmDhxHdlJRjuo6SQZEAdwDf4VbrqgWvtZwPhnGNqMAyTq5W2W6n9aVq AdHBO75SXAAAAZADWfqrAAAEAwBIMEYCIQCrAmDUrlX3oGhri1qCIb65Cuf8h2GR LC1VfXBenX7dCAIhALXwbhCQ1vO1WLv4CqyihMHOwFaICYqN/N6ylaBlVAM4MAoG CCqGSM49BAMDA2gAMGUCMFdgjOXGl+hE2ABDsAeuNq8wi34yTMUHk0KMTOjRAfy9 rOCGQqvP0myoYlyzXOH9uQIxAMdkG1ZWBZS1dHavbPf1I/MjYpzX6gy0jVHIXXu5 aYWylBi/Uf2RPj0LWFZh8tNa1Q== -----END CERTIFICATE----- ================================================ FILE: packages/core/acme-client/test/fixtures/private.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAo0nBEFBeo2XnR1kx0jV00W9EszE5Ei/zuJKLXXwTeUGMhy9h CqPFWQnTOD5PQUcja98p96LCdRZpfsfoL1RewksD6BCJN+9hZucImBASpmg2M432 wsF7fa3/FMtICrEmQh+LJ48zOottr93YcipY1fNxHWzFr8Hvv+OZCMmQvL4W5u5U rxdi7jptbAbFvv470TN8lpVwneG7kG3cemPhW6RmfTTUQ2Qp/QP6fptpWIIy5kQe zvgpql2xRPBjqb4VDy1kVTTCe/Lpt7bNGe2eZOzXJcjrU+d5LEOrfQoX5ZO4H5UC 9YlT6wqyPv9VZ5g2slz198LGV8hdGEVix8XPiQIDAQABAoIBAQCaoo4jVPlK5IZS GzYDTHyEmksFJ+hUQPUeJim1LnuCqYDbxRKxcMbDu3o8GUYVG7l/vqePzKM7Hy5o 0gggSlYyybe5XW+VeS1UthZ9azs+PBKYYCj/5xt7ufuHRbvD5F/G3vh5TjPFjaUi l4UTGOdoNlM4+nl8KL1Ti8axe7GGCztxmjJL7VnN4RWc5yzBrU6oiQED0BM/6KFx nJHPuwzRemRRjz8Lk1ryMsCymtZx70slxVJeHPdoMc9vkseOulooBMZtXqOixoHO UtFuKGgIkg6KA9qI+8RmqSPUeXrbrPeRZtu3N9NcsPUYVptNo1ZjLpa9Eigd0tkq 1+/TyGDBAoGBANCnK/+uXIZWt4QoF+7AUGeckOmRAbJnWf8KrScSa/TSGNObGP00 LpeM10eNDqvfY9RepM6RH5R75vDWltJd4+fyQPaHqG4AhlMk1JglZn71F91FWstx K/qrPfnBQP7qq4yuQ0zavPkgIUWzryLk0JnQ4wPNLiXFAfQYDt+F8Vg3AoGBAMhX S+sej87zRHbV/wj7zLa/QwnDLiU7wswv9zUf2Ot+49pfwEzzSGuLHFHoIXVrGo2y QQl6sovJ6dFi7GFPikiwj9em/EF4JgTmWZhoYH1HmThTUeziLa2/VT4eIZn7Viwb /goxKAvGvHkcdQIeNPPFaEi0m7vDkTAv/WG/prY/AoGAcKWwVWuXPFfY4BqdQSLG xgl7Gv5UgjLWHaFv9iY17oj3KlcT2K+xb9Rz7Yc0IoqKZP9rzrH+8LUr616PMqfK AVGCzRZUUn8qBf1eYX3fpi9AYQ+ugyNocP6+iPZS1s1vLJZwcy+s0nsMO4tUxGvw SvrBdS3y+iUwds3+SaMQt2UCgYAg0BuBIPpQ3QtDo30oDYXUELN8L9mpA4a+RsTo kJTIzXmoVLJ8aAReiORUjf6c6rPorV91nAEOYD3Jq7gnoA14JmMI4TLDzlf7yXa3 PbFAE7AGx67Na6YrpQDjMbAzNjVA+Dy9kpuKgjxwYbbQZ/4oRxbzgZFYSYnIKLQJ hIhbpQKBgEc8fYYc3UqyNIGNupYBZhb0pF7FPMwldzv4UufMjkYzKREaCT2D3HIC FEKiJxatIhOtCW5oa5sXK/mGR9EEzW1ltlljymu0u+slIoWvWWGfQU9DxFBnZ2x5 4/nzeq4zI+qYt8qXZTnhY/bpZI0pdQqWT9+AoFJJn8Bfdk1mLuIs -----END RSA PRIVATE KEY----- ================================================ FILE: packages/core/acme-client/test/fixtures/san-certificate.crt ================================================ -----BEGIN CERTIFICATE----- MIIC3zCCAcegAwIBAgIJAPZkD9qD+FX8MA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV BAMMC2V4YW1wbGUuY29tMB4XDTE3MDkyMTIwMzY1MFoXDTE4MDkyMTIwMzY1MFow FjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw ggEKAoIBAQDNygRzPEyGP90Q3ms1DzUIj597u4t22TU08TQywMTt+/Sd+LNDvgwI yhCbbwetVq+rvEayAMaQjFzqgoQOxY8GDrrqfPfQ50ED79vu5VPaqVSTN5FwK7hq 6Bl+kT2MUMIwhhGTfrn7inGhxB1hhYtAaUJDuLN2JjB6Ax9BfVv5NJLPeN1V6qdV edtmNrUV5eWwEPfl4kCJ8Ytes6YttN2UDnet/B19po3/JEdy5YgPmeAfW1wbA+kl oU475uPpKPV79M+6hrKNlS2hPFcGOiL/7glKgXURg7Ih+e53Qx6tgqKrgmjRM8Jq 0bLwM1+xY0O/2C9wbkpElBLU9CKS9I+PAgMBAAGjMDAuMCwGA1UdEQQlMCOCEHRl c3QuZXhhbXBsZS5jb22CD2FiYy5leGFtcGxlLmNvbTANBgkqhkiG9w0BAQUFAAOC AQEAGCVsiJZOe0LVYQ40a/N/PaVVs1zj1KXmVDrKEWW8fhjEMao/j/Bb4rXuKCkC DQIZR1jsFC4IWyL4eOpUp4SPFn6kbdzhxjl+42kuzQLqTc1EiEobwEbroQSoUJpT xj2j0YnrFn/9hVBHUgsA3tONNXL5McEtiHOQ+iXUmoPw9sRvs0DEshS1XeYvTuzY Jua6uev1QBXxll3+pw7i2Wbt9ifeX6NBe+MOGIYxn6aMwwmgtoLbxDMThtVJJGCH V0JrSBhEkVlfK1LukSUeSO1RpCsV+97Xx2jEsNwbiji/xKnXk44sVJhJ/yQnWkiC wZLUm/SNOOtPT68U5RopRC0IXA== -----END CERTIFICATE----- ================================================ FILE: packages/core/acme-client/test/get-cert-issuers.js ================================================ /** * Get ACME certificate issuers */ const acme = require('./../'); const util = require('./../src/util'); const pebbleManagementUrl = process.env.ACME_PEBBLE_MANAGEMENT_URL || null; /** * Pebble */ async function getPebbleCertIssuers() { /* Get intermediate certificate and resolve alternates */ const root = await acme.axios.get(`${pebbleManagementUrl}/intermediates/0`); const links = util.parseLinkHeader(root.headers.link || ''); const alternates = await Promise.all(links.map(async (link) => acme.axios.get(link))); /* Get certificate info */ const certs = [root].concat(alternates).map((c) => c.data); const info = certs.map((c) => acme.crypto.readCertificateInfo(c)); /* Return issuers */ return info.map((i) => i.issuer.commonName); } /** * Get certificate issuers */ module.exports = async () => { if (pebbleManagementUrl) { return getPebbleCertIssuers(); } throw new Error('Unable to resolve list of certificate issuers'); }; ================================================ FILE: packages/core/acme-client/test/retry.js ================================================ const { assert } = require('chai'); const Promise = require('bluebird'); const util = require('../src/util'); let count = 0; async function apiRequest() { count += 1; console.log(new Date(), 'retry count:', count); await Promise.delay(2000); return count; } async function waitForValidStatus() { const verifyFn = async (abort) => { const resp = await apiRequest(); /* Verify status */ console.log(new Date(), 'Item has status', resp); if (count < 3) { abort(); throw new Error(`${new Date()}error`, count); } console.log(new Date(), 'success'); return 'success'; // if (resp.data.status === 'invalid') { // abort(); // throw new Error(util.formatResponseError(resp)); // } // else if (resp.data.status === 'pending') { // throw new Error('Operation is pending'); // } // else if (resp.data.status === 'valid') { // return resp.data; // } // // throw new Error(`Unexpected item status: ${resp.data.status}`); }; console.log(new Date(), 'Waiting for valid status from', this.backoffOpts); return util.retry(verifyFn, this.backoffOpts); } /** */ describe('util', () => { it('retry', async function() { this.timeout(100000); try { await waitForValidStatus(); } catch (e) { console.error('1111', e); } // await Promise.delay(100000); }); }); ================================================ FILE: packages/core/acme-client/test/setup.js ================================================ /** * Setup testing */ const fs = require('fs'); const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); const axios = require('./../src/axios'); /** * Add promise support to Chai */ chai.use(chaiAsPromised); /** * Challenge test server ports */ if (process.env.ACME_HTTP_PORT) { axios.defaults.acmeSettings.httpChallengePort = process.env.ACME_HTTP_PORT; } if (process.env.ACME_HTTPS_PORT) { axios.defaults.acmeSettings.httpsChallengePort = process.env.ACME_HTTPS_PORT; } if (process.env.ACME_TLSALPN_PORT) { axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT; } /** * Greatly reduce retry duration while testing */ axios.defaults.acmeSettings.retryMaxAttempts = 3; axios.defaults.acmeSettings.retryDefaultDelay = 1; /** * External account binding */ if (('ACME_CAP_EAB_ENABLED' in process.env) && (process.env.ACME_CAP_EAB_ENABLED === '1')) { const pebbleConfig = JSON.parse(fs.readFileSync('/etc/pebble/pebble.json').toString()); const [kid, hmacKey] = Object.entries(pebbleConfig.pebble.externalAccountMACKeys)[0]; process.env.ACME_EAB_KID = kid; process.env.ACME_EAB_HMAC_KEY = hmacKey; } ================================================ FILE: packages/core/acme-client/test/soa.spec.mjs ================================================ import {assert} from 'chai' import {resolveDomainBySoaRecord} from "../src/util.js" describe('dns', () => { it('resolveDomainBySoaRecord', async () => { const resp = await resolveDomainBySoaRecord("a.corp.smartdeer.com") assert.equal(resp, "smartdeer.com") }); }) ================================================ FILE: packages/core/acme-client/test/spec.js ================================================ /** * Assertions */ const { assert } = require('chai'); const spec = {}; module.exports = spec; /** * ACME */ spec.rfc8555 = {}; spec.rfc8555.account = (obj) => { assert.isObject(obj); assert.isString(obj.status); assert.include(['valid', 'deactivated', 'revoked'], obj.status); assert.isString(obj.orders); if ('contact' in obj) { assert.isArray(obj.contact); obj.contact.forEach((c) => assert.isString(c)); } if ('termsOfServiceAgreed' in obj) { assert.isBoolean(obj.termsOfServiceAgreed); } if ('externalAccountBinding' in obj) { assert.isObject(obj.externalAccountBinding); } }; spec.rfc8555.order = (obj) => { assert.isObject(obj); assert.isString(obj.status); assert.include(['pending', 'ready', 'processing', 'valid', 'invalid'], obj.status); assert.isArray(obj.identifiers); obj.identifiers.forEach((i) => spec.rfc8555.identifier(i)); assert.isArray(obj.authorizations); obj.authorizations.forEach((a) => assert.isString(a)); assert.isString(obj.finalize); if ('expires' in obj) { assert.isString(obj.expires); } if ('notBefore' in obj) { assert.isString(obj.notBefore); } if ('notAfter' in obj) { assert.isString(obj.notAfter); } if ('error' in obj) { assert.isObject(obj.error); } if ('certificate' in obj) { assert.isString(obj.certificate); } /* Augmentations */ assert.isString(obj.url); }; spec.rfc8555.authorization = (obj) => { assert.isObject(obj); spec.rfc8555.identifier(obj.identifier); assert.isString(obj.status); assert.include(['pending', 'valid', 'invalid', 'deactivated', 'expires', 'revoked'], obj.status); assert.isArray(obj.challenges); obj.challenges.forEach((c) => spec.rfc8555.challenge(c)); if ('expires' in obj) { assert.isString(obj.expires); } if ('wildcard' in obj) { assert.isBoolean(obj.wildcard); } /* Augmentations */ assert.isString(obj.url); }; spec.rfc8555.identifier = (obj) => { assert.isObject(obj); assert.isString(obj.type); assert.isString(obj.value); }; spec.rfc8555.challenge = (obj) => { assert.isObject(obj); assert.isString(obj.type); assert.isString(obj.url); assert.isString(obj.status); assert.include(['pending', 'processing', 'valid', 'invalid'], obj.status); if ('validated' in obj) { assert.isString(obj.validated); } if ('error' in obj) { assert.isObject(obj.error); } }; /** * Crypto */ spec.crypto = {}; spec.crypto.csrDomains = (obj) => { assert.isObject(obj); assert.isDefined(obj.commonName); assert.isArray(obj.altNames); obj.altNames.forEach((a) => assert.isString(a)); }; spec.crypto.certificateInfo = (obj) => { assert.isObject(obj); assert.isObject(obj.issuer); assert.isDefined(obj.issuer.commonName); assert.isObject(obj.domains); assert.isDefined(obj.domains.commonName); assert.isArray(obj.domains.altNames); obj.domains.altNames.forEach((a) => assert.isString(a)); assert.strictEqual(Object.prototype.toString.call(obj.notBefore), '[object Date]'); assert.strictEqual(Object.prototype.toString.call(obj.notAfter), '[object Date]'); }; /** * JWK */ spec.jwk = {}; spec.jwk.rsa = (obj) => { assert.isObject(obj); assert.isString(obj.e); assert.isString(obj.kty); assert.isString(obj.n); assert.strictEqual(obj.e, 'AQAB'); assert.strictEqual(obj.kty, 'RSA'); }; spec.jwk.ecdsa = (obj) => { assert.isObject(obj); assert.isString(obj.crv); assert.isString(obj.kty); assert.isString(obj.x); assert.isString(obj.y); assert.strictEqual(obj.kty, 'EC'); }; ================================================ FILE: packages/core/acme-client/types/index.d.ts ================================================ /** * acme-client type definitions */ import { AxiosInstance } from 'axios'; import * as rfc8555 from './rfc8555'; import {CancelError} from '../src/error.js' export * from '../src/error.js' export type PrivateKeyBuffer = Buffer; export type PublicKeyBuffer = Buffer; export type CertificateBuffer = Buffer; export type CsrBuffer = Buffer; export type PrivateKeyString = string; export type PublicKeyString = string; export type CertificateString = string; export type CsrString = string; /** * Augmented ACME interfaces */ export interface Order extends rfc8555.Order { url: string; } export interface Authorization extends rfc8555.Authorization { url: string; } export type UrlMapping={ enabled: boolean mappings: Record } /** * Client */ export interface ClientOptions { sslProvider:string; directoryUrl: string; accountKey: PrivateKeyBuffer | PrivateKeyString; accountUrl?: string; externalAccountBinding?: ClientExternalAccountBindingOptions; backoffAttempts?: number; backoffMin?: number; backoffMax?: number; urlMapping?: UrlMapping; signal?: AbortSignal; } export interface ClientExternalAccountBindingOptions { kid: string; hmacKey: string; } export interface ClientAutoOptions { csr: CsrBuffer | CsrString; challengeCreateFn: (authz: Authorization, keyAuthorization: (challenge:rfc8555.Challenge)=>Promise) => Promise<{recordReq?:any,recordRes?:any,dnsProvider?:any,challenge: rfc8555.Challenge,keyAuthorization:string}>; challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string,recordReq:any, recordRes:any,dnsProvider:any,httpUploader:any) => Promise; email?: string; termsOfServiceAgreed?: boolean; skipChallengeVerification?: boolean; challengePriority?: string[]; preferredChain?: string; signal?: AbortSignal; profile?:string; } export class Client { constructor(opts: ClientOptions); getTermsOfServiceUrl(): Promise; getAccountUrl(): string; createAccount(data?: rfc8555.AccountCreateRequest): Promise; updateAccount(data?: rfc8555.AccountUpdateRequest): Promise; updateAccountKey(newAccountKey: PrivateKeyBuffer | PrivateKeyString, data?: object): Promise; createOrder(data: rfc8555.OrderCreateRequest): Promise; getOrder(order: Order): Promise; finalizeOrder(order: Order, csr: CsrBuffer | CsrString): Promise; getAuthorizations(order: Order): Promise; deactivateAuthorization(authz: Authorization): Promise; getChallengeKeyAuthorization(challenge: rfc8555.Challenge): Promise; verifyChallenge(authz: Authorization, challenge: rfc8555.Challenge): Promise; completeChallenge(challenge: rfc8555.Challenge): Promise; waitForValidStatus(item: T): Promise; getCertificate(order: Order, preferredChain?: string): Promise; revokeCertificate(cert: CertificateBuffer | CertificateString, data?: rfc8555.CertificateRevocationRequest): Promise; auto(opts: ClientAutoOptions): Promise; } /** * Directory URLs */ export const directory: { buypass: { staging: string, production: string }, google: { staging: string, production: string }, letsencrypt: { staging: string, production: string }, zerossl: { staging: string, production: string } }; /** * Crypto */ export interface CertificateDomains { commonName: string; altNames: string[]; } export interface CertificateIssuer { commonName: string; } export interface CertificateInfo { issuer: CertificateIssuer; domains: CertificateDomains; notAfter: Date; notBefore: Date; } export interface CsrOptions { keySize?: number; commonName?: string; altNames?: string[]; country?: string; state?: string; locality?: string; organization?: string; organizationUnit?: string; emailAddress?: string; } export interface RsaPublicJwk { e: string; kty: string; n: string; } export interface EcdsaPublicJwk { crv: string; kty: string; x: string; y: string; } export interface CryptoInterface { createPrivateKey(keySize?: number,encodingType?:string): Promise; createPrivateRsaKey(keySize?: number,encodingType?:string): Promise; createPrivateEcdsaKey(namedCurve?: 'P-256' | 'P-384' | 'P-521',encodingType?:string): Promise; getPublicKey(keyPem: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString): PublicKeyBuffer; getJwk(keyPem: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString): RsaPublicJwk | EcdsaPublicJwk; splitPemChain(chainPem: CertificateBuffer | CertificateString): string[]; getPemBodyAsB64u(pem: CertificateBuffer | CertificateString): string; readCsrDomains(csrPem: CsrBuffer | CsrString): CertificateDomains; readCertificateInfo(certPem: CertificateBuffer | CertificateString): CertificateInfo; createCsr(data: CsrOptions, keyPem?: PrivateKeyBuffer | PrivateKeyString,encodingType?:string): Promise<[PrivateKeyBuffer, CsrBuffer]>; createAlpnCertificate(authz: Authorization, keyAuthorization: string, keyPem?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CertificateBuffer]>; isAlpnCertificateAuthorizationValid(certPem: CertificateBuffer | CertificateString, keyAuthorization: string): boolean; } export const crypto: CryptoInterface; /* TODO: LEGACY */ export interface CryptoLegacyInterface { createPrivateKey(size?: number): Promise; createPublicKey(key: PrivateKeyBuffer | PrivateKeyString): Promise; getPemBody(str: string): string; splitPemChain(str: string): string[]; getModulus(input: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString | CertificateBuffer | CertificateString | CsrBuffer | CsrString): Promise; getPublicExponent(input: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString | CertificateBuffer | CertificateString | CsrBuffer | CsrString): Promise; readCsrDomains(csr: CsrBuffer | CsrString): Promise; readCertificateInfo(cert: CertificateBuffer | CertificateString): Promise; createCsr(data: CsrOptions, key?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CsrBuffer]>; } export const forge: CryptoLegacyInterface; /** * Axios */ export const axios: AxiosInstance; export const agents: any; /** * Logger */ export function setLogger(fn: (message: any, ...args: any[]) => void): void; export function walkTxtRecord(record: any): Promise; export function getAuthoritativeDnsResolver(record:string): Promise; export const CancelError: typeof CancelError; export function resolveDomainBySoaRecord(domain: string): Promise; ================================================ FILE: packages/core/acme-client/types/index.test-d.ts ================================================ /** * acme-client type definition tests */ import * as acme from 'acme-client'; (async () => { /* Client */ const accountKey = await acme.crypto.createPrivateKey(); const client = new acme.Client({ accountKey, directoryUrl: acme.directory.letsencrypt.staging }); /* Account */ await client.createAccount({ termsOfServiceAgreed: true, contact: ['mailto:test@example.com'] }); /* Order */ const order = await client.createOrder({ identifiers: [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' }, ] }); await client.getOrder(order); /* Authorizations / Challenges */ const authorizations = await client.getAuthorizations(order); const authorization = authorizations[0]; const challenge = authorization.challenges[0]; await client.getChallengeKeyAuthorization(challenge); await client.verifyChallenge(authorization, challenge); await client.completeChallenge(challenge); await client.waitForValidStatus(challenge); /* Finalize */ const [certKey, certCsr] = await acme.crypto.createCsr({ commonName: 'example.com', altNames: ['example.com', '*.example.com'] }); await client.finalizeOrder(order, certCsr); await client.getCertificate(order); await client.getCertificate(order, 'DST Root CA X3'); /* Auto */ await client.auto({ csr: certCsr, challengeCreateFn: async (authz, challenge, keyAuthorization) => {}, challengeRemoveFn: async (authz, challenge, keyAuthorization) => {} }); await client.auto({ csr: certCsr, email: 'test@example.com', termsOfServiceAgreed: false, skipChallengeVerification: false, challengePriority: ['http-01', 'dns-01'], preferredChain: 'DST Root CA X3', challengeCreateFn: async (authz, challenge, keyAuthorization) => {}, challengeRemoveFn: async (authz, challenge, keyAuthorization) => {} }); })(); ================================================ FILE: packages/core/acme-client/types/rfc8555.d.ts ================================================ /** * Account * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2 */ export interface Account { status: 'valid' | 'deactivated' | 'revoked'; orders: string; contact?: string[]; termsOfServiceAgreed?: boolean; externalAccountBinding?: object; } export interface AccountCreateRequest { contact?: string[]; termsOfServiceAgreed?: boolean; onlyReturnExisting?: boolean; externalAccountBinding?: object; } export interface AccountUpdateRequest { status?: string; contact?: string[]; termsOfServiceAgreed?: boolean; } /** * Order * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4 */ export interface Order { status: 'pending' | 'ready' | 'processing' | 'valid' | 'invalid'; identifiers: Identifier[]; authorizations: string[]; finalize: string; expires?: string; notBefore?: string; notAfter?: string; error?: object; certificate?: string; } export interface OrderCreateRequest { identifiers: Identifier[]; notBefore?: string; notAfter?: string; } /** * Authorization * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.4 */ export interface Authorization { identifier: Identifier; status: 'pending' | 'valid' | 'invalid' | 'deactivated' | 'expired' | 'revoked'; challenges: Challenge[]; expires?: string; wildcard?: boolean; } export interface Identifier { type: string; value: string; } /** * Challenge * * https://datatracker.ietf.org/doc/html/rfc8555#section-8 * https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-8.4 */ export interface ChallengeAbstract { type: string; url: string; status: 'pending' | 'processing' | 'valid' | 'invalid'; validated?: string; error?: object; } export interface HttpChallenge extends ChallengeAbstract { type: 'http-01'; token: string; } export interface DnsChallenge extends ChallengeAbstract { type: 'dns-01'; token: string; } export type Challenge = HttpChallenge | DnsChallenge; /** * Certificate * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.6 */ export enum CertificateRevocationReason { Unspecified = 0, KeyCompromise = 1, CACompromise = 2, AffiliationChanged = 3, Superseded = 4, CessationOfOperation = 5, CertificateHold = 6, RemoveFromCRL = 8, PrivilegeWithdrawn = 9, AACompromise = 10, } export interface CertificateRevocationRequest { reason?: CertificateRevocationReason; } ================================================ FILE: packages/core/basic/.eslintrc ================================================ { "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint" ], "extends": [ "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "prettier" ], "env": { "mocha": true }, "rules": { "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-unused-vars": "off" } } ================================================ FILE: packages/core/basic/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? test/user.secret.* test/**/*.js src/**/*.spec.ts ================================================ FILE: packages/core/basic/.npmignore ================================================ node_modules src dist/**/*.spec.* ================================================ FILE: packages/core/basic/.npmrc ================================================ link-workspace-packages=deep prefer-workspace-packages=true ================================================ FILE: packages/core/basic/.prettierrc ================================================ { "printWidth": 220, "bracketSpacing": true, "singleQuote": false, "trailingComma": "es5", "arrowParens": "avoid" } ================================================ FILE: packages/core/basic/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [1.36.10](https://github.com/certd/certd/compare/v1.36.9...v1.36.10) (2025-07-18) **Note:** Version bump only for package @certd/basic ## [1.36.9](https://github.com/certd/certd/compare/v1.36.7...v1.36.9) (2025-07-15) **Note:** Version bump only for package @certd/basic ## [1.36.7](https://github.com/certd/certd/compare/v1.36.6...v1.36.7) (2025-07-15) **Note:** Version bump only for package @certd/basic ## [1.36.6](https://github.com/certd/certd/compare/v1.36.5...v1.36.6) (2025-07-14) **Note:** Version bump only for package @certd/basic ## [1.36.5](https://github.com/certd/certd/compare/v1.36.4...v1.36.5) (2025-07-11) **Note:** Version bump only for package @certd/basic ## [1.36.4](https://github.com/certd/certd/compare/v1.36.3...v1.36.4) (2025-07-10) **Note:** Version bump only for package @certd/basic ## [1.36.3](https://github.com/certd/certd/compare/v1.36.2...v1.36.3) (2025-07-07) **Note:** Version bump only for package @certd/basic ## [1.36.2](https://github.com/certd/certd/compare/v1.36.1...v1.36.2) (2025-07-06) ### Performance Improvements * 证书检查支持自定义dns服务器 ([c53bb7c](https://github.com/certd/certd/commit/c53bb7cf677faa32729709ae0c10359db5194d7a)) ## [1.36.1](https://github.com/certd/certd/compare/v1.36.0...v1.36.1) (2025-07-02) **Note:** Version bump only for package @certd/basic # [1.36.0](https://github.com/certd/certd/compare/v1.35.5...v1.36.0) (2025-07-01) **Note:** Version bump only for package @certd/basic ## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20) **Note:** Version bump only for package @certd/basic ## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13) **Note:** Version bump only for package @certd/basic ## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12) **Note:** Version bump only for package @certd/basic ## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09) **Note:** Version bump only for package @certd/basic ## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07) **Note:** Version bump only for package @certd/basic # [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05) **Note:** Version bump only for package @certd/basic ## [1.34.11](https://github.com/certd/certd/compare/v1.34.10...v1.34.11) (2025-06-05) **Note:** Version bump only for package @certd/basic ## [1.34.10](https://github.com/certd/certd/compare/v1.34.9...v1.34.10) (2025-06-03) ### Performance Improvements * 支持部署到飞牛OS ([ddfd0fb](https://github.com/certd/certd/commit/ddfd0fb81d6638352920261065f1ab8e27bdd564)) * 支持日志写入文件 ([37edbf5](https://github.com/certd/certd/commit/37edbf5824d6aaae68ea1ef7259c6f739d418d2c)) ## [1.34.9](https://github.com/certd/certd/compare/v1.34.8...v1.34.9) (2025-05-30) **Note:** Version bump only for package @certd/basic ## [1.34.8](https://github.com/certd/certd/compare/v1.34.7...v1.34.8) (2025-05-28) **Note:** Version bump only for package @certd/basic ## [1.34.7](https://github.com/certd/certd/compare/v1.34.6...v1.34.7) (2025-05-26) **Note:** Version bump only for package @certd/basic ## [1.34.6](https://github.com/certd/certd/compare/v1.34.5...v1.34.6) (2025-05-25) **Note:** Version bump only for package @certd/basic ## [1.34.5](https://github.com/certd/certd/compare/v1.34.4...v1.34.5) (2025-05-19) ### Performance Improvements * 支持部署到宝塔aaWAF ([094565c](https://github.com/certd/certd/commit/094565ccd619ef671c6c11ce5fb7fd54a7a21d1c)) ## [1.34.4](https://github.com/certd/certd/compare/v1.34.3...v1.34.4) (2025-05-16) ### Bug Fixes * 修复导入在线插件不生效的bug ([fcf8309](https://github.com/certd/certd/commit/fcf8309c238208281ecb4575b2c3cfe50c11d783)) * 修复自建插件保存丢失部署策略的bug ([863e74d](https://github.com/certd/certd/commit/863e74dd2e3912f950ff5025b5ed0070aeb37035)) ## [1.34.3](https://github.com/certd/certd/compare/v1.34.2...v1.34.3) (2025-05-15) **Note:** Version bump only for package @certd/basic ## [1.34.2](https://github.com/certd/certd/compare/v1.34.1...v1.34.2) (2025-05-11) **Note:** Version bump only for package @certd/basic ## [1.34.1](https://github.com/certd/certd/compare/v1.34.0...v1.34.1) (2025-05-05) ### Performance Improvements * 支持部署证书到火山dcdn ([5f85219](https://github.com/certd/certd/commit/5f852194953dc1b4e6336770f417507b8f5a33ad)) # [1.34.0](https://github.com/certd/certd/compare/v1.33.8...v1.34.0) (2025-04-28) **Note:** Version bump only for package @certd/basic ## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26) **Note:** Version bump only for package @certd/basic ## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22) ### Performance Improvements * 支持51dns ([96a0900](https://github.com/certd/certd/commit/96a0900edc95dcfd9acccf9d13592f12f5a09b3d)) ## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20) **Note:** Version bump only for package @certd/basic ## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17) ### Performance Improvements * 多重认证登录 ([0f82cf4](https://github.com/certd/certd/commit/0f82cf409bc60706ab07e4ca4f272b9a1ca7eecb)) ## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15) **Note:** Version bump only for package @certd/basic ## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14) **Note:** Version bump only for package @certd/basic ## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12) ### Bug Fixes * 修复某些情况下无法输出日志的bug ([70101bf](https://github.com/certd/certd/commit/70101bfa7ade65678d9202c804bbae2cb808b594)) ## [1.33.1](https://github.com/certd/certd/compare/v1.33.0...v1.33.1) (2025-04-12) **Note:** Version bump only for package @certd/basic # [1.33.0](https://github.com/certd/certd/compare/v1.32.0...v1.33.0) (2025-04-11) **Note:** Version bump only for package @certd/basic # [1.32.0](https://github.com/certd/certd/compare/v1.31.11...v1.32.0) (2025-04-04) **Note:** Version bump only for package @certd/basic ## [1.31.11](https://github.com/certd/certd/compare/v1.31.10...v1.31.11) (2025-04-02) **Note:** Version bump only for package @certd/basic ## [1.31.10](https://github.com/certd/certd/compare/v1.31.9...v1.31.10) (2025-03-29) **Note:** Version bump only for package @certd/basic ## [1.31.9](https://github.com/certd/certd/compare/v1.31.8...v1.31.9) (2025-03-28) **Note:** Version bump only for package @certd/basic ## [1.31.8](https://github.com/certd/certd/compare/v1.31.7...v1.31.8) (2025-03-26) ### Performance Improvements * 支持又拍云cdn ([fd0536b](https://github.com/certd/certd/commit/fd0536bd4b41f15b6b5d42e0b447f0dcbf73b8a8)) ## [1.31.7](https://github.com/certd/certd/compare/v1.31.6...v1.31.7) (2025-03-24) **Note:** Version bump only for package @certd/basic ## [1.31.6](https://github.com/certd/certd/compare/v1.31.5...v1.31.6) (2025-03-24) **Note:** Version bump only for package @certd/basic ## [1.31.5](https://github.com/certd/certd/compare/v1.31.4...v1.31.5) (2025-03-22) **Note:** Version bump only for package @certd/basic ## [1.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21) **Note:** Version bump only for package @certd/basic ## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13) ### Performance Improvements * 支持部署到天翼云CDN ([82a72e0](https://github.com/certd/certd/commit/82a72e0b497efa043d342ad0e33c083a2de79a05)) ## [1.31.2](https://github.com/certd/certd/compare/v1.31.1...v1.31.2) (2025-03-12) **Note:** Version bump only for package @certd/basic ## [1.31.1](https://github.com/certd/certd/compare/v1.31.0...v1.31.1) (2025-03-11) **Note:** Version bump only for package @certd/basic # [1.31.0](https://github.com/certd/certd/compare/v1.30.6...v1.31.0) (2025-03-10) ### Performance Improvements * 流水线同一个阶段任务优化为并行执行 ([efa9c74](https://github.com/certd/certd/commit/efa9c748c5c07fc950af3db742ef9310f1ac9a4b)) * 支持易盾RCDN部署 ([065713c](https://github.com/certd/certd/commit/065713cdb6953d16df08585c316c1a7a8eaec437)) ## [1.30.6](https://github.com/certd/certd/compare/v1.30.5...v1.30.6) (2025-02-24) ### Performance Improvements * 支持新版本LeCDN ([44d43f4](https://github.com/certd/certd/commit/44d43f45cb9094619df7494c2a64a51ba77ad116)) ## [1.30.5](https://github.com/certd/certd/compare/v1.30.4...v1.30.5) (2025-02-14) **Note:** Version bump only for package @certd/basic ## [1.30.4](https://github.com/certd/certd/compare/v1.30.3...v1.30.4) (2025-02-14) **Note:** Version bump only for package @certd/basic ## [1.30.3](https://github.com/certd/certd/compare/v1.30.2...v1.30.3) (2025-02-13) **Note:** Version bump only for package @certd/basic ## [1.30.2](https://github.com/certd/certd/compare/v1.30.1...v1.30.2) (2025-02-09) **Note:** Version bump only for package @certd/basic ## [1.30.1](https://github.com/certd/certd/compare/v1.30.0...v1.30.1) (2025-01-20) **Note:** Version bump only for package @certd/basic # [1.30.0](https://github.com/certd/certd/compare/v1.29.5...v1.30.0) (2025-01-19) **Note:** Version bump only for package @certd/basic ## [1.29.5](https://github.com/certd/certd/compare/v1.29.4...v1.29.5) (2025-01-07) **Note:** Version bump only for package @certd/basic ## [1.29.4](https://github.com/certd/certd/compare/v1.29.3...v1.29.4) (2025-01-06) **Note:** Version bump only for package @certd/basic ## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04) **Note:** Version bump only for package @certd/basic ## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25) **Note:** Version bump only for package @certd/basic ## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25) ### Bug Fixes * 修复某处金额转换丢失精度的bug ([d2d6f12](https://github.com/certd/certd/commit/d2d6f12218cbe7bd55f4ae082b93084be85f0a7b)) # [1.29.0](https://github.com/certd/certd/compare/v1.28.4...v1.29.0) (2024-12-24) ### Features * 用户套餐,用户支付功能 ([a019956](https://github.com/certd/certd/commit/a019956698acaf2c4beb620b5ad8c18918ead6a1)) * 支持微信支付 ([45d6347](https://github.com/certd/certd/commit/45d6347f5b6199493b11aabdd74177f6dca2cea4)) ### Performance Improvements * 站点证书监控通知发送,每天定时检查 ([bb4910f](https://github.com/certd/certd/commit/bb4910f4e57234e42b44505f4620ae7af66025c5)) * 支持plesk网站证书部署 ([eda45c1](https://github.com/certd/certd/commit/eda45c1528199648b3970505e87f492d398226cd)) ## [1.28.4](https://github.com/certd/certd/compare/v1.28.3...v1.28.4) (2024-12-12) **Note:** Version bump only for package @certd/basic ## [1.28.3](https://github.com/certd/certd/compare/v1.28.2...v1.28.3) (2024-12-12) **Note:** Version bump only for package @certd/basic ## [1.28.2](https://github.com/certd/certd/compare/v1.28.1...v1.28.2) (2024-12-09) **Note:** Version bump only for package @certd/basic ## [1.28.1](https://github.com/certd/certd/compare/v1.28.0...v1.28.1) (2024-12-08) ### Performance Improvements * 通知选择器优化 ([2c0cbdd](https://github.com/certd/certd/commit/2c0cbdd29ecb74cc939b2ae7ee86b8d40f70ba31)) # [1.28.0](https://github.com/certd/certd/compare/v1.27.9...v1.28.0) (2024-11-30) ### Performance Improvements * 优化证书申请成功通知发送方式 ([8002a56](https://github.com/certd/certd/commit/8002a56efc5998aa03db5711ae87f9eb4bc9e160)) * 支持短信验证码登录 ([387bcc5](https://github.com/certd/certd/commit/387bcc5fa418cdeea81a06da5e3f8cd6b43cd082)) ## [1.27.9](https://github.com/certd/certd/compare/v1.27.8...v1.27.9) (2024-11-26) **Note:** Version bump only for package @certd/basic ## [1.27.8](https://github.com/certd/certd/compare/v1.27.7...v1.27.8) (2024-11-25) **Note:** Version bump only for package @certd/basic ## [1.27.7](https://github.com/certd/certd/compare/v1.27.6...v1.27.7) (2024-11-25) **Note:** Version bump only for package @certd/basic ## [1.27.6](https://github.com/certd/certd/compare/v1.27.5...v1.27.6) (2024-11-19) **Note:** Version bump only for package @certd/basic ## [1.27.5](https://github.com/certd/certd/compare/v1.27.4...v1.27.5) (2024-11-18) ### Performance Improvements * 系统设置中的代理设置优化为可全局生效,环境变量中的https_proxy设置将无效 ([381a37f](https://github.com/certd/certd/commit/381a37fbaa6b61c887eda743897ae00afb825bdf)) * 新手导航在非编辑模式下不显示 ([18bfcc2](https://github.com/certd/certd/commit/18bfcc24ad0bde57bb04db8a4209861ec6b8ff1d)) * 优化腾讯云 cloudflare 重复解析记录时的返回值 ([90d1b68](https://github.com/certd/certd/commit/90d1b68bd6cf232fbe085234efe07d29b7690044)) ## [1.27.4](https://github.com/certd/certd/compare/v1.27.3...v1.27.4) (2024-11-14) **Note:** Version bump only for package @certd/basic ## [1.27.3](https://github.com/certd/certd/compare/v1.27.2...v1.27.3) (2024-11-13) ### Bug Fixes * 修复ipv6未开启情况下,请求带有ipv6地址域名报ETIMEDOUT的bug ([a9a0967](https://github.com/certd/certd/commit/a9a0967a6f1d0bd27e69f3ec52c31d90d470bc23)) ## [1.27.2](https://github.com/certd/certd/compare/v1.27.1...v1.27.2) (2024-11-08) ### Performance Improvements * 支持公共cname服务 ([3c919ee](https://github.com/certd/certd/commit/3c919ee5d1aef5d26cf3620a7c49d920786bc941)) ## [1.27.1](https://github.com/certd/certd/compare/v1.27.0...v1.27.1) (2024-11-04) ### Performance Improvements * cname 域名映射记录可读性优化 ([b1117ed](https://github.com/certd/certd/commit/b1117ed54a3ef015752999324ff72b821ef5e4b9)) # [1.27.0](https://github.com/certd/certd/compare/v1.26.16...v1.27.0) (2024-10-31) **Note:** Version bump only for package @certd/basic ## [1.26.16](https://github.com/certd/certd/compare/v1.26.15...v1.26.16) (2024-10-30) ### Performance Improvements * 支持华为云cdn ([81a3fdb](https://github.com/certd/certd/commit/81a3fdbc29b71f380762008cc151493ec97458f9)) ## [1.26.15](https://github.com/certd/certd/compare/v1.26.14...v1.26.15) (2024-10-28) **Note:** Version bump only for package @certd/basic ## [1.26.14](https://github.com/certd/certd/compare/v1.26.13...v1.26.14) (2024-10-26) ### Bug Fixes * 修复启动时自签证书无法保存的bug ([526c484](https://github.com/certd/certd/commit/526c48450bcd37b3ccded9b448f17de8140bdc6e)) ## [1.26.13](https://github.com/certd/certd/compare/v1.26.12...v1.26.13) (2024-10-26) **Note:** Version bump only for package @certd/basic ## [1.26.12](https://github.com/certd/certd/compare/v1.26.11...v1.26.12) (2024-10-25) ### Performance Improvements * 新增部署到百度云CDN插件 ([f126f9f](https://github.com/certd/certd/commit/f126f9f932d37fa01fff1accc7bdd17d349f8db5)) ## [1.26.11](https://github.com/certd/certd/compare/v1.26.10...v1.26.11) (2024-10-23) ### Bug Fixes * 申请证书没有使用到系统设置的http代理的bug ([3db216f](https://github.com/certd/certd/commit/3db216f515ba404cb4330fdab452971b22a50f08)) ## [1.26.10](https://github.com/certd/certd/compare/v1.26.9...v1.26.10) (2024-10-20) **Note:** Version bump only for package @certd/basic ## [1.26.9](https://github.com/certd/certd/compare/v1.26.8...v1.26.9) (2024-10-19) **Note:** Version bump only for package @certd/basic ## [1.26.8](https://github.com/certd/certd/compare/v1.26.7...v1.26.8) (2024-10-15) **Note:** Version bump only for package @certd/basic ## [1.26.7](https://github.com/certd/certd/compare/v1.26.6...v1.26.7) (2024-10-14) **Note:** Version bump only for package @certd/basic ## [1.26.6](https://github.com/certd/certd/compare/v1.26.5...v1.26.6) (2024-10-14) **Note:** Version bump only for package @certd/basic ## [1.26.5](https://github.com/certd/certd/compare/v1.26.4...v1.26.5) (2024-10-14) **Note:** Version bump only for package @certd/basic ## [1.26.4](https://github.com/certd/certd/compare/v1.26.3...v1.26.4) (2024-10-14) ### Performance Improvements * 新增代理设置功能 ([273ab61](https://github.com/certd/certd/commit/273ab6139f5807f4d7fe865cc353b97f51b9a668)) ## [1.26.3](https://github.com/certd/certd/compare/v1.26.2...v1.26.3) (2024-10-12) **Note:** Version bump only for package @certd/basic ## [1.26.2](https://github.com/certd/certd/compare/v1.26.1...v1.26.2) (2024-10-11) **Note:** Version bump only for package @certd/basic ## [1.26.1](https://github.com/certd/certd/compare/v1.26.0...v1.26.1) (2024-10-10) **Note:** Version bump only for package @certd/basic # [1.26.0](https://github.com/certd/certd/compare/v1.25.9...v1.26.0) (2024-10-10) ### Performance Improvements * 检查cname是否正确配置 ([b5d8935](https://github.com/certd/certd/commit/b5d8935159374fbe7fc7d4c48ae0ed9396861bdd)) ================================================ FILE: packages/core/basic/LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: packages/core/basic/build.md ================================================ 23:02 ================================================ FILE: packages/core/basic/package.json ================================================ { "name": "@certd/basic", "private": false, "version": "1.36.10", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { "dev": "vite", "before-build": "rimraf dist && rimraf tsconfig.tsbuildinfo && rimraf .rollup.cache", "build": "npm run before-build && tsc --skipLibCheck", "dev-build": "npm run build", "preview": "vite preview", "test": "mocha --loader=ts-node/esm", "pub": "npm publish" }, "dependencies": { "axios": "^1.7.2", "dayjs": "^1.11.7", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "iconv-lite": "^0.6.3", "lodash-es": "^4.17.21", "log4js": "^6.9.1", "lru-cache": "^10.0.0", "mitt": "^3.0.1", "nanoid": "^5.0.7", "node-forge": "^1.3.1", "nodemailer": "^6.9.3" }, "devDependencies": { "@types/chai": "^4.3.10", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.1", "@types/node-forge": "^1.3.2", "@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/parser": "^8.26.1", "chai": "4.3.10", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", "prettier": "^2.8.8", "rimraf": "^5.0.5", "tslib": "^2.8.1", "typescript": "^5.4.2" }, "gitHead": "085bdf5cfa9140903611aa12cdd2542d05aba321" } ================================================ FILE: packages/core/basic/readme.md ================================================ ================================================ FILE: packages/core/basic/src/index.ts ================================================ export * from './utils/index.js'; export * from './utils/util.id.js'; ================================================ FILE: packages/core/basic/src/utils/index.ts ================================================ export * from "./util.request.js"; export * from "./util.env.js"; export * from "./util.log.js"; export * from "./util.file.js"; export * from "./util.sp.js"; export * from "./util.promise.js"; export * from "./util.hash.js"; export * from "./util.merge.js"; export * from "./util.cache.js"; export * from "./util.string.js"; export * from "./util.lock.js"; export * from "./util.mitter.js"; export * from "./util.id.js"; export * from "./util.domain.js"; export * from "./util.amount.js"; import { stringUtils } from "./util.string.js"; import sleep from "./util.sleep.js"; import { http, download } from "./util.request.js"; import { mergeUtils } from "./util.merge.js"; import { sp } from "./util.sp.js"; import { hashUtils } from "./util.hash.js"; import { promises } from "./util.promise.js"; import { fileUtils } from "./util.file.js"; import * as _ from "lodash-es"; import { cache } from "./util.cache.js"; import dayjs from "dayjs"; import { domainUtils } from "./util.domain.js"; import { optionsUtils } from "./util.options.js"; import { amountUtils } from "./util.amount.js"; import { nanoid } from "nanoid"; import * as id from "./util.id.js"; import { locker } from "./util.lock.js"; import { mitter } from "./util.mitter.js"; import * as request from "./util.request.js"; export * from "./util.cache.js"; export const utils = { sleep, http, download, sp, hash: hashUtils, promises, file: fileUtils, _, mergeUtils, cache, nanoid, id, dayjs, domain: domainUtils, options: optionsUtils, string: stringUtils, locker, mitter, amount: amountUtils, request, }; ================================================ FILE: packages/core/basic/src/utils/util.amount.ts ================================================ export const amountUtils = { toCent(amount: number): number { return parseInt((amount * 100).toFixed(0)); }, toYuan(amount: number): number { return parseFloat((amount / 100).toFixed(2)); }, }; ================================================ FILE: packages/core/basic/src/utils/util.cache.ts ================================================ // LRUCache import { LRUCache } from "lru-cache"; export const cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 10, }); export class LocalCache { cache: Map; constructor(opts: { clearInterval?: number } = {}) { this.cache = new Map(); setInterval(() => { this.clearExpires(); }, opts.clearInterval ?? 5 * 60 * 1000); } get(key: string): V | undefined { const entry = this.cache.get(key); if (!entry) { return undefined; } // 检查是否过期 if (Date.now() > entry.expiresAt) { this.cache.delete(key); return undefined; } return entry.value; } set(key: string, value: V, ttl = 300000) { // 默认5分钟 (300000毫秒) this.cache.set(key, { value, expiresAt: Date.now() + ttl, }); } clear() { this.cache.clear(); } clearExpires() { for (const [key, entry] of this.cache) { if (entry.expiresAt < Date.now()) { this.cache.delete(key); } } } } ================================================ FILE: packages/core/basic/src/utils/util.domain.ts ================================================ //域名是否匹配,支持通配符 function match(targetDomains: string | string[], inDomains: string[]) { if (!targetDomains || targetDomains.length == 0) { return false; } if (!inDomains || inDomains.length == 0) { return false; } if (typeof targetDomains === 'string') { targetDomains = [targetDomains]; } for (let targetDomain of targetDomains) { let matched = false; if (targetDomain.startsWith('.')) { targetDomain = '*' + targetDomain; } for (let inDomain of inDomains) { if (inDomain.startsWith('.')) { inDomain = '*' + inDomain; } if (targetDomain === inDomain) { matched = true; break; } if (!inDomain.startsWith('*.')) { //不可能匹配 continue; } //子域名匹配通配符即可 const firstDotIndex = targetDomain.indexOf('.'); const targetDomainSuffix = targetDomain.substring(firstDotIndex + 1); if (targetDomainSuffix === inDomain.substring(2)) { matched = true; break; } } //有一个没有匹配上,就失败 if (!matched) { return false; } //这个匹配上了,检查下一个 } //没有提前return 全部匹配上了 return true; } export const domainUtils = { match, }; ================================================ FILE: packages/core/basic/src/utils/util.env.ts ================================================ export function isDev() { const nodeEnv = process.env.NODE_ENV || ''; return nodeEnv === 'development' || nodeEnv.includes('local') || nodeEnv.startsWith('dev'); } ================================================ FILE: packages/core/basic/src/utils/util.file.ts ================================================ import fs from 'fs'; function getFileRootDir(rootDir?: string) { if (rootDir == null) { const userHome = process.env.HOME || process.env.USERPROFILE; rootDir = userHome + '/.certd/storage/'; } if (!fs.existsSync(rootDir)) { fs.mkdirSync(rootDir, { recursive: true }); } return rootDir; } export const fileUtils = { getFileRootDir, }; ================================================ FILE: packages/core/basic/src/utils/util.hash.ts ================================================ import crypto, { BinaryToTextEncoding } from "crypto"; function md5(data: string, digest: BinaryToTextEncoding = "hex") { return crypto.createHash("md5").update(data).digest(digest); } function sha256(data: string, digest: BinaryToTextEncoding = "hex") { return crypto.createHash("sha256").update(data).digest(digest); } function hmacSha256(data: string, digest: BinaryToTextEncoding = "base64") { return crypto.createHmac("sha256", data).update(Buffer.alloc(0)).digest(digest); } function base64(data: string) { return Buffer.from(data).toString("base64"); } function base64Decode(data: string) { return Buffer.from(data, "base64").toString("utf8"); } export const hashUtils = { md5, sha256, base64, base64Decode, hmacSha256, }; ================================================ FILE: packages/core/basic/src/utils/util.id.ts ================================================ import { customAlphabet } from "nanoid"; export const randomNumber = customAlphabet("1234567890", 4); export const simpleNanoId = customAlphabet("1234567890abcdefghijklmopqrstuvwxyzABCDEFGHIJKLMOPQRSTUVWXYZ", 12); ================================================ FILE: packages/core/basic/src/utils/util.lock.ts ================================================ import { logger, utils } from './index.js'; export class Locker { locked: Record = {}; async execute(lockStr: string, callback: any) { await this.lock(lockStr); const timeoutId = setTimeout(() => { logger.warn('Lock timeout,自动解锁', lockStr); this.unlock(lockStr); }, 20000); try { return await callback(); } finally { clearTimeout(timeoutId); this.unlock(lockStr); } } async lock(str: string) { const isLocked = this.isLocked(str); if (isLocked) { let count = 0; while (true) { await utils.sleep(100); if (!this.isLocked(str)) { break; } count++; if (count > 20) { throw new Error('Lock timeout'); } } } this.locked[str] = true; } unlock(str: string) { delete this.locked[str]; } isLocked(str: string) { return this.locked[str] ?? false; } } export const locker = new Locker(); ================================================ FILE: packages/core/basic/src/utils/util.log.ts ================================================ import log4js, { LoggingEvent, Logger } from "log4js"; const OutputAppender = { configure: (config: any, layouts: any, findAppender: any, levels: any) => { let layout = layouts.basicLayout; if (config.layout) { layout = layouts.layout(config.layout.type, config.layout); } function customAppender(layout: any, timezoneOffset: any) { return (loggingEvent: LoggingEvent) => { if (loggingEvent.context.outputHandler?.write) { const text = `${layout(loggingEvent, timezoneOffset)}\n`; loggingEvent.context.outputHandler.write(text); } }; } return customAppender(layout, config.timezoneOffset); }, }; let logFilePath = "./logs/app.log"; export function resetLogConfigure() { // @ts-ignore log4js.configure({ appenders: { std: { type: "stdout" }, output: { type: OutputAppender }, file: { type: "dateFile", filename: logFilePath, keepFileExt: true, compress: true, numBackups: 3, }, }, categories: { default: { appenders: ["std", "file"], level: "info" }, pipeline: { appenders: ["std", "file", "output"], level: "info" } }, }); } resetLogConfigure(); export const logger = log4js.getLogger("default"); export function resetLogFilePath(filePath: string) { logFilePath = filePath; resetLogConfigure(); } export function buildLogger(write: (text: string) => void) { const logger = log4js.getLogger("pipeline"); const _secrets: string[] = []; //@ts-ignore logger.addSecret = (secret: string) => { _secrets.push(secret); }; logger.addContext("outputHandler", { write: (text: string) => { for (const item of _secrets) { if (item == null) { continue; } if (item.includes(text)) { //整个包含 text = "*".repeat(text.length); continue; } if (text.includes(item)) { //换成同长度的*号, item可能有多行 text = text.replaceAll(item, "*".repeat(item.length)); } } write(text); }, }); return logger; } export type ILogger = Logger; ================================================ FILE: packages/core/basic/src/utils/util.merge.ts ================================================ import * as _ from 'lodash-es'; function isUnMergeable(srcValue: any) { return srcValue != null && srcValue instanceof UnMergeable; } function isUnCloneable(value: any) { return isUnMergeable(value) && !value.cloneable; } function merge(target: any, ...sources: any) { /** * 如果目标为不可合并对象,比如array、unMergeable、ref,则直接覆盖不合并 * @param objValue 被合并对象 * @param srcValue 来源对象 */ function customizer(objValue: any, srcValue: any) { if (srcValue == null) { return; } // 如果被合并对象为数组,则直接被覆盖对象覆盖,只要覆盖对象不为空 if (_.isArray(objValue)) { //原对象如果是数组 return srcValue; //来源对象 } if (isUnMergeable(srcValue)) { return srcValue; } } let found: any = null; for (const item of sources) { if (isUnMergeable(item)) { found = item; } } if (found) { return found; } return _.mergeWith(target, ...sources, customizer); } function cloneDeep(target: any) { if (isUnCloneable(target)) { return target; } function customizer(value: any) { if (isUnCloneable(value)) { return value; } } return _.cloneDeepWith(target, customizer); } export class UnMergeable { cloneable = false; setCloneable(cloneable: any) { this.cloneable = cloneable; } } export const mergeUtils = { merge, cloneDeep, }; ================================================ FILE: packages/core/basic/src/utils/util.mitter.ts ================================================ import mitt from 'mitt'; export const mitter = mitt(); ================================================ FILE: packages/core/basic/src/utils/util.options.ts ================================================ import { domainUtils } from "./util.domain.js"; function groupByDomain(options: any[], inDomains: string[]) { const matched = []; const notMatched = []; for (const item of options) { if (domainUtils.match(item.domain, inDomains)) { matched.push(item); } else { notMatched.push(item); } } return { matched, notMatched, }; } function buildGroupOptions(options: any[], inDomains: string[]) { const grouped = groupByDomain(options, inDomains); const groupOptions = []; groupOptions.push({ value: "matched", disabled: true, label: "----已匹配----" }); if (grouped.matched.length === 0) { options.push({ value: "", disabled: true, label: "没有可以匹配的域名" }); } else { for (const matched of grouped.matched) { groupOptions.push(matched); } } if (grouped.notMatched.length > 0) { groupOptions.push({ value: "unmatched", disabled: true, label: "----未匹配----" }); for (const notMatched of grouped.notMatched) { groupOptions.push(notMatched); } } return groupOptions; } export const optionsUtils = { //获取分组 groupByDomain, //构建分组后的选项列表,常用 buildGroupOptions, }; ================================================ FILE: packages/core/basic/src/utils/util.promise.ts ================================================ import { logger } from "./util.log.js"; export function TimeoutPromise(callback: () => Promise, ms = 30 * 1000) { let timeout: any; return Promise.race([ callback(), new Promise((resolve, reject) => { timeout = setTimeout(() => { reject(new Error(`Task timeout in ${ms} ms`)); }, ms); }), ]).finally(() => { clearTimeout(timeout); }); } export function safePromise(callback: (resolve: (ret: T) => void, reject: (ret: any) => void) => void): Promise { return new Promise((resolve, reject) => { try { callback(resolve, reject); } catch (e) { logger.error(e); reject(e); } }); } export function promisify(func: any) { return function (...args: any) { return new Promise((resolve, reject) => { try { func(...args, (err: any, data: any) => { if (err) { reject(err); } else { resolve(data); } }); } catch (e) { reject(e); } }); }; } export const promises = { TimeoutPromise, safePromise, promisify, }; ================================================ FILE: packages/core/basic/src/utils/util.request.ts ================================================ import axios, { AxiosHeaders, AxiosRequestConfig } from "axios"; import { ILogger, logger } from "./util.log.js"; import { Logger } from "log4js"; import { HttpProxyAgent } from "http-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent"; import nodeHttp from "http"; import * as https from "node:https"; import { merge } from "lodash-es"; import { safePromise } from "./util.promise.js"; import fs from "fs"; export class HttpError extends Error { status?: number; statusText?: string; code?: string; request?: { baseURL: string; url: string; method: string; params?: any; data?: any }; response?: { data: any; headers: AxiosHeaders }; cause?: any; constructor(error: any) { if (!error) { return; } super(error.message || error.response?.statusText); const message = error?.message; if (message && typeof message === "string") { if (message.indexOf && message.indexOf("ssl3_get_record:wrong version number") >= 0) { this.message = `${message}(http协议错误,服务端要求http协议,请检查是否使用了https请求)`; } else if (message.indexOf("getaddrinfo EAI_AGAIN") >= 0) { this.message = `${message}(无法解析域名,请检查网络连接或dns配置,更换docker-compose.yaml中dns配置)`; } } this.name = error.name; this.code = error.code; this.status = error.response?.status; this.statusText = error.response?.statusText || error.code; if (!this.message) { this.message = error.code; } this.request = { baseURL: error.config?.baseURL, url: error.config?.url, method: error.config?.method, params: error.config?.params, data: error.config?.data, }; let url = error.config?.url; if (error.config?.baseURL) { url = (error.config?.baseURL || "") + url; } if (url) { this.message = `${this.message} 【${url}】`; } this.response = { data: error.response?.data, headers: error.response?.headers, }; const { stack, cause } = error; this.cause = cause; this.stack = stack; delete error.response; delete error.config; delete error.request; // logger.error(error); } } export const HttpCommonError = HttpError; let defaultAgents = createAgent(); export function setGlobalProxy(opts: { httpProxy?: string; httpsProxy?: string }) { logger.info("setGlobalProxy:", opts); defaultAgents = createAgent(opts); } export function getGlobalAgents() { return defaultAgents; } /** * @description 创建请求实例 */ export function createAxiosService({ logger }: { logger: Logger }) { // 创建一个 axios 实例 const service = axios.create(); // 请求拦截 service.interceptors.request.use( (config: any) => { if (config.logParams == null) { config.logParams = false; } if (config.logRes == null) { config.logRes = false; } if (config.logData == null) { config.logData = false; } logger.info(`http request:${config.url},method:${config.method}`); if (config.logParams !== false && config.params) { logger.info(`params:${JSON.stringify(config.params)}`); } if (config.logData !== false && config.data) { logger.info(`data:${JSON.stringify(config.data)}`); } if (config.timeout == null) { config.timeout = 15000; } let agents = defaultAgents; if (config.skipSslVerify || config.httpProxy) { let rejectUnauthorized = true; if (config.skipSslVerify) { logger.info("跳过SSL验证"); rejectUnauthorized = false; } const proxy: any = {}; if (config.httpProxy) { logger.info("使用自定义http代理:", config.httpProxy); proxy.httpProxy = config.httpProxy; proxy.httpsProxy = config.httpProxy; } agents = createAgent({ rejectUnauthorized, ...proxy } as any); } delete config.skipSslVerify; config.httpsAgent = agents.httpsAgent; config.httpAgent = agents.httpAgent; // const agent = new https.Agent({ // rejectUnauthorized: false // 允许自签名证书 // }); // config.httpsAgent = agent; config.proxy = false; //必须 否则还会走一层代理, return config; }, (error: Error) => { // 发送失败 logger.error("接口请求失败:", error); return Promise.reject(error); } ); // 响应拦截 service.interceptors.response.use( (response: any) => { if (response?.config?.logRes !== false) { let resData = response?.data; try { resData = JSON.stringify(response?.data); } catch (e) {} logger.info(`http response : status=${response?.status},data=${resData}`); } else { logger.info("http response status:", response?.status); } if (response?.config?.returnOriginRes) { return response; } return response.data; }, (error: any) => { const status = error.response?.status; switch (status) { case 400: error.message = "请求错误"; break; case 401: error.message = "认证/登录失败"; break; case 403: error.message = "拒绝访问"; break; case 404: error.message = `请求地址出错`; break; case 408: error.message = "请求超时"; break; case 500: error.message = "服务器内部错误"; break; case 501: error.message = "服务未实现"; break; case 502: error.message = "网关错误"; break; case 503: error.message = "服务不可用"; break; case 504: error.message = "网关超时"; break; case 505: error.message = "HTTP版本不受支持"; break; default: break; } logger.error(`请求出错:status:${error.response?.status},statusText:${error.response?.statusText},url:${error.config?.url},method:${error.config?.method}。`); logger.error("返回数据:", JSON.stringify(error.response?.data)); if (error.response?.data) { const message = error.response.data.message || error.response.data.msg || error.response.data.error; if (typeof message === "string") { error.message = message; } } if (error instanceof AggregateError) { logger.error("AggregateError", error); } const err = new HttpError(error); if (error.response?.config?.logParams === false) { delete err.request?.params; delete err.request?.data; } return Promise.reject(err); } ); return service; } export const http = createAxiosService({ logger }) as HttpClient; export type HttpClientResponse = any; export type HttpRequestConfig = { skipSslVerify?: boolean; skipCheckRes?: boolean; logParams?: boolean; logRes?: boolean; logData?: boolean; httpProxy?: string; returnOriginRes?: boolean; } & AxiosRequestConfig; export type HttpClient = { request(config: HttpRequestConfig): Promise>; }; // const http_proxy_backup = process.env.HTTP_PROXY || process.env.http_proxy; // const https_proxy_backup = process.env.HTTPS_PROXY || process.env.https_proxy; export type CreateAgentOptions = { httpProxy?: string; httpsProxy?: string; } & nodeHttp.AgentOptions; export function createAgent(opts: CreateAgentOptions = {}) { opts = merge( { autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 1000, }, opts ); let httpAgent, httpsAgent; const httpProxy = opts.httpProxy; if (httpProxy) { process.env.HTTP_PROXY = httpProxy; process.env.http_proxy = httpProxy; logger.info("use httpProxy:", httpProxy); httpAgent = new HttpProxyAgent(httpProxy, opts as any); merge(httpAgent.options, opts); } else { process.env.HTTP_PROXY = ""; process.env.http_proxy = ""; httpAgent = new nodeHttp.Agent(opts); } const httpsProxy = opts.httpsProxy; if (httpsProxy) { process.env.HTTPS_PROXY = httpsProxy; process.env.https_proxy = httpsProxy; logger.info("use httpsProxy:", httpsProxy); httpsAgent = new HttpsProxyAgent(httpsProxy, opts as any); merge(httpsAgent.options, opts); } else { process.env.HTTPS_PROXY = ""; process.env.https_proxy = ""; httpsAgent = new https.Agent(opts); } return { httpAgent, httpsAgent, }; } export async function download(req: { http: HttpClient; config: HttpRequestConfig; savePath: string; logger: ILogger }) { const { http, config, savePath, logger } = req; return safePromise((resolve, reject) => { http .request({ logRes: false, responseType: "stream", ...config, }) .then(res => { const writer = fs.createWriteStream(savePath); res.pipe(writer); writer.on("close", () => { logger.info("文件下载成功"); resolve(true); }); //error writer.on("error", err => { logger.error("下载失败", err); reject(err); }); //进度条打印 const totalLength = res.headers["content-length"]; let currentLength = 0; // 每5%打印一次 const step = (totalLength / 100) * 5; res.on("data", (chunk: any) => { currentLength += chunk.length; if (currentLength % step < chunk.length) { const percent = ((currentLength / totalLength) * 100).toFixed(2); logger.info(`下载进度:${percent}%`); } }); }) .catch(err => { logger.info("下载失败", err); reject(err); }); }); } export function getCookie(response: any, name: string) { const cookies = response.headers["set-cookie"]; //根据name 返回对应的cookie const found = cookies.find((cookie: any) => cookie.includes(name)); if (!found) { return null; } const cookie = found.split(";")[0]; return cookie.substring(cookie.indexOf("=") + 1); } ================================================ FILE: packages/core/basic/src/utils/util.sleep.ts ================================================ export default function (timeout: number) { return new Promise(resolve => { setTimeout(() => { resolve({}); }, timeout); }); } ================================================ FILE: packages/core/basic/src/utils/util.sp.ts ================================================ //转换为import import childProcess from 'child_process'; import { safePromise } from './util.promise.js'; import { ILogger, logger } from './util.log.js'; import iconv from 'iconv-lite'; export type ExecOption = { cmd: string | string[]; env: any; logger?: ILogger; options?: any; }; async function exec(opts: ExecOption): Promise { let cmd = ''; const log = opts.logger || logger; if (opts.cmd instanceof Array) { for (const item of opts.cmd) { if (cmd) { cmd += ' && ' + item; } else { cmd = item; } } } log.info(`执行命令: ${cmd}`); return safePromise((resolve, reject) => { childProcess.exec( cmd, { env: { ...process.env, ...opts.env, }, ...opts.options, }, (error, stdout, stderr) => { if (error) { log.error(`exec error: ${error}`); reject(error); } else { const res = stdout.toString('utf-8'); log.info(`stdout: ${res}`); resolve(res); } } ); }); } export type SpawnOption = { cmd: string | string[]; onStdout?: (data: string) => void; onStderr?: (data: string) => void; env?: any; logger?: ILogger; options?: any; }; function isWindows() { return process.platform === 'win32'; } function convert(buffer: any) { if (isWindows()) { const decoded = iconv.decode(buffer, 'GBK'); // 检查是否有有效字符 return decoded && decoded.trim().length > 0 ? decoded : buffer.toString(); } else { return buffer; } } // function convert(buffer: any) { // return buffer; // } async function spawn(opts: SpawnOption): Promise { let cmd = ''; const log = opts.logger || logger; if (opts.cmd instanceof Array) { for (const item of opts.cmd) { if (cmd) { cmd += ' && ' + item; } else { cmd = item; } } } else { cmd = opts.cmd; } log.info(`执行命令: ${cmd}`); let stdout = ''; let stderr = ''; return safePromise((resolve, reject) => { const ls = childProcess.spawn(cmd, { shell: true, env: { ...process.env, ...opts.env, }, ...opts.options, }); ls.stdout.on('data', data => { data = convert(data); log.info(`stdout: ${data}`); stdout += data; }); ls.stderr.on('data', data => { data = convert(data); log.warn(`stderr: ${data}`); stderr += data; }); ls.on('error', error => { log.error(`child process error: ${error}`); reject(error); }); ls.on('close', (code: number) => { if (code !== 0) { log.error(`child process exited with code ${code}`); reject(new Error(stderr)); } else { resolve(stdout); } }); }); } export const sp = { spawn, exec, }; ================================================ FILE: packages/core/basic/src/utils/util.string.ts ================================================ export const stringUtils = { maxLength(str?: string, length = 100) { if (str) { return str.length > length ? str.slice(0, length) + '...' : str; } return ''; }, }; ================================================ FILE: packages/core/basic/tsconfig.json ================================================ { "compileOnSave": true, "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "node", "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "noImplicitThis": true, "noUnusedLocals": true, "stripInternal": true, "importHelpers": true, "skipLibCheck": true, "pretty": true, "declaration": true, "forceConsistentCasingInFileNames": true, "outDir": "dist", "rootDir": "src", "composite": false, "useDefineForClassFields": true, "strict": true, "typeRoots": [ "./typings", "./node_modules/@types"], "inlineSourceMap": true, "resolveJsonModule": true, "isolatedModules": false, "lib": ["ESNext", "DOM"], }, "include": [ "src/**/*.ts", "src/**/*.json" ], "exclude": [ "*.js", "*.ts", "*.spec.ts", "dist", "node_modules", "test" ], } ================================================ FILE: packages/core/pipeline/.eslintrc ================================================ { "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint" ], "extends": [ "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "prettier" ], "env": { "mocha": true }, "rules": { "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-unused-vars": "off" } } ================================================ FILE: packages/core/pipeline/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? test/user.secret.* test/**/*.js src/**/*.spec.ts .test.mjs ================================================ FILE: packages/core/pipeline/.mocharc.json ================================================ { "extension": ["ts"], "spec": "src/**/*.spec.ts" } ================================================ FILE: packages/core/pipeline/.npmignore ================================================ node_modules src dist/**/*.spec.* ================================================ FILE: packages/core/pipeline/.npmrc ================================================ link-workspace-packages=deep prefer-workspace-packages=true ================================================ FILE: packages/core/pipeline/.prettierrc ================================================ { "printWidth": 220, "bracketSpacing": true, "singleQuote": false, "trailingComma": "es5", "arrowParens": "avoid" } ================================================ FILE: packages/core/pipeline/CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. ## [1.36.10](https://github.com/certd/certd/compare/v1.36.9...v1.36.10) (2025-07-18) **Note:** Version bump only for package @certd/pipeline ## [1.36.9](https://github.com/certd/certd/compare/v1.36.7...v1.36.9) (2025-07-15) **Note:** Version bump only for package @certd/pipeline ## [1.36.7](https://github.com/certd/certd/compare/v1.36.6...v1.36.7) (2025-07-15) ### Performance Improvements * 支持邮箱发送证书 ([95332d5](https://github.com/certd/certd/commit/95332d5db96cd54ddab6ab737332417a09169b39)) ## [1.36.6](https://github.com/certd/certd/compare/v1.36.5...v1.36.6) (2025-07-14) **Note:** Version bump only for package @certd/pipeline ## [1.36.5](https://github.com/certd/certd/compare/v1.36.4...v1.36.5) (2025-07-11) **Note:** Version bump only for package @certd/pipeline ## [1.36.4](https://github.com/certd/certd/compare/v1.36.3...v1.36.4) (2025-07-10) **Note:** Version bump only for package @certd/pipeline ## [1.36.3](https://github.com/certd/certd/compare/v1.36.2...v1.36.3) (2025-07-07) **Note:** Version bump only for package @certd/pipeline ## [1.36.2](https://github.com/certd/certd/compare/v1.36.1...v1.36.2) (2025-07-06) **Note:** Version bump only for package @certd/pipeline ## [1.36.1](https://github.com/certd/certd/compare/v1.36.0...v1.36.1) (2025-07-02) **Note:** Version bump only for package @certd/pipeline # [1.36.0](https://github.com/certd/certd/compare/v1.35.5...v1.36.0) (2025-07-01) ### Performance Improvements * 阿里云waf cname站点选择支持翻页及域名查询 ([4cf9858](https://github.com/certd/certd/commit/4cf98584dacc5999752732f136246647a2f1f07d)) * 支持选择运行策略设置 ([60f055f](https://github.com/certd/certd/commit/60f055f293ce237c21cd9050333dad9609eceac1)) ## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20) ### Performance Improvements * 支持批量修改通知和定时 ([e11b3be](https://github.com/certd/certd/commit/e11b3becfd4abe6547e84d09adc38ebd6e1c4b87)) ## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13) ### Performance Improvements * 支持s3 access做测试 ([f00aeac](https://github.com/certd/certd/commit/f00aeacb8b5c81f0bafa4c1b76723dec2b6b7784)) ## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12) ### Bug Fixes * 修复消息内容存在()<>等括号情况下无法发送tg通知的bug ([c937583](https://github.com/certd/certd/commit/c937583a50d8513d76adead3648f83eee2fcc6f9)) ## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09) **Note:** Version bump only for package @certd/pipeline ## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07) **Note:** Version bump only for package @certd/pipeline # [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05) **Note:** Version bump only for package @certd/pipeline ## [1.34.11](https://github.com/certd/certd/compare/v1.34.10...v1.34.11) (2025-06-05) **Note:** Version bump only for package @certd/pipeline ## [1.34.10](https://github.com/certd/certd/compare/v1.34.9...v1.34.10) (2025-06-03) ### Performance Improvements * 支持部署到飞牛OS ([ddfd0fb](https://github.com/certd/certd/commit/ddfd0fb81d6638352920261065f1ab8e27bdd564)) ## [1.34.9](https://github.com/certd/certd/compare/v1.34.8...v1.34.9) (2025-05-30) **Note:** Version bump only for package @certd/pipeline ## [1.34.8](https://github.com/certd/certd/compare/v1.34.7...v1.34.8) (2025-05-28) ### Performance Improvements * 优化站点选择组件,切换选择时不刷新列表 ([3a14714](https://github.com/certd/certd/commit/3a147141b1a5d67c92a5ce88a5313eaa62859e03)) * 站点监控支持监控IP ([9cc4c01](https://github.com/certd/certd/commit/9cc4c017ae646a18284e732769b82636feda01d3)) * 支持批量重新运行 ([8189982](https://github.com/certd/certd/commit/818998259ddc75e722196ac5c365038818539b9b)) ## [1.34.7](https://github.com/certd/certd/compare/v1.34.6...v1.34.7) (2025-05-26) **Note:** Version bump only for package @certd/pipeline ## [1.34.6](https://github.com/certd/certd/compare/v1.34.5...v1.34.6) (2025-05-25) ### Bug Fixes * 修复公共插件配置修改不生效的bug,优化系统设置参数注入时机 ([e1e510c](https://github.com/certd/certd/commit/e1e510ce1e37a5ae82478226b6987a83f22d1ecb)) * 优化 RunnableError错误信息展示 ([36bc3ff](https://github.com/certd/certd/commit/36bc3ff22da93ba342c3c1103d7ee2bbcecf44f2)) ### Performance Improvements * 添加阿里云 ESA证书部署插件 ([1db1ffd](https://github.com/certd/certd/commit/1db1ffde99ac7e4684fa606ebc4c327f829b3a26)) ## [1.34.5](https://github.com/certd/certd/compare/v1.34.4...v1.34.5) (2025-05-19) ### Performance Improvements * 优化钉钉通知标题颜色 ([a560999](https://github.com/certd/certd/commit/a560999d13eed18d08dd32ee530166569e3f8746)) * 优化飞书通知为卡片模式 ([a818a3d](https://github.com/certd/certd/commit/a818a3d293e22fb46979bc77055c05621a6fed81)) ## [1.34.4](https://github.com/certd/certd/compare/v1.34.3...v1.34.4) (2025-05-16) ### Bug Fixes * 修复导入在线插件不生效的bug ([fcf8309](https://github.com/certd/certd/commit/fcf8309c238208281ecb4575b2c3cfe50c11d783)) ## [1.34.3](https://github.com/certd/certd/compare/v1.34.2...v1.34.3) (2025-05-15) **Note:** Version bump only for package @certd/pipeline ## [1.34.2](https://github.com/certd/certd/compare/v1.34.1...v1.34.2) (2025-05-11) **Note:** Version bump only for package @certd/pipeline ## [1.34.1](https://github.com/certd/certd/compare/v1.34.0...v1.34.1) (2025-05-05) **Note:** Version bump only for package @certd/pipeline # [1.34.0](https://github.com/certd/certd/compare/v1.33.8...v1.34.0) (2025-04-28) **Note:** Version bump only for package @certd/pipeline ## [1.33.8](https://github.com/certd/certd/compare/v1.33.7...v1.33.8) (2025-04-26) **Note:** Version bump only for package @certd/pipeline ## [1.33.7](https://github.com/certd/certd/compare/v1.33.6...v1.33.7) (2025-04-22) **Note:** Version bump only for package @certd/pipeline ## [1.33.6](https://github.com/certd/certd/compare/v1.33.5...v1.33.6) (2025-04-20) **Note:** Version bump only for package @certd/pipeline ## [1.33.5](https://github.com/certd/certd/compare/v1.33.4...v1.33.5) (2025-04-17) **Note:** Version bump only for package @certd/pipeline ## [1.33.4](https://github.com/certd/certd/compare/v1.33.3...v1.33.4) (2025-04-15) **Note:** Version bump only for package @certd/pipeline ## [1.33.3](https://github.com/certd/certd/compare/v1.33.2...v1.33.3) (2025-04-14) **Note:** Version bump only for package @certd/pipeline ## [1.33.2](https://github.com/certd/certd/compare/v1.33.1...v1.33.2) (2025-04-12) **Note:** Version bump only for package @certd/pipeline ## [1.33.1](https://github.com/certd/certd/compare/v1.33.0...v1.33.1) (2025-04-12) ### Bug Fixes * 修复ssh插件报length空指针的bug ([9c4cbe1](https://github.com/certd/certd/commit/9c4cbe17a22b548611cf1fbefecc83a421788e42)) # [1.33.0](https://github.com/certd/certd/compare/v1.32.0...v1.33.0) (2025-04-11) ### Performance Improvements * 隐藏运行策略选项 ([2951df0](https://github.com/certd/certd/commit/2951df0cd94c23e2efee84ff1b843055aac56cae)) # [1.32.0](https://github.com/certd/certd/compare/v1.31.11...v1.32.0) (2025-04-04) **Note:** Version bump only for package @certd/pipeline ## [1.31.11](https://github.com/certd/certd/compare/v1.31.10...v1.31.11) (2025-04-02) ### Performance Improvements * 支持部署到京东云cdn ([6f17c70](https://github.com/certd/certd/commit/6f17c700b84965baa01b40fe2abaa0a91bcbaffd)) ## [1.31.10](https://github.com/certd/certd/compare/v1.31.9...v1.31.10) (2025-03-29) ### Performance Improvements * tab增加图标显示 ([a03ae5a](https://github.com/certd/certd/commit/a03ae5a216a1df2c1d3da12ae18dcd0f089a92d3)) ## [1.31.9](https://github.com/certd/certd/compare/v1.31.8...v1.31.9) (2025-03-28) ### Performance Improvements * dns支持火山引擎 ([99ff879](https://github.com/certd/certd/commit/99ff879d93658c29ea493a4bde7e9e3f85996d64)) ## [1.31.8](https://github.com/certd/certd/compare/v1.31.7...v1.31.8) (2025-03-26) **Note:** Version bump only for package @certd/pipeline ## [1.31.7](https://github.com/certd/certd/compare/v1.31.6...v1.31.7) (2025-03-24) **Note:** Version bump only for package @certd/pipeline ## [1.31.6](https://github.com/certd/certd/compare/v1.31.5...v1.31.6) (2025-03-24) **Note:** Version bump only for package @certd/pipeline ## [1.31.5](https://github.com/certd/certd/compare/v1.31.4...v1.31.5) (2025-03-22) **Note:** Version bump only for package @certd/pipeline ## [1.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21) ### Bug Fixes * 修复站点监控通知通过webhook发送失败的bug ([9be1ecc](https://github.com/certd/certd/commit/9be1ecc8aab3ea23dd0dc2dab3688f4edb90ef2c)) ### Performance Improvements * 流水线增加上传证书快捷方式 ([425bba6](https://github.com/certd/certd/commit/425bba67c539b734e2a85a83a4f9ecc9b2434fb4)) * 支持飞书通知 ([b82e1dc](https://github.com/certd/certd/commit/b82e1dcd6217b09a7d7e21cd648bb31de320cadf)) * 支持手动上传证书并部署 ([a9fffa5](https://github.com/certd/certd/commit/a9fffa5180c83da27b35886aa2e858a92a2c5f94)) ## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13) **Note:** Version bump only for package @certd/pipeline ## [1.31.2](https://github.com/certd/certd/compare/v1.31.1...v1.31.2) (2025-03-12) **Note:** Version bump only for package @certd/pipeline ## [1.31.1](https://github.com/certd/certd/compare/v1.31.0...v1.31.1) (2025-03-11) **Note:** Version bump only for package @certd/pipeline # [1.31.0](https://github.com/certd/certd/compare/v1.30.6...v1.31.0) (2025-03-10) ### Performance Improvements * 流水线同一个阶段任务优化为并行执行 ([efa9c74](https://github.com/certd/certd/commit/efa9c748c5c07fc950af3db742ef9310f1ac9a4b)) ## [1.30.6](https://github.com/certd/certd/compare/v1.30.5...v1.30.6) (2025-02-24) ### Performance Improvements * 上传到阿里云证书名称后缀增加毫秒时间戳 ([9f0ee21](https://github.com/certd/certd/commit/9f0ee219d02907ffe128a5cf10173397d934ccd7)) ## [1.30.5](https://github.com/certd/certd/compare/v1.30.4...v1.30.5) (2025-02-14) **Note:** Version bump only for package @certd/pipeline ## [1.30.4](https://github.com/certd/certd/compare/v1.30.3...v1.30.4) (2025-02-14) **Note:** Version bump only for package @certd/pipeline ## [1.30.3](https://github.com/certd/certd/compare/v1.30.2...v1.30.3) (2025-02-13) **Note:** Version bump only for package @certd/pipeline ## [1.30.2](https://github.com/certd/certd/compare/v1.30.1...v1.30.2) (2025-02-09) **Note:** Version bump only for package @certd/pipeline ## [1.30.1](https://github.com/certd/certd/compare/v1.30.0...v1.30.1) (2025-01-20) ### Bug Fixes * 修复tg消息内容中存在.和*就会发送失败的bug ([ae5dfc3](https://github.com/certd/certd/commit/ae5dfc3bee950267123ae2fbd1c11e7ce36626ea)) # [1.30.0](https://github.com/certd/certd/compare/v1.29.5...v1.30.0) (2025-01-19) ### Performance Improvements * 证书仓库 ([91e7f45](https://github.com/certd/certd/commit/91e7f45a1c5ea1e0ec0aa3236b80028f03a6d0aa)) ## [1.29.5](https://github.com/certd/certd/compare/v1.29.4...v1.29.5) (2025-01-07) **Note:** Version bump only for package @certd/pipeline ## [1.29.4](https://github.com/certd/certd/compare/v1.29.3...v1.29.4) (2025-01-06) **Note:** Version bump only for package @certd/pipeline ## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04) ### Performance Improvements * 支持http校验方式申请证书 ([405591c](https://github.com/certd/certd/commit/405591c5d08fa1a3b228ee3980199e7731cfec4a)) ## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25) **Note:** Version bump only for package @certd/pipeline ## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25) ### Performance Improvements * 用户创建证书流水线没有购买套餐或者超限时提前报错 ([472f06c](https://github.com/certd/certd/commit/472f06c2d190d0ae48e8b53c18bc278437656a1c)) # [1.29.0](https://github.com/certd/certd/compare/v1.28.4...v1.29.0) (2024-12-24) ### Features * 套餐购买支持易支付、支付宝支付 ([faa28f8](https://github.com/certd/certd/commit/faa28f88f954cba4c1dd29125562e5acd2fd99af)) * 支持微信支付 ([45d6347](https://github.com/certd/certd/commit/45d6347f5b6199493b11aabdd74177f6dca2cea4)) ### Performance Improvements * 同一时间只允许一个套餐生效 ([8ebf95a](https://github.com/certd/certd/commit/8ebf95a222a900d1707716c7b1f3b39f8a6d8f94)) * 优化证书申请跳过的状态显示,成功通知现在在跳过时不会发送 ([67d762b](https://github.com/certd/certd/commit/67d762b6a520f1fa24719a124e5ae975a81f5f82)) * 支持plesk网站证书部署 ([eda45c1](https://github.com/certd/certd/commit/eda45c1528199648b3970505e87f492d398226cd)) ## [1.28.4](https://github.com/certd/certd/compare/v1.28.3...v1.28.4) (2024-12-12) ### Bug Fixes * 修复证书成功通知发送失败的bug ([0f5c690](https://github.com/certd/certd/commit/0f5c69040ba77340c909813220a26bc7ddada3ea)) ## [1.28.3](https://github.com/certd/certd/compare/v1.28.2...v1.28.3) (2024-12-12) ### Performance Improvements * 通知标题优化 ([ff083ce](https://github.com/certd/certd/commit/ff083ce6848a8bee3c8248e4b881086ae1517c28)) * 支持aws cloudfront ([0ae39f1](https://github.com/certd/certd/commit/0ae39f160a7c6b6696b3bf513d68aa28905810ad)) ## [1.28.2](https://github.com/certd/certd/compare/v1.28.1...v1.28.2) (2024-12-09) **Note:** Version bump only for package @certd/pipeline ## [1.28.1](https://github.com/certd/certd/compare/v1.28.0...v1.28.1) (2024-12-08) ### Performance Improvements * 通知选择器优化 ([2c0cbdd](https://github.com/certd/certd/commit/2c0cbdd29ecb74cc939b2ae7ee86b8d40f70ba31)) * 新增七牛云插件分组 ([49e7dc5](https://github.com/certd/certd/commit/49e7dc56e1a95fbdea3e30cdeb945b48415b69e3)) # [1.28.0](https://github.com/certd/certd/compare/v1.27.9...v1.28.0) (2024-11-30) ### Features * 手机号登录、邮箱验证码注册 ([7b55337](https://github.com/certd/certd/commit/7b55337c5edb470cca7aa62201eda8d274784004)) ### Performance Improvements * 首页新增修改密码提示 ([0772d3b](https://github.com/certd/certd/commit/0772d3b3fd24afdde4086d9f09ef19d037b431b4)) * 选项显示图标 ([aedc462](https://github.com/certd/certd/commit/aedc46213571a3bd93809b7af7fa17a08d546237)) * 优化证书申请成功通知发送方式 ([8002a56](https://github.com/certd/certd/commit/8002a56efc5998aa03db5711ae87f9eb4bc9e160)) * 支持短信验证码登录 ([387bcc5](https://github.com/certd/certd/commit/387bcc5fa418cdeea81a06da5e3f8cd6b43cd082)) ## [1.27.9](https://github.com/certd/certd/compare/v1.27.8...v1.27.9) (2024-11-26) ### Performance Improvements * 通知支持自定义webhook、anpush、iyuu、server酱 ([cbccd9e](https://github.com/certd/certd/commit/cbccd9e3d0a4c24aba772af62734666d40b22c57)) * 通知支持vocechat、bark、telegram、discord、slack ([642f57f](https://github.com/certd/certd/commit/642f57ff6d7152a9e14f59c7fc0e32a6b1751fb7)) ## [1.27.8](https://github.com/certd/certd/compare/v1.27.7...v1.27.8) (2024-11-25) **Note:** Version bump only for package @certd/pipeline ## [1.27.7](https://github.com/certd/certd/compare/v1.27.6...v1.27.7) (2024-11-25) ### Performance Improvements * 通知管理 ([d9a00ee](https://github.com/certd/certd/commit/d9a00eeaf72735ced67c59d7983d84e3c730064a)) * 通知渠道支持测试按钮 ([b54ae27](https://github.com/certd/certd/commit/b54ae272ebc2d31b32b049d44e2299a6be7f153c)) * 优化插件开发,dnsProvider无需写http logger 变量 ([fcbb5e4](https://github.com/certd/certd/commit/fcbb5e46a112174150a62648319b8224fce3b7ed)) * 支持企业微信群聊机器人通知 ([b805a29](https://github.com/certd/certd/commit/b805a2925984144a31575b8aaa622f0c30d41b56)) ## [1.27.6](https://github.com/certd/certd/compare/v1.27.5...v1.27.6) (2024-11-19) **Note:** Version bump only for package @certd/pipeline ## [1.27.5](https://github.com/certd/certd/compare/v1.27.4...v1.27.5) (2024-11-18) ### Performance Improvements * 新手导航在非编辑模式下不显示 ([18bfcc2](https://github.com/certd/certd/commit/18bfcc24ad0bde57bb04db8a4209861ec6b8ff1d)) ## [1.27.4](https://github.com/certd/certd/compare/v1.27.3...v1.27.4) (2024-11-14) **Note:** Version bump only for package @certd/pipeline ## [1.27.3](https://github.com/certd/certd/compare/v1.27.2...v1.27.3) (2024-11-13) **Note:** Version bump only for package @certd/pipeline ## [1.27.2](https://github.com/certd/certd/compare/v1.27.1...v1.27.2) (2024-11-08) ### Performance Improvements * 支持公共cname服务 ([3c919ee](https://github.com/certd/certd/commit/3c919ee5d1aef5d26cf3620a7c49d920786bc941)) ## [1.27.1](https://github.com/certd/certd/compare/v1.27.0...v1.27.1) (2024-11-04) ### Performance Improvements * 优化时间选择器,自动填写分钟和秒钟 ([396dc34](https://github.com/certd/certd/commit/396dc34a841c7d016b033736afdba8366fb2d211)) # [1.27.0](https://github.com/certd/certd/compare/v1.26.16...v1.27.0) (2024-10-31) **Note:** Version bump only for package @certd/pipeline ## [1.26.16](https://github.com/certd/certd/compare/v1.26.15...v1.26.16) (2024-10-30) ### Performance Improvements * 支持白山云cdn部署 ([b1b2cd0](https://github.com/certd/certd/commit/b1b2cd088b684eda764962abd61754c26a204d1c)) ## [1.26.15](https://github.com/certd/certd/compare/v1.26.14...v1.26.15) (2024-10-28) ### Performance Improvements * 默认证书更新时间设置为35天,增加腾讯云删除过期证书插件,可以避免腾讯云过期证书邮件 ([51b6fed](https://github.com/certd/certd/commit/51b6fed468eaa6f28ce4497ce303ace1a52abb96)) ## [1.26.14](https://github.com/certd/certd/compare/v1.26.13...v1.26.14) (2024-10-26) **Note:** Version bump only for package @certd/pipeline ## [1.26.13](https://github.com/certd/certd/compare/v1.26.12...v1.26.13) (2024-10-26) **Note:** Version bump only for package @certd/pipeline ## [1.26.12](https://github.com/certd/certd/compare/v1.26.11...v1.26.12) (2024-10-25) **Note:** Version bump only for package @certd/pipeline ## [1.26.11](https://github.com/certd/certd/compare/v1.26.10...v1.26.11) (2024-10-23) ### Performance Improvements * 申请证书启用新的反代地址 ([a705182](https://github.com/certd/certd/commit/a705182b85e51157883e48f23463263793bf3c12)) ## [1.26.10](https://github.com/certd/certd/compare/v1.26.9...v1.26.10) (2024-10-20) ### Bug Fixes * 修复cname服务普通用户access访问权限问题 ([c1e3e2e](https://github.com/certd/certd/commit/c1e3e2ee1f923ee5806479dd5f178c3286a01ae0)) ## [1.26.9](https://github.com/certd/certd/compare/v1.26.8...v1.26.9) (2024-10-19) ### Performance Improvements * 触发证书重新申请input变化对比规则优化,减少升级版本后触发申请证书的情况 ([c46a2a9](https://github.com/certd/certd/commit/c46a2a9a399c2a9a8bb59a48b9fb6e93227cce9b)) * 任务下所有步骤都跳过时,整个任务显示跳过 ([84fd3b2](https://github.com/certd/certd/commit/84fd3b250dd1161ea06c5582fdadece4b29c2e53)) ## [1.26.8](https://github.com/certd/certd/compare/v1.26.7...v1.26.8) (2024-10-15) ### Performance Improvements * 密钥备份 ([1c6028a](https://github.com/certd/certd/commit/1c6028abcf8849163462bb2f8441b6838357e09b)) * 证书直接查看 ([5dde5bd](https://github.com/certd/certd/commit/5dde5bd3f76db3959d411619d29bfb8064e3b307)) ## [1.26.7](https://github.com/certd/certd/compare/v1.26.6...v1.26.7) (2024-10-14) **Note:** Version bump only for package @certd/pipeline ## [1.26.6](https://github.com/certd/certd/compare/v1.26.5...v1.26.6) (2024-10-14) **Note:** Version bump only for package @certd/pipeline ## [1.26.5](https://github.com/certd/certd/compare/v1.26.4...v1.26.5) (2024-10-14) **Note:** Version bump only for package @certd/pipeline ## [1.26.4](https://github.com/certd/certd/compare/v1.26.3...v1.26.4) (2024-10-14) ### Performance Improvements * [comm] 支持插件管理 ([e8b617b](https://github.com/certd/certd/commit/e8b617b80ce882dd63006f0cfc719a80a1cc6acc)) * EAB授权支持绑定邮箱,支持公共EAB设置 ([07043af](https://github.com/certd/certd/commit/07043aff0ca7fd29c56dd3c363002cb15d78b464)) ## [1.26.3](https://github.com/certd/certd/compare/v1.26.2...v1.26.3) (2024-10-12) **Note:** Version bump only for package @certd/pipeline ## [1.26.2](https://github.com/certd/certd/compare/v1.26.1...v1.26.2) (2024-10-11) **Note:** Version bump only for package @certd/pipeline ## [1.26.1](https://github.com/certd/certd/compare/v1.26.0...v1.26.1) (2024-10-10) **Note:** Version bump only for package @certd/pipeline # [1.26.0](https://github.com/certd/certd/compare/v1.25.9...v1.26.0) (2024-10-10) ### Bug Fixes * 修复某些代理情况下 报 400 The plain HTTP request was sent to HTTPS port use proxy 的bug ([a13203f](https://github.com/certd/certd/commit/a13203fb3f48c427d0d81a504912248dcc07df1a)) ### Features * 域名验证方法支持CNAME间接方式,此方式支持所有域名注册商,且无需提供Access授权,但是需要手动添加cname解析 ([f3d3508](https://github.com/certd/certd/commit/f3d35084ed44f9f33845f7045e520be5c27eed93)) * 站点个性化设置 ([11a9fe9](https://github.com/certd/certd/commit/11a9fe9014d96cba929e5a066e78f2af7ae59d14)) ### Performance Improvements * 调整全部静态资源到static目录 ([a218890](https://github.com/certd/certd/commit/a21889080d6c7ffdf0af526a3a21f0b2d1c77288)) * 检查cname是否正确配置 ([b5d8935](https://github.com/certd/certd/commit/b5d8935159374fbe7fc7d4c48ae0ed9396861bdd)) * cname校验配置增加未校验通过提示 ([77cc3c4](https://github.com/certd/certd/commit/77cc3c4a5cbd81f8233a8e0bb33fab0621c0905f)) ## [1.25.9](https://github.com/certd/certd/compare/v1.25.8...v1.25.9) (2024-10-01) **Note:** Version bump only for package @certd/pipeline ## [1.25.8](https://github.com/certd/certd/compare/v1.25.7...v1.25.8) (2024-09-30) **Note:** Version bump only for package @certd/pipeline ## [1.25.7](https://github.com/certd/certd/compare/v1.25.6...v1.25.7) (2024-09-29) ### Bug Fixes * 修复某些地区被屏蔽无法激活专业版的bug ([7532a96](https://github.com/certd/certd/commit/7532a960851b84d4f2cc3dba02353c5235e1a364)) ## [1.25.6](https://github.com/certd/certd/compare/v1.25.5...v1.25.6) (2024-09-29) ### Performance Improvements * 部署支持1Panel ([d047234](https://github.com/certd/certd/commit/d047234d98d31504f2e5a472b66e1b75806af26e)) ## [1.25.5](https://github.com/certd/certd/compare/v1.25.4...v1.25.5) (2024-09-26) **Note:** Version bump only for package @certd/pipeline ## [1.25.4](https://github.com/certd/certd/compare/v1.25.3...v1.25.4) (2024-09-25) **Note:** Version bump only for package @certd/pipeline ## [1.25.3](https://github.com/certd/certd/compare/v1.25.2...v1.25.3) (2024-09-24) **Note:** Version bump only for package @certd/pipeline ## [1.25.2](https://github.com/certd/certd/compare/v1.25.1...v1.25.2) (2024-09-24) **Note:** Version bump only for package @certd/pipeline ## [1.25.1](https://github.com/certd/certd/compare/v1.25.0...v1.25.1) (2024-09-24) **Note:** Version bump only for package @certd/pipeline # [1.25.0](https://github.com/certd/certd/compare/v1.24.4...v1.25.0) (2024-09-24) ### Bug Fixes * 修复首次创建任务运行时不自动设置当前运行情况的bug ([ecd83ee](https://github.com/certd/certd/commit/ecd83ee136abdd3df9ed2f21ec2ff0f24c0ed9d9)) ### Performance Improvements * 群晖支持OTP双重验证登录 ([8b8039f](https://github.com/certd/certd/commit/8b8039f42bbce10a4d0e737cdeeeef9bb17bee5a)) * 任务支持禁用 ([8ed16b3](https://github.com/certd/certd/commit/8ed16b3ea2dfe847357863a0bfa614e4fa5fc041)) * 支持阿里云ACK证书部署 ([d331fea](https://github.com/certd/certd/commit/d331fea47789122650e057ec7c9e85ee8e66f09b)) * 支持七牛云 ([8ecc2f9](https://github.com/certd/certd/commit/8ecc2f9446a9ebd11b9bfbffbb6cf7812a043495)) * 支持k8s ingress secret ([e5a5d0a](https://github.com/certd/certd/commit/e5a5d0a607bb6b4e1a1f7a1a419bada5f2dee59f)) * http请求增加默认超时时间 ([664bd86](https://github.com/certd/certd/commit/664bd863e5b4895aabe2384277c0c65f5902fdb2)) * plugins增加图标 ([a8da658](https://github.com/certd/certd/commit/a8da658a9723342b4f43a579f7805bfef0648efb)) ## [1.24.4](https://github.com/certd/certd/compare/v1.24.3...v1.24.4) (2024-09-09) ### Performance Improvements * 前置任务步骤增加错误提示 ([ae3daa9](https://github.com/certd/certd/commit/ae3daa9bcf4fc363825aad9b77f5d3879aeeff70)) * 群晖部署教程 ([0f0af2f](https://github.com/certd/certd/commit/0f0af2f309390f388e7a272cea3a1dd30c01977d)) * 支持群晖 ([5c270b6](https://github.com/certd/certd/commit/5c270b6b9d45a2152f9fdb3c07bd98b7c803cb8e)) ## [1.24.3](https://github.com/certd/certd/compare/v1.24.2...v1.24.3) (2024-09-06) ### Performance Improvements * 支持多吉云cdn证书部署 ([65ef685](https://github.com/certd/certd/commit/65ef6857296784ca765926e09eafcb6fc8b6ecde)) ## [1.24.2](https://github.com/certd/certd/compare/v1.24.1...v1.24.2) (2024-09-06) ### Performance Improvements * 优化跳过处理逻辑 ([b80210f](https://github.com/certd/certd/commit/b80210f24bf5db1c958d06ab27c9e5d3db452eda)) * 支持pfx、der ([fbeaed2](https://github.com/certd/certd/commit/fbeaed203519f59b6d9396c4e8953353ccb5e723)) ## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02) ### Performance Improvements * 部署插件支持宝塔、易盾云等 ([ee61709](https://github.com/certd/certd/commit/ee617095efa1171548cf52fd45f0f98a368555a3)) * 授权配置支持加密 ([42a56b5](https://github.com/certd/certd/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb)) * 支持阿里云 DCDN ([98b77f8](https://github.com/certd/certd/commit/98b77f80843834616fb26f83b4c42245326abd06)) * 支持已跳过的步骤重新运行 ([ea775ad](https://github.com/certd/certd/commit/ea775adae18d57a04470cfba6b9460d761d74035)) # [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25) ### Bug Fixes * 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/certd/certd/commit/37eb762afe25c5896b75dee25f32809f8426e7b7)) * 修复执行日志没有清理的bug ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5)) ### Features * 支持google证书申请(需要使用代理) ([a593056](https://github.com/certd/certd/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe)) ### Performance Improvements * 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b)) * 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345)) * email proxy ([453f1ba](https://github.com/certd/certd/commit/453f1baa0b9eb0f648aa1b71ccf5a95b202ce13f)) ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06) **Note:** Version bump only for package @certd/pipeline ## [1.22.8](https://github.com/certd/certd/compare/v1.22.7...v1.22.8) (2024-08-05) ### Performance Improvements * 优化pipeline删除时,删除其他history ([b425203](https://github.com/certd/certd/commit/b4252033d56a9ad950f3e204ff021497c3978015)) ## [1.22.7](https://github.com/certd/certd/compare/v1.22.6...v1.22.7) (2024-08-04) **Note:** Version bump only for package @certd/pipeline ## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03) ### Bug Fixes * 修复在相同的cron时偶尔无法触发定时任务的bug ([680941a](https://github.com/certd/certd/commit/680941af119619006b592e3ab6fb112cb5556a8b)) ### Performance Improvements * 流水线支持名称模糊查询 ([59897c4](https://github.com/certd/certd/commit/59897c4ceae992ebe2972ca9e8f9196616ffdfd7)) * 优化前置任务输出为空的提示 ([6ed1e18](https://github.com/certd/certd/commit/6ed1e18c7d9c46d964ecc6abc90f3908297b7632)) ## [1.22.5](https://github.com/certd/certd/compare/v1.22.4...v1.22.5) (2024-07-26) **Note:** Version bump only for package @certd/pipeline ## [1.22.3](https://github.com/certd/certd/compare/v1.22.2...v1.22.3) (2024-07-25) **Note:** Version bump only for package @certd/pipeline ## [1.22.2](https://github.com/certd/certd/compare/v1.22.1...v1.22.2) (2024-07-23) **Note:** Version bump only for package @certd/pipeline ## [1.22.1](https://github.com/certd/certd/compare/v1.22.0...v1.22.1) (2024-07-20) ### Performance Improvements * 创建证书任务可以选择lege插件 ([affef13](https://github.com/certd/certd/commit/affef130378030c517250c58a4e787b0fc85d7d1)) # [1.22.0](https://github.com/certd/certd/compare/v1.21.2...v1.22.0) (2024-07-19) ### Features * 升级midway,支持esm ([485e603](https://github.com/certd/certd/commit/485e603b5165c28bc08694997726eaf2a585ebe7)) * 支持lego,海量DNS提供商 ([0bc6d0a](https://github.com/certd/certd/commit/0bc6d0a211920fb0084d705e1db67ee1e7262c44)) * 支持postgresql ([3b19bfb](https://github.com/certd/certd/commit/3b19bfb4291e89064b3b407a80dae092d54747d5)) ## [1.21.2](https://github.com/certd/certd/compare/v1.21.1...v1.21.2) (2024-07-08) **Note:** Version bump only for package @certd/pipeline ## [1.21.1](https://github.com/certd/certd/compare/v1.21.0...v1.21.1) (2024-07-08) **Note:** Version bump only for package @certd/pipeline # [1.21.0](https://github.com/certd/certd/compare/v1.20.17...v1.21.0) (2024-07-03) **Note:** Version bump only for package @certd/pipeline ## [1.20.17](https://github.com/certd/certd/compare/v1.20.16...v1.20.17) (2024-07-03) **Note:** Version bump only for package @certd/pipeline ## [1.20.16](https://github.com/certd/certd/compare/v1.20.15...v1.20.16) (2024-07-01) **Note:** Version bump only for package @certd/pipeline ## [1.20.15](https://github.com/certd/certd/compare/v1.20.14...v1.20.15) (2024-06-28) **Note:** Version bump only for package @certd/pipeline ## [1.20.14](https://github.com/certd/certd/compare/v1.20.13...v1.20.14) (2024-06-23) **Note:** Version bump only for package @certd/pipeline ## [1.20.13](https://github.com/certd/certd/compare/v1.20.12...v1.20.13) (2024-06-18) **Note:** Version bump only for package @certd/pipeline ## [1.20.12](https://github.com/certd/certd/compare/v1.20.10...v1.20.12) (2024-06-17) ### Performance Improvements * 支持cloudflare域名 ([fbb9a47](https://github.com/certd/certd/commit/fbb9a47e8f7bb805289b9ee64bd46ffee0f01c06)) ## [1.20.10](https://github.com/certd/certd/compare/v1.20.9...v1.20.10) (2024-05-30) ### Performance Improvements * 优化文件下载包名 ([d9eb927](https://github.com/certd/certd/commit/d9eb927b0a1445feab08b1958aa9ea80637a5ae6)) ## [1.20.9](https://github.com/certd/certd/compare/v1.20.8...v1.20.9) (2024-03-22) **Note:** Version bump only for package @certd/pipeline ## [1.20.8](https://github.com/certd/certd/compare/v1.20.7...v1.20.8) (2024-03-22) **Note:** Version bump only for package @certd/pipeline ## [1.20.7](https://github.com/certd/certd/compare/v1.20.6...v1.20.7) (2024-03-22) **Note:** Version bump only for package @certd/pipeline ## [1.20.6](https://github.com/certd/certd/compare/v1.20.5...v1.20.6) (2024-03-21) ### Performance Improvements * 插件贡献文档及示例 ([72fb20a](https://github.com/certd/certd/commit/72fb20abf3ba5bdd862575d2907703a52fd7eb17)) ## [1.20.5](https://github.com/certd/certd/compare/v1.20.2...v1.20.5) (2024-03-11) **Note:** Version bump only for package @certd/pipeline ## [1.20.2](https://github.com/certd/certd/compare/v1.2.1...v1.20.2) (2024-02-28) **Note:** Version bump only for package @certd/pipeline ## [1.2.1](https://github.com/certd/certd/compare/v1.2.0...v1.2.1) (2023-12-12) **Note:** Version bump only for package @certd/pipeline **Note:** Version bump only for package @certd/pipeline # [1.2.0](https://github.com/certd/certd/compare/v1.1.6...v1.2.0) (2023-10-27) **Note:** Version bump only for package @certd/pipeline ## [1.1.6](https://github.com/certd/certd/compare/v1.1.5...v1.1.6) (2023-07-10) ### Bug Fixes * 修复上传证书到腾讯云失败的bug ([e950322](https://github.com/certd/certd/commit/e950322232e19d1263b8552eefa5b0150fd7864e)) ## [1.1.5](https://github.com/certd/certd/compare/v1.1.4...v1.1.5) (2023-07-03) **Note:** Version bump only for package @certd/pipeline ## [1.1.4](https://github.com/certd/certd/compare/v1.1.3...v1.1.4) (2023-07-03) ### Performance Improvements * cancel task ([bc65c0a](https://github.com/certd/certd/commit/bc65c0a786360c087fe95cad93ec6a87804cc5ee)) * flush log ([891a43a](https://github.com/certd/certd/commit/891a43ae6716ff98ed06643f7da2e35199ee195c)) * flush logger ([91be682](https://github.com/certd/certd/commit/91be6826b902e0f302b1a6cbdb1d24e15914c18d)) * timeout ([3eeb1f7](https://github.com/certd/certd/commit/3eeb1f77aa2922f3545f3d2067f561d95621d54f)) ## [1.1.3](https://github.com/certd/certd/compare/v1.1.2...v1.1.3) (2023-07-03) **Note:** Version bump only for package @certd/pipeline ## [1.1.2](https://github.com/certd/certd/compare/v1.1.1...v1.1.2) (2023-07-03) **Note:** Version bump only for package @certd/pipeline ## [1.1.1](https://github.com/certd/certd/compare/v1.1.0...v1.1.1) (2023-06-28) **Note:** Version bump only for package @certd/pipeline # [1.1.0](https://github.com/certd/certd/compare/v1.0.6...v1.1.0) (2023-06-28) ### Bug Fixes * 修复access选择类型trigger ([2851a33](https://github.com/certd/certd/commit/2851a33eb2510f038fadb55da29512597a4ba512)) ### Features * 邮件通知 ([937e3fa](https://github.com/certd/certd/commit/937e3fac19cd03b8aa91db8ba03fda7fcfbacea2)) * cert download ([5a51c14](https://github.com/certd/certd/commit/5a51c14de521cb8075a80d2ae41a16e6d5281259)) * save files ([99522fb](https://github.com/certd/certd/commit/99522fb49adb42c1dfdf7bec3dd52d641158285b)) * save files ([671d273](https://github.com/certd/certd/commit/671d273e2f9136d16896536b0ca127cf372f1619)) ## [1.0.6](https://github.com/certd/certd/compare/v1.0.5...v1.0.6) (2023-05-25) **Note:** Version bump only for package @certd/pipeline ## [1.0.5](https://github.com/certd/certd/compare/v1.0.4...v1.0.5) (2023-05-25) **Note:** Version bump only for package @certd/pipeline ## [1.0.4](https://github.com/certd/certd/compare/v1.0.3...v1.0.4) (2023-05-25) **Note:** Version bump only for package @certd/pipeline ## [1.0.3](https://github.com/certd/certd/compare/v1.0.2...v1.0.3) (2023-05-25) **Note:** Version bump only for package @certd/pipeline ## [1.0.2](https://github.com/certd/certd/compare/v1.0.1...v1.0.2) (2023-05-24) **Note:** Version bump only for package @certd/pipeline ## [1.0.1](https://github.com/certd/certd/compare/v1.0.0...v1.0.1) (2023-05-24) **Note:** Version bump only for package @certd/pipeline ================================================ FILE: packages/core/pipeline/LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: packages/core/pipeline/README.md ================================================ # Vue 3 + TypeScript + Vite This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `
================================================ FILE: packages/ui/certd-client/package.json ================================================ { "name": "@certd/ui-client", "version": "1.36.10", "private": true, "scripts": { "dev": "vite --open", "dev:pm": "vite --mode pm", "dev:force": "vite --force", "debug": "vite --mode debug --open", "debug:pm": "vite --mode debugpm", "debug:force": "vite --force --mode debug", "build": "cross-env NODE_OPTIONS=--max-old-space-size=40960 vite build ", "dev-build": "echo 1", "test:unit": "vitest", "serve": "vite preview", "preview": "vite preview", "pretty-quick": "pretty-quick", "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue --ext .ts --ext .tsx src/", "format": "prettier --write src", "upgrade": "yarn upgrade-interactive --latest", "tsc": "vue-tsc --noEmit --skipLibCheck", "circle:check": "pnpm dependency-cruise --validate --output-type err-html -f dependency-report.html src", "afterPubPush": "git add . && git commit -m \"build: publish success\" && git push", "pub": "echo 1" }, "author": "greper", "license": "AGPL-3.0", "dependencies": { "@ant-design/colors": "^7.0.2", "@ant-design/icons-vue": "^7.0.1", "@aws-sdk/client-s3": "^3.535.0", "@aws-sdk/s3-request-presigner": "^3.535.0", "@ctrl/tinycolor": "^4.1.0", "@fast-crud/fast-crud": "^1.25.13", "@fast-crud/fast-extends": "^1.25.13", "@fast-crud/ui-antdv4": "^1.25.13", "@fast-crud/ui-interface": "^1.25.13", "@iconify/tailwind": "^1.2.0", "@iconify/vue": "^4.1.1", "@manypkg/get-packages": "^2.2.2", "@soerenmartius/vue3-clipboard": "^0.1.2", "@tailwindcss/nesting": "0.0.0-insiders.565cd3e", "@tailwindcss/typography": "^0.5.16", "@tanstack/vue-store": "^0.7.0", "@vee-validate/zod": "^4.15.0", "@vue-js-cron/light": "^4.0.5", "@vue/shared": "^3.5.13", "@vueuse/core": "^10.11.0", "ant-design-vue": "^4.2.6", "async-validator": "^4.2.5", "axios": "^1.7.2", "axios-mock-adapter": "^1.22.0", "base64-js": "^1.5.1", "better-scroll": "^2.5.1", "china-division": "^2.7.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "core-js": "^3.36.0", "cos-js-sdk-v5": "^1.7.0", "cron-parser": "^4.9.0", "cropperjs": "^1.6.1", "cross-env": "^7.0.3", "cssnano": "^7.0.6", "dayjs": "^1.11.7", "defu": "^6.1.4", "echarts": "^5.5.1", "highlight.js": "^11.9.0", "humanize-duration": "^3.27.3", "js-yaml": "^4.1.0", "lodash-es": "^4.17.21", "lucide-vue-next": "^0.477.0", "mitt": "^3.0.1", "monaco-editor": "^0.52.2", "monaco-yaml": "^5.3.1", "nanoid": "^4.0.0", "node-forge": "^1.3.1", "nprogress": "^0.2.0", "object-assign": "^4.1.1", "pinia": "2.1.7", "pinia-plugin-persistedstate": "^4.2.0", "postcss-antd-fixes": "^0.2.0", "postcss-import": "^16.1.0", "postcss-preset-env": "^10.1.5", "psl": "^1.9.0", "qiniu-js": "^3.4.2", "qrcode": "^1.5.4", "radix-vue": "^1.9.16", "sortablejs": "^1.15.3", "spark-md5": "^3.0.2", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", "theme-colors": "^0.1.0", "vee-validate": "^4.15.0", "vitest": "^0.34.6", "vue": "^3.4.21", "vue-cropperjs": "^5.0.0", "vue-echarts": "^7.0.3", "vue-i18n": "^9.10.2", "vue-router": "^4.3.0", "vuedraggable": "^4.1.0", "watermark-js-plus": "^1.5.8", "zod": "^3.24.2", "zod-defaults": "^0.1.3" }, "devDependencies": { "@certd/lib-iframe": "^1.36.10", "@certd/pipeline": "^1.36.10", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@types/chai": "^4.3.12", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.6", "@types/node": "^18", "@types/nprogress": "^0.2.3", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-legacy": "^5.3.2", "@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue-jsx": "^3.1.0", "@vue/compiler-sfc": "^3.4.21", "@vue/eslint-config-typescript": "^13.0.0", "@vue/test-utils": "^2.4.6", "autoprefixer": "^10.4.20", "caller-path": "^4.0.0", "chai": "^5.1.0", "dependency-cruiser": "^16.2.3", "dot": "^1.1.3", "eslint": "8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-vue": "^9.23.0", "less": "^4.2.0", "less-loader": "^12.2.0", "postcss": "^8.4.35", "prettier": "3.3.3", "pretty-quick": "^4.0.0", "rimraf": "^5.0.5", "rollup": "^4.13.0", "rollup-plugin-visualizer": "^5.12.0", "stylelint": "^15.11.0", "stylelint-order": "^6.0.4", "tailwindcss": "^3.4.14", "terser": "^5.29.2", "ts-node": "^10.9.2", "tslint": "^6.1.3", "typescript": "^5.4.2", "unplugin-vue-define-options": "^1.4.2", "vite": "^5.3.1", "vite-plugin-compression": "^0.5.1", "vite-plugin-html": "^3.2.2", "vite-plugin-theme": "^0.8.6", "vite-plugin-windicss": "^1.9.3", "vitest": "^2.1.2", "vue-eslint-parser": "^9.4.2", "vue-tsc": "^1.8.8" }, "husky": { "hooks": { "pre-commit": "pretty-quick --staged" } }, "pnpm": { "neverBuiltDependencies": [] }, "gitHead": "9c2162697f3affea22c9a8cbc0ca74f4034ab27e", "vite": { "optimizeDeps": { "include": [ "@iconify/iconify" ] } } } ================================================ FILE: packages/ui/certd-client/postcss.config.mjs ================================================ import config from "./build/tailwind-config/index.mjs"; export default { plugins: { ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}), // Specifying the config is not necessary in most cases, but it is included autoprefixer: {}, // 修复 element-plus 和 ant-design-vue 的样式和tailwindcss冲突问题 "postcss-antd-fixes": { prefixes: ["ant", "el"] }, "postcss-import": {}, "postcss-preset-env": {}, tailwindcss: { config }, "tailwindcss/nesting": {} } }; ================================================ FILE: packages/ui/certd-client/public/site-import-template.csv ================================================ name,mobile 张三,18603040102 李四,18603040103 王五,18603040104 赵六,18603040105 田七,18603040106 ================================================ FILE: packages/ui/certd-client/public/static/icons/demo.css ================================================ /* Logo 字体 */ @font-face { font-family: "iconfont logo"; src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); } .logo { font-family: "iconfont logo"; font-size: 160px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* tabs */ .nav-tabs { position: relative; } .nav-tabs .nav-more { position: absolute; right: 0; bottom: 0; height: 42px; line-height: 42px; color: #666; } #tabs { border-bottom: 1px solid #eee; } #tabs li { cursor: pointer; width: 100px; height: 40px; line-height: 40px; text-align: center; font-size: 16px; border-bottom: 2px solid transparent; position: relative; z-index: 1; margin-bottom: -1px; color: #666; } #tabs .active { border-bottom-color: #f00; color: #222; } .tab-container .content { display: none; } /* 页面布局 */ .main { padding: 30px 100px; width: 960px; margin: 0 auto; } .main .logo { color: #333; text-align: left; margin-bottom: 30px; line-height: 1; height: 110px; margin-top: -50px; overflow: hidden; *zoom: 1; } .main .logo a { font-size: 160px; color: #333; } .helps { margin-top: 40px; } .helps pre { padding: 20px; margin: 10px 0; border: solid 1px #e7e1cd; background-color: #fffdef; overflow: auto; } .icon_lists { width: 100% !important; overflow: hidden; *zoom: 1; } .icon_lists li { width: 100px; margin-bottom: 10px; margin-right: 20px; text-align: center; list-style: none !important; cursor: default; } .icon_lists li .code-name { line-height: 1.2; } .icon_lists .icon { display: block; height: 100px; line-height: 100px; font-size: 42px; margin: 10px auto; color: #333; -webkit-transition: font-size 0.25s linear, width 0.25s linear; -moz-transition: font-size 0.25s linear, width 0.25s linear; transition: font-size 0.25s linear, width 0.25s linear; } .icon_lists .icon:hover { font-size: 100px; } .icon_lists .svg-icon { /* 通过设置 font-size 来改变图标大小 */ width: 1em; /* 图标和文字相邻时,垂直对齐 */ vertical-align: -0.15em; /* 通过设置 color 来改变 SVG 的颜色/fill */ fill: currentColor; /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 normalize.css 中也包含这行 */ overflow: hidden; } .icon_lists li .name, .icon_lists li .code-name { color: #666; } /* markdown 样式 */ .markdown { color: #666; font-size: 14px; line-height: 1.8; } .highlight { line-height: 1.5; } .markdown img { vertical-align: middle; max-width: 100%; } .markdown h1 { color: #404040; font-weight: 500; line-height: 40px; margin-bottom: 24px; } .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { color: #404040; margin: 1.6em 0 0.6em 0; font-weight: 500; clear: both; } .markdown h1 { font-size: 28px; } .markdown h2 { font-size: 22px; } .markdown h3 { font-size: 16px; } .markdown h4 { font-size: 14px; } .markdown h5 { font-size: 12px; } .markdown h6 { font-size: 12px; } .markdown hr { height: 1px; border: 0; background: #e9e9e9; margin: 16px 0; clear: both; } .markdown p { margin: 1em 0; } .markdown>p, .markdown>blockquote, .markdown>.highlight, .markdown>ol, .markdown>ul { width: 80%; } .markdown ul>li { list-style: circle; } .markdown>ul li, .markdown blockquote ul>li { margin-left: 20px; padding-left: 4px; } .markdown>ul li p, .markdown>ol li p { margin: 0.6em 0; } .markdown ol>li { list-style: decimal; } .markdown>ol li, .markdown blockquote ol>li { margin-left: 20px; padding-left: 4px; } .markdown code { margin: 0 3px; padding: 0 5px; background: #eee; border-radius: 3px; } .markdown strong, .markdown b { font-weight: 600; } .markdown>table { border-collapse: collapse; border-spacing: 0px; empty-cells: show; border: 1px solid #e9e9e9; width: 95%; margin-bottom: 24px; } .markdown>table th { white-space: nowrap; color: #333; font-weight: 600; } .markdown>table th, .markdown>table td { border: 1px solid #e9e9e9; padding: 8px 16px; text-align: left; } .markdown>table th { background: #F7F7F7; } .markdown blockquote { font-size: 90%; color: #999; border-left: 4px solid #e9e9e9; padding-left: 0.8em; margin: 1em 0; } .markdown blockquote p { margin: 0; } .markdown .anchor { opacity: 0; transition: opacity 0.3s ease; margin-left: 8px; } .markdown .waiting { color: #ccc; } .markdown h1:hover .anchor, .markdown h2:hover .anchor, .markdown h3:hover .anchor, .markdown h4:hover .anchor, .markdown h5:hover .anchor, .markdown h6:hover .anchor { opacity: 1; display: inline-block; } .markdown>br, .markdown>p>br { clear: both; } .hljs { display: block; background: white; padding: 0.5em; color: #333333; overflow-x: auto; } .hljs-comment, .hljs-meta { color: #969896; } .hljs-string, .hljs-variable, .hljs-template-variable, .hljs-strong, .hljs-emphasis, .hljs-quote { color: #df5000; } .hljs-keyword, .hljs-selector-tag, .hljs-type { color: #a71d5d; } .hljs-literal, .hljs-symbol, .hljs-bullet, .hljs-attribute { color: #0086b3; } .hljs-section, .hljs-name { color: #63a35c; } .hljs-tag { color: #333333; } .hljs-title, .hljs-attr, .hljs-selector-id, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo { color: #795da3; } .hljs-addition { color: #55a532; background-color: #eaffea; } .hljs-deletion { color: #bd2c00; background-color: #ffecec; } .hljs-link { text-decoration: underline; } /* 代码高亮 */ /* PrismJS 1.15.0 https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ /** * prism.js default theme for JavaScript, CSS and HTML * Based on dabblet (http://dabblet.com) * @author Lea Verou */ code[class*="language-"], pre[class*="language-"] { color: black; background: none; text-shadow: 0 1px white; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; line-height: 1.5; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { text-shadow: none; background: #b3d4fc; } pre[class*="language-"]::selection, pre[class*="language-"] ::selection, code[class*="language-"]::selection, code[class*="language-"] ::selection { text-shadow: none; background: #b3d4fc; } @media print { code[class*="language-"], pre[class*="language-"] { text-shadow: none; } } /* Code blocks */ pre[class*="language-"] { padding: 1em; margin: .5em 0; overflow: auto; } :not(pre)>code[class*="language-"], pre[class*="language-"] { background: #f5f2f0; } /* Inline code */ :not(pre)>code[class*="language-"] { padding: .1em; border-radius: .3em; white-space: normal; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: slategray; } .token.punctuation { color: #999; } .namespace { opacity: .7; } .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol, .token.deleted { color: #905; } .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #690; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #9a6e3a; background: hsla(0, 0%, 100%, .5); } .token.atrule, .token.attr-value, .token.keyword { color: #07a; } .token.function, .token.class-name { color: #DD4A68; } .token.regex, .token.important, .token.variable { color: #e90; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } ================================================ FILE: packages/ui/certd-client/public/static/icons/demo_index.html ================================================ iconfont Demo

  • cdn
    &#xe6e4;
  • 京东云
    &#xe653;
  • volcengine
    &#xe651;
  • upyun
    &#xe62f;
  • plesk_
    &#xecc0;
  • 易支付-01
    &#xe741;
  • 1Panel
    &#xe606;
  • 西部数码
    &#xe73c;
  • qnap
    &#xe607;
  • proxmox
    &#xe9cc;
  • aws
    &#xe604;
  • uni-app
    &#xe602;
  • lucky
    &#xe752;
  • ctyun
    &#xe719;
  • 雷池
    &#xe748;
  • 华为
    &#xe610;
  • qiniuyun
    &#xe603;
  • aliyun
    &#xe601;
  • 腾讯云
    &#xe747;
  • doge
    &#xe605;
  • bt
    &#xe600;

Unicode 引用


Unicode 是字体在网页端最原始的应用方式,特点是:

  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 默认情况下不支持多色,直接添加多色图标会自动去色。

注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)

Unicode 使用步骤如下:

第一步:拷贝项目下面生成的 @font-face

@font-face {
  font-family: 'iconfont';
  src: url('iconfont.svg?t=1743267254898#iconfont') format('svg');
}

第二步:定义使用 iconfont 的样式

.iconfont {
  font-family: "iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

第三步:挑选相应图标并获取字体编码,应用于页面

<span class="iconfont">&#x33;</span>

"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

  • cdn
    .icon-cdn
  • 京东云
    .icon-jdcloud
  • volcengine
    .icon-volcengine
  • upyun
    .icon-upyun
  • plesk_
    .icon-plesk
  • 易支付-01
    .icon-yizhifu
  • 1Panel
    .icon-onepanel
  • 西部数码
    .icon-xibushuma
  • qnap
    .icon-qnap
  • proxmox
    .icon-proxmox
  • aws
    .icon-aws
  • uni-app
    .icon-uniapp
  • lucky
    .icon-lucky
  • ctyun
    .icon-ctyun
  • 雷池
    .icon-safeline
  • 华为
    .icon-huawei
  • qiniuyun
    .icon-qiniuyun
  • aliyun
    .icon-aliyun
  • 腾讯云
    .icon-tencentcloud
  • doge
    .icon-dogecloud
  • bt
    .icon-bt

font-class 引用


font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

与 Unicode 使用方式相比,具有如下特点:

  • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。

使用步骤如下:

第一步:引入项目下面生成的 fontclass 代码:

<link rel="stylesheet" href="./iconfont.css">

第二步:挑选相应图标并获取类名,应用于页面:

<span class="iconfont icon-xxx"></span>

" iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

  • cdn
    #icon-cdn
  • 京东云
    #icon-jdcloud
  • volcengine
    #icon-volcengine
  • upyun
    #icon-upyun
  • plesk_
    #icon-plesk
  • 易支付-01
    #icon-yizhifu
  • 1Panel
    #icon-onepanel
  • 西部数码
    #icon-xibushuma
  • qnap
    #icon-qnap
  • proxmox
    #icon-proxmox
  • aws
    #icon-aws
  • uni-app
    #icon-uniapp
  • lucky
    #icon-lucky
  • ctyun
    #icon-ctyun
  • 雷池
    #icon-safeline
  • 华为
    #icon-huawei
  • qiniuyun
    #icon-qiniuyun
  • aliyun
    #icon-aliyun
  • 腾讯云
    #icon-tencentcloud
  • doge
    #icon-dogecloud
  • bt
    #icon-bt

Symbol 引用


这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:

  • 支持多色图标了,不再受单色限制。
  • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
  • 兼容性较差,支持 IE9+,及现代浏览器。
  • 浏览器渲染 SVG 的性能一般,还不如 png。

使用步骤如下:

第一步:引入项目下面生成的 symbol 代码:

<script src="./iconfont.js"></script>

第二步:加入通用 CSS 代码(引入一次就行):

<style>
.icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>

第三步:挑选相应图标并获取类名,应用于页面:

<svg class="icon" aria-hidden="true">
  <use xlink:href="#icon-xxx"></use>
</svg>
================================================ FILE: packages/ui/certd-client/public/static/icons/iconfont.css ================================================ @font-face { font-family: "iconfont"; /* Project id 4688792 */ src: url('iconfont.svg?t=1743267254898#iconfont') format('svg'); } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-cdn:before { content: "\e6e4"; } .icon-jdcloud:before { content: "\e653"; } .icon-volcengine:before { content: "\e651"; } .icon-upyun:before { content: "\e62f"; } .icon-plesk:before { content: "\ecc0"; } .icon-yizhifu:before { content: "\e741"; } .icon-onepanel:before { content: "\e606"; } .icon-xibushuma:before { content: "\e73c"; } .icon-qnap:before { content: "\e607"; } .icon-proxmox:before { content: "\e9cc"; } .icon-aws:before { content: "\e604"; } .icon-uniapp:before { content: "\e602"; } .icon-lucky:before { content: "\e752"; } .icon-ctyun:before { content: "\e719"; } .icon-safeline:before { content: "\e748"; } .icon-huawei:before { content: "\e610"; } .icon-qiniuyun:before { content: "\e603"; } .icon-aliyun:before { content: "\e601"; } .icon-tencentcloud:before { content: "\e747"; } .icon-dogecloud:before { content: "\e605"; } .icon-bt:before { content: "\e600"; } ================================================ FILE: packages/ui/certd-client/public/static/icons/iconfont.js ================================================ window._iconfont_svg_string_4688792='',(a=>{var c=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,i,h,o,e,d=function(c,l){l.parentNode.insertBefore(c,l)};if(c&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(c){console&&console.log(c)}}t=function(){var c,l=document.createElement("div");l.innerHTML=a._iconfont_svg_string_4688792,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(c=document.body).firstChild?d(l,c.firstChild):c.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(i=function(){document.removeEventListener("DOMContentLoaded",i,!1),t()},document.addEventListener("DOMContentLoaded",i,!1)):document.attachEvent&&(h=t,o=a.document,e=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,n())})}function n(){e||(e=!0,h())}function p(){try{o.documentElement.doScroll("left")}catch(c){return void setTimeout(p,50)}n()}})(window); ================================================ FILE: packages/ui/certd-client/public/static/icons/iconfont.json ================================================ { "id": "4688792", "name": "certd", "font_family": "iconfont", "css_prefix_text": "icon-", "description": "", "glyphs": [ { "icon_id": "13592652", "name": "cdn", "font_class": "cdn", "unicode": "e6e4", "unicode_decimal": 59108 }, { "icon_id": "30950102", "name": "京东云", "font_class": "jdcloud", "unicode": "e653", "unicode_decimal": 58963 }, { "icon_id": "43404901", "name": "volcengine", "font_class": "volcengine", "unicode": "e651", "unicode_decimal": 58961 }, { "icon_id": "5771134", "name": "upyun", "font_class": "upyun", "unicode": "e62f", "unicode_decimal": 58927 }, { "icon_id": "27272666", "name": "plesk_", "font_class": "plesk", "unicode": "ecc0", "unicode_decimal": 60608 }, { "icon_id": "23930871", "name": "易支付-01", "font_class": "yizhifu", "unicode": "e741", "unicode_decimal": 59201 }, { "icon_id": "40476533", "name": "1Panel", "font_class": "onepanel", "unicode": "e606", "unicode_decimal": 58886 }, { "icon_id": "26435508", "name": "西部数码", "font_class": "xibushuma", "unicode": "e73c", "unicode_decimal": 59196 }, { "icon_id": "27487624", "name": "qnap", "font_class": "qnap", "unicode": "e607", "unicode_decimal": 58887 }, { "icon_id": "27268231", "name": "proxmox", "font_class": "proxmox", "unicode": "e9cc", "unicode_decimal": 59852 }, { "icon_id": "31636255", "name": "aws", "font_class": "aws", "unicode": "e604", "unicode_decimal": 58884 }, { "icon_id": "34071209", "name": "uni-app", "font_class": "uniapp", "unicode": "e602", "unicode_decimal": 58882 }, { "icon_id": "3467975", "name": "lucky", "font_class": "lucky", "unicode": "e752", "unicode_decimal": 59218 }, { "icon_id": "41854563", "name": "ctyun", "font_class": "ctyun", "unicode": "e719", "unicode_decimal": 59161 }, { "icon_id": "43757703", "name": "雷池", "font_class": "safeline", "unicode": "e748", "unicode_decimal": 59208 }, { "icon_id": "24164616", "name": "华为", "font_class": "huawei", "unicode": "e610", "unicode_decimal": 58896 }, { "icon_id": "9612999", "name": "qiniuyun", "font_class": "qiniuyun", "unicode": "e603", "unicode_decimal": 58883 }, { "icon_id": "26492886", "name": "aliyun", "font_class": "aliyun", "unicode": "e601", "unicode_decimal": 58881 }, { "icon_id": "9126093", "name": "腾讯云", "font_class": "tencentcloud", "unicode": "e747", "unicode_decimal": 59207 }, { "icon_id": "29654736", "name": "doge", "font_class": "dogecloud", "unicode": "e605", "unicode_decimal": 58885 }, { "icon_id": "39910795", "name": "bt", "font_class": "bt", "unicode": "e600", "unicode_decimal": 58880 } ] } ================================================ FILE: packages/ui/certd-client/public/static/index.css ================================================ html, body, #app { height: 100%; margin: 0; padding: 0; width: 100%;} .fs-bootstrap { background-color: #474949; height: 100%; display: flex; flex-direction: column;position: fixed;width: 100% } .fs-bootstrap__main {flex:1; user-select: none; width: 100%; flex-grow: 1; display: flex; justify-content: center; align-items: center; flex-direction: column; } .fs-bootstrap__footer { width: 100%; flex-grow: 0; text-align: center; padding: 10px 0; } .fs-bootstrap__footer > a { font-size: 12px; color: #ABABAB; text-decoration: none; } .fs-bootstrap__loading {box-sizing: border-box; height: 50px; width: 50px; margin-bottom: 5px;border:5px solid #333333;border-bottom:#aaa 5px solid; border-radius:1000px; animation:load 1.1s infinite linear;-webkit-animation:load 1.1s infinite linear;-moz-animation:load 1.1s infinite linear; -o-animation:load 1.1s infinite linear; } @keyframes load {from {transform:rotate(0deg);-ms-transform:rotate(0deg);}to { transform:rotate(360deg);-ms-transform:rotate(360deg); } }@-webkit-keyframes load {from {-webkit-transform:rotate(0deg); }to { -webkit-transform:rotate(360deg);} }@-moz-keyframes load { from { -moz-transform:rotate(0deg); } to { -moz-transform:rotate(360deg);} }@-o-keyframes load { from { -o-transform:rotate(0deg);} to { -o-transform:rotate(360deg);}} ================================================ FILE: packages/ui/certd-client/src/App.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/api/service.ts ================================================ import axios from "axios"; import { get } from "lodash-es"; import { errorLog, errorCreate } from "./tools"; import { env } from "/src/utils/util.env"; import { useUserStore } from "/@/store/user"; export class CodeError extends Error { code: number; data?: any; constructor(message: string, code: number, data?: any) { super(message); this.code = code; this.data = data; } } /** * @description 创建请求实例 */ function createService() { // 创建一个 axios 实例 const service = axios.create(); // 请求拦截 service.interceptors.request.use( config => config, error => { // 发送失败 console.log(error); return Promise.reject(error); } ); // 响应拦截 service.interceptors.response.use( response => { if (response.config.responseType === "blob") { return response; } //@ts-ignore if (response.config.returnOriginRes) { return response; } // dataAxios 是 axios 返回数据中的 data const dataAxios = response.data; // @ts-ignore if (response.config.unpack === false) { //如果不需要解包 return dataAxios; } // 这个状态码是和后端约定的 if (dataAxios?.code === undefined) { // 如果没有 code 代表这不是项目后端开发的接口 errorCreate(`非标准返回:${dataAxios}, ${response.config.url}`); return dataAxios; } const { code } = dataAxios; // 有 code 代表这是一个后端接口 可以进行进一步的判断 switch (code) { case 0: // [ 示例 ] code === 0 代表没有错误 // @ts-ignore return dataAxios?.data; default: // 不是正确的 code const errorMessage = dataAxios.msg || dataAxios.message || "未知错误"; // @ts-ignore if (response?.config?.onError) { const err = new CodeError(errorMessage, dataAxios.code, dataAxios.data); // @ts-ignore response.config.onError(err); } //@ts-ignore const showErrorNotify = response?.config?.showErrorNotify; errorCreate(`${errorMessage}: ${response.config.url}`, showErrorNotify, dataAxios); } }, error => { const status = get(error, "response.status"); switch (status) { case 400: error.message = "请求错误"; break; case 401: error.message = "未授权,请登录"; break; case 403: error.message = "拒绝访问"; break; case 404: error.message = `请求地址出错`; break; case 408: error.message = "请求超时"; break; case 500: error.message = "服务器内部错误"; break; case 501: error.message = "服务未实现"; break; case 502: error.message = "网关错误"; break; case 503: error.message = "服务不可用"; break; case 504: error.message = "网关超时"; break; case 505: error.message = "HTTP版本不受支持"; break; default: break; } error.message += `: ${error.response?.config?.url}`; errorLog(error, error?.response?.config?.showErrorNotify); if (status === 401) { const userStore = useUserStore(); userStore.logout(true, true); } if (error?.config?.onError) { error.config.onError(error); } return Promise.reject(error); } ); return service; } /** * @description 创建请求方法 * @param {Object} service axios 实例 */ function createRequestFunction(service: any) { return function (config: any) { const configDefault = { headers: { "Content-Type": get(config, "headers.Content-Type", "application/json"), }, timeout: 30000, baseURL: env.API, data: {}, }; const userStore = useUserStore(); const token = userStore.getToken; if (token != null) { // @ts-ignore configDefault.headers.Authorization = token; } return service(Object.assign(configDefault, config)); }; } // 用于真实网络请求的实例和请求方法 export const service = createService(); export const request = createRequestFunction(service); ================================================ FILE: packages/ui/certd-client/src/api/tools.ts ================================================ /** * @description 安全地解析 json 字符串 * @param {String} jsonString 需要解析的 json 字符串 * @param {String} defaultValue 默认值 */ import { uiContext } from "@fast-crud/fast-crud"; import { CodeError } from "/@/api/service"; export function parse(jsonString = "{}", defaultValue = {}) { let result = defaultValue; try { result = JSON.parse(jsonString); } catch (error) { console.log(error); } return result; } /** * @description 接口请求返回 * @param {Any} data 返回值 * @param {String} msg 状态信息 * @param {Number} code 状态码 */ export function response(data = {}, msg = "", code = 0) { return [200, { code, msg, data }]; } /** * @description 接口请求返回 正确返回 * @param {Any} data 返回值 * @param {String} msg 状态信息 */ export function responseSuccess(data = {}, msg = "成功") { return response(data, msg); } /** * @description 接口请求返回 错误返回 * @param {Any} data 返回值 * @param {String} msg 状态信息 * @param {Number} code 状态码 */ export function responseError(data = {}, msg = "请求失败", code = 500) { return response(data, msg, code); } /** * @description 记录和显示错误 * @param {Error} error 错误对象 */ export function errorLog(error: any, notify = true) { // 打印到控制台 console.error("errorLog", error); let message = error.message; if (error.response?.data?.message) { message = error.response.data.message; } if (message.indexOf("ssl3_get_record:wrong version number") >= 0) { message = "http协议错误,服务端要求http协议,请检查是否使用了https请求"; } if (notify) { // 显示提示 uiContext.get().notification.error({ message }); } } /** * @description 创建一个错误 */ export function errorCreate(msg: string, notify = true, data?: any) { const err = new CodeError(msg, data.code, data.data); console.error("errorCreate", err); if (notify) { uiContext.get().notification.error({ message: err.message }); } throw err; } ================================================ FILE: packages/ui/certd-client/src/components/ai/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/code-editor/async-import.ts ================================================ export async function importJsYaml() { return await import("js-yaml"); } export async function importYamlContribution() { await import("monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution"); } export async function importJsonContribution() { await import("monaco-editor/esm/vs/language/json/monaco.contribution"); } export async function importJavascriptContribution() { await import("monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution"); } export async function importMonacoYaml() { return await import("monaco-yaml"); } export async function importWorks() { const editorWorker = await import("monaco-editor/esm/vs/editor/editor.worker?worker"); const jsonWorker = await import("monaco-editor/esm/vs/language/json/json.worker?worker"); const cssWorker = await import("monaco-editor/esm/vs/language/css/css.worker?worker"); const htmlWorker = await import("monaco-editor/esm/vs/language/html/html.worker?worker"); const tsWorker = await import("monaco-editor/esm/vs/language/typescript/ts.worker?worker"); const yamlWorker = await import("./yaml.worker?worker"); return { editorWorker, jsonWorker, cssWorker, htmlWorker, tsWorker, yamlWorker, }; } ================================================ FILE: packages/ui/certd-client/src/components/code-editor/import-works.ts ================================================ import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; import yamlWorker from "./yaml.worker?worker"; const WorkerBucket: any = {}; /** * 注册自定义worker * @param name * @param worker */ export function registerWorker(name: string, worker: any) { WorkerBucket[name] = worker; } //@ts-ignore window.MonacoEnvironment = { //@ts-ignore getWorker(_, label) { const custom = WorkerBucket[label]; if (custom) { return new custom(); } if (label === "json") { return new jsonWorker(); } else if (label === "css" || label === "scss" || label === "less") { return new cssWorker(); } else if (label === "html" || label === "handlebars" || label === "razor") { return new htmlWorker(); } else if (label === "typescript" || label === "javascript") { return new tsWorker(); } else if (label === "yaml" || label === "yml") { //@ts-ignore return new yamlWorker(); } return new editorWorker(); }, }; ================================================ FILE: packages/ui/certd-client/src/components/code-editor/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/code-editor/validators.ts ================================================ import { importJsYaml } from "./async-import"; const jsonRule = { validator: async (rule: any, value: any) => { //校验value json的有效性 if (value) { try { JSON.parse(value); } catch (e: any) { console.error(e); throw new Error("json格式错误:" + e.message); } } }, message: "json格式错误", }; const yamlRule = { validator: async (rule: any, value: any) => { //校验value yaml的有效性 if (value) { try { const yaml = await importJsYaml(); yaml.load(value, { schema: yaml.JSON_SCHEMA }); } catch (e: any) { console.error(e); throw new Error("yaml格式错误:" + e.message); } } }, message: "yaml格式错误", }; export const FsEditorCodeValidators = { jsonRule, yamlRule, }; ================================================ FILE: packages/ui/certd-client/src/components/code-editor/workers.ts ================================================ const WorkerBucket: any = {}; /** * 注册自定义worker * @param name * @param worker */ export function registerWorker(name: string, worker: any) { WorkerBucket[name] = worker; } export async function initWorkers() { if (window.MonacoEnvironment) { return; } // const { editorWorker, jsonWorker, cssWorker, htmlWorker, tsWorker } = await importWorks(); // // // const editorWorker = new Worker(new URL("monaco-editor/esm/vs/editor/editor.worker.js", import.meta.url)); // // const jsonWorker = new Worker(new URL("monaco-editor/esm/vs/language/json/json.worker.js", import.meta.url)); // // const cssWorker = new Worker(new URL("monaco-editor/esm/vs/language/css/css.worker.js", import.meta.url)); // // const htmlWorker = new Worker(new URL("monaco-editor/esm/vs/language/html/html.worker.js", import.meta.url)); // // const tsWorker = new Worker(new URL("monaco-editor/esm/vs/language/typescript/ts.worker.js", import.meta.url)); // // const yamlWorker = new Worker(new URL("./yaml.worker.js", import.meta.url)); const editorWorker = await import("monaco-editor/esm/vs/editor/editor.worker?worker"); const jsonWorker = await import("monaco-editor/esm/vs/language/json/json.worker?worker"); const cssWorker = await import("monaco-editor/esm/vs/language/css/css.worker?worker"); const htmlWorker = await import("monaco-editor/esm/vs/language/html/html.worker?worker"); const tsWorker = await import("monaco-editor/esm/vs/language/typescript/ts.worker?worker"); const yamlWorker = await import("./yaml.worker?worker"); //@ts-ignore window.MonacoEnvironment = { //@ts-ignore getWorker(_, label) { const custom = WorkerBucket[label]; if (custom) { return new custom(); } if (label === "json") { return new jsonWorker.default(); } else if (label === "css" || label === "scss" || label === "less") { return new cssWorker.default(); } else if (label === "html" || label === "handlebars" || label === "razor") { return new htmlWorker.default(); } else if (label === "typescript" || label === "javascript") { return new tsWorker.default(); } else if (label === "yaml" || label === "yml") { //@ts-ignore return new yamlWorker.default(); } return new editorWorker.default(); }, }; } ================================================ FILE: packages/ui/certd-client/src/components/code-editor/yaml.worker.ts ================================================ export * from "monaco-yaml/yaml.worker.js"; ================================================ FILE: packages/ui/certd-client/src/components/container.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/cron-editor/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/cron-editor/utils.ts ================================================ import parser from "cron-parser"; import dayjs from "dayjs"; export function getCronNextTimes(cron: string, count: number = 1) { if (cron == null) { return []; } const nextTimes = []; const interval = parser.parseExpression(cron); for (let i = 0; i < count; i++) { const next = interval.next().getTime(); nextTimes.push(dayjs(next).format("YYYY-MM-DD HH:mm:ss")); } return nextTimes; } ================================================ FILE: packages/ui/certd-client/src/components/editable.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/email-selector/api.ts ================================================ import { request } from "/src/api/service"; export async function EmailList() { return await request({ url: "/mine/email/list", method: "post", data: {}, }); } export async function EmailDelete(email: string) { return await request({ url: "/mine/email/delete", method: "post", data: { email: email, }, }); } export async function EmailAdd(email: string) { return await request({ url: "/mine/email/add", method: "post", data: { email: email, }, }); } ================================================ FILE: packages/ui/certd-client/src/components/email-selector/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/expires-time-text.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/file-input.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/fold-box.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/highlight/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/highlight/libs/htmlFormat.js ================================================ /* eslint-disable */ // 功能 // 将HTML字符串格式化 const format = (function () { function style_html(html_source, indent_size, indent_character, max_char) { var Parser, multi_parser; function Parser() { this.pos = 0; this.token = ""; this.current_mode = "CONTENT"; this.tags = { parent: "parent1", parentcount: 1, parent1: "", }; this.tag_type = ""; this.token_text = this.last_token = this.last_text = this.token_type = ""; this.Utils = { whitespace: "\n\r\t ".split(""), single_token: "br,input,link,meta,!doctype,basefont,base,area,hr,wbr,param,img,isindex,?xml,embed".split(","), extra_liners: "head,body,/html".split(","), in_array: function (what, arr) { for (var i = 0; i < arr.length; i++) { if (what === arr[i]) { return true; } } return false; }, }; this.get_content = function () { var char = ""; var content = []; var space = false; while (this.input.charAt(this.pos) !== "<") { if (this.pos >= this.input.length) { return content.length ? content.join("") : ["", "TK_EOF"]; } char = this.input.charAt(this.pos); this.pos++; this.line_char_count++; if (this.Utils.in_array(char, this.Utils.whitespace)) { if (content.length) { space = true; } this.line_char_count--; continue; } else if (space) { if (this.line_char_count >= this.max_char) { content.push("\n"); for (var i = 0; i < this.indent_level; i++) { content.push(this.indent_string); } this.line_char_count = 0; } else { content.push(" "); this.line_char_count++; } space = false; } content.push(char); } return content.length ? content.join("") : ""; }; this.get_script = function () { var char = ""; var content = []; var reg_match = new RegExp("", "igm"); reg_match.lastIndex = this.pos; var reg_array = reg_match.exec(this.input); var end_script = reg_array ? reg_array.index : this.input.length; while (this.pos < end_script) { if (this.pos >= this.input.length) { return content.length ? content.join("") : ["", "TK_EOF"]; } char = this.input.charAt(this.pos); this.pos++; content.push(char); } return content.length ? content.join("") : ""; }; this.record_tag = function (tag) { if (this.tags[tag + "count"]) { this.tags[tag + "count"]++; this.tags[tag + this.tags[tag + "count"]] = this.indent_level; } else { this.tags[tag + "count"] = 1; this.tags[tag + this.tags[tag + "count"]] = this.indent_level; } this.tags[tag + this.tags[tag + "count"] + "parent"] = this.tags.parent; this.tags.parent = tag + this.tags[tag + "count"]; }; this.retrieve_tag = function (tag) { if (this.tags[tag + "count"]) { var temp_parent = this.tags.parent; while (temp_parent) { if (tag + this.tags[tag + "count"] === temp_parent) { break; } temp_parent = this.tags[temp_parent + "parent"]; } if (temp_parent) { this.indent_level = this.tags[tag + this.tags[tag + "count"]]; this.tags.parent = this.tags[temp_parent + "parent"]; } delete this.tags[tag + this.tags[tag + "count"] + "parent"]; delete this.tags[tag + this.tags[tag + "count"]]; if (this.tags[tag + "count"] == 1) { delete this.tags[tag + "count"]; } else { this.tags[tag + "count"]--; } } }; this.get_tag = function () { var char = ""; var content = []; var space = false; do { if (this.pos >= this.input.length) { return content.length ? content.join("") : ["", "TK_EOF"]; } char = this.input.charAt(this.pos); this.pos++; this.line_char_count++; if (this.Utils.in_array(char, this.Utils.whitespace)) { space = true; this.line_char_count--; continue; } if (char === "'" || char === '"') { if (!content[1] || content[1] !== "!") { char += this.get_unformatted(char); space = true; } } if (char === "=") { space = false; } if (content.length && content[content.length - 1] !== "=" && char !== ">" && space) { if (this.line_char_count >= this.max_char) { this.print_newline(false, content); this.line_char_count = 0; } else { content.push(" "); this.line_char_count++; } space = false; } content.push(char); } while (char !== ">"); var tag_complete = content.join(""); var tag_index; if (tag_complete.indexOf(" ") != -1) { tag_index = tag_complete.indexOf(" "); } else { tag_index = tag_complete.indexOf(">"); } var tag_check = tag_complete.substring(1, tag_index).toLowerCase(); if (tag_complete.charAt(tag_complete.length - 2) === "/" || this.Utils.in_array(tag_check, this.Utils.single_token)) { this.tag_type = "SINGLE"; } else if (tag_check === "script") { this.record_tag(tag_check); this.tag_type = "SCRIPT"; } else if (tag_check === "style") { this.record_tag(tag_check); this.tag_type = "STYLE"; } else if (tag_check.charAt(0) === "!") { if (tag_check.indexOf("[if") != -1) { if (tag_complete.indexOf("!IE") != -1) { var comment = this.get_unformatted("-->", tag_complete); content.push(comment); } this.tag_type = "START"; } else if (tag_check.indexOf("[endif") != -1) { this.tag_type = "END"; this.unindent(); } else if (tag_check.indexOf("[cdata[") != -1) { var comment = this.get_unformatted("]]>", tag_complete); content.push(comment); this.tag_type = "SINGLE"; } else { var comment = this.get_unformatted("-->", tag_complete); content.push(comment); this.tag_type = "SINGLE"; } } else { if (tag_check.charAt(0) === "/") { this.retrieve_tag(tag_check.substring(1)); this.tag_type = "END"; } else { this.record_tag(tag_check); this.tag_type = "START"; } if (this.Utils.in_array(tag_check, this.Utils.extra_liners)) { this.print_newline(true, this.output); } } return content.join(""); }; this.get_unformatted = function (delimiter, orig_tag) { if (orig_tag && orig_tag.indexOf(delimiter) != -1) { return ""; } var char = ""; var content = ""; var space = true; do { char = this.input.charAt(this.pos); this.pos++; if (this.Utils.in_array(char, this.Utils.whitespace)) { if (!space) { this.line_char_count--; continue; } if (char === "\n" || char === "\r") { content += "\n"; for (var i = 0; i < this.indent_level; i++) { content += this.indent_string; } space = false; this.line_char_count = 0; continue; } } content += char; this.line_char_count++; space = true; } while (content.indexOf(delimiter) == -1); return content; }; this.get_token = function () { var token; if (this.last_token === "TK_TAG_SCRIPT") { var temp_token = this.get_script(); if (typeof temp_token !== "string") { return temp_token; } //token = js_beautify(temp_token, this.indent_size, this.indent_character, this.indent_level); //return [token, 'TK_CONTENT']; return [temp_token, "TK_CONTENT"]; } if (this.current_mode === "CONTENT") { token = this.get_content(); if (typeof token !== "string") { return token; } else { return [token, "TK_CONTENT"]; } } if (this.current_mode === "TAG") { token = this.get_tag(); if (typeof token !== "string") { return token; } else { var tag_name_type = "TK_TAG_" + this.tag_type; return [token, tag_name_type]; } } }; this.printer = function (js_source, indent_character, indent_size, max_char) { this.input = js_source || ""; this.output = []; this.indent_character = indent_character || " "; this.indent_string = ""; this.indent_size = indent_size || 2; this.indent_level = 0; this.max_char = max_char || 70; this.line_char_count = 0; for (var i = 0; i < this.indent_size; i++) { this.indent_string += this.indent_character; } this.print_newline = function (ignore, arr) { this.line_char_count = 0; if (!arr || !arr.length) { return; } if (!ignore) { while (this.Utils.in_array(arr[arr.length - 1], this.Utils.whitespace)) { arr.pop(); } } arr.push("\n"); for (var i = 0; i < this.indent_level; i++) { arr.push(this.indent_string); } }; this.print_token = function (text) { this.output.push(text); }; this.indent = function () { this.indent_level++; }; this.unindent = function () { if (this.indent_level > 0) { this.indent_level--; } }; }; return this; } multi_parser = new Parser(); multi_parser.printer(html_source, indent_character, indent_size); while (true) { var t = multi_parser.get_token(); multi_parser.token_text = t[0]; multi_parser.token_type = t[1]; if (multi_parser.token_type === "TK_EOF") { break; } switch (multi_parser.token_type) { case "TK_TAG_START": case "TK_TAG_SCRIPT": case "TK_TAG_STYLE": multi_parser.print_newline(false, multi_parser.output); multi_parser.print_token(multi_parser.token_text); multi_parser.indent(); multi_parser.current_mode = "CONTENT"; break; case "TK_TAG_END": multi_parser.print_newline(true, multi_parser.output); multi_parser.print_token(multi_parser.token_text); multi_parser.current_mode = "CONTENT"; break; case "TK_TAG_SINGLE": multi_parser.print_newline(false, multi_parser.output); multi_parser.print_token(multi_parser.token_text); multi_parser.current_mode = "CONTENT"; break; case "TK_CONTENT": if (multi_parser.token_text !== "") { multi_parser.print_newline(false, multi_parser.output); multi_parser.print_token(multi_parser.token_text); } multi_parser.current_mode = "TAG"; break; } multi_parser.last_token = multi_parser.token_type; multi_parser.last_text = multi_parser.token_text; } return multi_parser.output.join(""); } return function (data) { var dataHolder = ["__dataHolder_", [Math.random(), Math.random(), Math.random(), Math.random()].join("_").replace(/[^0-9]/g, "_"), "_"].join("_"); var dataHolders = {}; var index = 0; data = data.replace(/(\")(data:[^\"]*)(\")/g, function ($0, $1, $2, $3) { var name = dataHolder + index++; dataHolders[name] = $2; return $1 + name + $3; }); data = style_html(data, 2, " ", 0x10000000); data = data.replace(new RegExp(dataHolder + "[0-9]+", "g"), function ($0) { return dataHolders[$0]; }); return data; }; })(); export default format; ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/agate.css ================================================ /*! * Agate by Taufik Nurrohman * ---------------------------------------------------- * * #ade5fc * #a2fca2 * #c6b4f0 * #d36363 * #fcc28c * #fc9b9b * #ffa * #fff * #333 * #62c8f3 * #888 * */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #333; color: white; } .hljs-name, .hljs-strong { font-weight: bold; } .hljs-code, .hljs-emphasis { font-style: italic; } .hljs-tag { color: #62c8f3; } .hljs-variable, .hljs-template-variable, .hljs-selector-id, .hljs-selector-class { color: #ade5fc; } .hljs-string, .hljs-bullet { color: #a2fca2; } .hljs-type, .hljs-title, .hljs-section, .hljs-attribute, .hljs-quote, .hljs-built_in, .hljs-builtin-name { color: #ffa; } .hljs-number, .hljs-symbol, .hljs-bullet { color: #d36363; } .hljs-keyword, .hljs-selector-tag, .hljs-literal { color: #fcc28c; } .hljs-comment, .hljs-deletion, .hljs-code { color: #888; } .hljs-regexp, .hljs-link { color: #c6b4f0; } .hljs-meta { color: #fc9b9b; } .hljs-deletion { background-color: #fc9b9b; color: #333; } .hljs-addition { background-color: #a2fca2; color: #333; } .hljs a { color: inherit; } .hljs a:focus, .hljs a:hover { color: inherit; text-decoration: underline; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/androidstudio.css ================================================ /* Date: 24 Fev 2015 Author: Pedro Oliveira */ .hljs { color: #a9b7c6; background: #282b2e; display: block; overflow-x: auto; padding: 0.5em; } .hljs-number, .hljs-literal, .hljs-symbol, .hljs-bullet { color: #6897bb; } .hljs-keyword, .hljs-selector-tag, .hljs-deletion { color: #cc7832; } .hljs-variable, .hljs-template-variable, .hljs-link { color: #629755; } .hljs-comment, .hljs-quote { color: #808080; } .hljs-meta { color: #bbb529; } .hljs-string, .hljs-attribute, .hljs-addition { color: #6a8759; } .hljs-section, .hljs-title, .hljs-type { color: #ffc66d; } .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #e8bf6a; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/arduino-light.css ================================================ /* Arduino® Light Theme - Stefania Mellai */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #ffffff; } .hljs, .hljs-subst { color: #434f54; } .hljs-keyword, .hljs-attribute, .hljs-selector-tag, .hljs-doctag, .hljs-name { color: #00979d; } .hljs-built_in, .hljs-literal, .hljs-bullet, .hljs-code, .hljs-addition { color: #d35400; } .hljs-regexp, .hljs-symbol, .hljs-variable, .hljs-template-variable, .hljs-link, .hljs-selector-attr, .hljs-selector-pseudo { color: #00979d; } .hljs-type, .hljs-string, .hljs-selector-id, .hljs-selector-class, .hljs-quote, .hljs-template-tag, .hljs-deletion { color: #005c5f; } .hljs-title, .hljs-section { color: #880000; font-weight: bold; } .hljs-comment { color: rgba(149, 165, 166, 0.8); } .hljs-meta-keyword { color: #728e00; } .hljs-meta { color: #728e00; color: #434f54; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .hljs-function { color: #728e00; } .hljs-number { color: #8a7b52; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/arta.css ================================================ /* Date: 17.V.2011 Author: pumbur */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #222; } .hljs, .hljs-subst { color: #aaa; } .hljs-section { color: #fff; } .hljs-comment, .hljs-quote, .hljs-meta { color: #444; } .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-regexp { color: #ffcc33; } .hljs-number, .hljs-addition { color: #00cc66; } .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-template-variable, .hljs-attribute, .hljs-link { color: #32aaee; } .hljs-keyword, .hljs-selector-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #6644aa; } .hljs-title, .hljs-variable, .hljs-deletion, .hljs-template-tag { color: #bb1166; } .hljs-section, .hljs-doctag, .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/ascetic.css ================================================ /* Original style from softwaremaniacs.org (c) Ivan Sagalaev */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: white; color: black; } .hljs-string, .hljs-variable, .hljs-template-variable, .hljs-symbol, .hljs-bullet, .hljs-section, .hljs-addition, .hljs-attribute, .hljs-link { color: #888; } .hljs-comment, .hljs-quote, .hljs-meta, .hljs-deletion { color: #ccc; } .hljs-keyword, .hljs-selector-tag, .hljs-section, .hljs-name, .hljs-type, .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-cave-dark.css ================================================ /* Base16 Atelier Cave Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Cave Comment */ .hljs-comment, .hljs-quote { color: #7e7887; } /* Atelier-Cave Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-regexp, .hljs-link, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #be4678; } /* Atelier-Cave Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #aa573c; } /* Atelier-Cave Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #2a9292; } /* Atelier-Cave Blue */ .hljs-title, .hljs-section { color: #576ddb; } /* Atelier-Cave Purple */ .hljs-keyword, .hljs-selector-tag { color: #955ae7; } .hljs-deletion, .hljs-addition { color: #19171c; display: inline-block; width: 100%; } .hljs-deletion { background-color: #be4678; } .hljs-addition { background-color: #2a9292; } .hljs { display: block; overflow-x: auto; background: #19171c; color: #8b8792; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-cave-light.css ================================================ /* Base16 Atelier Cave Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Cave Comment */ .hljs-comment, .hljs-quote { color: #655f6d; } /* Atelier-Cave Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #be4678; } /* Atelier-Cave Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #aa573c; } /* Atelier-Cave Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #2a9292; } /* Atelier-Cave Blue */ .hljs-title, .hljs-section { color: #576ddb; } /* Atelier-Cave Purple */ .hljs-keyword, .hljs-selector-tag { color: #955ae7; } .hljs-deletion, .hljs-addition { color: #19171c; display: inline-block; width: 100%; } .hljs-deletion { background-color: #be4678; } .hljs-addition { background-color: #2a9292; } .hljs { display: block; overflow-x: auto; background: #efecf4; color: #585260; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-dune-dark.css ================================================ /* Base16 Atelier Dune Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Dune Comment */ .hljs-comment, .hljs-quote { color: #999580; } /* Atelier-Dune Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #d73737; } /* Atelier-Dune Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #b65611; } /* Atelier-Dune Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #60ac39; } /* Atelier-Dune Blue */ .hljs-title, .hljs-section { color: #6684e1; } /* Atelier-Dune Purple */ .hljs-keyword, .hljs-selector-tag { color: #b854d4; } .hljs { display: block; overflow-x: auto; background: #20201d; color: #a6a28c; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-dune-light.css ================================================ /* Base16 Atelier Dune Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Dune Comment */ .hljs-comment, .hljs-quote { color: #7d7a68; } /* Atelier-Dune Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #d73737; } /* Atelier-Dune Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #b65611; } /* Atelier-Dune Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #60ac39; } /* Atelier-Dune Blue */ .hljs-title, .hljs-section { color: #6684e1; } /* Atelier-Dune Purple */ .hljs-keyword, .hljs-selector-tag { color: #b854d4; } .hljs { display: block; overflow-x: auto; background: #fefbec; color: #6e6b5e; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-estuary-dark.css ================================================ /* Base16 Atelier Estuary Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Estuary Comment */ .hljs-comment, .hljs-quote { color: #878573; } /* Atelier-Estuary Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #ba6236; } /* Atelier-Estuary Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #ae7313; } /* Atelier-Estuary Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #7d9726; } /* Atelier-Estuary Blue */ .hljs-title, .hljs-section { color: #36a166; } /* Atelier-Estuary Purple */ .hljs-keyword, .hljs-selector-tag { color: #5f9182; } .hljs-deletion, .hljs-addition { color: #22221b; display: inline-block; width: 100%; } .hljs-deletion { background-color: #ba6236; } .hljs-addition { background-color: #7d9726; } .hljs { display: block; overflow-x: auto; background: #22221b; color: #929181; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-estuary-light.css ================================================ /* Base16 Atelier Estuary Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/estuary) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Estuary Comment */ .hljs-comment, .hljs-quote { color: #6c6b5a; } /* Atelier-Estuary Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #ba6236; } /* Atelier-Estuary Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #ae7313; } /* Atelier-Estuary Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #7d9726; } /* Atelier-Estuary Blue */ .hljs-title, .hljs-section { color: #36a166; } /* Atelier-Estuary Purple */ .hljs-keyword, .hljs-selector-tag { color: #5f9182; } .hljs-deletion, .hljs-addition { color: #22221b; display: inline-block; width: 100%; } .hljs-deletion { background-color: #ba6236; } .hljs-addition { background-color: #7d9726; } .hljs { display: block; overflow-x: auto; background: #f4f3ec; color: #5f5e4e; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-forest-dark.css ================================================ /* Base16 Atelier Forest Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Forest Comment */ .hljs-comment, .hljs-quote { color: #9c9491; } /* Atelier-Forest Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #f22c40; } /* Atelier-Forest Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #df5320; } /* Atelier-Forest Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #7b9726; } /* Atelier-Forest Blue */ .hljs-title, .hljs-section { color: #407ee7; } /* Atelier-Forest Purple */ .hljs-keyword, .hljs-selector-tag { color: #6666ea; } .hljs { display: block; overflow-x: auto; background: #1b1918; color: #a8a19f; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-forest-light.css ================================================ /* Base16 Atelier Forest Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Forest Comment */ .hljs-comment, .hljs-quote { color: #766e6b; } /* Atelier-Forest Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #f22c40; } /* Atelier-Forest Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #df5320; } /* Atelier-Forest Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #7b9726; } /* Atelier-Forest Blue */ .hljs-title, .hljs-section { color: #407ee7; } /* Atelier-Forest Purple */ .hljs-keyword, .hljs-selector-tag { color: #6666ea; } .hljs { display: block; overflow-x: auto; background: #f1efee; color: #68615e; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-heath-dark.css ================================================ /* Base16 Atelier Heath Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Heath Comment */ .hljs-comment, .hljs-quote { color: #9e8f9e; } /* Atelier-Heath Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #ca402b; } /* Atelier-Heath Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #a65926; } /* Atelier-Heath Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #918b3b; } /* Atelier-Heath Blue */ .hljs-title, .hljs-section { color: #516aec; } /* Atelier-Heath Purple */ .hljs-keyword, .hljs-selector-tag { color: #7b59c0; } .hljs { display: block; overflow-x: auto; background: #1b181b; color: #ab9bab; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-heath-light.css ================================================ /* Base16 Atelier Heath Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/heath) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Heath Comment */ .hljs-comment, .hljs-quote { color: #776977; } /* Atelier-Heath Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #ca402b; } /* Atelier-Heath Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #a65926; } /* Atelier-Heath Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #918b3b; } /* Atelier-Heath Blue */ .hljs-title, .hljs-section { color: #516aec; } /* Atelier-Heath Purple */ .hljs-keyword, .hljs-selector-tag { color: #7b59c0; } .hljs { display: block; overflow-x: auto; background: #f7f3f7; color: #695d69; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-lakeside-dark.css ================================================ /* Base16 Atelier Lakeside Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Lakeside Comment */ .hljs-comment, .hljs-quote { color: #7195a8; } /* Atelier-Lakeside Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #d22d72; } /* Atelier-Lakeside Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #935c25; } /* Atelier-Lakeside Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #568c3b; } /* Atelier-Lakeside Blue */ .hljs-title, .hljs-section { color: #257fad; } /* Atelier-Lakeside Purple */ .hljs-keyword, .hljs-selector-tag { color: #6b6bb8; } .hljs { display: block; overflow-x: auto; background: #161b1d; color: #7ea2b4; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-lakeside-light.css ================================================ /* Base16 Atelier Lakeside Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/lakeside) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Lakeside Comment */ .hljs-comment, .hljs-quote { color: #5a7b8c; } /* Atelier-Lakeside Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #d22d72; } /* Atelier-Lakeside Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #935c25; } /* Atelier-Lakeside Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #568c3b; } /* Atelier-Lakeside Blue */ .hljs-title, .hljs-section { color: #257fad; } /* Atelier-Lakeside Purple */ .hljs-keyword, .hljs-selector-tag { color: #6b6bb8; } .hljs { display: block; overflow-x: auto; background: #ebf8ff; color: #516d7b; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-plateau-dark.css ================================================ /* Base16 Atelier Plateau Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Plateau Comment */ .hljs-comment, .hljs-quote { color: #7e7777; } /* Atelier-Plateau Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #ca4949; } /* Atelier-Plateau Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #b45a3c; } /* Atelier-Plateau Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #4b8b8b; } /* Atelier-Plateau Blue */ .hljs-title, .hljs-section { color: #7272ca; } /* Atelier-Plateau Purple */ .hljs-keyword, .hljs-selector-tag { color: #8464c4; } .hljs-deletion, .hljs-addition { color: #1b1818; display: inline-block; width: 100%; } .hljs-deletion { background-color: #ca4949; } .hljs-addition { background-color: #4b8b8b; } .hljs { display: block; overflow-x: auto; background: #1b1818; color: #8a8585; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-plateau-light.css ================================================ /* Base16 Atelier Plateau Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/plateau) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Plateau Comment */ .hljs-comment, .hljs-quote { color: #655d5d; } /* Atelier-Plateau Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #ca4949; } /* Atelier-Plateau Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #b45a3c; } /* Atelier-Plateau Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #4b8b8b; } /* Atelier-Plateau Blue */ .hljs-title, .hljs-section { color: #7272ca; } /* Atelier-Plateau Purple */ .hljs-keyword, .hljs-selector-tag { color: #8464c4; } .hljs-deletion, .hljs-addition { color: #1b1818; display: inline-block; width: 100%; } .hljs-deletion { background-color: #ca4949; } .hljs-addition { background-color: #4b8b8b; } .hljs { display: block; overflow-x: auto; background: #f4ecec; color: #585050; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-savanna-dark.css ================================================ /* Base16 Atelier Savanna Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Savanna Comment */ .hljs-comment, .hljs-quote { color: #78877d; } /* Atelier-Savanna Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #b16139; } /* Atelier-Savanna Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #9f713c; } /* Atelier-Savanna Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #489963; } /* Atelier-Savanna Blue */ .hljs-title, .hljs-section { color: #478c90; } /* Atelier-Savanna Purple */ .hljs-keyword, .hljs-selector-tag { color: #55859b; } .hljs-deletion, .hljs-addition { color: #171c19; display: inline-block; width: 100%; } .hljs-deletion { background-color: #b16139; } .hljs-addition { background-color: #489963; } .hljs { display: block; overflow-x: auto; background: #171c19; color: #87928a; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-savanna-light.css ================================================ /* Base16 Atelier Savanna Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/savanna) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Savanna Comment */ .hljs-comment, .hljs-quote { color: #5f6d64; } /* Atelier-Savanna Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #b16139; } /* Atelier-Savanna Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #9f713c; } /* Atelier-Savanna Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #489963; } /* Atelier-Savanna Blue */ .hljs-title, .hljs-section { color: #478c90; } /* Atelier-Savanna Purple */ .hljs-keyword, .hljs-selector-tag { color: #55859b; } .hljs-deletion, .hljs-addition { color: #171c19; display: inline-block; width: 100%; } .hljs-deletion { background-color: #b16139; } .hljs-addition { background-color: #489963; } .hljs { display: block; overflow-x: auto; background: #ecf4ee; color: #526057; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-seaside-dark.css ================================================ /* Base16 Atelier Seaside Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Seaside Comment */ .hljs-comment, .hljs-quote { color: #809980; } /* Atelier-Seaside Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #e6193c; } /* Atelier-Seaside Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #87711d; } /* Atelier-Seaside Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #29a329; } /* Atelier-Seaside Blue */ .hljs-title, .hljs-section { color: #3d62f5; } /* Atelier-Seaside Purple */ .hljs-keyword, .hljs-selector-tag { color: #ad2bee; } .hljs { display: block; overflow-x: auto; background: #131513; color: #8ca68c; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-seaside-light.css ================================================ /* Base16 Atelier Seaside Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/seaside) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Seaside Comment */ .hljs-comment, .hljs-quote { color: #687d68; } /* Atelier-Seaside Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #e6193c; } /* Atelier-Seaside Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #87711d; } /* Atelier-Seaside Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #29a329; } /* Atelier-Seaside Blue */ .hljs-title, .hljs-section { color: #3d62f5; } /* Atelier-Seaside Purple */ .hljs-keyword, .hljs-selector-tag { color: #ad2bee; } .hljs { display: block; overflow-x: auto; background: #f4fbf4; color: #5e6e5e; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-sulphurpool-dark.css ================================================ /* Base16 Atelier Sulphurpool Dark - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Sulphurpool Comment */ .hljs-comment, .hljs-quote { color: #898ea4; } /* Atelier-Sulphurpool Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #c94922; } /* Atelier-Sulphurpool Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #c76b29; } /* Atelier-Sulphurpool Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #ac9739; } /* Atelier-Sulphurpool Blue */ .hljs-title, .hljs-section { color: #3d8fd1; } /* Atelier-Sulphurpool Purple */ .hljs-keyword, .hljs-selector-tag { color: #6679cc; } .hljs { display: block; overflow-x: auto; background: #202746; color: #979db4; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atelier-sulphurpool-light.css ================================================ /* Base16 Atelier Sulphurpool Light - Theme */ /* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool) */ /* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */ /* Atelier-Sulphurpool Comment */ .hljs-comment, .hljs-quote { color: #6b7394; } /* Atelier-Sulphurpool Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-regexp, .hljs-link, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #c94922; } /* Atelier-Sulphurpool Orange */ .hljs-number, .hljs-meta, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #c76b29; } /* Atelier-Sulphurpool Green */ .hljs-string, .hljs-symbol, .hljs-bullet { color: #ac9739; } /* Atelier-Sulphurpool Blue */ .hljs-title, .hljs-section { color: #3d8fd1; } /* Atelier-Sulphurpool Purple */ .hljs-keyword, .hljs-selector-tag { color: #6679cc; } .hljs { display: block; overflow-x: auto; background: #f5f7ff; color: #5e6687; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atom-one-dark.css ================================================ /* Atom One Dark by Daniel Gamage Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax base: #282c34 mono-1: #abb2bf mono-2: #818896 mono-3: #5c6370 hue-1: #56b6c2 hue-2: #61aeee hue-3: #c678dd hue-4: #98c379 hue-5: #e06c75 hue-5-2: #be5046 hue-6: #d19a66 hue-6-2: #e6c07b */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #abb2bf; background: #282c34; } .hljs-comment, .hljs-quote { color: #5c6370; font-style: italic; } .hljs-doctag, .hljs-keyword, .hljs-formula { color: #c678dd; } .hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst { color: #e06c75; } .hljs-literal { color: #56b6c2; } .hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string { color: #98c379; } .hljs-built_in, .hljs-class .hljs-title { color: #e6c07b; } .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number { color: #d19a66; } .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title { color: #61aeee; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .hljs-link { text-decoration: underline; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/atom-one-light.css ================================================ /* Atom One Light by Daniel Gamage Original One Light Syntax theme from https://github.com/atom/one-light-syntax base: #fafafa mono-1: #383a42 mono-2: #686b77 mono-3: #a0a1a7 hue-1: #0184bb hue-2: #4078f2 hue-3: #a626a4 hue-4: #50a14f hue-5: #e45649 hue-5-2: #c91243 hue-6: #986801 hue-6-2: #c18401 */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #383a42; background: #fafafa; } .hljs-comment, .hljs-quote { color: #a0a1a7; font-style: italic; } .hljs-doctag, .hljs-keyword, .hljs-formula { color: #a626a4; } .hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst { color: #e45649; } .hljs-literal { color: #0184bb; } .hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string { color: #50a14f; } .hljs-built_in, .hljs-class .hljs-title { color: #c18401; } .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number { color: #986801; } .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title { color: #4078f2; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .hljs-link { text-decoration: underline; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/brown-paper.css ================================================ /* Brown Paper style from goldblog.com.ua (c) Zaripov Yura */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #b7a68e url(./brown-papersq.png); } .hljs-keyword, .hljs-selector-tag, .hljs-literal { color: #005599; font-weight: bold; } .hljs, .hljs-subst { color: #363c69; } .hljs-string, .hljs-title, .hljs-section, .hljs-type, .hljs-attribute, .hljs-symbol, .hljs-bullet, .hljs-built_in, .hljs-addition, .hljs-variable, .hljs-template-tag, .hljs-template-variable, .hljs-link, .hljs-name { color: #2c009f; } .hljs-comment, .hljs-quote, .hljs-meta, .hljs-deletion { color: #802022; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-doctag, .hljs-title, .hljs-section, .hljs-type, .hljs-name, .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/codepen-embed.css ================================================ /* codepen.io Embed Theme Author: Justin Perry Original theme - https://github.com/chriskempson/tomorrow-theme */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #222; color: #fff; } .hljs-comment, .hljs-quote { color: #777; } .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-regexp, .hljs-meta, .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-params, .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-deletion { color: #ab875d; } .hljs-section, .hljs-title, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-type, .hljs-attribute { color: #9b869b; } .hljs-string, .hljs-keyword, .hljs-selector-tag, .hljs-addition { color: #8f9c6c; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/color-brewer.css ================================================ /* Colorbrewer theme Original: https://github.com/mbostock/colorbrewer-theme (c) Mike Bostock Ported by Fabrício Tavares de Oliveira */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #fff; } .hljs, .hljs-subst { color: #000; } .hljs-string, .hljs-meta, .hljs-symbol, .hljs-template-tag, .hljs-template-variable, .hljs-addition { color: #756bb1; } .hljs-comment, .hljs-quote { color: #636363; } .hljs-number, .hljs-regexp, .hljs-literal, .hljs-bullet, .hljs-link { color: #31a354; } .hljs-deletion, .hljs-variable { color: #88f; } .hljs-keyword, .hljs-selector-tag, .hljs-title, .hljs-section, .hljs-built_in, .hljs-doctag, .hljs-type, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-strong { color: #3182bd; } .hljs-emphasis { font-style: italic; } .hljs-attribute { color: #e6550d; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/darcula.css ================================================ /* Darcula color scheme from the JetBrains family of IDEs */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #2b2b2b; } .hljs { color: #bababa; } .hljs-strong, .hljs-emphasis { color: #a8a8a2; } .hljs-bullet, .hljs-quote, .hljs-link, .hljs-number, .hljs-regexp, .hljs-literal { color: #6896ba; } .hljs-code, .hljs-selector-class { color: #a6e22e; } .hljs-emphasis { font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-section, .hljs-attribute, .hljs-name, .hljs-variable { color: #cb7832; } .hljs-params { color: #b9b9b9; } .hljs-string { color: #6a8759; } .hljs-subst, .hljs-type, .hljs-built_in, .hljs-builtin-name, .hljs-symbol, .hljs-selector-id, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-template-tag, .hljs-template-variable, .hljs-addition { color: #e0c46c; } .hljs-comment, .hljs-deletion, .hljs-meta { color: #7f7f7f; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/dark.css ================================================ /* Dark style from softwaremaniacs.org (c) Ivan Sagalaev */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #444; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-section, .hljs-link { color: white; } .hljs, .hljs-subst { color: #ddd; } .hljs-string, .hljs-title, .hljs-name, .hljs-type, .hljs-attribute, .hljs-symbol, .hljs-bullet, .hljs-built_in, .hljs-addition, .hljs-variable, .hljs-template-tag, .hljs-template-variable { color: #d88; } .hljs-comment, .hljs-quote, .hljs-deletion, .hljs-meta { color: #777; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-title, .hljs-section, .hljs-doctag, .hljs-type, .hljs-name, .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/darkula.css ================================================ /* Deprecated due to a typo in the name and left here for compatibility purpose only. Please use darcula.css instead. */ @import url("darcula.css"); ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/default.css ================================================ /* Original highlight.js style (c) Ivan Sagalaev */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #f0f0f0; } /* Base color: saturation 0; */ .hljs, .hljs-subst { color: #444; } .hljs-comment { color: #888888; } .hljs-keyword, .hljs-attribute, .hljs-selector-tag, .hljs-meta-keyword, .hljs-doctag, .hljs-name { font-weight: bold; } /* User color: hue: 0 */ .hljs-type, .hljs-string, .hljs-number, .hljs-selector-id, .hljs-selector-class, .hljs-quote, .hljs-template-tag, .hljs-deletion { color: #880000; } .hljs-title, .hljs-section { color: #880000; font-weight: bold; } .hljs-regexp, .hljs-symbol, .hljs-variable, .hljs-template-variable, .hljs-link, .hljs-selector-attr, .hljs-selector-pseudo { color: #bc6060; } /* Language color: hue: 90; */ .hljs-literal { color: #78a960; } .hljs-built_in, .hljs-bullet, .hljs-code, .hljs-addition { color: #397300; } /* Meta color: hue: 200 */ .hljs-meta { color: #1f7199; } .hljs-meta-string { color: #4d99bf; } /* Misc effects */ .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/docco.css ================================================ /* Docco style used in http://jashkenas.github.com/docco/ converted by Simon Madine (@thingsinjars) */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #000; background: #f8f8ff; } .hljs-comment, .hljs-quote { color: #408080; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-subst { color: #954121; } .hljs-number { color: #40a070; } .hljs-string, .hljs-doctag { color: #219161; } .hljs-selector-id, .hljs-selector-class, .hljs-section, .hljs-type { color: #19469d; } .hljs-params { color: #00f; } .hljs-title { color: #458; font-weight: bold; } .hljs-tag, .hljs-name, .hljs-attribute { color: #000080; font-weight: normal; } .hljs-variable, .hljs-template-variable { color: #008080; } .hljs-regexp, .hljs-link { color: #b68; } .hljs-symbol, .hljs-bullet { color: #990073; } .hljs-built_in, .hljs-builtin-name { color: #0086b3; } .hljs-meta { color: #999; font-weight: bold; } .hljs-deletion { background: #fdd; } .hljs-addition { background: #dfd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/dracula.css ================================================ /* Dracula Theme v1.2.0 https://github.com/zenorocha/dracula-theme Copyright 2015, All rights reserved Code licensed under the MIT license http://zenorocha.mit-license.org @author Éverton Ribeiro @author Zeno Rocha */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #282a36; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-section, .hljs-link { color: #8be9fd; } .hljs-function .hljs-keyword { color: #ff79c6; } .hljs, .hljs-subst { color: #f8f8f2; } .hljs-string, .hljs-title, .hljs-name, .hljs-type, .hljs-attribute, .hljs-symbol, .hljs-bullet, .hljs-addition, .hljs-variable, .hljs-template-tag, .hljs-template-variable { color: #f1fa8c; } .hljs-comment, .hljs-quote, .hljs-deletion, .hljs-meta { color: #6272a4; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-title, .hljs-section, .hljs-doctag, .hljs-type, .hljs-name, .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/far.css ================================================ /* FAR Style (c) MajestiC */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #000080; } .hljs, .hljs-subst { color: #0ff; } .hljs-string, .hljs-attribute, .hljs-symbol, .hljs-bullet, .hljs-built_in, .hljs-builtin-name, .hljs-template-tag, .hljs-template-variable, .hljs-addition { color: #ff0; } .hljs-keyword, .hljs-selector-tag, .hljs-section, .hljs-type, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-variable { color: #fff; } .hljs-comment, .hljs-quote, .hljs-doctag, .hljs-deletion { color: #888; } .hljs-number, .hljs-regexp, .hljs-literal, .hljs-link { color: #0f0; } .hljs-meta { color: #008080; } .hljs-keyword, .hljs-selector-tag, .hljs-title, .hljs-section, .hljs-name, .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/foundation.css ================================================ /* Description: Foundation 4 docs style for highlight.js Author: Dan Allen Website: http://foundation.zurb.com/docs/ Version: 1.0 Date: 2013-04-02 */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #eee; color: black; } .hljs-link, .hljs-emphasis, .hljs-attribute, .hljs-addition { color: #070; } .hljs-emphasis { font-style: italic; } .hljs-strong, .hljs-string, .hljs-deletion { color: #d14; } .hljs-strong { font-weight: bold; } .hljs-quote, .hljs-comment { color: #998; font-style: italic; } .hljs-section, .hljs-title { color: #900; } .hljs-class .hljs-title, .hljs-type { color: #458; } .hljs-variable, .hljs-template-variable { color: #336699; } .hljs-bullet { color: #997700; } .hljs-meta { color: #3344bb; } .hljs-code, .hljs-number, .hljs-literal, .hljs-keyword, .hljs-selector-tag { color: #099; } .hljs-regexp { background-color: #fff0ff; color: #880088; } .hljs-symbol { color: #990073; } .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #007700; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/github-gist.css ================================================ /** * GitHub Gist Theme * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro */ .hljs { display: block; background: white; padding: 0.5em; color: #333333; overflow-x: auto; } .hljs-comment, .hljs-meta { color: #969896; } .hljs-string, .hljs-variable, .hljs-template-variable, .hljs-strong, .hljs-emphasis, .hljs-quote { color: #df5000; } .hljs-keyword, .hljs-selector-tag, .hljs-type { color: #a71d5d; } .hljs-literal, .hljs-symbol, .hljs-bullet, .hljs-attribute { color: #0086b3; } .hljs-section, .hljs-name { color: #63a35c; } .hljs-tag { color: #333333; } .hljs-title, .hljs-attr, .hljs-selector-id, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo { color: #795da3; } .hljs-addition { color: #55a532; background-color: #eaffea; } .hljs-deletion { color: #bd2c00; background-color: #ffecec; } .hljs-link { text-decoration: underline; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/github.css ================================================ /* github.com style (c) Vasily Polovnyov */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #333; background: #f8f8f8; } .hljs-comment, .hljs-quote { color: #998; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #333; font-weight: bold; } .hljs-number, .hljs-literal, .hljs-variable, .hljs-template-variable, .hljs-tag .hljs-attr { color: #008080; } .hljs-string, .hljs-doctag { color: #d14; } .hljs-title, .hljs-section, .hljs-selector-id { color: #900; font-weight: bold; } .hljs-subst { font-weight: normal; } .hljs-type, .hljs-class .hljs-title { color: #458; font-weight: bold; } .hljs-tag, .hljs-name, .hljs-attribute { color: #000080; font-weight: normal; } .hljs-regexp, .hljs-link { color: #009926; } .hljs-symbol, .hljs-bullet { color: #990073; } .hljs-built_in, .hljs-builtin-name { color: #0086b3; } .hljs-meta { color: #999; font-weight: bold; } .hljs-deletion { background: #fdd; } .hljs-addition { background: #dfd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/googlecode.css ================================================ /* Google Code style (c) Aahan Krish */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: white; color: black; } .hljs-comment, .hljs-quote { color: #800; } .hljs-keyword, .hljs-selector-tag, .hljs-section, .hljs-title, .hljs-name { color: #008; } .hljs-variable, .hljs-template-variable { color: #660; } .hljs-string, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-regexp { color: #080; } .hljs-literal, .hljs-symbol, .hljs-bullet, .hljs-meta, .hljs-number, .hljs-link { color: #066; } .hljs-title, .hljs-doctag, .hljs-type, .hljs-attr, .hljs-built_in, .hljs-builtin-name, .hljs-params { color: #606; } .hljs-attribute, .hljs-subst { color: #000; } .hljs-formula { background-color: #eee; font-style: italic; } .hljs-selector-id, .hljs-selector-class { color: #9b703f; } .hljs-addition { background-color: #baeeba; } .hljs-deletion { background-color: #ffc8bd; } .hljs-doctag, .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/grayscale.css ================================================ /* grayscale style (c) MY Sun */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #333; background: #fff; } .hljs-comment, .hljs-quote { color: #777; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #333; font-weight: bold; } .hljs-number, .hljs-literal { color: #777; } .hljs-string, .hljs-doctag, .hljs-formula { color: #333; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAJ0lEQVQIW2O8e/fufwYGBgZBQUEQxcCIIfDu3Tuwivfv30NUoAsAALHpFMMLqZlPAAAAAElFTkSuQmCC) repeat; } .hljs-title, .hljs-section, .hljs-selector-id { color: #000; font-weight: bold; } .hljs-subst { font-weight: normal; } .hljs-class .hljs-title, .hljs-type, .hljs-name { color: #333; font-weight: bold; } .hljs-tag { color: #333; } .hljs-regexp { color: #333; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAICAYAAADA+m62AAAAPUlEQVQYV2NkQAN37979r6yszIgujiIAU4RNMVwhuiQ6H6wQl3XI4oy4FMHcCJPHcDS6J2A2EqUQpJhohQDexSef15DBCwAAAABJRU5ErkJggg==) repeat; } .hljs-symbol, .hljs-bullet, .hljs-link { color: #000; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAKElEQVQIW2NkQAO7d+/+z4gsBhJwdXVlhAvCBECKwIIwAbhKZBUwBQA6hBpm5efZsgAAAABJRU5ErkJggg==) repeat; } .hljs-built_in, .hljs-builtin-name { color: #000; text-decoration: underline; } .hljs-meta { color: #999; font-weight: bold; } .hljs-deletion { color: #fff; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAE0lEQVQIW2MMDQ39zzhz5kwIAQAyxweWgUHd1AAAAABJRU5ErkJggg==) repeat; } .hljs-addition { color: #000; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAALUlEQVQYV2N89+7dfwYk8P79ewZBQUFkIQZGOiu6e/cuiptQHAPl0NtNxAQBAM97Oejj3Dg7AAAAAElFTkSuQmCC) repeat; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/gruvbox-dark.css ================================================ /* Gruvbox style (dark) (c) Pavel Pertsev (original style at https://github.com/morhetz/gruvbox) */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #282828; } .hljs, .hljs-subst { color: #ebdbb2; } /* Gruvbox Red */ .hljs-deletion, .hljs-formula, .hljs-keyword, .hljs-link, .hljs-selector-tag { color: #fb4934; } /* Gruvbox Blue */ .hljs-built_in, .hljs-emphasis, .hljs-name, .hljs-quote, .hljs-strong, .hljs-title, .hljs-variable { color: #83a598; } /* Gruvbox Yellow */ .hljs-attr, .hljs-params, .hljs-template-tag, .hljs-type { color: #fabd2f; } /* Gruvbox Purple */ .hljs-builtin-name, .hljs-doctag, .hljs-literal, .hljs-number { color: #8f3f71; } /* Gruvbox Orange */ .hljs-code, .hljs-meta, .hljs-regexp, .hljs-selector-id, .hljs-template-variable { color: #fe8019; } /* Gruvbox Green */ .hljs-addition, .hljs-meta-string, .hljs-section, .hljs-selector-attr, .hljs-selector-class, .hljs-string, .hljs-symbol { color: #b8bb26; } /* Gruvbox Aqua */ .hljs-attribute, .hljs-bullet, .hljs-class, .hljs-function, .hljs-function .hljs-keyword, .hljs-meta-keyword, .hljs-selector-pseudo, .hljs-tag { color: #8ec07c; } /* Gruvbox Gray */ .hljs-comment { color: #928374; } /* Gruvbox Purple */ .hljs-link_label, .hljs-literal, .hljs-number { color: #d3869b; } .hljs-comment, .hljs-emphasis { font-style: italic; } .hljs-section, .hljs-strong, .hljs-tag { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/gruvbox-light.css ================================================ /* Gruvbox style (light) (c) Pavel Pertsev (original style at https://github.com/morhetz/gruvbox) */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #fbf1c7; } .hljs, .hljs-subst { color: #3c3836; } /* Gruvbox Red */ .hljs-deletion, .hljs-formula, .hljs-keyword, .hljs-link, .hljs-selector-tag { color: #9d0006; } /* Gruvbox Blue */ .hljs-built_in, .hljs-emphasis, .hljs-name, .hljs-quote, .hljs-strong, .hljs-title, .hljs-variable { color: #076678; } /* Gruvbox Yellow */ .hljs-attr, .hljs-params, .hljs-template-tag, .hljs-type { color: #b57614; } /* Gruvbox Purple */ .hljs-builtin-name, .hljs-doctag, .hljs-literal, .hljs-number { color: #8f3f71; } /* Gruvbox Orange */ .hljs-code, .hljs-meta, .hljs-regexp, .hljs-selector-id, .hljs-template-variable { color: #af3a03; } /* Gruvbox Green */ .hljs-addition, .hljs-meta-string, .hljs-section, .hljs-selector-attr, .hljs-selector-class, .hljs-string, .hljs-symbol { color: #79740e; } /* Gruvbox Aqua */ .hljs-attribute, .hljs-bullet, .hljs-class, .hljs-function, .hljs-function .hljs-keyword, .hljs-meta-keyword, .hljs-selector-pseudo, .hljs-tag { color: #427b58; } /* Gruvbox Gray */ .hljs-comment { color: #928374; } /* Gruvbox Purple */ .hljs-link_label, .hljs-literal, .hljs-number { color: #8f3f71; } .hljs-comment, .hljs-emphasis { font-style: italic; } .hljs-section, .hljs-strong, .hljs-tag { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/hopscotch.css ================================================ /* * Hopscotch * by Jan T. Sott * https://github.com/idleberg/Hopscotch * * This work is licensed under the Creative Commons CC0 1.0 Universal License */ /* Comment */ .hljs-comment, .hljs-quote { color: #989498; } /* Red */ .hljs-variable, .hljs-template-variable, .hljs-attribute, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-link, .hljs-deletion { color: #dd464c; } /* Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params { color: #fd8b19; } /* Yellow */ .hljs-class .hljs-title { color: #fdcc59; } /* Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #8fc13e; } /* Aqua */ .hljs-meta { color: #149b93; } /* Blue */ .hljs-function, .hljs-section, .hljs-title { color: #1290bf; } /* Purple */ .hljs-keyword, .hljs-selector-tag { color: #c85e7c; } .hljs { display: block; background: #322931; color: #b9b5b8; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/hybrid.css ================================================ /* vim-hybrid theme by w0ng (https://github.com/w0ng/vim-hybrid) */ /*background color*/ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #1d1f21; } /*selection color*/ .hljs::selection, .hljs span::selection { background: #373b41; } .hljs::-moz-selection, .hljs span::-moz-selection { background: #373b41; } /*foreground color*/ .hljs { color: #c5c8c6; } /*color: fg_yellow*/ .hljs-title, .hljs-name { color: #f0c674; } /*color: fg_comment*/ .hljs-comment, .hljs-meta, .hljs-meta .hljs-keyword { color: #707880; } /*color: fg_red*/ .hljs-number, .hljs-symbol, .hljs-literal, .hljs-deletion, .hljs-link { color: #cc6666; } /*color: fg_green*/ .hljs-string, .hljs-doctag, .hljs-addition, .hljs-regexp, .hljs-selector-attr, .hljs-selector-pseudo { color: #b5bd68; } /*color: fg_purple*/ .hljs-attribute, .hljs-code, .hljs-selector-id { color: #b294bb; } /*color: fg_blue*/ .hljs-keyword, .hljs-selector-tag, .hljs-bullet, .hljs-tag { color: #81a2be; } /*color: fg_aqua*/ .hljs-subst, .hljs-variable, .hljs-template-tag, .hljs-template-variable { color: #8abeb7; } /*color: fg_orange*/ .hljs-type, .hljs-built_in, .hljs-builtin-name, .hljs-quote, .hljs-section, .hljs-selector-class { color: #de935f; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/idea.css ================================================ /* Intellij Idea-like styling (c) Vasily Polovnyov */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #000; background: #fff; } .hljs-subst, .hljs-title { font-weight: normal; color: #000; } .hljs-comment, .hljs-quote { color: #808080; font-style: italic; } .hljs-meta { color: #808000; } .hljs-tag { background: #efefef; } .hljs-section, .hljs-name, .hljs-literal, .hljs-keyword, .hljs-selector-tag, .hljs-type, .hljs-selector-id, .hljs-selector-class { font-weight: bold; color: #000080; } .hljs-attribute, .hljs-number, .hljs-regexp, .hljs-link { font-weight: bold; color: #0000ff; } .hljs-number, .hljs-regexp, .hljs-link { font-weight: normal; } .hljs-string { color: #008000; font-weight: bold; } .hljs-symbol, .hljs-bullet, .hljs-formula { color: #000; background: #d0eded; font-style: italic; } .hljs-doctag { text-decoration: underline; } .hljs-variable, .hljs-template-variable { color: #660e7a; } .hljs-addition { background: #baeeba; } .hljs-deletion { background: #ffc8bd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/ir-black.css ================================================ /* IR_Black style (c) Vasily Mikhailitchenko */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #000; color: #f8f8f8; } .hljs-comment, .hljs-quote, .hljs-meta { color: #7c7c7c; } .hljs-keyword, .hljs-selector-tag, .hljs-tag, .hljs-name { color: #96cbfe; } .hljs-attribute, .hljs-selector-id { color: #ffffb6; } .hljs-string, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-addition { color: #a8ff60; } .hljs-subst { color: #daefa3; } .hljs-regexp, .hljs-link { color: #e9c062; } .hljs-title, .hljs-section, .hljs-type, .hljs-doctag { color: #ffffb6; } .hljs-symbol, .hljs-bullet, .hljs-variable, .hljs-template-variable, .hljs-literal { color: #c6c5fe; } .hljs-number, .hljs-deletion { color: #ff73fd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/kimbie.dark.css ================================================ /* Name: Kimbie (dark) Author: Jan T. Sott License: Creative Commons Attribution-ShareAlike 4.0 Unported License URL: https://github.com/idleberg/Kimbie-highlight.js */ /* Kimbie Comment */ .hljs-comment, .hljs-quote { color: #d6baad; } /* Kimbie Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-meta { color: #dc3958; } /* Kimbie Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-deletion, .hljs-link { color: #f79a32; } /* Kimbie Yellow */ .hljs-title, .hljs-section, .hljs-attribute { color: #f06431; } /* Kimbie Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #889b4a; } /* Kimbie Purple */ .hljs-keyword, .hljs-selector-tag, .hljs-function { color: #98676a; } .hljs { display: block; overflow-x: auto; background: #221a0f; color: #d3af86; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/kimbie.light.css ================================================ /* Name: Kimbie (light) Author: Jan T. Sott License: Creative Commons Attribution-ShareAlike 4.0 Unported License URL: https://github.com/idleberg/Kimbie-highlight.js */ /* Kimbie Comment */ .hljs-comment, .hljs-quote { color: #a57a4c; } /* Kimbie Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-meta { color: #dc3958; } /* Kimbie Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-deletion, .hljs-link { color: #f79a32; } /* Kimbie Yellow */ .hljs-title, .hljs-section, .hljs-attribute { color: #f06431; } /* Kimbie Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #889b4a; } /* Kimbie Purple */ .hljs-keyword, .hljs-selector-tag, .hljs-function { color: #98676a; } .hljs { display: block; overflow-x: auto; background: #fbebd4; color: #84613d; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/magula.css ================================================ /* Description: Magula style for highligh.js Author: Ruslan Keba Website: http://rukeba.com/ Version: 1.0 Date: 2009-01-03 Music: Aphex Twin / Xtal */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background-color: #f4f4f4; } .hljs, .hljs-subst { color: black; } .hljs-string, .hljs-title, .hljs-symbol, .hljs-bullet, .hljs-attribute, .hljs-addition, .hljs-variable, .hljs-template-tag, .hljs-template-variable { color: #050; } .hljs-comment, .hljs-quote { color: #777; } .hljs-number, .hljs-regexp, .hljs-literal, .hljs-type, .hljs-link { color: #800; } .hljs-deletion, .hljs-meta { color: #00e; } .hljs-keyword, .hljs-selector-tag, .hljs-doctag, .hljs-title, .hljs-section, .hljs-built_in, .hljs-tag, .hljs-name { font-weight: bold; color: navy; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/mono-blue.css ================================================ /* Five-color theme from a single blue hue. */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #eaeef3; } .hljs { color: #00193a; } .hljs-keyword, .hljs-selector-tag, .hljs-title, .hljs-section, .hljs-doctag, .hljs-name, .hljs-strong { font-weight: bold; } .hljs-comment { color: #738191; } .hljs-string, .hljs-title, .hljs-section, .hljs-built_in, .hljs-literal, .hljs-type, .hljs-addition, .hljs-tag, .hljs-quote, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #0048ab; } .hljs-meta, .hljs-subst, .hljs-symbol, .hljs-regexp, .hljs-attribute, .hljs-deletion, .hljs-variable, .hljs-template-variable, .hljs-link, .hljs-bullet { color: #4c81c9; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/monokai-sublime.css ================================================ /* Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #23241f; } .hljs, .hljs-tag, .hljs-subst { color: #f8f8f2; } .hljs-strong, .hljs-emphasis { color: #a8a8a2; } .hljs-bullet, .hljs-quote, .hljs-number, .hljs-regexp, .hljs-literal, .hljs-link { color: #ae81ff; } .hljs-code, .hljs-title, .hljs-section, .hljs-selector-class { color: #a6e22e; } .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-name, .hljs-attr { color: #f92672; } .hljs-symbol, .hljs-attribute { color: #66d9ef; } .hljs-params, .hljs-class .hljs-title { color: #f8f8f2; } .hljs-string, .hljs-type, .hljs-built_in, .hljs-builtin-name, .hljs-selector-id, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-addition, .hljs-variable, .hljs-template-variable { color: #e6db74; } .hljs-comment, .hljs-deletion, .hljs-meta { color: #75715e; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/monokai.css ================================================ /* Monokai style - ported by Luigi Maselli - http://grigio.org */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #272822; color: #ddd; } .hljs-tag, .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-strong, .hljs-name { color: #f92672; } .hljs-code { color: #66d9ef; } .hljs-class .hljs-title { color: white; } .hljs-attribute, .hljs-symbol, .hljs-regexp, .hljs-link { color: #bf79db; } .hljs-string, .hljs-bullet, .hljs-subst, .hljs-title, .hljs-section, .hljs-emphasis, .hljs-type, .hljs-built_in, .hljs-builtin-name, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-addition, .hljs-variable, .hljs-template-tag, .hljs-template-variable { color: #a6e22e; } .hljs-comment, .hljs-quote, .hljs-deletion, .hljs-meta { color: #75715e; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-doctag, .hljs-title, .hljs-section, .hljs-type, .hljs-selector-id { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/obsidian.css ================================================ /** * Obsidian style * ported by Alexander Marenin (http://github.com/ioncreature) */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #282b2e; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-selector-id { color: #93c763; } .hljs-number { color: #ffcd22; } .hljs { color: #e0e2e4; } .hljs-attribute { color: #668bb0; } .hljs-code, .hljs-class .hljs-title, .hljs-section { color: white; } .hljs-regexp, .hljs-link { color: #d39745; } .hljs-meta { color: #557182; } .hljs-tag, .hljs-name, .hljs-bullet, .hljs-subst, .hljs-emphasis, .hljs-type, .hljs-built_in, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-addition, .hljs-variable, .hljs-template-tag, .hljs-template-variable { color: #8cbbad; } .hljs-string, .hljs-symbol { color: #ec7600; } .hljs-comment, .hljs-quote, .hljs-deletion { color: #818e96; } .hljs-selector-class { color: #a082bd; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-doctag, .hljs-title, .hljs-section, .hljs-type, .hljs-name, .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/ocean.css ================================================ /* Ocean Dark Theme */ /* https://github.com/gavsiu */ /* Original theme - https://github.com/chriskempson/base16 */ /* Ocean Comment */ .hljs-comment, .hljs-quote { color: #65737e; } /* Ocean Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-deletion { color: #bf616a; } /* Ocean Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-meta, .hljs-link { color: #d08770; } /* Ocean Yellow */ .hljs-attribute { color: #ebcb8b; } /* Ocean Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #a3be8c; } /* Ocean Blue */ .hljs-title, .hljs-section { color: #8fa1b3; } /* Ocean Purple */ .hljs-keyword, .hljs-selector-tag { color: #b48ead; } .hljs { display: block; overflow-x: auto; background: #2b303b; color: #c0c5ce; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/paraiso-dark.css ================================================ /* Paraíso (dark) Created by Jan T. Sott (http://github.com/idleberg) Inspired by the art of Rubens LP (http://www.rubenslp.com.br) */ /* Paraíso Comment */ .hljs-comment, .hljs-quote { color: #8d8687; } /* Paraíso Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-link, .hljs-meta { color: #ef6155; } /* Paraíso Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-deletion { color: #f99b15; } /* Paraíso Yellow */ .hljs-title, .hljs-section, .hljs-attribute { color: #fec418; } /* Paraíso Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #48b685; } /* Paraíso Purple */ .hljs-keyword, .hljs-selector-tag { color: #815ba4; } .hljs { display: block; overflow-x: auto; background: #2f1e2e; color: #a39e9b; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/paraiso-light.css ================================================ /* Paraíso (light) Created by Jan T. Sott (http://github.com/idleberg) Inspired by the art of Rubens LP (http://www.rubenslp.com.br) */ /* Paraíso Comment */ .hljs-comment, .hljs-quote { color: #776e71; } /* Paraíso Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-link, .hljs-meta { color: #ef6155; } /* Paraíso Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-deletion { color: #f99b15; } /* Paraíso Yellow */ .hljs-title, .hljs-section, .hljs-attribute { color: #fec418; } /* Paraíso Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #48b685; } /* Paraíso Purple */ .hljs-keyword, .hljs-selector-tag { color: #815ba4; } .hljs { display: block; overflow-x: auto; background: #e7e9db; color: #4f424c; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/pojoaque.css ================================================ /* Pojoaque Style by Jason Tate http://web-cms-designs.com/ftopict-10-pojoaque-style-for-highlight-js-code-highlighter.html Based on Solarized Style from http://ethanschoonover.com/solarized */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #dccf8f; background: url(./pojoaque.jpg) repeat scroll left top #181914; } .hljs-comment, .hljs-quote { color: #586e75; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-addition { color: #b64926; } .hljs-number, .hljs-string, .hljs-doctag, .hljs-regexp { color: #468966; } .hljs-title, .hljs-section, .hljs-built_in, .hljs-name { color: #ffb03b; } .hljs-variable, .hljs-template-variable, .hljs-class .hljs-title, .hljs-type, .hljs-tag { color: #b58900; } .hljs-attribute { color: #b89859; } .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-subst, .hljs-meta { color: #cb4b16; } .hljs-deletion { color: #dc322f; } .hljs-selector-id, .hljs-selector-class { color: #d3a60c; } .hljs-formula { background: #073642; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/purebasic.css ================================================ /* PureBASIC native IDE style ( version 1.0 - April 2016 ) by Tristano Ajmone Public Domain NOTE_1: PureBASIC code syntax highlighting only applies the following classes: .hljs-comment .hljs-function .hljs-keywords .hljs-string .hljs-symbol Other classes are added here for the benefit of styling other languages with the look and feel of PureBASIC native IDE style. If you need to customize a stylesheet for PureBASIC only, remove all non-relevant classes -- PureBASIC-related classes are followed by a "--- used for PureBASIC ... ---" comment on same line. NOTE_2: Color names provided in comments were derived using "Name that Color" online tool: http://chir.ag/projects/name-that-color */ .hljs { /* Common set of rules required by highlight.js (don'r remove!) */ display: block; overflow-x: auto; padding: 0.5em; background: #ffffdf; /* Half and Half (approx.) */ /* --- Uncomment to add PureBASIC native IDE styled font! font-family: Consolas; */ } .hljs, /* --- used for PureBASIC base color --- */ .hljs-type, /* --- used for PureBASIC Procedures return type --- */ .hljs-function, /* --- used for wrapping PureBASIC Procedures definitions --- */ .hljs-name, .hljs-number, .hljs-attr, .hljs-params, .hljs-subst { color: #000000; /* Black */ } .hljs-comment, /* --- used for PureBASIC Comments --- */ .hljs-regexp, .hljs-section, .hljs-selector-pseudo, .hljs-addition { color: #00aaaa; /* Persian Green (approx.) */ } .hljs-title, /* --- used for PureBASIC Procedures Names --- */ .hljs-tag, .hljs-variable, .hljs-code { color: #006666; /* Blue Stone (approx.) */ } .hljs-keyword, /* --- used for PureBASIC Keywords --- */ .hljs-class, .hljs-meta-keyword, .hljs-selector-class, .hljs-built_in, .hljs-builtin-name { color: #006666; /* Blue Stone (approx.) */ font-weight: bold; } .hljs-string, /* --- used for PureBASIC Strings --- */ .hljs-selector-attr { color: #0080ff; /* Azure Radiance (approx.) */ } .hljs-symbol, /* --- used for PureBASIC Constants --- */ .hljs-link, .hljs-deletion, .hljs-attribute { color: #924b72; /* Cannon Pink (approx.) */ } .hljs-meta, .hljs-literal, .hljs-selector-id { color: #924b72; /* Cannon Pink (approx.) */ font-weight: bold; } .hljs-strong, .hljs-name { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/qtcreator_dark.css ================================================ /* Qt Creator dark color scheme */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #000000; } .hljs, .hljs-subst, .hljs-tag, .hljs-title { color: #aaaaaa; } .hljs-strong, .hljs-emphasis { color: #a8a8a2; } .hljs-bullet, .hljs-quote, .hljs-number, .hljs-regexp, .hljs-literal { color: #ff55ff; } .hljs-code .hljs-selector-class { color: #aaaaff; } .hljs-emphasis, .hljs-stronge, .hljs-type { font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-function, .hljs-section, .hljs-symbol, .hljs-name { color: #ffff55; } .hljs-attribute { color: #ff5555; } .hljs-variable, .hljs-params, .hljs-class .hljs-title { color: #8888ff; } .hljs-string, .hljs-selector-id, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-type, .hljs-built_in, .hljs-builtin-name, .hljs-template-tag, .hljs-template-variable, .hljs-addition, .hljs-link { color: #ff55ff; } .hljs-comment, .hljs-meta, .hljs-deletion { color: #55ffff; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/qtcreator_light.css ================================================ /* Qt Creator light color scheme */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #ffffff; } .hljs, .hljs-subst, .hljs-tag, .hljs-title { color: #000000; } .hljs-strong, .hljs-emphasis { color: #000000; } .hljs-bullet, .hljs-quote, .hljs-number, .hljs-regexp, .hljs-literal { color: #000080; } .hljs-code .hljs-selector-class { color: #800080; } .hljs-emphasis, .hljs-stronge, .hljs-type { font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-function, .hljs-section, .hljs-symbol, .hljs-name { color: #808000; } .hljs-attribute { color: #800000; } .hljs-variable, .hljs-params, .hljs-class .hljs-title { color: #0055af; } .hljs-string, .hljs-selector-id, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-type, .hljs-built_in, .hljs-builtin-name, .hljs-template-tag, .hljs-template-variable, .hljs-addition, .hljs-link { color: #008000; } .hljs-comment, .hljs-meta, .hljs-deletion { color: #008000; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/railscasts.css ================================================ /* Railscasts-like style (c) Visoft, Inc. (Damien White) */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #232323; color: #e6e1dc; } .hljs-comment, .hljs-quote { color: #bc9458; font-style: italic; } .hljs-keyword, .hljs-selector-tag { color: #c26230; } .hljs-string, .hljs-number, .hljs-regexp, .hljs-variable, .hljs-template-variable { color: #a5c261; } .hljs-subst { color: #519f50; } .hljs-tag, .hljs-name { color: #e8bf6a; } .hljs-type { color: #da4939; } .hljs-symbol, .hljs-bullet, .hljs-built_in, .hljs-builtin-name, .hljs-attr, .hljs-link { color: #6d9cbe; } .hljs-params { color: #d0d0ff; } .hljs-attribute { color: #cda869; } .hljs-meta { color: #9b859d; } .hljs-title, .hljs-section { color: #ffc66d; } .hljs-addition { background-color: #144212; color: #e6e1dc; display: inline-block; width: 100%; } .hljs-deletion { background-color: #600; color: #e6e1dc; display: inline-block; width: 100%; } .hljs-selector-class { color: #9b703f; } .hljs-selector-id { color: #8b98ab; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .hljs-link { text-decoration: underline; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/rainbow.css ================================================ /* Style with support for rainbow parens */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #474949; color: #d1d9e1; } .hljs-comment, .hljs-quote { color: #969896; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type, .hljs-addition { color: #cc99cc; } .hljs-number, .hljs-selector-attr, .hljs-selector-pseudo { color: #f99157; } .hljs-string, .hljs-doctag, .hljs-regexp { color: #8abeb7; } .hljs-title, .hljs-name, .hljs-section, .hljs-built_in { color: #b5bd68; } .hljs-variable, .hljs-template-variable, .hljs-selector-id, .hljs-class .hljs-title { color: #ffcc66; } .hljs-section, .hljs-name, .hljs-strong { font-weight: bold; } .hljs-symbol, .hljs-bullet, .hljs-subst, .hljs-meta, .hljs-link { color: #f99157; } .hljs-deletion { color: #dc322f; } .hljs-formula { background: #eee8d5; } .hljs-attr, .hljs-attribute { color: #81a2be; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/routeros.css ================================================ /* highlight.js style for Microtik RouterOS script */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #f0f0f0; } /* Base color: saturation 0; */ .hljs, .hljs-subst { color: #444; } .hljs-comment { color: #888888; } .hljs-keyword, .hljs-selector-tag, .hljs-meta-keyword, .hljs-doctag, .hljs-name { font-weight: bold; } .hljs-attribute { color: #0e9a00; } .hljs-function { color: #99069a; } .hljs-builtin-name { color: #99069a; } /* User color: hue: 0 */ .hljs-type, .hljs-string, .hljs-number, .hljs-selector-id, .hljs-selector-class, .hljs-quote, .hljs-template-tag, .hljs-deletion { color: #880000; } .hljs-title, .hljs-section { color: #880000; font-weight: bold; } .hljs-regexp, .hljs-symbol, .hljs-variable, .hljs-template-variable, .hljs-link, .hljs-selector-attr, .hljs-selector-pseudo { color: #bc6060; } /* Language color: hue: 90; */ .hljs-literal { color: #78a960; } .hljs-built_in, .hljs-bullet, .hljs-code, .hljs-addition { color: #0c9a9a; } /* Meta color: hue: 200 */ .hljs-meta { color: #1f7199; } .hljs-meta-string { color: #4d99bf; } /* Misc effects */ .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/school-book.css ================================================ /* School Book style from goldblog.com.ua (c) Zaripov Yura */ .hljs { display: block; overflow-x: auto; padding: 15px 0.5em 0.5em 30px; font-size: 11px; line-height: 16px; } pre { background: #f6f6ae url(./school-book.png); border-top: solid 2px #d2e8b9; border-bottom: solid 1px #d2e8b9; } .hljs-keyword, .hljs-selector-tag, .hljs-literal { color: #005599; font-weight: bold; } .hljs, .hljs-subst { color: #3e5915; } .hljs-string, .hljs-title, .hljs-section, .hljs-type, .hljs-symbol, .hljs-bullet, .hljs-attribute, .hljs-built_in, .hljs-builtin-name, .hljs-addition, .hljs-variable, .hljs-template-tag, .hljs-template-variable, .hljs-link { color: #2c009f; } .hljs-comment, .hljs-quote, .hljs-deletion, .hljs-meta { color: #e60415; } .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-doctag, .hljs-title, .hljs-section, .hljs-type, .hljs-name, .hljs-selector-id, .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/solarized-dark.css ================================================ /* Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #002b36; color: #839496; } .hljs-comment, .hljs-quote { color: #586e75; } /* Solarized Green */ .hljs-keyword, .hljs-selector-tag, .hljs-addition { color: #859900; } /* Solarized Cyan */ .hljs-number, .hljs-string, .hljs-meta .hljs-meta-string, .hljs-literal, .hljs-doctag, .hljs-regexp { color: #2aa198; } /* Solarized Blue */ .hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #268bd2; } /* Solarized Yellow */ .hljs-attribute, .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-class .hljs-title, .hljs-type { color: #b58900; } /* Solarized Orange */ .hljs-symbol, .hljs-bullet, .hljs-subst, .hljs-meta, .hljs-meta .hljs-keyword, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-link { color: #cb4b16; } /* Solarized Red */ .hljs-built_in, .hljs-deletion { color: #dc322f; } .hljs-formula { background: #073642; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/solarized-light.css ================================================ /* Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #fdf6e3; color: #657b83; } .hljs-comment, .hljs-quote { color: #93a1a1; } /* Solarized Green */ .hljs-keyword, .hljs-selector-tag, .hljs-addition { color: #859900; } /* Solarized Cyan */ .hljs-number, .hljs-string, .hljs-meta .hljs-meta-string, .hljs-literal, .hljs-doctag, .hljs-regexp { color: #2aa198; } /* Solarized Blue */ .hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #268bd2; } /* Solarized Yellow */ .hljs-attribute, .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-class .hljs-title, .hljs-type { color: #b58900; } /* Solarized Orange */ .hljs-symbol, .hljs-bullet, .hljs-subst, .hljs-meta, .hljs-meta .hljs-keyword, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-link { color: #cb4b16; } /* Solarized Red */ .hljs-built_in, .hljs-deletion { color: #dc322f; } .hljs-formula { background: #eee8d5; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/sunburst.css ================================================ /* Sunburst-like style (c) Vasily Polovnyov */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #000; color: #f8f8f8; } .hljs-comment, .hljs-quote { color: #aeaeae; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-type { color: #e28964; } .hljs-string { color: #65b042; } .hljs-subst { color: #daefa3; } .hljs-regexp, .hljs-link { color: #e9c062; } .hljs-title, .hljs-section, .hljs-tag, .hljs-name { color: #89bdff; } .hljs-class .hljs-title, .hljs-doctag { text-decoration: underline; } .hljs-symbol, .hljs-bullet, .hljs-number { color: #3387cc; } .hljs-params, .hljs-variable, .hljs-template-variable { color: #3e87e3; } .hljs-attribute { color: #cda869; } .hljs-meta { color: #8996a8; } .hljs-formula { background-color: #0e2231; color: #f8f8f8; font-style: italic; } .hljs-addition { background-color: #253b22; color: #f8f8f8; } .hljs-deletion { background-color: #420e09; color: #f8f8f8; } .hljs-selector-class { color: #9b703f; } .hljs-selector-id { color: #8b98ab; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/tomorrow-night-blue.css ================================================ /* Tomorrow Night Blue Theme */ /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ /* Original theme - https://github.com/chriskempson/tomorrow-theme */ /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ /* Tomorrow Comment */ .hljs-comment, .hljs-quote { color: #7285b7; } /* Tomorrow Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-deletion { color: #ff9da4; } /* Tomorrow Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-meta, .hljs-link { color: #ffc58f; } /* Tomorrow Yellow */ .hljs-attribute { color: #ffeead; } /* Tomorrow Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #d1f1a9; } /* Tomorrow Blue */ .hljs-title, .hljs-section { color: #bbdaff; } /* Tomorrow Purple */ .hljs-keyword, .hljs-selector-tag { color: #ebbbff; } .hljs { display: block; overflow-x: auto; background: #002451; color: white; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/tomorrow-night-bright.css ================================================ /* Tomorrow Night Bright Theme */ /* Original theme - https://github.com/chriskempson/tomorrow-theme */ /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ /* Tomorrow Comment */ .hljs-comment, .hljs-quote { color: #969896; } /* Tomorrow Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-deletion { color: #d54e53; } /* Tomorrow Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-meta, .hljs-link { color: #e78c45; } /* Tomorrow Yellow */ .hljs-attribute { color: #e7c547; } /* Tomorrow Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #b9ca4a; } /* Tomorrow Blue */ .hljs-title, .hljs-section { color: #7aa6da; } /* Tomorrow Purple */ .hljs-keyword, .hljs-selector-tag { color: #c397d8; } .hljs { display: block; overflow-x: auto; background: black; color: #eaeaea; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/tomorrow-night-eighties.css ================================================ /* Tomorrow Night Eighties Theme */ /* Original theme - https://github.com/chriskempson/tomorrow-theme */ /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ /* Tomorrow Comment */ .hljs-comment, .hljs-quote { color: #999999; } /* Tomorrow Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-deletion { color: #f2777a; } /* Tomorrow Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-meta, .hljs-link { color: #f99157; } /* Tomorrow Yellow */ .hljs-attribute { color: #ffcc66; } /* Tomorrow Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #99cc99; } /* Tomorrow Blue */ .hljs-title, .hljs-section { color: #6699cc; } /* Tomorrow Purple */ .hljs-keyword, .hljs-selector-tag { color: #cc99cc; } .hljs { display: block; overflow-x: auto; background: #2d2d2d; color: #cccccc; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/tomorrow-night.css ================================================ /* Tomorrow Night Theme */ /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ /* Original theme - https://github.com/chriskempson/tomorrow-theme */ /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ /* Tomorrow Comment */ .hljs-comment, .hljs-quote { color: #969896; } /* Tomorrow Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-deletion { color: #cc6666; } /* Tomorrow Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-meta, .hljs-link { color: #de935f; } /* Tomorrow Yellow */ .hljs-attribute { color: #f0c674; } /* Tomorrow Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #b5bd68; } /* Tomorrow Blue */ .hljs-title, .hljs-section { color: #81a2be; } /* Tomorrow Purple */ .hljs-keyword, .hljs-selector-tag { color: #b294bb; } .hljs { display: block; overflow-x: auto; background: #1d1f21; color: #c5c8c6; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/tomorrow.css ================================================ /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ /* Tomorrow Comment */ .hljs-comment, .hljs-quote { color: #8e908c; } /* Tomorrow Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-deletion { color: #c82829; } /* Tomorrow Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-meta, .hljs-link { color: #f5871f; } /* Tomorrow Yellow */ .hljs-attribute { color: #eab700; } /* Tomorrow Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #718c00; } /* Tomorrow Blue */ .hljs-title, .hljs-section { color: #4271ae; } /* Tomorrow Purple */ .hljs-keyword, .hljs-selector-tag { color: #8959a8; } .hljs { display: block; overflow-x: auto; background: white; color: #4d4d4c; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/vs.css ================================================ /* Visual Studio-like style based on original C# coloring by Jason Diamond */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: white; color: black; } .hljs-comment, .hljs-quote, .hljs-variable { color: #008000; } .hljs-keyword, .hljs-selector-tag, .hljs-built_in, .hljs-name, .hljs-tag { color: #00f; } .hljs-string, .hljs-title, .hljs-section, .hljs-attribute, .hljs-literal, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-addition { color: #a31515; } .hljs-deletion, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-meta { color: #2b91af; } .hljs-doctag { color: #808080; } .hljs-attr { color: #f00; } .hljs-symbol, .hljs-bullet, .hljs-link { color: #00b0e8; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/vs2015.css ================================================ /* * Visual Studio 2015 dark style * Author: Nicolas LLOBERA */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #1e1e1e; color: #dcdcdc; } .hljs-keyword, .hljs-literal, .hljs-symbol, .hljs-name { color: #569cd6; } .hljs-link { color: #569cd6; text-decoration: underline; } .hljs-built_in, .hljs-type { color: #4ec9b0; } .hljs-number, .hljs-class { color: #b8d7a3; } .hljs-string, .hljs-meta-string { color: #d69d85; } .hljs-regexp, .hljs-template-tag { color: #9a5334; } .hljs-subst, .hljs-function, .hljs-title, .hljs-params, .hljs-formula { color: #dcdcdc; } .hljs-comment, .hljs-quote { color: #57a64a; font-style: italic; } .hljs-doctag { color: #608b4e; } .hljs-meta, .hljs-meta-keyword, .hljs-tag { color: #9b9b9b; } .hljs-variable, .hljs-template-variable { color: #bd63c5; } .hljs-attr, .hljs-attribute, .hljs-builtin-name { color: #9cdcfe; } .hljs-section { color: gold; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } /*.hljs-code { font-family:'Monospace'; }*/ .hljs-bullet, .hljs-selector-tag, .hljs-selector-id, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo { color: #d7ba7d; } .hljs-addition { background-color: #144212; display: inline-block; width: 100%; } .hljs-deletion { background-color: #600; display: inline-block; width: 100%; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/xcode.css ================================================ /* XCode style (c) Angel Garcia */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #fff; color: black; } .hljs-comment, .hljs-quote { color: #006a00; } .hljs-keyword, .hljs-selector-tag, .hljs-literal { color: #aa0d91; } .hljs-name { color: #008; } .hljs-variable, .hljs-template-variable { color: #660; } .hljs-string { color: #c41a16; } .hljs-regexp, .hljs-link { color: #080; } .hljs-title, .hljs-tag, .hljs-symbol, .hljs-bullet, .hljs-number, .hljs-meta { color: #1c00cf; } .hljs-section, .hljs-class .hljs-title, .hljs-type, .hljs-attr, .hljs-built_in, .hljs-builtin-name, .hljs-params { color: #5c2699; } .hljs-attribute, .hljs-subst { color: #000; } .hljs-formula { background-color: #eee; font-style: italic; } .hljs-addition { background-color: #baeeba; } .hljs-deletion { background-color: #ffc8bd; } .hljs-selector-id, .hljs-selector-class { color: #9b703f; } .hljs-doctag, .hljs-strong { font-weight: bold; } .hljs-emphasis { font-style: italic; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/xt256.css ================================================ /* xt256.css Contact: initbar [at] protonmail [dot] ch : github.com/initbar */ .hljs { display: block; overflow-x: auto; color: #eaeaea; background: #000; padding: 0.5; } .hljs-subst { color: #eaeaea; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .hljs-builtin-name, .hljs-type { color: #eaeaea; } .hljs-params { color: #da0000; } .hljs-literal, .hljs-number, .hljs-name { color: #ff0000; font-weight: bolder; } .hljs-comment { color: #969896; } .hljs-selector-id, .hljs-quote { color: #00ffff; } .hljs-template-variable, .hljs-variable, .hljs-title { color: #00ffff; font-weight: bold; } .hljs-selector-class, .hljs-keyword, .hljs-symbol { color: #fff000; } .hljs-string, .hljs-bullet { color: #00ff00; } .hljs-tag, .hljs-section { color: #000fff; } .hljs-selector-tag { color: #000fff; font-weight: bold; } .hljs-attribute, .hljs-built_in, .hljs-regexp, .hljs-link { color: #ff00ff; } .hljs-meta { color: #fff; font-weight: bolder; } ================================================ FILE: packages/ui/certd-client/src/components/highlight-styles/zenburn.css ================================================ /* Zenburn style from voldmar.ru (c) Vladimir Epifanov based on dark.css by Ivan Sagalaev */ .hljs { display: block; overflow-x: auto; padding: 0.5em; background: #3f3f3f; color: #dcdcdc; } .hljs-keyword, .hljs-selector-tag, .hljs-tag { color: #e3ceab; } .hljs-template-tag { color: #dcdcdc; } .hljs-number { color: #8cd0d3; } .hljs-variable, .hljs-template-variable, .hljs-attribute { color: #efdcbc; } .hljs-literal { color: #efefaf; } .hljs-subst { color: #8f8f8f; } .hljs-title, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-section, .hljs-type { color: #efef8f; } .hljs-symbol, .hljs-bullet, .hljs-link { color: #dca3a3; } .hljs-deletion, .hljs-string, .hljs-built_in, .hljs-builtin-name { color: #cc9393; } .hljs-addition, .hljs-comment, .hljs-quote, .hljs-meta { color: #7f9f7f; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: packages/ui/certd-client/src/components/icon-select.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/index.ts ================================================ import PiContainer from "./container.vue"; import TextEditable from "./editable.vue"; import vip from "./vip-button/install.js"; import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue"; import CronEditor from "./cron-editor/index.vue"; import FoldBox from "./fold-box.vue"; import { CronLight } from "@vue-js-cron/light"; import "@vue-js-cron/light/dist/light.css"; import Plugins from "./plugins/index"; import LoadingButton from "./loading-button.vue"; import IconSelect from "./icon-select.vue"; import ExpiresTimeText from "./expires-time-text.vue"; import FileInput from "./file-input.vue"; import PemInput from "./pem-input.vue"; import { defineAsyncComponent } from "vue"; import NotificationSelector from "../views/certd/notification/notification-selector/index.vue"; import EmailSelector from "./email-selector/index.vue"; import ValidTimeFormat from "./valid-time-format.vue"; export default { install(app: any) { app.component( "CodeEditor", defineAsyncComponent(() => import("./code-editor/index.vue")) ); app.component("EmailSelector", EmailSelector); app.component("NotificationSelector", NotificationSelector); app.component("PiContainer", PiContainer); app.component("TextEditable", TextEditable); app.component("FileInput", FileInput); app.component("PemInput", PemInput); app.component("ValidTimeFormat", ValidTimeFormat); // app.component("CodeEditor", CodeEditor); app.component("CronLight", CronLight); app.component("CronEditor", CronEditor); app.component("FoldBox", FoldBox); app.component("CheckCircleOutlined", CheckCircleOutlined); app.component("InfoCircleOutlined", InfoCircleOutlined); app.component("UndoOutlined", UndoOutlined); app.component("LoadingButton", LoadingButton); app.component("IconSelect", IconSelect); app.component("ExpiresTimeText", ExpiresTimeText); app.use(vip); app.use(Plugins); }, }; ================================================ FILE: packages/ui/certd-client/src/components/loading-button.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/pem-input.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/dns-provider-selector/api.ts ================================================ import { request } from "/src/api/service"; const apiPrefix = "/pi/dnsProvider"; export async function GetList() { return await request({ url: apiPrefix + "/list", method: "post", }); } ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/dns-provider-selector/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/api.ts ================================================ import { request } from "/src/api/service"; const apiPrefix = "/cname/record"; const subDomainApiPrefix = "/pi/subDomain"; export type CnameRecord = { id?: number; status?: string; hostRecord?: string; recordValue?: string; error?: string; }; export type DomainGroupItem = { domain: string; domains?: string[]; keySubDomains?: string[]; }; export async function GetList() { return await request({ url: apiPrefix + "/list", method: "post", }); } export async function GetByDomain(domain: string) { return await request({ url: apiPrefix + "/getByDomain", method: "post", data: { domain, createOnNotFound: true, }, }); } export async function DoVerify(id: number) { return await request({ url: apiPrefix + "/verify", method: "post", data: { id, }, }); } export async function ParseDomain(fullDomain: string) { return await request({ url: subDomainApiPrefix + "/parseDomain", method: "post", data: { fullDomain, }, }); } ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/cname-record-info.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/cname-tip.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/cname-verify-plan.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/http-verify-plan.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/type.ts ================================================ import { CnameRecord } from "@certd/pipeline"; export type HttpRecord = { domain: string; httpUploaderType: string; httpUploaderAccess: number; httpUploadRootDir: string; }; export type DomainVerifyPlanInput = { domain: string; domains: string[]; type: "cname" | "dns" | "http"; dnsProviderType?: string; dnsProviderAccessType?: string; dnsProviderAccessId?: number; cnameVerifyPlan?: Record; httpVerifyPlan?: Record; }; export type DomainsVerifyPlanInput = { [key: string]: DomainVerifyPlanInput; }; ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/validator.ts ================================================ import Validator from "async-validator"; import { DomainsVerifyPlanInput } from "./type"; function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) { if (value == null) { return true; } for (const domain in value) { const type = value[domain].type; if (type === "cname") { const subDomains = Object.keys(value[domain].cnameVerifyPlan); if (subDomains.length > 0) { for (const subDomain of subDomains) { const plan = value[domain].cnameVerifyPlan[subDomain]; if (plan.status !== "valid") { throw new Error(`域名${subDomain}的CNAME未验证通过,请先设置CNAME记录,点击验证按钮`); } } } } else if (type === "http") { const domains = value[domain].domains || []; for (const item of domains) { //如果有通配符域名则不允许使用http校验 if (item.startsWith("*.")) { throw new Error(`域名${item}为通配符域名,不支持HTTP校验`); } } const subDomains = Object.keys(value[domain].httpVerifyPlan); if (subDomains.length > 0) { for (const subDomain of subDomains) { const plan = value[domain].httpVerifyPlan[subDomain]; if (!plan.httpUploaderType) { throw new Error(`域名${subDomain}的上传方式必须填写`); } if (!plan.httpUploaderAccess) { throw new Error(`域名${subDomain}的上传授权信息必须填写`); } if (!plan.httpUploadRootDir) { throw new Error(`域名${subDomain}的网站根路径必须填写`); } } } } else if (type === "dns") { if (!value[domain].dnsProviderType || !value[domain].dnsProviderAccessId) { throw new Error(`DNS模式下,域名${domain}的DNS类型和授权信息必须填写`); } } } return true; } // 注册自定义验证器 Validator.register("checkDomainVerifyPlan", checkDomainVerifyPlan); ================================================ FILE: packages/ui/certd-client/src/components/plugins/cert/index.ts ================================================ export * from "./domains-verify-plan-editor/validator.js"; ================================================ FILE: packages/ui/certd-client/src/components/plugins/common/api-test.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/common/cert-domains-getter.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/common/input-password.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/common/output-selector/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/common/remote-input.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/common/remote-select.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/plugins/index.ts ================================================ import SynologyIdDeviceGetter from "./synology/device-id-getter.vue"; import RemoteSelect from "./common/remote-select.vue"; import RemoteInput from "./common/remote-input.vue"; import CertDomainsGetter from "./common/cert-domains-getter.vue"; import OutputSelector from "/@/components/plugins/common/output-selector/index.vue"; import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue"; import DomainsVerifyPlanEditor from "/@/components/plugins/cert/domains-verify-plan-editor/index.vue"; import AccessSelector from "/@/views/certd/access/access-selector/index.vue"; import InputPassword from "./common/input-password.vue"; import CertInfoUpdater from "/@/views/certd/pipeline/cert-upload/index.vue"; import ApiTest from "./common/api-test.vue"; export * from "./cert/index.js"; export default { install(app: any) { app.component("OutputSelector", OutputSelector); app.component("DnsProviderSelector", DnsProviderSelector); app.component("DomainsVerifyPlanEditor", DomainsVerifyPlanEditor); app.component("AccessSelector", AccessSelector); app.component("CertInfoUpdater", CertInfoUpdater); app.component("ApiTest", ApiTest); app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter); app.component("RemoteSelect", RemoteSelect); app.component("RemoteInput", RemoteInput); app.component("CertDomainsGetter", CertDomainsGetter); app.component("InputPassword", InputPassword); }, }; ================================================ FILE: packages/ui/certd-client/src/components/plugins/lib/dicts.ts ================================================ import { dict } from "@fast-crud/fast-crud"; export const Dicts = { sslProviderDict: dict({ data: [ { value: "letsencrypt", label: "Let‘s Encrypt" }, { value: "zerossl", label: "ZeroSSL" }, ], }), challengeTypeDict: dict({ data: [ { value: "dns", label: "DNS校验", color: "green" }, { value: "cname", label: "CNAME代理校验", color: "blue" }, { value: "http", label: "HTTP校验", color: "yellow" }, ], }), dnsProviderTypeDict: dict({ url: "pi/dnsProvider/dnsProviderTypeDict", }), uploaderTypeDict: dict({ data: [ { label: "SFTP", value: "sftp" }, { label: "FTP", value: "ftp" }, { label: "阿里云OSS", value: "alioss" }, { label: "腾讯云COS", value: "tencentcos" }, { label: "七牛OSS", value: "qiniuoss" }, { label: "S3/Minio", value: "s3" }, { label: "SSH(已废弃,请选择SFTP方式)", value: "ssh", disabled: true }, ], }), }; ================================================ FILE: packages/ui/certd-client/src/components/plugins/lib/index.ts ================================================ import { request } from "/@/api/service"; export type ComponentPropsType = { type?: string; typeName?: string; action: string; form?: any; value?: any; }; export type RequestHandleReq = { type: string; typeName: string; action: string; data?: any; input: T; }; export async function doRequest(req: RequestHandleReq, opts: any = {}) { const url = `/pi/handle/${req.type}`; const { typeName, action, data, input } = req; const res = await request({ url, method: "post", data: { typeName, action, data, input, }, ...opts, }); return res; } ================================================ FILE: packages/ui/certd-client/src/components/plugins/synology/device-id-getter.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/tutorial/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/tutorial/simple-steps.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/tutorial/tutorial-steps.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/valid-time-format.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/vip-button/api.ts ================================================ import { request } from "/src/api/service"; export async function doActive(form: any) { return await request({ url: "/sys/plus/active", method: "post", data: form, }); } export async function getVipTrial() { return await request({ url: "/sys/plus/getVipTrial", method: "post", data: {}, }); } ================================================ FILE: packages/ui/certd-client/src/components/vip-button/directive.ts ================================================ import { notification } from "ant-design-vue"; import { useSettingStore } from "/@/store/settings"; export default { mounted(el: any, binding: any, vnode: any) { const { value } = binding; const settingStore = useSettingStore(); el.className = el.className + " need-plus"; if (!settingStore.isPlus) { function checkPlus() { // 事件处理代码 notification.warn({ message: "此为专业版功能,请升级到专业版", }); } el.addEventListener("click", function (event: any) { checkPlus(); }); el.addEventListener("move", function (event: any) { checkPlus(); }); } }, }; ================================================ FILE: packages/ui/certd-client/src/components/vip-button/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/components/vip-button/install.ts ================================================ import VipButton from "./index.vue"; import plus from "./directive.js"; export default function (app: any) { app.component("VipButton", VipButton); app.directive("plus", plus); } ================================================ FILE: packages/ui/certd-client/src/constants/index.ts ================================================ export const CertApplyPluginNames = [":cert:"]; ================================================ FILE: packages/ui/certd-client/src/layout/components/footer/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/components/locale/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/components/menu/index.less ================================================ .fs-menu-wrapper { height: 100%; overflow-y: auto; &.fs-menu-better-scroll { overflow-y: hidden; } .menu-item-title { display: flex; align-items: center; .fs-icon { font-size: 16px !important; min-width: 16px !important; } } } ================================================ FILE: packages/ui/certd-client/src/layout/components/menu/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/components/menu/index1.tsx ================================================ import { useRoute, useRouter } from "vue-router"; import { ref, watch, onMounted, onUnmounted, resolveComponent, nextTick, defineComponent } from "vue"; import * as _ from "lodash-es"; import BScroll from "better-scroll"; import "./index.less"; import { utils } from "@fast-crud/fast-crud"; import { routerUtils } from "/@/utils/util.router"; defineOptions(); export default defineComponent({ name: "FsMenu", inheritAttrs: true, props: { menus: {}, expandSelected: { default: false, }, scroll: {}, }, setup(props, ctx) { async function onSelect(item: any) { await routerUtils.open(item.key); } const items = ref([]); function buildItemMenus(menus: any) { if (menus == null) { return; } const list: any = []; for (const sub of menus) { const item: any = { key: sub.path, label: sub.title, title: sub.title, icon: () => { return ; }, }; list.push(item); if (sub.children && sub.children.length > 0) { item.children = buildItemMenus(sub.children); } } return list; } items.value = buildItemMenus(props.menus); console.log("items", items.value); const fsIcon = resolveComponent("FsIcon"); const buildMenus = (children: any) => { const slots: any = []; if (children == null) { return slots; } for (const sub of children) { if (sub.meta?.show != null) { if (sub.meta.show === false || (typeof sub.meta.show === "function" && !sub.meta.show())) { continue; } } const title: any = () => { const icon = sub.icon || sub?.meta?.icon; if (icon) { // @ts-ignore , anticon必须要有,不然不能折叠 return (
{sub.title}
); } return sub.title; }; if (sub.children && sub.children.length > 0) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const subSlots = { default: () => { return buildMenus(sub.children); }, title, }; function onTitleClick() { if (sub.path && ctx.attrs.mode === "horizontal") { open(sub.path); } } slots.push(); } else { slots.push( {title} ); } } return slots; }; const slots = { default() { return buildMenus(props.menus); }, }; const selectedKeys = ref([]); const openKeys = ref([]); const route = useRoute(); const router = useRouter(); function openSelectedParents(fullPath: any) { if (!props.expandSelected) { return; } if (props.menus == null) { return; } const keys: any = []; let changed = false; utils.deepdash.forEachDeep(props.menus, (value: any, key: any, parent: any, context: any) => { if (value == null) { return; } if (value.path === fullPath) { _.forEach(context.parents, item => { if (item.value instanceof Array) { return; } keys.push(item.value.path); }); } }); if (keys.length > 0) { for (const key of keys) { if (openKeys.value.indexOf(key) === -1) { openKeys.value.push(key); changed = true; } } } return changed; } // const { asideMenuRef, onOpenChange } = useBetterScroll(props.scroll as any); watch( () => { return route.fullPath; }, path => { // path = route.fullPath; selectedKeys.value = [path]; const changed = openSelectedParents(path); if (changed) { // onOpenChange(); } }, { immediate: true, } ); return () => { const menu = ( ); const classNames = { "fs-menu-wrapper": true, "fs-menu-better-scroll": props.scroll }; return
{menu}
; }; }, }); ================================================ FILE: packages/ui/certd-client/src/layout/components/source-link/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/components/tabs/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/components/theme/color-picker.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/components/theme/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/components/theme/mode-set.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/components/user-info/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/layout-basic.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/layout-framework.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/layout-outside.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/layout/layout-pass.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/locales/antdv.ts ================================================ import { ref } from "vue"; import "dayjs/locale/zh-cn"; import "dayjs/locale/en"; import zhCN from "ant-design-vue/es/locale/zh_CN"; import enUS from "ant-design-vue/es/locale/en_US"; import dayjs from "dayjs"; export const antdvLocale = ref(zhCN); export async function setAntdvLocale(value: string) { console.log("locale changed:", value); if (value.startsWith("zh")) { dayjs.locale("zh-cn"); antdvLocale.value = zhCN; } else { dayjs.locale("en"); antdvLocale.value = enUS; } } ================================================ FILE: packages/ui/certd-client/src/locales/i18n.ts ================================================ import type { App } from "vue"; import type { Locale } from "vue-i18n"; import { setAntdvLocale } from "./antdv"; import type { ImportLocaleFn, LoadMessageFn, LocaleSetupOptions, SupportedLanguagesType } from "./typing"; import { unref } from "vue"; import { createI18n } from "vue-i18n"; import en_US from "./langs/en-US/index"; import zh_CN from "./langs/zh-CN/index"; import { useSimpleLocale } from "/@/vben/composables"; const i18n = createI18n({ globalInjection: true, legacy: false, fallbackLocale: "en-US", locale: "en-US", messages: { "zh-CN": zh_CN, "en-US": en_US, }, }); const modules = import.meta.glob("./langs/**/*.json"); const { setSimpleLocale } = useSimpleLocale(); const localesMap = loadLocalesMapFromDir(/\.\/langs\/([^/]+)\/(.*)\.json$/, modules); let loadMessages: LoadMessageFn; /** * Load locale modules * @param modules */ function loadLocalesMap(modules: Record Promise>) { const localesMap: Record = {}; for (const [path, loadLocale] of Object.entries(modules)) { const key = path.match(/([\w-]*)\.(json)/)?.[1]; if (key) { localesMap[key] = loadLocale as ImportLocaleFn; } } return localesMap; } /** * Load locale modules with directory structure * @param regexp - Regular expression to match language and file names * @param modules - The modules object containing paths and import functions * @returns A map of locales to their corresponding import functions */ function loadLocalesMapFromDir(regexp: RegExp, modules: Record Promise>): Record { const localesRaw: Record Promise>> = {}; const localesMap: Record = {}; // Iterate over the modules to extract language and file names for (const path in modules) { const match = path.match(regexp); if (match) { const [_, locale, fileName] = match; if (locale && fileName) { if (!localesRaw[locale]) { localesRaw[locale] = {}; } if (modules[path]) { localesRaw[locale][fileName] = modules[path]; } } } } // Convert raw locale data into async import functions for (const [locale, files] of Object.entries(localesRaw)) { localesMap[locale] = async () => { const messages: Record = {}; for (const [fileName, importFn] of Object.entries(files)) { messages[fileName] = ((await importFn()) as any)?.default; } return { default: messages }; }; } return localesMap; } /** * Set i18n language * @param locale */ function setI18nLanguage(locale: Locale) { setAntdvLocale(locale); //@ts-ignore i18n.global.locale.value = locale; document?.querySelector("html")?.setAttribute("lang", locale); } async function setupI18n(app: App, options: LocaleSetupOptions = {}) { const { defaultLocale = "en-US" } = options; // app可以自行扩展一些第三方库和组件库的国际化 loadMessages = options.loadMessages || (async () => ({})); app.use(i18n); await loadLocaleMessages(defaultLocale); // 在控制台打印警告 i18n.global.setMissingHandler((locale, key) => { if (options.missingWarn && key.includes(".")) { console.warn(`[intlify] Not found '${key}' key in '${locale}' locale messages.`); } }); } /** * Load locale messages * @param lang */ async function loadLocaleMessages(lang: SupportedLanguagesType) { if (unref(i18n.global.locale) === lang) { return setI18nLanguage(lang); } setSimpleLocale(lang); const message = await localesMap[lang]?.(); if (message?.default) { //@ts-ignore i18n.global.setLocaleMessage(lang, message.default); } const mergeMessage = await loadMessages(lang); i18n.global.mergeLocaleMessage(lang, mergeMessage); return setI18nLanguage(lang); } export function useI18n() { return { t: i18n.global.t, locale: i18n.global.locale, }; } export { i18n, loadLocaleMessages, loadLocalesMap, loadLocalesMapFromDir, setupI18n, setI18nLanguage }; export default i18n; ================================================ FILE: packages/ui/certd-client/src/locales/index.ts ================================================ import { i18n, loadLocaleMessages, loadLocalesMap, loadLocalesMapFromDir, setupI18n, setI18nLanguage, useI18n } from "./i18n"; const $t = i18n.global.t; const $te = i18n.global.te; export { $t, $te, i18n, loadLocaleMessages, loadLocalesMap, loadLocalesMapFromDir, setupI18n, setI18nLanguage, useI18n }; export { type ImportLocaleFn, type LocaleSetupOptions, type SupportedLanguagesType } from "./typing"; // export type { CompileError } from "@intlify/core-base"; // export { useI18n } from "vue-i18n"; export type { Locale } from "vue-i18n"; ================================================ FILE: packages/ui/certd-client/src/locales/langs/en-US/authentication.ts ================================================ export default { welcomeBack: "Welcome Back", pageTitle: "Plug-and-play Admin system", pageDesc: "Efficient, versatile frontend template", loginSuccess: "Login Successful", loginSuccessDesc: "Welcome Back", loginSubtitle: "Enter your account details to manage your projects", selectAccount: "Quick Select Account", username: "Username", password: "Password", usernameTip: "Please enter username", passwordErrorTip: "Password is incorrect", passwordTip: "Please enter password", verifyRequiredTip: "Please complete the verification first", rememberMe: "Remember Me", createAnAccount: "Create an Account", createAccount: "Create Account", alreadyHaveAccount: "Already have an account?", accountTip: "Don't have an account?", signUp: "Sign Up", signUpSubtitle: "Make managing your applications simple and fun", confirmPassword: "Confirm Password", confirmPasswordTip: "The passwords do not match", agree: "I agree to", privacyPolicy: "Privacy-policy", terms: "Terms", agreeTip: "Please agree to the Privacy Policy and Terms", goToLogin: "Login instead", passwordStrength: "Use 8 or more characters with a mix of letters, numbers & symbols", forgetPassword: "Forget Password?", forgetPasswordSubtitle: "Enter your email and we'll send you instructions to reset your password", emailTip: "Please enter email", emailValidErrorTip: "The email format you entered is incorrect", sendResetLink: "Send Reset Link", email: "Email", qrcodeSubtitle: "Scan the QR code with your phone to login", qrcodePrompt: "Click 'Confirm' after scanning to complete login", qrcodeLogin: "QR Code Login", codeSubtitle: "Enter your phone number to start managing your project", code: "Security code", codeTip: "Security code required {0} characters", mobile: "Mobile", mobileLogin: "Mobile Login", mobileTip: "Please enter mobile number", mobileErrortip: "The phone number format is incorrect", sendCode: "Get Security code", sendText: "Resend in {0}s", thirdPartyLogin: "Or continue with", loginAgainTitle: "Please Log In Again", loginAgainSubTitle: "Your login session has expired. Please log in again to continue.", layout: { center: "Align Center", alignLeft: "Align Left", alignRight: "Align Right", }, usernamePlaceholder: "Please enter username/email/phone number", passwordPlaceholder: "Please enter your password", mobilePlaceholder: "Please enter your mobile number", loginButton: "Log In", forgotAdminPassword: "Forgot admin password?", registerLink: "Register", smsTab: "Login via SMS code", passwordTab: "Password login", title: "Change Password", weakPasswordWarning: "For your account security, please change your password immediately", changeNow: "Change Now", successMessage: "Changed successfully", oldPassword: "Old Password", oldPasswordRequired: "Please enter the old password", newPassword: "New Password", newPasswordRequired: "Please enter the new password", confirmNewPassword: "Confirm New Password", confirmNewPasswordRequired: "Please confirm the new password", changePasswordButton: "Change Password", enterPassword: "Please enter the password", newPasswordNotSameOld: "The new password cannot be the same as the old password", enterPasswordAgain: "Please enter the password again", passwordsNotMatch: "The two passwords do not match!", avatar: "Avatar", nickName: "Nickname", phoneNumber: "Phone Number", changePassword: "Change Password", updateProfile: "Update Profile", }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/en-US/certd.ts ================================================ export default { app: { crud: { i18n: { name: "name", city: "city", status: "status", }, }, }, fs: { rowHandle: { title: "Operation", }, }, order: { confirmTitle: "Order Confirmation", package: "Package", description: "Description", specifications: "Specifications", pipeline: "Pipeline", domain: "Domain", deployTimes: "Deployments", duration: "Duration", price: "Price", paymentMethod: "Payment Method", free: "Free", unit: { pieces: "pieces", count: "count", times: "times", }, }, framework: { title: "Framework", home: "Home", }, title: "Certificate Automation", pipeline: "Pipeline", pipelineEdit: "Edit Pipeline", history: "Execution History", certStore: "Certificate Repository", siteMonitor: "Site Certificate Monitor", settings: "Settings", accessManager: "Access Management", cnameRecord: "CNAME Record Management", subDomain: "Subdomain Delegation Settings", pipelineGroup: "Pipeline Group Management", openKey: "Open API Key", notification: "Notification Settings", siteMonitorSetting: "Site Monitor Settings", userSecurity: "Security Settings", userProfile: "Account Info", suite: "Suite", mySuite: "My Suite", suiteBuy: "Suite Purchase", myTrade: "My Orders", paymentReturn: "Payment Return", user: { greeting: "Hello", profile: "Account Info", logout: "Logout", }, dashboard: { greeting: "Hello, {name}, welcome to 【{site}】", latestVersion: "Latest version: {version}", validUntil: "Valid until:", tutorialTooltip: "Click to view detailed tutorial", tutorialText: "Only 3 steps to automatically apply and deploy certificates", alertMessage: "Certificates and credentials are sensitive. Do not use untrusted online Certd services or images. Always self-host and use official release channels:", helpDoc: "Help Docs", pipelineCount: "Number of Certificate Pipelines", noPipeline: "You have no certificate pipelines yet", createNow: "Create Now", managePipeline: "Manage Pipelines", pipelineStatus: "Pipeline Status", recentRun: "Recent Run Statistics", runCount: "Run Count", expiringCerts: "Soon-to-Expire Certificates", supportedTasks: "Overview of Supported Deployment Tasks", }, steps: { createPipeline: "Create Certificate Pipeline", addTask: "Add Deployment Task", scheduledRun: "Scheduled Run", }, customPipeline: "Custom Pipeline", createCertdPipeline: "Create Certificate Pipeline", commercialCertHosting: "Commercial Certificate Hosting", tooltip: { manualUploadOwnCert: "Manually upload your own certificate for automatic deployment", noAutoApplyCommercialCert: "Does not automatically apply for commercial certificates", manualUploadOnUpdate: "Must manually upload once when the certificate is updated", }, table: { confirmDeleteTitle: "Are you sure you want to delete?", confirmDeleteMessage: "This will delete all data related to the pipeline, including execution history, certificate files, and certificate repository records.", }, play: { runPipeline: "Run Pipeline", confirm: "Confirm", confirmTrigger: "Are you sure you want to trigger the run?", pipelineStarted: "Pipeline has started running", }, actions: { editPipeline: "Edit Pipeline", editConfigGroup: "Modify Configuration/Group", viewCertificate: "View Certificate", downloadCertificate: "Download Certificate", }, fields: { userId: "User ID", pipelineName: "Pipeline Name", keyword: "Keyword", required: "This field is required", pipelineContent: "Pipeline Content", scheduledTaskCount: "Scheduled Task Count", deployTaskCount: "Deployment Task Count", remainingValidity: "Remaining Validity", expiryTime: "Expiry Time", status: "Status", lastRun: "Last Run", enabled: "Enabled", enabledLabel: "Enabled", disabledLabel: "Disabled", group: "Group", type: "Type", order: "Order Number", keepHistoryCount: "History Record Retention Count", keepHistoryHelper: "Number of history records to keep; excess will be deleted", createTime: "Creation Time", updateTime: "Update Time", triggerType: "Trigger Type", pipelineId: "Pipeline Id", }, types: { certApply: "Certificate Application", certUpload: "Certificate Upload", custom: "Custom", }, myPipelines: "My Pipelines", selectedCount: "Selected {count} items", batchDelete: "Batch Delete", batchForceRerun: "Force Rerun", applyCertificate: "Apply for Certificate", pipelineExecutionRecords: "Pipeline Execution Records", confirm: "Confirm", confirmBatchDeleteContent: "Are you sure you want to batch delete these {count} records?", deleteSuccess: "Delete successful", pleaseSelectRecords: "Please select records first", triggerTypes: { manual: "Manual Execution", timer: "Scheduled Execution", }, sysResources: { sysRoot: "System Management", sysConsole: "Console", sysSettings: "System Settings", cnameSetting: "CNAME Service Settings", emailSetting: "Email Server Settings", siteSetting: "Site Personalization", headerMenus: "Top Menu Settings", sysAccess: "System-level Authorization", sysPlugin: "Plugin Management", sysPluginEdit: "Edit Plugin", sysPluginConfig: "Certificate Plugin Configuration", accountBind: "Account Binding", permissionManager: "Permission Management", roleManager: "Role Management", userManager: "User Management", suiteManager: "Suite Management", suiteSetting: "Suite Settings", orderManager: "Order Management", userSuites: "User Suites", }, certificateRepo: { title: "Certificate Repository", sub: "Certificates generated from pipeline", }, certificateNotGenerated: "Certificate not yet generated, please run the pipeline first", viewCertificateTitle: "View Certificate", close: "Close", viewCert: { title: "View Certificate", }, download: { title: "Download Certificate", }, source: "Source Code", github: "GitHub", gitee: "Gitee", cron: { clearTip: "Clear Selection", nextTrigger: "Next Trigger Time", tip: "Please set a valid cron expression first", }, cronForm: { title: "Scheduled Script", helper: "Click the button above to select the time for daily execution.\nIt is recommended to run once a day. Tasks will be skipped if the certificate is not expiring.", required: "This field is required", }, email: { title: "Recipient Email", helper: "Enter your recipient email addresses. Multiple addresses are supported.", required: "This field is required", }, plugin: { selectTitle: "Certificate Apply Plugin", jsAcme: "JS-ACME: Easy to use, powerful features [Recommended]", legoAcme: "Lego-ACME: Based on Lego, supports a wide range of DNS providers, suitable for users familiar with Lego", }, pipelineForm: { createTitle: "Create Certificate Pipeline", moreParams: "More Parameters", triggerCronTitle: "Scheduled Trigger", triggerCronHelper: "Click the button above to choose a daily execution time.\nIt is recommended to trigger once per day. The task will be skipped if the certificate has not expired and will not be executed repeatedly.", notificationTitle: "Failure Notification", notificationHelper: "Get real-time alerts when the task fails", groupIdTitle: "Pipeline Group", }, notificationDefault: "Use Default Notification", monitor: { title: "Site Certificate Monitoring", description: "Check website certificates' expiration at 0:00 daily; reminders sent 10 days before expiration (using default notification channel);", settingLink: "Site Monitoring Settings", limitInfo: "Basic edition limited to 1, professional and above unlimited, current", checkAll: "Check All", confirmTitle: "Confirm", confirmContent: "Confirm to trigger check for all site certificates?", checkSubmitted: "Check task submitted", pleaseRefresh: "Please refresh the page later to see the results", siteName: "Site Name", enterSiteName: "Please enter the site name", domain: "Domain", enterDomain: "Please enter the domain", enterValidDomain: "Please enter a valid domain", httpsPort: "HTTPS Port", enterPort: "Please enter the port", certInfo: "Certificate Info", issuer: "Issuer", certDomains: "Certificate Domains", certProvider: "Issuer", certStatus: "Certificate Status", status: { ok: "Valid", expired: "Expired", }, certExpiresTime: "Certificate Expiration", expired: "expired", days: "days", lastCheckTime: "Last Check Time", disabled: "Enable/Disable", ipCheck: "Enable IP Check", selectRequired: "Please select", ipCheckConfirm: "Are you sure to {status} IP check?", ipCount: "IP Count", checkStatus: "Check Status", pipelineId: "Linked Pipeline ID", certInfoId: "Certificate ID", checkSubmittedRefresh: "Check task submitted. Please refresh later to view the result.", ipManagement: "IP Management", bulkImport: "Bulk Import", basicLimitError: "Basic version allows only one monitoring site. Please upgrade to the Pro version.", limitExceeded: "Sorry, you can only create up to {max} monitoring records. Please purchase or upgrade your plan.", setting: { siteMonitorSettings: "Site Monitor Settings", notificationChannel: "Notification Channel", setNotificationChannel: "Set the notification channel", retryTimes: "Retry Times", monitorRetryTimes: "Number of retry attempts for monitoring requests", monitorCronSetting: "Monitoring Schedule", cronTrigger: "Scheduled trigger for monitoring", dnsServer: "DNS Server", dnsServerHelper: "Use a custom domain name resolution server, such as: 1.1.1.1 , support multiple", }, }, checkStatus: { success: "Success", checking: "Checking", error: "Error", }, domainList: { title: "Domain List", helper: "Format: domain:port:name, one per line. Port and name are optional.\nExamples:\nwww.baidu.com:443:Baidu\nwww.taobao.com::Taobao\nwww.google.com", required: "Please enter domains to import", placeholder: "www.baidu.com:443:Baidu\nwww.taobao.com::Taobao\nwww.google.com\n", }, accountInfo: "Account Information", securitySettings: "Security & Settings", confirmDisable2FA: "Are you sure you want to disable two-factor authentication login?", disabledSuccess: "Disabled successfully", saveSuccess: "Saved successfully", twoFactorAuth: "2FA Two-Factor Authentication Login", rebind: "Rebind", twoFactorAuthHelper: "Enable or disable two-factor authentication login", bindDevice: "Bind Device", step1: "1. Install any authenticator app, for example:", tooltipGoogleServiceError: "If you get a Google service not found error, you can install KK Google Assistant", step2: "2. Scan the QR code to add the account", step3: "3. Enter the verification code", inputVerifyCode: "Please enter the verification code", cancel: "Cancel", authorizationManagement: "Authorization Management", manageThirdPartyAuth: "Manage third-party system authorization information", name: "Name", pleaseEnterName: "Please enter the name", nameHelper: "Fill in as you like, useful to distinguish when multiple authorizations of the same type exist", level: "Level", system: "System", usera: "User", nickName: "Nickname", max50Chars: "Maximum 50 characters", myInfo: "My Information", save: "Save", editSchedule: "Edit Schedule", timerTrigger: "Timer Trigger", schedule: "Schedule", selectCron: "Please select a schedule Cron", batchEditSchedule: "Batch Edit Schedule", editTrigger: "Edit Trigger", triggerName: "Trigger Name", requiredField: "This field is required", type: "Type", enterName: "Please enter a name", confirmDeleteTrigger: "Are you sure you want to delete this trigger?", notificationType: "Notification Type", selectNotificationType: "Please select a notification type", notificationName: "Notification Name", helperNotificationName: "Fill freely, helps to distinguish when multiple notifications of the same type exist", isDefault: "Is Default", yes: "Yes", no: "No", selectIsDefault: "Please select if default", prompt: "Prompt", confirmSetDefaultNotification: "Are you sure to set as default notification?", test: "Test", scope: "Scope", scopeOpenApiOnly: "Open API Only", scopeFullAccount: "Full Account Permissions", required: "This field is required", scopeHelper: "Open API only allows access to open APIs; full account permissions allow access to all APIs", add: "Generate New Key", gen: { text: "API Test", title: "x-certd-token", okText: "Confirm", contentPart1: "Test the x-certd-token below, you can use it within 3 minutes to test ", openApi: "Open API", contentPart2: " request testing", }, pending_cname_setup: "Pending CNAME setup", validating: "Validating", validation_successful: "Validation successful", validation_failed: "Validation failed", validation_timed_out: "Validation timed out", proxied_domain: "Proxied Domain", host_record: "Host Record", please_set_cname: "Please set CNAME", cname_service: "CNAME Service", default_public_cname: "Default public CNAME service, you can also ", customize_cname: "Customize CNAME Service", public_cname: "Public CNAME", custom_cname: "Custom CNAME", validate: "Validate", validation_started: "Validation started, please wait patiently", click_to_validate: "Click to Validate", all: "All", cname_feature_guide: "CNAME feature principle and usage guide", batch_delete: "Batch Delete", confirm_delete_count: "Are you sure to delete these {count} records in batch?", delete_successful: "Delete successful", please_select_records: "Please select records first", edit_notification: "Edit Notification", other_notification_method: "Other Notification Method", trigger_time: "Trigger Time", start_time: "At Start", success_time: "On Success", fail_to_success_time: "Fail to Success", fail_time: "On Failure", helper_suggest_fail_only: "It is recommended to select only 'On Failure' and 'Fail to Success'", notification_config: "Notification Configuration", please_select_notification: "Please select a notification method", please_select_type: "Please select type", please_select_trigger_time: "Please select notification trigger time", please_select_notification_config: "Please select notification configuration", confirm_delete_trigger: "Are you sure you want to delete this trigger?", gift_package: "Gift Package", package_name: "Package Name", click_to_select: "Click to select", please_select_package: "Please select a package", package: "Package", addon_package: "Addon Package", domain_count: "Domain Count", unit_count: "pcs", field_required: "This field is required", pipeline_count: "Pipeline Count", unit_item: "items", deploy_count: "Deploy Count", unit_times: "times", monitor_count: "Certificate Monitor Count", duration: "Duration", status: "Status", active_time: "Activation Time", expires_time: "Expiration Time", is_present: "Is Present", is_present_yes: "Yes", is_present_no: "No", basicInfo: "Basic Information", titlea: "Title", disabled: "Disabled", ordera: "Order", supportBuy: "Support Purchase", intro: "Introduction", packageContent: "Package Content", maxDomainCount: "Max Domain Count", maxPipelineCount: "Max Pipeline Count", maxDeployCount: "Max Deploy Count", maxMonitorCount: "Max Monitor Count", price: "Price", durationPrices: "Duration Prices", packageName: "Package Name", addon: "Addon", typeHelper: "Suite: Only the most recently purchased one is active at a time\nAddon: Multiple can be purchased, effective immediately without affecting the suite\nThe quantities of suite and addon can be accumulated", domainCount: "Domain Count", pipelineCount: "Pipeline Count", unitPipeline: "pipelines", deployCount: "Deployment Count", unitDeploy: "times", monitorCount: "Certificate Monitor Count", unitCount: "pcs", durationPriceTitle: "Duration and Price", selectDuration: "Select Duration", supportPurchase: "Support Purchase", cannotPurchase: "Cannot Purchase", shelfStatus: "Shelf Status", onShelf: "On Shelf", offShelf: "Off Shelf", orderHelper: "Smaller values appear first", description: "Description", createTime: "Creation Time", updateTime: "Update Time", edit: "Edit", groupName: "Group Name", enterGroupName: "Please enter group name", subdomainHosting: "Subdomain Hosting", subdomainHostingHint: "When your domain has subdomain hosting set, you need to create records here, otherwise certificate application will fail", batchDeleteConfirm: "Are you sure to batch delete these {count} records?", selectRecordFirst: "Please select records first", subdomainHosted: "Hosted Subdomain", subdomainHelpText: "If you don't understand what subdomain hosting is,Do not set it randomly, as it may result in the inability to apply for the certificate. please refer to the documentation ", subdomainManagement: "Subdomain Management", isDisabled: "Is Disabled", enabled: "Enabled", uploadCustomCert: "Upload Custom Certificate", sourcee: "Source", sourcePipeline: "Pipeline", sourceManualUpload: "Manual Upload", domains: "Domains", enterDomain: "Please enter domain", validDays: "Valid Days", expires: " expires", days: " days", expireTime: "Expiration Time", certIssuer: "Certificate Issuer", applyTime: "Application Time", relatedPipeline: "Related Pipeline", statusSuccess: "Success", statusChecking: "Checking", statusError: "Error", actionImportBatch: "Batch Import", actionSyncIp: "Sync IP", modalTitleSyncIp: "Sync IP", modalContentSyncIp: "Are you sure to sync IP?", notificationSyncComplete: "Sync Complete", actionCheckAll: "Check All", modalTitleConfirm: "Confirm", modalContentCheckAll: "Confirm to trigger checking all IP site's certificates?", notificationCheckSubmitted: "Check task submitted", notificationCheckDescription: "Please refresh later to see results", tooltipCheckNow: "Check Now", notificationCheckSubmittedPleaseRefresh: "Check task submitted, please refresh later", columnId: "ID", columnIp: "IP", helperIpCname: "Supports entering CNAME domain name", ruleIpRequired: "Please enter IP", columnCertDomains: "Certificate Domains", columnCertProvider: "Issuer", columnCertStatus: "Certificate Status", statusNormal: "Normal", statusExpired: "Expired", columnCertExpiresTime: "Certificate Expiration Time", expired: "expired", columnCheckStatus: "Check Status", columnLastCheckTime: "Last Check Time", columnSource: "Source", sourceSync: "Sync", sourceManual: "Manual", sourceImport: "Import", columnDisabled: "Enabled/Disabled", columnRemark: "Remark", pluginFile: "Plugin File", selectPluginFile: "Select plugin file", overrideSameName: "Override same name", override: "Override", noOverride: "No override", overrideHelper: "If a plugin with the same name exists, override it directly", importPlugin: "Import Plugin", operationSuccess: "Operation successful", customPlugin: "Custom Plugin", import: "Import", export: "Export", pluginType: "Plugin Type", auth: "Authorization", dns: "DNS", deployPlugin: "Deploy Plugin", icon: "Icon", pluginName: "Plugin Name", pluginNameHelper: "Must be English letters or digits, camelCase with type prefix\nExample: AliyunDeployToCDN\nDo not modify name once plugin is used", pluginNameRuleMsg: "Must be English letters or digits, camelCase with type prefix", author: "Author", authorHelper: "Used as prefix when uploading to plugin store, e.g., greper/pluginName", authorRuleMsg: "Must be English letters or digits", titleHelper: "Plugin name in Chinese", descriptionHelper: "Description of the plugin", builtIn: "Built-in", custom: "Custom", store: "Store", version: "Version", pluginDependencies: "Plugin Dependencies", pluginDependenciesHelper: "Dependencies to install first in format: [author/]pluginName[:version]", editableRunStrategy: "Editable Run Strategy", editable: "Editable", notEditable: "Not Editable", runStrategy: "Run Strategy", normalRun: "Normal Run", skipOnSuccess: "Skip on success (Deploy task)", defaultRunStrategyHelper: "Default run strategy", enableDisable: "Enable/Disable", clickToToggle: "Click to toggle enable/disable", confirmToggle: "Are you sure to", disable: "disable", enable: "enable", pluginGroup: "Plugin Group", icpRegistrationNumber: "ICP Registration Number", icpPlaceholder: "Guangdong ICP xxxxxxx Number", publicSecurityRegistrationNumber: "Public Security Registration Number", publicSecurityPlaceholder: "Beijing Public Security xxxxxxx Number", enableAssistant: "Enable Assistant", allowCrawlers: "Allow Crawlers", httpProxy: "HTTP Proxy", httpProxyPlaceholder: "http://192.168.1.2:18010/", httpProxyHelper: "Configure when some websites are blocked", httpsProxy: "HTTPS Proxy", httpsProxyPlaceholder: "http://192.168.1.2:18010/", saveThenTestTitle: "Save first, then click test", testButton: "Test", httpsProxyHelper: "Usually both proxies are the same, save first then test", dualStackNetwork: "Dual Stack Network", default: "Default", ipv4Priority: "IPv4 Priority", ipv6Priority: "IPv6 Priority", dualStackNetworkHelper: "If IPv6 priority is selected, enable IPv6 in docker-compose.yaml", enableCommonCnameService: "Enable Public CNAME Service", commonCnameHelper: "Allow use of public CNAME service. If disabled and no custom CNAME service is set, CNAME proxy certificate application will not work.", saveButton: "Save", stopSuccess: "Stopped successfully", google: "Google", baidu: "Baidu", success: "Success", testFailed: "Test Failed", testCompleted: "Test Completed", manageOtherUserPipeline: "Manage other users' pipelines", limitUserPipelineCount: "Limit user pipeline count", limitUserPipelineCountHelper: "0 means no limit", enableSelfRegistration: "Enable self-registration", enableUserValidityPeriod: "Enable user validity period", userValidityPeriodHelper: "Users can use normally within validity; pipelines disabled after expiry", enableUsernameRegistration: "Enable username registration", enableEmailRegistration: "Enable email registration", proFeature: "Pro feature", emailServerSetup: "Set up email server", enableSmsLoginRegister: "Enable SMS login and registration", commFeature: "Commercial feature", smsProvider: "SMS provider", aliyunSms: "Aliyun SMS", yfySms: "YFY SMS", smsTest: "SMS test", testMobilePlaceholder: "Enter test mobile number", saveThenTest: "Save first then test", enterTestMobile: "Please enter test mobile number", sendSuccess: "Sent successfully", atLeastOneLoginRequired: "At least one of password login or SMS login must be enabled", fieldRequired: "This field is required", siteHide: "Site Hide", enableSiteHide: "Enable Site Hide", siteHideDescription: "You can disable site accessibility normally and enable it when needed to enhance site security", helpDoc: "Help Document", randomAddress: "Random Address", siteHideUrlHelper: "After the site is hidden, you need to visit this URL to unlock to access normally", fullUnlockUrl: "Full Unlock URL", saveThisUrl: "Please save this URL carefully", unlockPassword: "Unlock Password", unlockPasswordHelper: "Password needed to unlock the hide; set on first time or reset when filled", autoHideTime: "Auto Hide Time", autoHideTimeHelper: "Minutes without requests before auto hiding", hideOpenApi: "Hide Open API", hideOpenApiHelper: "Whether to hide open APIs; whether to expose /api/v1 prefixed endpoints", hideSiteImmediately: "Hide Site Immediately", hideImmediately: "Hide Immediately", confirmHideSiteTitle: "Are you sure to hide the site immediately?", confirmHideSiteContent: "After hiding, the site will be inaccessible. Please operate cautiously.", siteHiddenSuccess: "Site has been hidden", emailServerSettings: "Email Server Settings", setEmailSendingServer: "Set the email sending server", useCustomEmailServer: "Use Custom Email Server", smtpDomain: "SMTP Domain", pleaseEnterSmtpDomain: "Please enter SMTP domain or IP", smtpPort: "SMTP Port", pleaseEnterSmtpPort: "Please enter SMTP port", username: "Username", pleaseEnterUsername: "Please enter username", password: "Password", pleaseEnterPassword: "Please enter password", qqEmailAuthCodeHelper: "If using QQ email, get an authorization code in QQ email settings as the password", senderEmail: "Sender Email", pleaseEnterSenderEmail: "Please enter sender email", useSsl: "Use SSL", sslPortNote: "SSL and non-SSL SMTP ports are different, please adjust port accordingly", ignoreCertValidation: "Ignore Certificate Validation", useOfficialEmailServer: "Use Official Email Server", useOfficialEmailServerHelper: "Send emails directly using the official server to avoid complicated setup", testReceiverEmail: "Test Receiver Email", pleaseEnterTestReceiverEmail: "Please enter test receiver email", saveBeforeTest: "Save before testing", sendFailHelpDoc: "Failed to send??? ", emailConfigHelpDoc: "Email configuration help document", tryOfficialEmailServer: "You can also try using the official email server ↗↗↗↗↗↗↗↗", pluginManagement: "Plugin Management", pluginBetaWarning: "Custom plugins are in BETA and may have breaking changes in future", pleaseSelectRecord: "Please select records first", permissionManagement: "Permission Management", adda: "Add", rootNode: "Root Node", permissionName: "Permission Name", enterPermissionName: "Please enter permission name", permissionCode: "Permission Code", enterPermissionCode: "Please enter permission code", max100Chars: "Maximum 100 characters", examplePermissionCode: "e.g.: sys:user:view", sortOrder: "Sort Order", sortRequired: "Sort order is required", parentNode: "Parent Node", roleManagement: "Role Management", assignPermissions: "Assign Permissions", roleName: "Role Name", enterRoleName: "Please enter role name", unlockLogin: "Unlock Login", notice: "Notice", confirmUnlock: "Are you sure you want to unlock this user's login?", unlockSuccess: "Unlock successful", enterUsername: "Please enter username", modifyPasswordIfFilled: "Fill in to change the password", emaila: "Email", mobile: "Mobile", avatar: "Avatar", validTime: "Valid Time", remark: "Remark", roles: "Roles", cnameTitle: "CNAME Service Configuration", cnameDescription: "The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.", cnameLinkText: "CNAME principle and usage instructions", confirmTitle: "Confirm", confirmDeleteBatch: "Are you sure you want to delete these {count} records?", selectRecordsFirst: "Please select records first", cnameDomain: "CNAME Domain", cnameDomainPlaceholder: "cname.handsfree.work", cnameDomainHelper: "Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.", dnsProvider: "DNS Provider", dnsProviderAuthorization: "DNS Provider Authorization", setDefault: "Set Default", confirmSetDefault: "Are you sure to set as default?", setAsDefault: "Set as Default", disabledLabel: "Disabled", confirmToggleStatus: "Are you sure to {action}?", template: { title: "Pipeline Template", edit: "Pipeline Template Edit", importCreate: "Pipeline Batch Create", // intro: "可根据模版批量创建流水线", intro: "Batch create pipeline based on template", createTemplate: "Create Template", useTemplate: "Use This Template", batchCreate: "Batch Create Pipeline", singleCreate: "Create Single Pipeline", templateName: "Template Name", enterTemplateName: "Please enter template name", copyPipelineConfig: "Copy this pipeline configuration as template source", pipeline: "Pipeline", }, sys: { setting: { showRunStrategy: "Show RunStrategy", showRunStrategyHelper: "Allow modify the run strategy of the task", }, }, modal: { close: "Close", viewCertificateTitle: "View Certificate", }, domain: { domainManager: "Domain Manager", domainDescription: "used to auto apply for certificate", //管理域名的校验方式,用于申请证书时自动选择验证方式 domain: "Domain", challengeType: "Challenge Type", dnsProviderType: "DNS Provider Type", dnsProviderAccess: "DNS Provider Access", httpUploaderType: "HTTP Uploader Type", httpUploaderAccess: "HTTP Uploader Access", httpUploadRootDir: "HTTP Upload Root Dir", disabled: "Disabled", challengeSetting: "Challenge Setting", gotoCnameTip: "Please go to CNAME Record Page", }, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/en-US/common.ts ================================================ export default { back: "Back", backToHome: "Back To Home", login: "Login", logout: "Logout", prompt: "Prompt", cancel: "Cancel", confirm: "Confirm", reset: "Reset", noData: "No Data", refresh: "Refresh", loadingMenu: "Loading Menu", query: "Search", search: "Search", enabled: "Enabled", disabled: "Disabled", enable: "Enable", disable: "Disable", edit: "Edit", delete: "Delete", create: "Create", yes: "Yes", no: "No", handle: "Handle", }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/en-US/guide.ts ================================================ export default { createCertPipeline: { title: "Create Certificate Application Pipeline", description: "Demonstrate how to configure a certificate application task", items: { tutorialTitle: "Tutorial Demo Content", tutorialDesc1: "This tutorial demonstrates how to automatically apply for a certificate and deploy it to Nginx", tutorialDesc2: "Only 3 steps, fully automatic application and deployment", createTitle: "Create Certificate Pipeline", createDesc: "Click to add a certificate pipeline and fill in the certificate application information", successTitle: "Pipeline Created Successfully", successDesc: "Click manual trigger to apply for the certificate", nextTitle: "Next, demonstrate how to automatically deploy the certificate", nextDesc: "If you only need to apply for a certificate, you can stop here", }, }, buttons: { prev: "Previous Step", next: "Next Step", }, addDeployTask: { title: "Add Deployment Certificate Task", description: "Demonstrate deployment of certificate to Nginx", items: { addTaskTitle: "Add Certificate Deployment Task", addTaskDesc1: "Demonstrate automatic deployment of certificate to nginx", addTaskDesc2: "Our system provides numerous deployment plugins to meet your needs", fillParamsTitle: "Fill Task Parameters", fillParamsDesc1: "Fill in the certificate file path on the host", fillParamsDesc2: "Select SSH login authorization for the host", activateCertTitle: "Make New Certificate Effective", activateCertDesc1: "Execute restart script", activateCertDesc2: "Make the certificate effective", taskSuccessTitle: "Deployment Task Added Successfully", taskSuccessDesc: "Now you can run it", pluginsTitle: "Our System Provides Numerous Deployment Plugins", pluginsDesc: "You can deploy certificates to various applications and platforms according to your needs", }, }, runAndTestTask: { runAndTestTitle: "Run and Test", runAndTestDescription: "Demonstrate pipeline running, view logs, skip on success, etc.", runTestOnce: "Run a Test", clickManualTriggerToTest: "Click the manual trigger button to test the run", viewLogs: "View Logs", clickTaskToViewStatusAndLogs: "Click the task to view status and logs", howToTroubleshootFailure: "How to Troubleshoot Failure", viewErrorLogs: "View error logs", nginxContainerNotExistFix: "Shows nginx container not found error, fix by changing to correct nginx container name", executionSuccess: "Execution Success", retryAfterFix: "After fixing, click manual trigger again to rerun successfully", autoSkipAfterSuccess: "Auto Skip After Success", successSkipExplanation: "Successful runs will be skipped automatically, rerun only if parameters or certificates update", viewCertDeploymentSuccess: "View Certificate Deployment Success", visitNginxToSeeCert: "Visit website on nginx to see certificate deployed successfully", downloadCertManualDeploy: "Download Certificate for Manual Deployment", downloadIfNoAutoDeployPlugin: "If no deployment plugin available, download certificate for manual deployment", }, scheduleAndEmailTask: { title: "Set Scheduled Execution and Email Notifications", description: "Automatic running", setSchedule: "Set Scheduled Execution", pipelineSuccessThenSchedule: "Pipeline tests succeed, then configure scheduled triggers so it runs automatically daily", recommendDailyRun: "Recommend configuring to run once daily; new certs requested 35 days before expiry and auto-skipped otherwise", setEmailNotification: "Set Email Notifications", suggestErrorAndRecoveryEmails: "Suggest listening for 'On Error' and 'Error to Success' to quickly troubleshoot failures (basic version requires mail server setup)", basicVersionNeedsMailServer: "(basic version requires configuring mail server)", tutorialEndTitle: "Tutorial End", thanksForWatching: "Thank you for watching, hope it helps you", }, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/en-US/index.ts ================================================ import certd from "./certd"; import authentication from "./authentication"; import vip from "./vip"; import tutorial from "./tutorial"; import preferences from "./preferences"; import ui from "./ui"; import guide from "./guide"; import common from "./common"; export default { certd, authentication, vip, ui, tutorial, preferences, guide, common, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/en-US/preferences.ts ================================================ export default { title: "Preferences", subtitle: "Customize Preferences & Preview in Real Time", resetTip: "Data has changed, click to reset", resetTitle: "Reset Preferences", resetSuccess: "Preferences reset successfully", appearance: "Appearance", layout: "Layout", content: "Content", other: "Other", wide: "Wide", compact: "Fixed", followSystem: "Follow System", vertical: "Vertical", verticalTip: "Side vertical menu mode", horizontal: "Horizontal", horizontalTip: "Horizontal menu mode, all menus displayed at the top", twoColumn: "Two Column", twoColumnTip: "Vertical Two Column Menu Mode", headerSidebarNav: "Header Vertical", headerSidebarNavTip: "Header Full Width, Sidebar Navigation Mode", headerTwoColumn: "Header Two Column", headerTwoColumnTip: "Header Navigation & Sidebar Two Column co-exists", mixedMenu: "Mixed Menu", mixedMenuTip: "Vertical & Horizontal Menu Co-exists", fullContent: "Full Content", fullContentTip: "Only display content body, hide all menus", normal: "Normal", plain: "Plain", rounded: "Rounded", copyPreferences: "Copy Preferences", copyPreferencesSuccessTitle: "Copy successful", copyPreferencesSuccess: "Copy successful, please override in `src/preferences.ts` under app", clearAndLogout: "Clear Cache & Logout", mode: "Mode", general: "General", language: "Language", dynamicTitle: "Dynamic Title", watermark: "Watermark", checkUpdates: "Periodic update check", position: { title: "Preferences Postion", header: "Header", auto: "Auto", fixed: "Fixed", }, sidebar: { title: "Sidebar", width: "Width", visible: "Show Sidebar", collapsed: "Collpase Menu", collapsedShowTitle: "Show Menu Title", autoActivateChild: "Auto Activate SubMenu", autoActivateChildTip: "`Enabled` to automatically activate the submenu while click menu.", expandOnHover: "Expand On Hover", expandOnHoverTip: "When the mouse hovers over menu, \n `Enabled` to expand children menus \n `Disabled` to expand whole sidebar.", }, tabbar: { title: "Tabbar", enable: "Enable Tab Bar", icon: "Show Tabbar Icon", showMore: "Show More Button", showMaximize: "Show Maximize Button", persist: "Persist Tabs", maxCount: "Max Count of Tabs", maxCountTip: "When the number of tabs exceeds the maximum,\nthe oldest tab will be closed.\n Set to 0 to disable count checking.", draggable: "Enable Draggable Sort", wheelable: "Support Mouse Wheel", middleClickClose: "Close Tab when Mouse Middle Button Click", wheelableTip: "When enabled, the Tabbar area responds to vertical scrolling events of the scroll wheel.", styleType: { title: "Tabs Style", chrome: "Chrome", card: "Card", plain: "Plain", brisk: "Brisk", }, contextMenu: { reload: "Reload", close: "Close", pin: "Pin", unpin: "Unpin", closeLeft: "Close Left Tabs", closeRight: "Close Right Tabs", closeOther: "Close Other Tabs", closeAll: "Close All Tabs", openInNewWindow: "Open in New Window", maximize: "Maximize", restoreMaximize: "Restore", }, }, navigationMenu: { title: "Navigation Menu", style: "Navigation Menu Style", accordion: "Sidebar Accordion Menu", split: "Navigation Menu Separation", splitTip: "When enabled, the sidebar displays the top bar's submenu", }, breadcrumb: { title: "Breadcrumb", home: "Show Home Button", enable: "Enable Breadcrumb", icon: "Show Breadcrumb Icon", background: "background", style: "Breadcrumb Style", hideOnlyOne: "Hidden when only one", }, animation: { title: "Animation", loading: "Page Loading", transition: "Page Transition", progress: "Page Progress", }, theme: { title: "Theme", radius: "Radius", light: "Light", dark: "Dark", darkSidebar: "Semi Dark Sidebar", darkHeader: "Semi Dark Header", weakMode: "Weak Mode", grayMode: "Gray Mode", builtin: { title: "Built-in", default: "Default", violet: "Violet", pink: "Pink", rose: "Rose", skyBlue: "Sky Blue", deepBlue: "Deep Blue", green: "Green", deepGreen: "Deep Green", orange: "Orange", yellow: "Yellow", zinc: "Zinc", neutral: "Neutral", slate: "Slate", gray: "Gray", custom: "Custom", }, }, header: { title: "Header", visible: "Show Header", modeStatic: "Static", modeFixed: "Fixed", modeAuto: "Auto hide & Show", modeAutoScroll: "Scroll to Hide & Show", menuAlign: "Menu Align", menuAlignStart: "Start", menuAlignEnd: "End", menuAlignCenter: "Center", }, footer: { title: "Footer", visible: "Show Footer", fixed: "Fixed at Bottom", }, copyright: { title: "Copyright", enable: "Enable Copyright", companyName: "Company Name", companySiteLink: "Company Site Link", date: "Date", icp: "ICP License Number", icpLink: "ICP Site Link", }, shortcutKeys: { title: "Shortcut Keys", global: "Global", search: "Global Search", logout: "Logout", preferences: "Preferences", }, widget: { title: "Widget", globalSearch: "Enable Global Search", fullscreen: "Enable Fullscreen", themeToggle: "Enable Theme Toggle", languageToggle: "Enable Language Toggle", notification: "Enable Notification", sidebarToggle: "Enable Sidebar Toggle", lockScreen: "Enable Lock Screen", refresh: "Enable Refresh", }, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/en-US/tutorial.ts ================================================ export default { title: "Tutorial", }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/en-US/ui.ts ================================================ export default { formRules: { required: "Please enter {0}", selectRequired: "Please select {0}", minLength: "{0} must be at least {1} characters", maxLength: "{0} can be at most {1} characters", length: "{0} must be {1} characters long", alreadyExists: "{0} `{1}` already exists", startWith: "{0} must start with `{1}`", invalidURL: "Please input a valid URL", }, actionTitle: { edit: "Modify {0}", create: "Create {0}", delete: "Delete {0}", view: "View {0}", }, actionMessage: { deleteConfirm: "Are you sure to delete {0}?", deleting: "Deleting {0} ...", deleteSuccess: "{0} deleted successfully", operationSuccess: "Operation succeeded", operationFailed: "Operation failed", }, placeholder: { input: "Please enter", select: "Please select", }, captcha: { title: "Please complete the security verification", sliderSuccessText: "Passed", sliderDefaultText: "Slider and drag", alt: "Supports img tag src attribute value", sliderRotateDefaultTip: "Click picture to refresh", sliderRotateFailTip: "Validation failed", sliderRotateSuccessTip: "Validation successful, time {0} seconds", refreshAriaLabel: "Refresh captcha", confirmAriaLabel: "Confirm selection", confirm: "Confirm", pointAriaLabel: "Click point", clickInOrder: "Please click in order", }, iconPicker: { placeholder: "Select an icon", search: "Search icon...", }, jsonViewer: { copy: "Copy", copied: "Copied", }, fallback: { pageNotFound: "Oops! Page Not Found", pageNotFoundDesc: "Sorry, we couldn't find the page you were looking for.", forbidden: "Oops! Access Denied", forbiddenDesc: "Sorry, but you don't have permission to access this page.", internalError: "Oops! Something Went Wrong", internalErrorDesc: "Sorry, but the server encountered an error.", offline: "Offline Page", offlineError: "Oops! Network Error", offlineErrorDesc: "Sorry, can't connect to the internet. Check your connection.", comingSoon: "Coming Soon", http: { requestTimeout: "The request timed out. Please try again later.", networkError: "A network error occurred. Please check your internet connection and try again.", badRequest: "Bad Request. Please check your input and try again.", unauthorized: "Unauthorized. Please log in to continue.", forbidden: "Forbidden. You do not have permission to access this resource.", notFound: "Not Found. The requested resource could not be found.", internalServerError: "Internal Server Error. Something went wrong on our end. Please try again later.", }, }, widgets: { document: "Document", qa: "Q&A", setting: "Settings", logoutTip: "Do you want to logout?", viewAll: "View All Messages", notifications: "Notifications", markAllAsRead: "Make All as Read", clearNotifications: "Clear", checkUpdatesTitle: "New Version Available", checkUpdatesDescription: "Click to refresh and get the latest version", search: { title: "Search", searchNavigate: "Search Navigation", select: "Select", navigate: "Navigate", close: "Close", noResults: "No Search Results Found", noRecent: "No Search History", recent: "Search History", }, lockScreen: { title: "Lock Screen", screenButton: "Locking", password: "Password", placeholder: "Please enter password", unlock: "Click to unlock", errorPasswordTip: "Password error, please re-enter", backToLogin: "Back to login", entry: "Enter the system", }, }, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/en-US/vip.ts ================================================ export default { label: { comm: "Business Edition", plus: "Professional Edition", free: "Basic Edition", }, comm: { name: "{vipLabel} Activated", title: "Expires on: {expire}", nav: "{vipLabel}", }, plus: { name: "Pro Features", title: "Upgrade to Pro for commercial license", }, free: { comm: { name: "Pro Features", title: "Upgrade to Pro for commercial license", }, button: { name: "Advanced Features", title: "Upgrade to Advanced for more VIP privileges", }, nav: { name: "Basic Version", title: "Upgrade to Advanced for more VIP privileges", }, }, enterCode: "Please enter the activation code", successTitle: "Activation Successful", successContent: "You have successfully activated {vipLabel}, valid until: {expireDate}", bindAccountTitle: "Bind Your Account", bindAccountContent: "Binding your account helps prevent license loss. Strongly recommended.", congratulations_vip_trial: "Congratulations, you have received a Pro version {duration} days trial", trial_modal_title: "7-day Pro version trial acquisition", trial_modal_ok_text: "Get now", trial_modal_thanks: "Thank you for supporting the open source project", trial_modal_click_confirm: "Click confirm to get a 7-day Pro version trial", get_7_day_pro_trial: "7-day professional version trial", star_now: "Star Now", please_help_star: "Could you please help by starring? Thanks a lot!", admin_only_operation: "Admin operation only", enter_activation_code: "Please enter the activation code", activate_pro_business: "Activate Professional/Business Edition", renew_business: "Renew Business Edition", renew_pro_upgrade_business: "Renew Professional Edition / Upgrade to Business Edition", basic_edition: "Basic Edition", community_free_version: "Community Free Version", unlimited_certificate_application: "Unlimited certificate applications", unlimited_domain_count: "Unlimited domain count", unlimited_certificate_pipelines: "Unlimited certificate pipelines", common_deployment_plugins: "Common host, cloud platform, CDN, Baota, 1Panel deployment plugins", email_webhook_notifications: "Email, webhook notification methods", professional_edition: "Professional Edition", open_source_support: "Open source requires your sponsorship support", vip_group_priority: "Access to VIP group, your requests will have priority", unlimited_site_certificate_monitoring: "Unlimited site certificate monitoring", more_notification_methods: "More notification methods", plugins_fully_open: "All plugins open, including Synology and more", click_to_get_7_day_trial: "Click to get 7-day trial", years: "years", afdian_support_vip: 'Get a one-year professional activation code after supporting "VIP membership" on Afdian, open source needs your support', get_after_support: "Get after sponsoring", business_edition: "Business Edition", commercial_license: "Commercial license, allowed for external operation", all_pro_privileges: "All professional edition privileges", allow_commercial_use_modify_logo_title: "Allows commercial use, can modify logo and title", data_statistics: "Data statistics", plugin_management: "Plugin management", unlimited_multi_users: "Unlimited multi-users", support_user_payment: "Supports user payments", contact_author_for_trial: "Please contact the author for trial", activate: "Activate", get_pro_code_after_support: 'Get a one-year professional activation code after supporting "VIP membership" on Afdian', business_contact_author: "Business edition please contact the author directly", year: "year", freee: "Free", renew: "Renew", activate_immediately: "Activate Immediately", current: "Current", activated_expire_time: " activated, expiration date: ", site_id: "Site ID", invite_code_optional: "Invite code [optional], can get extra 30 days for Professional / 15 days for Business", no_activation_code: "No activation code?", activation_code_one_use: "Activation code can only be used once. To change site, please ", bind_account: "bind account", transfer_vip: ' then "Transfer VIP"', }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/zh-CN/authentication.ts ================================================ export default { welcomeBack: "欢迎回来", pageTitle: "开箱即用的大型中后台管理系统", pageDesc: "工程化、高性能、跨组件库的前端模版", loginSuccess: "登录成功", loginSuccessDesc: "欢迎回来", loginSubtitle: "请输入您的帐户信息以开始管理您的项目", selectAccount: "快速选择账号", username: "账号", password: "密码", usernameTip: "请输入用户名", passwordTip: "请输入密码", verifyRequiredTip: "请先完成验证", passwordErrorTip: "密码错误", rememberMe: "记住账号", createAnAccount: "创建一个账号", createAccount: "创建账号", alreadyHaveAccount: "已经有账号了?", accountTip: "还没有账号?", signUp: "注册", signUpSubtitle: "让您的应用程序管理变得简单而有趣", confirmPassword: "确认密码", confirmPasswordTip: "两次输入的密码不一致", agree: "我同意", privacyPolicy: "隐私政策", terms: "条款", agreeTip: "请同意隐私政策和条款", goToLogin: "去登录", passwordStrength: "使用 8 个或更多字符,混合字母、数字和符号", forgetPassword: "忘记密码?", forgetPasswordSubtitle: "输入您的电子邮件,我们将向您发送重置密码的连接", emailTip: "请输入邮箱", emailValidErrorTip: "你输入的邮箱格式不正确", sendResetLink: "发送重置链接", email: "邮箱", qrcodeSubtitle: "请用手机扫描二维码登录", qrcodePrompt: "扫码后点击 '确认',即可完成登录", qrcodeLogin: "扫码登录", codeSubtitle: "请输入您的手机号码以开始管理您的项目", code: "验证码", codeTip: "请输入{0}位验证码", mobile: "手机号码", mobileTip: "请输入手机号", mobileErrortip: "手机号码格式错误", mobileLogin: "手机号登录", sendCode: "获取验证码", sendText: "{0}秒后重新获取", thirdPartyLogin: "其他登录方式", loginAgainTitle: "重新登录", loginAgainSubTitle: "您的登录状态已过期,请重新登录以继续。", layout: { center: "居中", alignLeft: "居左", alignRight: "居右", }, usernamePlaceholder: "请输入用户名/邮箱/手机号", passwordPlaceholder: "请输入密码", mobilePlaceholder: "请输入手机号", loginButton: "登录", forgotAdminPassword: "忘记管理员密码?", registerLink: "注册", smsTab: "短信验证码登录", passwordTab: "密码登录", title: "修改密码", weakPasswordWarning: "为了您的账户安全,请立即修改密码", changeNow: "立即修改", successMessage: "修改成功", oldPassword: "旧密码", oldPasswordRequired: "请输入旧密码", newPassword: "新密码", newPasswordRequired: "请输入新密码", confirmNewPassword: "确认新密码", confirmNewPasswordRequired: "请输入确认密码", changePasswordButton: "修改密码", enterPassword: "请输入密码", newPasswordNotSameOld: "新密码不能和旧密码相同", enterPasswordAgain: "请再次输入密码", passwordsNotMatch: "两次输入密码不一致!", avatar: "头像", nickName: "昵称", phoneNumber: "手机号", changePassword: "修改密码", updateProfile: "修改个人信息", }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts ================================================ export default { app: { crud: { i18n: { name: "姓名", city: "城市", status: "状态", }, }, login: { logoutTip: "确认", logoutMessage: "确定要注销登录吗?", }, }, fs: { rowHandle: { title: "操作列", }, }, order: { confirmTitle: "订单确认", package: "套餐", description: "说明", specifications: "规格", pipeline: "流水线", domain: "域名", deployTimes: "部署次数", duration: "时长", price: "价格", paymentMethod: "支付方式", free: "免费", unit: { pieces: "条", count: "个", times: "次", }, }, framework: { title: "框架", home: "首页", }, title: "证书自动化", pipeline: "证书自动化流水线", pipelineEdit: "编辑流水线", history: "执行历史记录", certStore: "证书仓库", siteMonitor: "站点证书监控", settings: "设置", accessManager: "授权管理", cnameRecord: "CNAME记录管理", subDomain: "子域名托管设置", pipelineGroup: "流水线分组管理", openKey: "开放接口密钥", notification: "通知设置", siteMonitorSetting: "站点监控设置", userSecurity: "认证安全设置", userProfile: "账号信息", suite: "套餐", mySuite: "我的套餐", suiteBuy: "套餐购买", myTrade: "我的订单", paymentReturn: "支付返回", user: { greeting: "您好", profile: "账号信息", logout: "注销登录", }, dashboard: { greeting: "您好,{name},欢迎使用 【{site}】", latestVersion: "最新版本: {version}", validUntil: "账户有效期:", tutorialTooltip: "点击查看详细教程", tutorialText: "仅需3步,全自动申请部署证书", alertMessage: "证书和授权为敏感信息,不要使用来历不明的在线Certd服务和镜像,以免泄露;请务必私有化部署使用,认准官方版本发布渠道:", helpDoc: "帮助文档", pipelineCount: "证书流水线数量", noPipeline: "您还没有证书流水线", createNow: "立即创建", managePipeline: "管理流水线", pipelineStatus: "流水线状态", recentRun: "最近运行统计", runCount: "运行次数", expiringCerts: "最快到期证书", supportedTasks: "已支持的部署任务总览", }, steps: { createPipeline: "创建证书流水线", addTask: "添加部署任务", scheduledRun: "定时运行", }, customPipeline: "自定义流水线", createCertdPipeline: "创建证书流水线", commercialCertHosting: "商用证书托管", tooltip: { manualUploadOwnCert: "手动上传自有证书,执行自动部署", noAutoApplyCommercialCert: "并不能自动申请商业证书", manualUploadOnUpdate: "证书有更新时,都需要手动上传一次", }, table: { confirmDeleteTitle: "确定要删除吗?", confirmDeleteMessage: "将删除该流水线相关的所有数据,包括执行历史、证书文件、证书仓库记录等", }, play: { runPipeline: "运行流水线", confirm: "确认", confirmTrigger: "确定要触发运行吗?", pipelineStarted: "管道已经开始运行", }, actions: { editPipeline: "编辑流水线", editConfigGroup: "修改配置/分组", viewCertificate: "查看证书", downloadCertificate: "下载证书", }, fields: { userId: "用户Id", pipelineName: "流水线名称", keyword: "关键字", required: "此项必填", pipelineContent: "流水线内容", scheduledTaskCount: "定时任务数", deployTaskCount: "部署任务数", remainingValidity: "到期剩余", expiryTime: "过期时间", status: "状态", lastRun: "最后运行", enabled: "启用", enabledLabel: "启用", disabledLabel: "禁用", group: "分组", type: "类型", order: "排序号", keepHistoryCount: "历史记录保持数", keepHistoryHelper: "历史记录保持条数,多余的会被删除", createTime: "创建时间", updateTime: "更新时间", triggerType: "触发类型", pipelineId: "流水线Id", }, types: { certApply: "证书申请", certUpload: "证书上传", custom: "自定义", }, myPipelines: "我的流水线", selectedCount: "已选择 {count} 项", batchDelete: "批量删除", batchForceRerun: "强制重新运行", applyCertificate: "申请证书", pipelineExecutionRecords: "流水线执行记录", confirm: "确认", confirmBatchDeleteContent: "确定要批量删除这{count}条记录吗", deleteSuccess: "删除成功", pleaseSelectRecords: "请先勾选记录", triggerTypes: { manual: "手动执行", timer: "定时执行", }, sysResources: { sysRoot: "系统管理", sysConsole: "控制台", sysSettings: "系统设置", cnameSetting: "CNAME服务设置", emailSetting: "邮件服务器设置", siteSetting: "站点个性化", headerMenus: "顶部菜单设置", sysAccess: "系统级授权", sysPlugin: "插件管理", sysPluginEdit: "编辑插件", sysPluginConfig: "证书插件配置", accountBind: "账号绑定", permissionManager: "权限管理", roleManager: "角色管理", userManager: "用户管理", suiteManager: "套餐管理", suiteSetting: "套餐设置", orderManager: "订单管理", userSuites: "用户套餐", }, certificateRepo: { title: "证书仓库", sub: "从流水线生成的证书", }, certificateNotGenerated: "证书还未生成,请先运行流水线", viewCertificateTitle: "查看证书", close: "关闭", viewCert: { title: "查看证书", }, download: { title: "下载证书", }, source: "源码", github: "github", gitee: "gitee", cron: { clearTip: "清除选择", nextTrigger: "下次触发时间", tip: "请先设置正确的cron表达式", }, cronForm: { title: "定时脚本", helper: "点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次,证书未到期之前任务会跳过,不会重复执行", required: "此项必填", }, email: { title: "收件邮箱", helper: "输入你的收件邮箱地址,支持多个邮箱", required: "此项必填", }, plugin: { selectTitle: "证书申请插件", jsAcme: "JS-ACME:使用简单方便,功能强大【推荐】", legoAcme: "Lego-ACME:基于Lego实现,支持海量DNS提供商,熟悉LEGO的用户可以使用", }, pipelineForm: { createTitle: "创建证书流水线", moreParams: "更多参数", triggerCronTitle: "定时触发", triggerCronHelper: "点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次,证书未到期之前任务会跳过,不会重复执行", notificationTitle: "失败通知", notificationHelper: "任务执行失败实时提醒", groupIdTitle: "流水线分组", }, notificationDefault: "使用默认通知", monitor: { title: "站点证书监控", description: "每天0点,检查网站证书的过期时间,到期前10天时将发出提醒(使用默认通知渠道);", settingLink: "站点监控设置", limitInfo: "基础版限制1条,专业版以上无限制,当前", checkAll: "检查全部", confirmTitle: "确认", confirmContent: "确认触发检查全部站点证书吗?", checkSubmitted: "检查任务已提交", pleaseRefresh: "请稍后刷新页面查看结果", siteName: "站点名称", enterSiteName: "请输入站点名称", domain: "网站域名", enterDomain: "请输入域名", enterValidDomain: "请输入正确的域名", httpsPort: "HTTPS端口", enterPort: "请输入端口", certInfo: "证书信息", issuer: "证书颁发机构", certDomains: "证书域名", certProvider: "颁发机构", certStatus: "证书状态", status: { ok: "正常", expired: "过期", }, certExpiresTime: "证书到期时间", expired: "过期", days: "天", lastCheckTime: "上次检查时间", disabled: "禁用启用", ipCheck: "开启IP检查", selectRequired: "请选择", ipCheckConfirm: "确定{status}IP检查?", ipCount: "IP数量", checkStatus: "检查状态", pipelineId: "关联流水线ID", certInfoId: "证书ID", checkSubmittedRefresh: "检查任务已提交,请稍后刷新查看结果", ipManagement: "IP管理", bulkImport: "批量导入", basicLimitError: "基础版只能添加一个监控站点,请赞助升级专业版", limitExceeded: "对不起,您最多只能创建条{max}监控记录,请购买或升级套餐", setting: { siteMonitorSettings: "站点监控设置", notificationChannel: "通知渠道", setNotificationChannel: "设置通知渠道", retryTimes: "重试次数", monitorRetryTimes: "监控请求重试次数", monitorCronSetting: "监控定时设置", cronTrigger: "定时触发监控", dnsServer: "DNS服务器", dnsServerHelper: "使用自定义的域名解析服务器,如:1.1.1.1 , 支持多个", }, }, checkStatus: { success: "成功", checking: "检查中", error: "异常", }, domainList: { title: "域名列表", helper: "格式【域名:端口:名称】,一行一个,其中端口、名称可以省略\n比如:\nwww.baidu.com:443:百度\nwww.taobao.com::淘宝\nwww.google.com", required: "请输入要导入的域名", placeholder: "www.baidu.com:443:百度\nwww.taobao.com::淘宝\nwww.google.com\n", }, accountInfo: "账号信息", securitySettings: "认证安全设置", confirmDisable2FA: "确定要关闭多重验证登录吗?", disabledSuccess: "关闭成功", saveSuccess: "保存成功", twoFactorAuth: "2FA多重验证登录", rebind: "重新绑定", twoFactorAuthHelper: "是否开启多重验证登录", bindDevice: "绑定设备", step1: "1. 安装任意一款支持Authenticator的验证APP,比如:", tooltipGoogleServiceError: "如果报没有找到谷歌服务的错误,您可以安装KK谷歌助手", step2: "2. 扫描二维码添加账号", step3: "3. 输入验证码", inputVerifyCode: "请输入验证码", cancel: "取消", authorizationManagement: "授权管理", manageThirdPartyAuth: "管理第三方系统授权信息", name: "名称", pleaseEnterName: "请填写名称", nameHelper: "随便填,当多个相同类型的授权时,便于区分", level: "级别", system: "系统", usera: "用户", nickName: "昵称", max50Chars: "最大50个字符", myInfo: "我的信息", save: "保存", editSchedule: "修改定时", timerTrigger: "定时触发", schedule: "定时", selectCron: "请选择定时Cron", batchEditSchedule: "批量修改定时", editTrigger: "编辑触发器", triggerName: "触发器名称", requiredField: "此项必填", type: "类型", enterName: "请输入名称", confirmDeleteTrigger: "确定要删除此触发器吗?", notificationType: "通知类型", selectNotificationType: "请选择通知类型", notificationName: "通知名称", helperNotificationName: "随便填,当多个相同类型的通知时,便于区分", isDefault: "是否默认", yes: "是", no: "否", selectIsDefault: "请选择是否默认", prompt: "提示", confirmSetDefaultNotification: "确定设置为默认通知?", test: "测试", scope: "权限范围", scopeOpenApiOnly: "仅开放接口", scopeFullAccount: "账户所有权限", required: "此项必填", scopeHelper: "仅开放接口只可以访问开放接口,账户所有权限可以访问所有接口", add: "生成新的Key", gen: { text: "接口测试", title: "x-certd-token", okText: "确定", contentPart1: "测试x-certd-token如下,您可以在3分钟内使用它进行", openApi: "开放接口", contentPart2: "请求测试", }, pending_cname_setup: "待设置CNAME", validating: "验证中", validation_successful: "验证成功", validation_failed: "验证失败", validation_timed_out: "验证超时", proxied_domain: "被代理域名", host_record: "主机记录", please_set_cname: "请设置CNAME", cname_service: "CNAME服务", default_public_cname: "默认提供公共CNAME服务,您还可以", customize_cname: "自定义CNAME服务", public_cname: "公共CNAME", custom_cname: "自定义CNAME", validate: "验证", validation_started: "开始验证,请耐心等待", click_to_validate: "点击验证", all: "全部", cname_feature_guide: "CNAME功能原理及使用说明", batch_delete: "批量删除", confirm_delete_count: "确定要批量删除这{count}条记录吗", delete_successful: "删除成功", please_select_records: "请先勾选记录", edit_notification: "编辑通知", other_notification_method: "其他通知方式", trigger_time: "触发时机", start_time: "开始时", success_time: "成功时", fail_to_success_time: "失败转成功时", fail_time: "失败时", helper_suggest_fail_only: "建议仅选择'失败时'和'失败转成功'两种即可", notification_config: "通知配置", please_select_notification: "请选择通知方式", please_select_type: "请选择类型", please_select_trigger_time: "请选择通知时机", please_select_notification_config: "请选择通知配置", confirm_delete_trigger: "确定要删除此触发器吗?", gift_package: "赠送套餐", package_name: "套餐名称", click_to_select: "点击选择", please_select_package: "请选择套餐", package: "套餐", addon_package: "加量包", domain_count: "域名数量", unit_count: "个", field_required: "此项必填", pipeline_count: "流水线数量", unit_item: "条", deploy_count: "部署次数", unit_times: "次", monitor_count: "证书监控数量", duration: "时长", status: "状态", active_time: "激活时间", expires_time: "过期时间", is_present: "是否赠送", is_present_yes: "是", is_present_no: "否", basicInfo: "基础信息", titlea: "名称", disabled: "禁用", ordera: "排序", supportBuy: "支持购买", intro: "介绍", packageContent: "套餐内容", maxDomainCount: "最大域名数", maxPipelineCount: "最大流水线数", maxDeployCount: "最大部署数", maxMonitorCount: "最大监控数", price: "价格", durationPrices: "时长价格", packageName: "套餐名称", addon: "加量包", typeHelper: "套餐:同一时间只有最新购买的一个生效\n加量包:可购买多个,购买后立即生效,不影响套餐\n套餐和加量包数量可叠加", domainCount: "域名数量", pipelineCount: "流水线数量", unitPipeline: "条", deployCount: "部署次数", unitDeploy: "次", monitorCount: "证书监控数量", unitCount: "个", durationPriceTitle: "时长及价格", selectDuration: "选择时长", supportPurchase: "支持购买", cannotPurchase: "不能购买", shelfStatus: "上下架", onShelf: "上架", offShelf: "下架", orderHelper: "越小越靠前", description: "说明", createTime: "创建时间", updateTime: "更新时间", edit: "编辑", groupName: "分组名称", enterGroupName: "请输入分组名称", subdomainHosting: "子域名托管", subdomainHostingHint: "当你的域名设置了子域名托管,需要在此处创建记录,否则申请证书将失败", batchDeleteConfirm: "确定要批量删除这{count}条记录吗", selectRecordFirst: "请先勾选记录", subdomainHosted: "托管的子域名", subdomainHelpText: "如果您不理解什么是子域托管,请不要随意设置,可能导致证书无法申请,可以参考文档", subdomainManagement: "子域管理", isDisabled: "是否禁用", enabled: "启用", uploadCustomCert: "上传自定义证书", sourcee: "来源", sourcePipeline: "流水线", sourceManualUpload: "手动上传", domains: "域名", enterDomain: "请输入域名", validDays: "有效天数", expires: "过期", days: "天", expireTime: "过期时间", certIssuer: "证书颁发机构", applyTime: "申请时间", relatedPipeline: "关联流水线", statusSuccess: "成功", statusChecking: "检查中", statusError: "异常", actionImportBatch: "批量导入", actionSyncIp: "同步IP", modalTitleSyncIp: "同步IP", modalContentSyncIp: "确定要同步IP吗?", notificationSyncComplete: "同步完成", actionCheckAll: "检查全部", modalTitleConfirm: "确认", modalContentCheckAll: "确认触发检查全部IP站点的证书吗?", notificationCheckSubmitted: "检查任务已提交", notificationCheckDescription: "请稍后刷新页面查看结果", tooltipCheckNow: "立即检查", notificationCheckSubmittedPleaseRefresh: "检查任务已提交,请稍后刷新查看结果", columnId: "ID", columnIp: "IP", helperIpCname: "也支持填写CNAME域名", ruleIpRequired: "请输入IP", columnCertDomains: "证书域名", columnCertProvider: "颁发机构", columnCertStatus: "证书状态", statusNormal: "正常", statusExpired: "过期", columnCertExpiresTime: "证书到期时间", expired: "过期", columnCheckStatus: "检查状态", columnLastCheckTime: "上次检查时间", columnSource: "来源", sourceSync: "同步", sourceManual: "手动", sourceImport: "导入", columnDisabled: "禁用启用", columnRemark: "备注", pluginFile: "插件文件", selectPluginFile: "选择插件文件", overrideSameName: "同名覆盖", override: "覆盖", noOverride: "不覆盖", overrideHelper: "如果已有相同名称插件,直接覆盖", importPlugin: "导入插件", operationSuccess: "操作成功", customPlugin: "自定义插件", import: "导入", export: "导出", pluginType: "插件类型", auth: "授权", dns: "DNS", deployPlugin: "部署插件", icon: "图标", pluginName: "插件名称", pluginNameHelper: "必须为英文或数字,驼峰命名,类型作为前缀\n示例:AliyunDeployToCDN\n插件使用后,名称不可修改", pluginNameRuleMsg: "必须为英文或数字,驼峰命名,类型作为前缀", author: "作者", authorHelper: "上传插件市场时作为前缀,如 greper/pluginName", authorRuleMsg: "必须为英文或数字", titleHelper: "插件中文名称", descriptionHelper: "插件描述", builtIn: "内置", custom: "自定义", store: "市场", version: "版本", pluginDependencies: "插件依赖", pluginDependenciesHelper: "格式: [作者/]插件名[:版本],需先安装依赖插件", editableRunStrategy: "可编辑运行策略", editable: "可编辑", notEditable: "不可编辑", runStrategy: "运行策略", normalRun: "正常运行", skipOnSuccess: "成功跳过(部署任务)", defaultRunStrategyHelper: "默认运行策略", enableDisable: "启用/禁用", clickToToggle: "点击切换启用/禁用", confirmToggle: "确认要", disable: "禁用", enable: "启用", pluginGroup: "插件分组", icpRegistrationNumber: "ICP备案号", icpPlaceholder: "粤ICP备xxxxxxx号", publicSecurityRegistrationNumber: "网安备案号", publicSecurityPlaceholder: "京公网安备xxxxxxx号", enableAssistant: "开启小助手", allowCrawlers: "允许爬虫", httpProxy: "HTTP代理", httpProxyPlaceholder: "http://192.168.1.2:18010/", httpProxyHelper: "当某些网站被墙时可以配置", httpsProxy: "HTTPS代理", httpsProxyPlaceholder: "http://192.168.1.2:18010/", saveThenTestTitle: "保存后,再点击测试", testButton: "测试", httpsProxyHelper: "一般这两个代理填一样的,保存后再测试", dualStackNetwork: "双栈网络", default: "默认", ipv4Priority: "IPV4优先", ipv6Priority: "IPV6优先", dualStackNetworkHelper: "如果选择IPv6优先,需要在docker-compose.yaml中启用ipv6", enableCommonCnameService: "启用公共CNAME服务", commonCnameHelper: "是否可以使用公共CNAME服务,如果禁用,且没有设置自定义CNAME服务,则无法使用CNAME代理方式申请证书", saveButton: "保存", stopSuccess: "停止成功", google: "Google", baidu: "百度", success: "成功", testFailed: "测试失败", testCompleted: "测试完成", manageOtherUserPipeline: "管理其他用户流水线", limitUserPipelineCount: "限制用户流水线数量", limitUserPipelineCountHelper: "0为不限制", enableSelfRegistration: "开启自助注册", enableUserValidityPeriod: "开启用户有效期", userValidityPeriodHelper: "有效期内用户可正常使用,失效后流水线将被停用", enableUsernameRegistration: "开启用户名注册", enableEmailRegistration: "开启邮箱注册", proFeature: "专业版功能", emailServerSetup: "设置邮箱服务器", enableSmsLoginRegister: "开启手机号登录、注册", commFeature: "商业版功能", smsProvider: "短信提供商", aliyunSms: "阿里云短信", yfySms: "易发云短信", smsTest: "短信测试", testMobilePlaceholder: "输入测试手机号", saveThenTest: "保存后再点击测试", enterTestMobile: "请输入测试手机号", sendSuccess: "发送成功", atLeastOneLoginRequired: "密码登录和手机号登录至少开启一个", fieldRequired: "此项必填", siteHide: "站点隐藏", enableSiteHide: "启用站点隐藏", siteHideDescription: "可以在平时关闭站点的可访问性,需要时再打开,增强站点安全性", helpDoc: "帮助说明", randomAddress: "随机地址", siteHideUrlHelper: "站点被隐藏后,需要访问此URL解锁,才能正常访问", fullUnlockUrl: "完整解除隐藏地址", saveThisUrl: "请保存好此地址", unlockPassword: "解除密码", unlockPasswordHelper: "解除隐藏时需要输入密码,第一次需要设置密码,填写则重置密码", autoHideTime: "自动隐藏时间", autoHideTimeHelper: "多少分钟内无请求自动隐藏", hideOpenApi: "隐藏开放接口", hideOpenApiHelper: "是否隐藏开放接口,是否放开/api/v1开头的接口", hideSiteImmediately: "立即隐藏站点", hideImmediately: "立即隐藏", confirmHideSiteTitle: "确定要立即隐藏站点吗?", confirmHideSiteContent: "隐藏后,将无法访问站点,请谨慎操作", siteHiddenSuccess: "站点已隐藏", emailServerSettings: "邮件服务器设置", setEmailSendingServer: "设置邮件发送服务器", useCustomEmailServer: "使用自定义邮件服务器", smtpDomain: "SMTP域名", pleaseEnterSmtpDomain: "请输入smtp域名或ip", smtpPort: "SMTP端口", pleaseEnterSmtpPort: "请输入smtp端口号", username: "用户名", pleaseEnterUsername: "请输入用户名", password: "密码", pleaseEnterPassword: "请输入密码", qqEmailAuthCodeHelper: "如果是qq邮箱,需要到qq邮箱的设置里面申请授权码作为密码", senderEmail: "发件邮箱", pleaseEnterSenderEmail: "请输入发件邮箱", useSsl: "是否ssl", sslPortNote: "ssl和非ssl的smtp端口是不一样的,注意修改端口", ignoreCertValidation: "忽略证书校验", useOfficialEmailServer: "使用官方邮件服务器", useOfficialEmailServerHelper: "使用官方邮箱服务器直接发邮件,免除繁琐的配置", testReceiverEmail: "测试收件邮箱", pleaseEnterTestReceiverEmail: "请输入测试收件邮箱", saveBeforeTest: "保存后再点击测试", sendFailHelpDoc: "发送失败???", emailConfigHelpDoc: "邮件配置帮助文档", tryOfficialEmailServer: "您还可以试试使用官方邮件服务器↗↗↗↗↗↗↗↗", pluginManagement: "插件管理", pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更", pleaseSelectRecord: "请先勾选记录", permissionManagement: "权限管理", adda: "添加", rootNode: "根节点", permissionName: "权限名称", enterPermissionName: "请输入权限名称", permissionCode: "权限代码", enterPermissionCode: "请输入权限代码", max100Chars: "最大100个字符", examplePermissionCode: "例如:sys:user:view", sortOrder: "排序", sortRequired: "排序号必填", parentNode: "父节点", roleManagement: "角色管理", assignPermissions: "分配权限", roleName: "角色名称", enterRoleName: "请输入角色名称", unlockLogin: "解除登录锁定", notice: "提示", confirmUnlock: "确定要解除该用户的登录锁定吗?", unlockSuccess: "解除成功", enterUsername: "请输入用户名", modifyPasswordIfFilled: "填写则修改密码", emaila: "邮箱", mobile: "手机号", avatar: "头像", validTime: "有效期", remark: "备注", roles: "角色", cnameTitle: "CNAME服务配置", cnameDescription: "此处配置的域名作为其他域名校验的代理,当别的域名需要申请证书时,通过CNAME映射到此域名上来验证所有权。好处是任何域名都可以通过此方式申请证书,也无需填写AccessSecret。", cnameLinkText: "CNAME功能原理及使用说明", confirmTitle: "确认", confirmDeleteBatch: "确定要批量删除这{count}条记录吗", selectRecordsFirst: "请先勾选记录", cnameDomain: "CNAME域名", cnameDomainPlaceholder: "cname.handsfree.work", cnameDomainHelper: "需要一个右边DNS提供商注册的域名(也可以将其他域名的dns服务器转移到这几家来)。\nCNAME域名一旦确定不可修改,建议使用一级子域名", dnsProvider: "DNS提供商", dnsProviderAuthorization: "DNS提供商授权", setDefault: "设置默认", confirmSetDefault: "确定要设置为默认吗?", setAsDefault: "设为默认", disabledLabel: "禁用", confirmToggleStatus: "确定要{action}吗?", template: { title: "流水线模版", edit: "流水线模版编辑", importCreate: "流水线模版批量创建", intro: "可根据模版批量创建流水线", createTemplate: "创建模版", useTemplate: "使用此模板", batchCreate: "批量创建流水线", singleCreate: "创建单个流水线", templateName: "模板名称", enterTemplateName: "请输入模板名称", copyPipelineConfig: "复制该流水线配置作为模板来源", pipeline: "流水线", }, sys: { setting: { showRunStrategy: "显示运行策略选择", showRunStrategyHelper: "任务设置中是否允许选择运行策略", }, }, modal: { close: "关闭", viewCertificateTitle: "查看证书", }, domain: { domainManager: "域名管理", domainDescription: "管理域名的校验方式,用于申请证书时自动选择验证方式", domain: "域名", challengeType: "校验类型", dnsProviderType: "DNS提供商类型", dnsProviderAccess: "DNS提供商授权", httpUploaderType: "上传方式", httpUploaderAccess: "上传授权信息", httpUploadRootDir: "网站根路径", disabled: "禁用/启用", challengeSetting: "校验配置", gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加", }, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/zh-CN/common.ts ================================================ export default { back: "返回", backToHome: "返回首页", login: "登录", logout: "退出登录", prompt: "提示", cancel: "取消", confirm: "确认", reset: "重置", noData: "暂无数据", refresh: "刷新", loadingMenu: "加载菜单中", query: "查询", search: "搜索", enabled: "已启用", disabled: "已禁用", enable: "启用", disable: "禁用", edit: "修改", delete: "删除", create: "新增", yes: "是", no: "否", handle: "操作", }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/zh-CN/guide.ts ================================================ export default { createCertPipeline: { title: "创建证书申请流水线", description: "演示证书申请任务如何配置", items: { tutorialTitle: "教程演示内容", tutorialDesc1: "本教程演示如何自动申请证书并部署到Nginx上", tutorialDesc2: "仅需3步,全自动申请部署证书", createTitle: "创建证书流水线", createDesc: "点击添加证书流水线,填写证书申请信息", successTitle: "流水线创建成功", successDesc: "点击手动触发即可申请证书", nextTitle: "接下来演示如何自动部署证书", nextDesc: "如果您只需要申请证书,那么到这一步就可以了", }, }, buttons: { prev: "上一步", next: "下一步", }, addDeployTask: { title: "添加部署证书任务", description: "这里演示部署证书到Nginx", items: { addTaskTitle: "添加证书部署任务", addTaskDesc1: "这里演示自动部署证书到nginx", addTaskDesc2: "本系统提供海量部署插件,满足您的各种部署需求", fillParamsTitle: "填写任务参数", fillParamsDesc1: "填写主机上证书文件的路径", fillParamsDesc2: "选择主机ssh登录授权", activateCertTitle: "让新证书生效", activateCertDesc1: "执行重启脚本", activateCertDesc2: "让证书生效", taskSuccessTitle: "部署任务添加成功", taskSuccessDesc: "现在可以运行", pluginsTitle: "本系统提供茫茫多的部署插件", pluginsDesc: "您可以根据自身需求将证书部署到各种应用和平台", }, }, runAndTestTask: { runAndTestTitle: "运行与测试", runAndTestDescription: "演示流水线运行,查看日志,成功后跳过等", runTestOnce: "运行测试一下", clickManualTriggerToTest: "点击手动触发按钮,即可测试运行", viewLogs: "查看日志", clickTaskToViewStatusAndLogs: "点击任务可以查看状态和日志", howToTroubleshootFailure: "执行失败如何排查", viewErrorLogs: "查看错误日志", nginxContainerNotExistFix: "这里报的是nginx容器不存在,修改命令,改成正确的nginx容器名称即可", executionSuccess: "执行成功", retryAfterFix: "修改正确后,重新点击手动触发,重新运行一次,执行成功", autoSkipAfterSuccess: "成功后自动跳过", successSkipExplanation: "可以看到成功过的将会自动跳过,不会重复执行,只有当参数变更或者证书更新了,才会重新运行", viewCertDeploymentSuccess: "查看证书部署成功", visitNginxToSeeCert: "访问nginx上的网站,可以看到证书已经部署成功", downloadCertManualDeploy: "还可以下载证书,手动部署", downloadIfNoAutoDeployPlugin: "如果还没有好用的部署插件,没办法自动部署,你还可以下载证书,手动部署", }, scheduleAndEmailTask: { title: "设置定时执行和邮件通知", description: "自动运行", setSchedule: "设置定时执行", pipelineSuccessThenSchedule: "流水线测试成功,接下来配置定时触发,以后每天定时执行就不用管了", recommendDailyRun: "推荐配置每天运行一次,在到期前35天才会重新申请新证书并部署,没到期前会自动跳过,不会重复申请。", setEmailNotification: "设置邮件通知", suggestErrorAndRecoveryEmails: "建议选择监听'错误时'和'错误转成功'两种即可,在意外失败时可以尽快去排查问题,(基础版需要配置邮件服务器)", basicVersionNeedsMailServer: "(基础版需要配置邮件服务器)", tutorialEndTitle: "教程结束", thanksForWatching: "感谢观看,希望对你有所帮助", }, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/zh-CN/index.ts ================================================ import certd from "./certd"; import authentication from "./authentication"; import vip from "./vip"; import tutorial from "./tutorial"; import preferences from "./preferences"; import ui from "./ui"; import guide from "./guide"; import common from "./common"; export default { certd, authentication, vip, ui, tutorial, preferences, guide, common, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts ================================================ export default { title: "偏好设置", subtitle: "自定义偏好设置 & 实时预览", resetTitle: "重置偏好设置", resetTip: "数据有变化,点击可进行重置", resetSuccess: "重置偏好设置成功", appearance: "外观", layout: "布局", content: "内容", other: "其它", wide: "流式", compact: "定宽", followSystem: "跟随系统", vertical: "垂直", verticalTip: "侧边垂直菜单模式", horizontal: "水平", horizontalTip: "水平菜单模式,菜单全部显示在顶部", twoColumn: "双列菜单", twoColumnTip: "垂直双列菜单模式", headerSidebarNav: "侧边导航", headerSidebarNavTip: "顶部通栏,侧边导航模式", headerTwoColumn: "混合双列", headerTwoColumnTip: "双列、水平菜单共存模式", mixedMenu: "混合垂直", mixedMenuTip: "垂直水平菜单共存", fullContent: "内容全屏", fullContentTip: "不显示任何菜单,只显示内容主体", normal: "常规", plain: "朴素", rounded: "圆润", copyPreferences: "复制偏好设置", copyPreferencesSuccessTitle: "复制成功", copyPreferencesSuccess: "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖", clearAndLogout: "清空缓存 & 退出登录", mode: "模式", general: "通用", language: "语言", dynamicTitle: "动态标题", watermark: "水印", checkUpdates: "定时检查更新", position: { title: "偏好设置位置", header: "顶栏", auto: "自动", fixed: "固定", }, sidebar: { title: "侧边栏", width: "宽度", visible: "显示侧边栏", collapsed: "折叠菜单", collapsedShowTitle: "折叠显示菜单名", autoActivateChild: "自动激活子菜单", autoActivateChildTip: "点击顶层菜单时,自动激活第一个子菜单或者上一次激活的子菜单", expandOnHover: "鼠标悬停展开", expandOnHoverTip: "鼠标在折叠区域悬浮时,`启用`则展开当前子菜单,`禁用`则展开整个侧边栏", }, tabbar: { title: "标签栏", enable: "启用标签栏", icon: "显示标签栏图标", showMore: "显示更多按钮", showMaximize: "显示最大化按钮", persist: "持久化标签页", maxCount: "最大标签数", maxCountTip: "每次打开新的标签时如果超过最大标签数,\n会自动关闭一个最先打开的标签\n设置为 0 则不限制", draggable: "启动拖拽排序", wheelable: "启用纵向滚轮响应", middleClickClose: "点击鼠标中键关闭标签页", wheelableTip: "开启后,标签栏区域可以响应滚轮的纵向滚动事件。\n关闭时,只能响应系统的横向滚动事件(需要按下Shift再滚动滚轮)", styleType: { title: "标签页风格", chrome: "谷歌", card: "卡片", plain: "朴素", brisk: "轻快", }, contextMenu: { reload: "重新加载", close: "关闭", pin: "固定", unpin: "取消固定", closeLeft: "关闭左侧标签页", closeRight: "关闭右侧标签页", closeOther: "关闭其它标签页", closeAll: "关闭全部标签页", openInNewWindow: "在新窗口打开", maximize: "最大化", restoreMaximize: "还原", }, }, navigationMenu: { title: "导航菜单", style: "导航菜单风格", accordion: "侧边导航菜单手风琴模式", split: "导航菜单分离", splitTip: "开启时,侧边栏显示顶栏对应菜单的子菜单", }, breadcrumb: { title: "面包屑导航", enable: "开启面包屑导航", icon: "显示面包屑图标", home: "显示首页按钮", style: "面包屑风格", hideOnlyOne: "仅有一个时隐藏", background: "背景", }, animation: { title: "动画", loading: "页面切换 Loading", transition: "页面切换动画", progress: "页面切换进度条", }, theme: { title: "主题", radius: "圆角", light: "浅色", dark: "深色", darkSidebar: "深色侧边栏", darkHeader: "深色顶栏", weakMode: "色弱模式", grayMode: "灰色模式", builtin: { title: "内置主题", default: "默认", violet: "紫罗兰", pink: "樱花粉", rose: "玫瑰红", skyBlue: "天蓝色", deepBlue: "深蓝色", green: "浅绿色", deepGreen: "深绿色", orange: "橙黄色", yellow: "柠檬黄", zinc: "锌色灰", neutral: "中性色", slate: "石板灰", gray: "中灰色", custom: "自定义", }, }, header: { title: "顶栏", modeStatic: "静止", modeFixed: "固定", modeAuto: "自动隐藏和显示", modeAutoScroll: "滚动隐藏和显示", visible: "显示顶栏", menuAlign: "菜单位置", menuAlignStart: "左侧", menuAlignEnd: "右侧", menuAlignCenter: "居中", }, footer: { title: "底栏", visible: "显示底栏", fixed: "固定在底部", }, copyright: { title: "版权", enable: "启用版权", companyName: "公司名", companySiteLink: "公司主页", date: "日期", icp: "ICP 备案号", icpLink: "ICP 网站链接", }, shortcutKeys: { title: "快捷键", global: "全局", search: "全局搜索", logout: "退出登录", preferences: "偏好设置", }, widget: { title: "小部件", globalSearch: "启用全局搜索", fullscreen: "启用全屏", themeToggle: "启用主题切换", languageToggle: "启用语言切换", notification: "启用通知", sidebarToggle: "启用侧边栏切换", lockScreen: "启用锁屏", refresh: "启用刷新", }, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/zh-CN/tutorial.ts ================================================ export default { title: "使用教程", }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/zh-CN/ui.ts ================================================ export default { formRules: { required: "请输入{0}", selectRequired: "请选择{0}", minLength: "{0}至少{1}个字符", maxLength: "{0}最多{1}个字符", length: "{0}长度必须为{1}个字符", alreadyExists: "{0} `{1}` 已存在", startWith: "{0}必须以 {1} 开头", invalidURL: "请输入有效的链接", }, actionTitle: { edit: "修改{0}", create: "新增{0}", delete: "删除{0}", view: "查看{0}", }, actionMessage: { deleteConfirm: "确定删除 {0} 吗?", deleting: "正在删除 {0} ...", deleteSuccess: "{0} 删除成功", operationSuccess: "操作成功", operationFailed: "操作失败", }, placeholder: { input: "请输入", select: "请选择", }, captcha: { title: "请完成安全验证", sliderSuccessText: "验证通过", sliderDefaultText: "请按住滑块拖动", sliderRotateDefaultTip: "点击图片可刷新", sliderRotateFailTip: "验证失败", sliderRotateSuccessTip: "验证成功,耗时{0}秒", alt: "支持img标签src属性值", refreshAriaLabel: "刷新验证码", confirmAriaLabel: "确认选择", confirm: "确认", pointAriaLabel: "点击点", clickInOrder: "请依次点击", }, iconPicker: { placeholder: "选择一个图标", search: "搜索图标...", }, jsonViewer: { copy: "复制", copied: "已复制", }, fallback: { pageNotFound: "哎呀!未找到页面", pageNotFoundDesc: "抱歉,我们无法找到您要找的页面。", forbidden: "哎呀!访问被拒绝", forbiddenDesc: "抱歉,您没有权限访问此页面。", internalError: "哎呀!出错了", internalErrorDesc: "抱歉,服务器遇到错误。", offline: "离线页面", offlineError: "哎呀!网络错误", offlineErrorDesc: "抱歉,无法连接到互联网,请检查您的网络连接并重试。", comingSoon: "即将推出", http: { requestTimeout: "请求超时,请稍后再试。", networkError: "网络异常,请检查您的网络连接后重试。", badRequest: "请求错误。请检查您的输入并重试。", unauthorized: "登录认证过期,请重新登录后继续。", forbidden: "禁止访问, 您没有权限访问此资源。", notFound: "未找到, 请求的资源不存在。", internalServerError: "内部服务器错误,请稍后再试。", }, }, widgets: { document: "文档", qa: "问题 & 帮助", setting: "设置", logoutTip: "是否退出登录?", viewAll: "查看所有消息", notifications: "通知", markAllAsRead: "全部标记为已读", clearNotifications: "清空", checkUpdatesTitle: "新版本可用", checkUpdatesDescription: "点击刷新以获取最新版本", search: { title: "搜索", searchNavigate: "搜索导航菜单", select: "选择", navigate: "导航", close: "关闭", noResults: "未找到搜索结果", noRecent: "没有搜索历史", recent: "搜索历史", }, lockScreen: { title: "锁定屏幕", screenButton: "锁定", password: "密码", placeholder: "请输入锁屏密码", unlock: "点击解锁", errorPasswordTip: "密码错误,请重新输入", backToLogin: "返回登录", entry: "进入系统", }, }, }; ================================================ FILE: packages/ui/certd-client/src/locales/langs/zh-CN/vip.ts ================================================ export default { label: { comm: "商业版", plus: "专业版", free: "基础版", }, comm: { name: "{vipLabel}已开通", title: "到期时间:{expire}", nav: "{vipLabel}", }, plus: { name: "商业版功能", title: "升级商业版,获取商业授权", }, free: { comm: { name: "商业版功能", title: "升级商业版,获取商业授权", }, button: { name: "专业版功能", title: "升级专业版,享受更多VIP特权", }, nav: { name: "基础版", title: "升级专业版,享受更多VIP特权", }, }, enterCode: "请输入激活码", successTitle: "激活成功", successContent: "您已成功激活{vipLabel},有效期至:{expireDate}", bindAccountTitle: "是否绑定袖手账号", bindAccountContent: "绑定账号后,可以避免License丢失,强烈建议绑定", congratulations_vip_trial: "恭喜,您已获得专业版{duration}天试用", trial_modal_title: "7天专业版试用获取", trial_modal_ok_text: "立即获取", trial_modal_thanks: "感谢您对开源项目的支持", trial_modal_click_confirm: "点击确认,即可获取7天专业版试用", get_7_day_pro_trial: "7天专业版试用获取", star_now: "立即去Star", please_help_star: "可以先请您帮忙点个star吗?感谢感谢", admin_only_operation: "仅限管理员操作", enter_activation_code: "请输入激活码", activate_pro_business: "激活专业版/商业版", renew_business: "续期商业版", renew_pro_upgrade_business: "续期专业版/升级商业版", basic_edition: "基础版", community_free_version: "社区免费版", unlimited_certificate_application: "证书申请无限制", unlimited_domain_count: "域名数量无限制", unlimited_certificate_pipelines: "证书流水线数量无限制", common_deployment_plugins: "常用的主机、云平台、cdn、宝塔、1Panel等部署插件", email_webhook_notifications: "邮件、webhook通知方式", professional_edition: "专业版", open_source_support: "开源需要您的赞助支持", vip_group_priority: "可加VIP群,您的需求将优先实现", unlimited_site_certificate_monitoring: "站点证书监控无限制", more_notification_methods: "更多通知方式", plugins_fully_open: "插件全开放,群辉等更多插件", click_to_get_7_day_trial: "点击获取7天试用", years: "年", afdian_support_vip: "爱发电赞助“VIP会员”后获取一年期专业版激活码,开源需要您的支持", get_after_support: "爱发电赞助后获取", business_edition: "商业版", commercial_license: "商业授权,可对外运营", all_pro_privileges: "拥有专业版所有特权", allow_commercial_use_modify_logo_title: "允许商用,可修改logo、标题", data_statistics: "数据统计", plugin_management: "插件管理", unlimited_multi_users: "多用户无限制", support_user_payment: "支持用户支付", contact_author_for_trial: "请联系作者获取试用", activate: "激活", get_pro_code_after_support: "爱发电赞助“VIP会员”后获取一年期专业版激活码", business_contact_author: "商业版请直接联系作者", year: "年", freee: "免费", renew: "续期", activate_immediately: "立刻激活", current: "当前", activated_expire_time: "已激活,到期时间:", site_id: "站点ID", invite_code_optional: "邀请码【选填】,可额外获得专业版30天/商业版15天时长", no_activation_code: "没有激活码?", activation_code_one_use: "激活码使用过一次之后,不可再次使用,如果要更换站点,请", bind_account: "绑定账号", transfer_vip: ',然后"转移VIP"即可', }; ================================================ FILE: packages/ui/certd-client/src/locales/typing.ts ================================================ export type SupportedLanguagesType = "en-US" | "zh-CN"; export type ImportLocaleFn = () => Promise<{ default: Record }>; export type LoadMessageFn = (lang: SupportedLanguagesType) => Promise | undefined>; export interface LocaleSetupOptions { /** * Default language * @default zh-CN */ defaultLocale?: SupportedLanguagesType; /** * Load message function * @param lang * @returns */ loadMessages?: LoadMessageFn; /** * Whether to warn when the key is not found */ missingWarn?: boolean; } ================================================ FILE: packages/ui/certd-client/src/main.ts ================================================ import { createApp } from "vue"; import App from "./App.vue"; // import Antd from "ant-design-vue"; import Antd from "./plugin/antdv-async/index"; import "./style/common.less"; import { i18n, loadLocaleMessages } from "/@/locales"; import components from "./components"; import router from "./router"; import plugin from "./plugin/"; // 正式项目请删除mock,避免影响性能 //import "./mock"; import { setupVben } from "./vben"; import { util } from "/@/utils"; import { initPreferences } from "/@/vben/preferences"; // import "./components/code-editor/import-works"; // @ts-ignore async function bootstrap() { const app = createApp(App); // app.use(Antd); app.use(Antd); await setupVben(app, { loadLocaleMessages, router }); app.use(router); // app.use(i18n); // app.use(store); app.use(components); app.use(plugin, { i18n }); const envMode = util.env.MODE; const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${envMode}`; // app偏好设置初始化 await initPreferences({ namespace, overrides: { app: { name: import.meta.env.VITE_APP_TITLE, }, }, }); app.mount("#app"); } bootstrap(); ================================================ FILE: packages/ui/certd-client/src/mock/base.ts ================================================ import { cloneDeep, mergeWith, isArray } from "lodash-es"; function copyList(originList: any, newList: any, options: any, parentId?: any) { for (const item of originList) { const newItem: any = cloneDeep(item); if (parentId != null && newItem.parentId == null) { newItem.parentId = parentId; } newItem.id = ++options.idGenerator; newList.push(newItem); if (item.children != null) { newItem.children = []; copyList(item.children, newItem.children, options, newItem.id); } } } function delById(req: any, list: any) { for (let i = 0; i < list.length; i++) { const item = list[i]; console.log("remove i", i, req, req.params.id, item.id); if (item.id === parseInt(req.params.id)) { console.log("remove i", i); list.splice(i, 1); break; } if (item.children != null && item.children.length > 0) { delById(req, item.children); } } } function findById(id: any, list: any) { for (const item of list) { if (item.id === id) { return item; } if (item.children != null && item.children.length > 0) { const sub: any = findById(id, item.children); if (sub != null) { return sub; } } } } function findByIds(ids: any[], list: any) { const res = []; for (const id of ids) { const item = findById(id, list); if (item != null) { res.push(item); } } console.log("findbyids", res, ids); return res; } const mockUtil: any = { findById, buildMock(options: any) { const name = options.name; if (options.copyTimes == null) { options.copyTimes = 29; } const list: any = []; for (let i = 0; i < options.copyTimes; i++) { copyList(options.list, list, options); } options.list = list; return [ { path: "/mock/" + name + "/page", method: "get", handle(req: any) { let data = [...list]; let limit = 20; let offset = 0; for (const item of list) { if (item.children != null && item.children.length === 0) { item.hasChildren = false; item.lazy = false; } } let orderProp: any, orderAsc: any; if (req && req.body) { const { page, sort } = req.body; let query = req.body.query; if (page.limit != null) { limit = parseInt(page.limit); } if (page.offset != null) { offset = parseInt(page.offset); } orderProp = sort.prop; orderAsc = sort.asc; query = query || {}; if (Object.keys(query).length > 0) { data = list.filter((item: any) => { let allFound = true; // 是否所有条件都符合 for (const key in query) { // 判定某一个条件 const value = query[key]; if (value == null || value === "") { continue; } if (value instanceof Array) { // 如果条件中的value是数组的话,只要查到一个就行 if (value.length === 0) { continue; } let found = false; for (const i of value) { if (item[key] instanceof Array) { for (const j of item[key]) { if (i === j) { found = true; break; } } if (found) { break; } } else if (item[key] === i || (typeof item[key] === "string" && item[key].indexOf(i + "") >= 0)) { found = true; break; } if (found) { break; } } if (!found) { allFound = false; } } else if (value instanceof Object) { for (const key2 in value) { const v = value[key2]; if (v && item[key] && v !== item[key][key2]) { return false; } } } else if (item[key] !== value) { allFound = false; } } return allFound; }); } } const start = offset; let end = offset + limit; if (data.length < end) { end = data.length; } if (orderProp) { // 排序 data.sort((a, b) => { let ret = 0; if (a[orderProp] > b[orderProp]) { ret = 1; } else { ret = -1; } return orderAsc ? ret : -ret; }); } const records = data.slice(start, end); const lastOffset = data.length - (data.length % limit); if (offset > lastOffset) { offset = lastOffset; } return { code: 0, msg: "success", data: { records: records, total: data.length, limit, offset, }, }; }, }, { path: "/mock/" + name + "/get", method: "get", handle(req: any) { let id = req.params.id; id = parseInt(id); const current = findById(req.body.id, list); return { code: 0, msg: "success", data: current, }; }, }, { path: "/mock/" + name + "/byIds", method: "post", handle(req: any) { const ids = req.body.ids; const res = findByIds(ids, list); return { code: 0, msg: "success", data: res, }; }, }, { path: "/mock/" + name + "/add", method: "post", handle(req: any) { req.body.id = ++options.idGenerator; list.unshift(req.body); return { code: 0, msg: "success", data: cloneDeep(req.body), }; }, }, { path: "/mock/" + name + "/update", method: "post", handle(req: any): any { const item = findById(req.body.id, list); if (item) { mergeWith(item, req.body, (objValue: any, srcValue: any) => { if (srcValue == null) { return; } // 如果被合并对象为数组,则直接被覆盖对象覆盖,只要覆盖对象不为空 if (isArray(objValue)) { return srcValue; } }); } return { code: 0, msg: "success", data: null, }; }, }, { path: "/mock/" + name + "/delete", method: "post", handle(req: any): any { delById(req, list); return { code: 0, msg: "success", data: null, }; }, }, { path: "/mock/" + name + "/batchDelete", method: "post", handle(req: any): any { const ids = req.body.ids; for (let i = list.length - 1; i >= 0; i--) { const item = list[i]; if (ids.indexOf(item.id) >= 0) { list.splice(i, 1); } } return { code: 0, msg: "success", data: null, }; }, }, { path: "/mock/" + name + "/delete", method: "post", handle(req: any): any { delById(req, list); return { code: 0, msg: "success", data: null, }; }, }, { path: "/mock/" + name + "/all", method: "post", handle(req: any): any { return { code: 0, msg: "success", data: list, }; }, }, { path: "/mock/" + name + "/cellUpdate", method: "post", handle(req: any): any { console.log("req", req); let item = findById(req.body.id, list); if (item) { mergeWith(item, { [req.body.key]: req.body.value }, (objValue: any, srcValue: any) => { if (srcValue == null) { return; } // 如果被合并对象为数组,则直接被覆盖对象覆盖,只要覆盖对象不为空 if (isArray(objValue)) { return srcValue; } }); } else { item = { id: ++options.idGenerator, [req.body.key]: req.body.value, }; list.unshift(item); } return { code: 0, msg: "success", data: item, }; }, }, { path: "/mock/" + name + "/columnUpdate", method: "post", handle(req: any): any { for (const item of req.body) { const item2 = findById(item.id, list); if (item2) { mergeWith(item2, item, (objValue: any, srcValue: any) => { if (srcValue == null) { return; } // 如果被合并对象为数组,则直接被覆盖对象覆盖,只要覆盖对象不为空 if (isArray(objValue)) { return srcValue; } }); } } return { code: 0, msg: "success", data: null, }; }, }, ]; }, }; export default mockUtil; ================================================ FILE: packages/ui/certd-client/src/mock/common/cascader-data.ts ================================================ export default [ { value: "zhinan", label: "指南", children: [ { value: "shejiyuanze", label: "设计原则", children: [ { value: "yizhi", label: "一致", }, { value: "fankui", label: "反馈", }, { value: "xiaolv", label: "效率", }, { value: "kekong", label: "可控", }, ], }, { value: "daohang", label: "导航", children: [ { value: "cexiangdaohang", label: "侧向导航", }, { value: "dingbudaohang", label: "顶部导航", }, ], }, ], }, { value: "zujian", label: "组件", children: [ { value: "basic", label: "Basic", children: [ { value: "layout", label: "Layout 布局", }, { value: "color", label: "Color 色彩", }, { value: "typography", label: "Typography 字体", }, { value: "icon", label: "Icon 图标", }, { value: "button", label: "Button 按钮", }, ], }, { value: "form", label: "Form", children: [ { value: "radio", label: "Radio 单选框", }, { value: "checkbox", label: "Checkbox 多选框", }, { value: "input", label: "Input 输入框", }, { value: "input-number", label: "InputNumber 计数器", }, { value: "select", label: "Select 选择器", }, { value: "cascader", label: "Cascader 级联选择器", }, { value: "switch", label: "Switch 开关", }, { value: "slider", label: "Slider 滑块", }, { value: "time-picker", label: "TimePicker 时间选择器", }, { value: "date-picker", label: "DatePicker 日期选择器", }, { value: "datetime-picker", label: "DateTimePicker 日期时间选择器", }, { value: "upload", label: "Upload 上传", }, { value: "rate", label: "Rate 评分", }, { value: "form1", label: "Form 表单", }, ], }, { value: "data", label: "Data", children: [ { value: "table", label: "Table 表格", }, { value: "tag", label: "Tag 标签", }, { value: "progress", label: "Progress 进度条", }, { value: "tree", label: "Tree 树形控件", }, { value: "pagination", label: "Pagination 分页", }, { value: "badge", label: "Badge 标记", }, ], }, { value: "notice", label: "Notice", children: [ { value: "alert", label: "Alert 警告", }, { value: "loading", label: "Loading 加载", }, { value: "message", label: "Message 消息提示", }, { value: "message-box", label: "MessageBox 弹框", }, { value: "notification", label: "Notification 通知", }, ], }, { value: "navigation", label: "Navigation", children: [ { value: "menu", label: "NavMenu 导航菜单", }, { value: "tabs", label: "Tabs 标签页", }, { value: "breadcrumb", label: "Breadcrumb 面包屑", }, { value: "dropdown", label: "Dropdown 下拉菜单", }, { value: "steps", label: "Steps 步骤条", }, ], }, { value: "others", label: "Others", children: [ { value: "dialog", label: "Dialog 对话框", }, { value: "tooltip", label: "Tooltip 文字提示", }, { value: "popover", label: "Popover 弹出框", }, { value: "card", label: "Card 卡片", }, { value: "carousel", label: "Carousel 走马灯", }, { value: "collapse", label: "Collapse 折叠面板", }, ], }, ], }, { value: "ziyuan", label: "资源", children: [ { value: "axure", label: "Axure Components", }, { value: "sketch", label: "Sketch Templates", }, { value: "jiaohu", label: "组件交互文档", }, ], }, ]; ================================================ FILE: packages/ui/certd-client/src/mock/common/mock.dict.ts ================================================ import cascaderData from "./cascader-data"; // @ts-ignore import pcaDataLittle from "./pca-data-little"; // @ts-ignore import { TreeNodesLazyLoader, getPcaData } from "./pcas-data"; import { cloneDeep } from "lodash-es"; const openStatus = [ { value: "1", label: "打开", color: "success", icon: "ion:radio-button-on" }, { value: "2", label: "停止", color: "cyan" }, { value: "0", label: "关闭", color: "red", icon: "ion:radio-button-off" }, ]; const moreOpenStatus = [ { value: "1", label: "打开(open)", color: "success" }, { value: "2", label: "停止(stop)", color: "cyan" }, { value: "0", label: "关闭(close)", color: "red" }, ]; const textStatus = [ { id: "1", text: "打开", color: "success" }, { id: "2", text: "停止", color: "cyan" }, { id: "0", text: "关闭", color: "red" }, ]; let manyStatus = [ { value: "1", label: "打开", color: "success", icon: "ion:radio-button-on" }, { value: "2", label: "停止", color: "cyan" }, { value: "0", label: "关闭", color: "red", icon: "ion:radio-button-off" }, ]; let tempManyStatus: any[] = []; for (let i = 0; i < 100; i++) { tempManyStatus = tempManyStatus.concat(cloneDeep(manyStatus)); } manyStatus = tempManyStatus; let idIndex = 0; for (const item of manyStatus) { idIndex++; item.value = idIndex + ""; } export function GetTreeChildrenByParentId(parentId: any) { return TreeNodesLazyLoader.getChildren(parentId); } export function GetNodesByValues(values: any) { return TreeNodesLazyLoader.getNodesByValues(values); } export default [ { path: "/mock/dicts/OpenStatusEnum", method: "get", handle() { return { code: 0, msg: "success", data: openStatus, }; }, }, { path: "/mock/dicts/_OpenStatusEnum2", method: "get", handle() { return { code: 0, msg: "success", data: textStatus, }; }, }, { path: "/mock/dicts/ManyOpenStatusEnum", method: "get", handle() { return { code: 0, msg: "success", data: manyStatus, }; }, }, { path: "/mock/dicts/moreOpenStatusEnum", method: "get", handle() { return { code: 0, msg: "success", data: moreOpenStatus, }; }, }, { path: "/mock/dicts/cascaderData", method: "get", handle() { return { code: 0, msg: "success", data: cascaderData, }; }, }, { path: "/mock/dicts/pca", method: "get", async handle() { const data = await getPcaData(); return { code: 0, msg: "success", data: data, }; }, }, { path: "/mock/dicts/littlePca", method: "get", async handle() { return { code: 0, msg: "success", data: pcaDataLittle, }; }, }, { path: "/mock/tree/GetTreeChildrenByParentId", method: "get", async handle({ params }: any) { const list = await GetTreeChildrenByParentId(params.parentId); return { code: 0, msg: "success", data: list, }; }, }, { path: "/mock/tree/GetNodesByValues", method: "get", async handle({ params }: any) { const list = await GetNodesByValues(params.values); return { code: 0, msg: "success", data: list, }; }, }, ]; ================================================ FILE: packages/ui/certd-client/src/mock/common/pca-data-little.ts ================================================ export default [ { code: "1", name: "北京", children: [ { code: "2", name: "北京市区", children: [ { code: "3", name: "海淀", }, { code: "4", name: "朝阳", }, ], }, { code: "5", name: "北京郊区", children: [ { code: "6", name: "海淀郊区", }, { code: "7", name: "朝阳郊区", }, ], }, ], }, { code: "11", name: "深圳", children: [ { code: "12", name: "深圳市区", children: [ { code: "13", name: "南山", }, { code: "14", name: "福田", }, ], }, { code: "15", name: "深圳郊区", children: [ { code: "16", name: "南山郊区", }, { code: "17", name: "福田郊区", }, ], }, ], }, ]; ================================================ FILE: packages/ui/certd-client/src/mock/common/pcas-data.ts ================================================ import * as _ from "lodash-es"; export async function getPcasData() { // @ts-ignore const pcasData = () => import("china-division/dist/pcas-code.json"); const ret = await pcasData(); return ret.default; } export async function getPcaData() { // @ts-ignore const pcaData = () => import("china-division/dist/pca-code.json"); const ret = await pcaData(); return ret.default; } export const TreeNodesLazyLoader = { getNodesByValues(values: any) { console.log("getNodesByValues", values); if (!(values instanceof Array)) { values = [values]; } return getPcasData().then(data => { const nodes = []; for (const value of values) { const found = this.getNode(data, value); if (found) { const target = _.cloneDeep(found); delete target.children; nodes.push(target); } } return nodes; }); }, getNode(list: any, value: any) { for (const item of list) { if (item.code === value) { return item; } if (item.children && item.children.length > 0) { const found: any = this.getNode(item.children, value); if (found) { return found; } } } }, getChildren(parent: any) { return getPcasData().then(data => { const list = this.getChildrenByParent(parent, data); if (list == null) { return []; } return this.cloneAndDeleteChildren(list); }); }, getChildrenByParent(parentId: any, tree: any) { if (!parentId) { // 取第一级 return tree; } else { for (const node of tree) { if (node.code === parentId) { return node.children; } if (node.children && node.children.length > 0) { // 递归查找 const list: any = this.getChildrenByParent(parentId, node.children); if (list) { return list; } } } } }, cloneAndDeleteChildren(list: any) { const newList = []; for (const node of list) { const newNode: any = {}; Object.assign(newNode, node); if (newNode.children == null || newNode.children.length === 0) { newNode.isLeaf = true; newNode.leaf = true; } delete newNode.children; newList.push(newNode); } console.log("found children:", newList); return newList; }, }; ================================================ FILE: packages/ui/certd-client/src/mock/index.ts ================================================ import { mock } from "../api/service"; import * as tools from "../api/tools"; import { forEach } from "lodash-es"; import { utils } from "@fast-crud/fast-crud"; // @ts-ignore const commonMocks: any = import.meta.glob("./common/mock.*.[j|t]s", { eager: true }); // @ts-ignore const apiMocks: any = import.meta.glob("../api/modules/*.mock.ts", { eager: true }); // @ts-ignore const viewMocks: any = import.meta.glob("../views/**/mock.[j|t]s", { eager: true }); const list: any = []; forEach(commonMocks, (value: any) => { list.push(value.default); }); forEach(apiMocks, (value: any) => { list.push(value.default); }); forEach(viewMocks, (value: any) => { list.push(value.default); }); list.forEach((apiFile: any) => { for (const item of apiFile) { mock.onAny(new RegExp(item.path)).reply(async (config: any) => { utils.logger.debug("------------fake request start -------------"); utils.logger.debug("request:", config); const data = config.data ? JSON.parse(config.data) : {}; const query = config.url.indexOf("?") >= 0 ? config.url.substring(config.url.indexOf("?") + 1) : undefined; const params = config.params || {}; if (query) { const arr = query.split("&"); for (const item of arr) { const kv = item.split("="); params[kv[0]] = kv[1]; } } const req = { body: data, params: params, }; const ret = await item.handle(req); utils.logger.debug("response:", ret); utils.logger.debug("------------fake request end-------------"); if (ret.code === 0) { return tools.responseSuccess(ret.data, ret.msg); } else { return tools.responseError(ret.data, ret.msg, ret.code); } }); } }); ================================================ FILE: packages/ui/certd-client/src/plugin/antdv-async/index-bak.ts ================================================ import { defineAsyncComponent } from "vue"; import Input from "ant-design-vue/es/input/Input"; import Button from "ant-design-vue/es/button/button"; import Divider from "ant-design-vue/es/divider"; import Badge from "ant-design-vue/es/badge"; import Empty from "ant-design-vue/es/empty"; import Avatar from "ant-design-vue/es/avatar"; import Steps from "ant-design-vue/es/steps"; import Select from "ant-design-vue/es/select"; import PageHeader from "ant-design-vue/es/page-header"; import Card from "ant-design-vue/es/card"; export default { install(app: any) { app.use(Input); app.use(Button); app.use(Divider); app.use(Badge); app.use(Empty); app.use(Avatar); app.use(PageHeader); app.use(Steps); app.use(Select); app.use(Card); app.component( "AAutoComplete", defineAsyncComponent(() => import("ant-design-vue/es/auto-complete/index")) ); app.component( "ARadio", defineAsyncComponent(() => import("ant-design-vue/es/radio/Radio")) ); app.component( "ARadioGroup", defineAsyncComponent(() => import("ant-design-vue/es/radio/Group")) ); app.component( "ATable", defineAsyncComponent(() => import("ant-design-vue/es/table/Table")) ); app.component( "AModal", defineAsyncComponent(() => import("ant-design-vue/es/modal/Modal")) ); app.component( "AForm", defineAsyncComponent(() => import("ant-design-vue/es/form/Form")) ); app.component( "AFormItem", defineAsyncComponent(() => import("ant-design-vue/es/form/FormItem")) ); app.component( "AFormItemRest", defineAsyncComponent(() => import("ant-design-vue/es/form/FormItemContext")) ); app.component( "ATabs", defineAsyncComponent(() => import("ant-design-vue/es/tabs/src/Tabs")) ); app.component( "ATabPane", defineAsyncComponent(() => import("ant-design-vue/es/tabs/src/TabPanelList/TabPane")) ); app.component( "ATextarea", defineAsyncComponent(() => import("ant-design-vue/es/input/TextArea")) ); app.component( "AInputNumber", defineAsyncComponent(() => import("ant-design-vue/es/input-number/index")) ); app.component( "ADrawer", defineAsyncComponent(() => import("ant-design-vue/es/drawer/index")) ); app.component( "ASwitch", defineAsyncComponent(() => import("ant-design-vue/es/switch/index")) ); app.component( "AUpload", defineAsyncComponent(() => import("ant-design-vue/es/upload/index")) ); app.component( "ADatePicker", defineAsyncComponent(() => import("ant-design-vue/es/date-picker/index")) ); app.component( "ARangePicker", defineAsyncComponent(async () => { const { RangePicker } = await import("ant-design-vue/es/date-picker/index"); return RangePicker; }) ); app.component( "ATimePicker", defineAsyncComponent(() => import("ant-design-vue/es/time-picker/index")) ); app.component( "ATag", defineAsyncComponent(() => import("ant-design-vue/es/tag/index")) ); app.component( "AAlert", defineAsyncComponent(() => import("ant-design-vue/es/alert/index")) ); app.component( "AInputAutoComplete", defineAsyncComponent(() => import("ant-design-vue/es/auto-complete/index")) ); app.component( "ACascader", defineAsyncComponent(() => import("ant-design-vue/es/cascader/index")) ); app.component( "ACheckbox", defineAsyncComponent(() => import("ant-design-vue/es/checkbox")) ); app.component( "ACheckboxGroup", defineAsyncComponent(() => import("ant-design-vue/es/checkbox/Group")) ); app.component( "ACol", defineAsyncComponent(() => import("ant-design-vue/es/col")) ); app.component( "ARow", defineAsyncComponent(() => import("ant-design-vue/es/row")) ); app.component( "ADropdown", defineAsyncComponent(() => import("ant-design-vue/es/dropdown")) ); app.component( "AGrid", defineAsyncComponent(() => import("ant-design-vue/es/grid")) ); app.component( "AImage", defineAsyncComponent(() => import("ant-design-vue/es/image")) ); app.component( "APagination", defineAsyncComponent(() => import("ant-design-vue/es/pagination")) ); app.component( "ATooltip", defineAsyncComponent(() => import("ant-design-vue/es/tooltip")) ); app.component( "ATree", defineAsyncComponent(() => import("ant-design-vue/es/tree")) ); app.component( "ATreeSelect", defineAsyncComponent(() => import("ant-design-vue/es/tree-select")) ); app.component( "ATour", defineAsyncComponent(() => import("ant-design-vue/es/tour")) ); app.component( "AMenu", defineAsyncComponent(() => import("ant-design-vue/es/menu/index")) ); app.component( "ASubMenu", defineAsyncComponent(() => import("ant-design-vue/es/menu/src/SubMenu")) ); app.component( "AMenuItem", defineAsyncComponent(() => import("ant-design-vue/es/menu/src/MenuItem")) ); app.component( "AProgress", defineAsyncComponent(() => import("ant-design-vue/es/progress")) ); app.component( "ATimelineItem", defineAsyncComponent(() => import("ant-design-vue/es/timeline/TimelineItem")) ); app.component( "ATimeline", defineAsyncComponent(() => import("ant-design-vue/es/timeline/Timeline")) ); app.component( "APageHeader", defineAsyncComponent(() => import("ant-design-vue/es/page-header/index")) ); app.component( "APopover", defineAsyncComponent(() => import("ant-design-vue/es/popover")) ); app.component( "APopconfirm", defineAsyncComponent(() => import("ant-design-vue/es/popconfirm")) ); app.component( "ACollapse", defineAsyncComponent(() => import("ant-design-vue/es/collapse")) ); app.component( "ADescriptions", defineAsyncComponent(() => import("ant-design-vue/es/descriptions")) ); app.component( "ADescriptionsItem", defineAsyncComponent(async () => { const m = await import("ant-design-vue/es/descriptions/"); return m.DescriptionsItem; }) ); app.component( "AResult", defineAsyncComponent(() => import("ant-design-vue/es/result")) ); }, }; ================================================ FILE: packages/ui/certd-client/src/plugin/antdv-async/index.ts ================================================ import antdv from "ant-design-vue"; export default { install(app: any) { app.use(antdv); }, }; ================================================ FILE: packages/ui/certd-client/src/plugin/fast-crud/index.tsx ================================================ import { request } from "/src/api/service"; // import "/src/mock"; import { ColumnCompositionProps, CrudOptions, FastCrud, PageQuery, PageRes, setLogger, TransformResProps, useColumns, UseCrudProps, UserPageQuery, useTypes, utils } from "@fast-crud/fast-crud"; import "@fast-crud/fast-crud/dist/style.css"; import { FsExtendsCopyable, FsExtendsEditor, FsExtendsJson, FsExtendsTime, FsExtendsUploader, FsExtendsInput, FsUploaderS3SignedUrlType, FsUploaderGetAuthContext, FsUploaderAliossSTS } from "@fast-crud/fast-extends"; import "@fast-crud/fast-extends/dist/style.css"; import UiAntdv from "@fast-crud/ui-antdv4"; import "@fast-crud/ui-antdv4/dist/style.css"; import { debounce, merge } from "lodash-es"; import { useCrudPermission } from "../permission"; import { App } from "vue"; import { notification } from "ant-design-vue"; import { usePreferences } from "/@/vben/preferences"; import { LocalStorage } from "/@/utils/util.storage"; class ColumnSizeSaver { save: (key: string, size: number) => void; constructor() { this.save = debounce((key: string, size: number) => { const saveKey = this.getKey(); let data = LocalStorage.get(saveKey); if (!data) { data = {}; } data[key] = size; LocalStorage.set(saveKey, data); }); } getKey() { const loc = window.location; const currentUrl = `${loc.pathname}${loc.search}${loc.hash}`; return `columnSize-${currentUrl}`; } get(key: string) { const saveKey = this.getKey(); const row = LocalStorage.get(saveKey); return row?.[key]; } clear() { const saveKey = this.getKey(); LocalStorage.remove(saveKey); } } const columnSizeSaver = new ColumnSizeSaver(); function install(app: App, options: any = {}) { app.use(UiAntdv); //设置日志级别 setLogger({ level: "info" }); app.use(FastCrud, { i18n: options.i18n, async dictRequest({ url }: any) { return await request({ url, method: "post" }); }, /** * useCrud时会被执行 * @param props,useCrud的参数 */ commonOptions(props: UseCrudProps): CrudOptions { utils.logger.debug("commonOptions:", props); const crudBinding = props.crudExpose?.crudBinding; const { isMobile } = usePreferences(); const opts: CrudOptions = { settings: { plugins: { mobile: { enabled: true, props: { isMobile: isMobile, }, }, }, }, table: { scroll: { x: 960, }, size: "small", pagination: false, onResizeColumn: (w: number, col: any) => { if (crudBinding.value?.table?.columnsMap && crudBinding.value?.table?.columnsMap[col.key]) { crudBinding.value.table.columnsMap[col.key].width = w; columnSizeSaver.save(col.key, w); } }, conditionalRender: { match(scope) { if (scope.column.conditionalRenderDisabled) { return false; } if (scope.key === "__blank__") { return false; } //不能用 !scope.value , 否则switch组件设置为关之后就消失了 const { value, key, props } = scope; return !value && key != "_index" && value != false; }, render() { return "-"; }, }, }, toolbar: { export: { fileType: "excel", }, columnsFilter: { async onReset() { columnSizeSaver.clear(); }, }, buttons: { export: { show: false, }, }, }, rowHandle: { fixed: "right", buttons: { view: { type: "link", text: null, icon: "ion:eye-outline", tooltip: { title: "查看" } }, copy: { show: true, type: "link", text: null, icon: "ion:copy-outline", tooltip: { title: "复制" } }, edit: { type: "link", text: null, icon: "ion:create-outline", tooltip: { title: "编辑" } }, remove: { type: "link", style: { color: "red" }, text: null, icon: "ion:trash-outline", tooltip: { title: "删除" } }, }, dropdown: { more: { type: "link", }, }, resizable: true, width: 200, }, request: { transformQuery: ({ page, form, sort }: PageQuery): UserPageQuery => { const limit = page.pageSize; const currentPage = page.currentPage ?? 1; const offset = limit * (currentPage - 1); sort = sort == null ? {} : sort; return { page: { limit, offset, }, query: form, sort, }; }, transformRes: ({ res }: TransformResProps): PageRes => { const pageSize = res.limit; let currentPage = res.offset / pageSize; if (res.offset % pageSize === 0) { currentPage++; } return { currentPage, pageSize, records: res.records, total: res.total, ...res }; }, }, search: { formItem: { wrapperCol: { style: { width: "50%", }, }, }, }, form: { display: "flex", labelCol: { //固定label宽度 span: null, style: { width: "145px", }, }, async afterSubmit({ mode }) { if (mode === "add") { notification.success({ message: "添加成功" }); } else if (mode === "edit") { notification.success({ message: "保存成功" }); } }, wrapperCol: { span: null, }, wrapper: { saveRemind: true, // inner: true, // innerContainerSelector: "main.fs-framework-content" }, }, columns: { //最后一列空白,用于自动伸缩列宽 __blank__: { title: "", type: "text", form: { show: false, }, column: { order: 99999, width: -1, columnSetShow: false, resizable: false, }, }, }, }; // 从 useCrud({permission}) 里获取permission参数,去设置各个按钮的权限 const permission = props.context?.permission || null; const crudPermission = useCrudPermission({ permission }); return crudPermission.merge(opts); }, }); // fast-extends里面的扩展组件均为异步组件,只有在使用时才会被加载,并不会影响首页加载速度 //安装uploader 公共参数 // @ts-ignore app.use(FsExtendsUploader, { // @ts-ignore defaultType: "form", form: { keepName: true, type: "form", action: "/basic/file/upload", name: "file", withCredentials: false, test: 22, custom: { aaa: 22 }, uploadRequest: async (opts: any) => { console.log("uploadRequest:", opts); const { action, file, onProgress } = opts; // @ts-ignore const data = new FormData(); data.append("file", file); return await request({ url: action, method: "post", headers: { "Content-Type": "multipart/form-data", }, timeout: 60000, data, onUploadProgress: (p: any) => { onProgress({ percent: Math.round((p.loaded / p.total) * 100) }); }, }); }, successHandle(res: any) { return res; }, }, }); //安装editor app.use(FsExtendsEditor, { //编辑器的公共配置 wangEditor: { editorConfig: { MENU_CONF: {}, }, toolbarConfig: {}, }, }); app.use(FsExtendsJson); app.use(FsExtendsTime); app.use(FsExtendsCopyable); app.use(FsExtendsInput); const { addTypes, getType } = useTypes(); //此处演示修改官方字段类型 const textType = getType("text"); textType.search.autoSearchTrigger = "change"; //修改官方的字段类型,变化就触发 , "enter"=回车键触发 if (!textType.column) { textType.column = {}; } textType.column.ellipsis = true; textType.column.showTitle = true; // 此处演示自定义字段类型 addTypes({ time2: { //如果与官方字段类型同名,将会覆盖官方的字段类型 form: { component: { name: "a-date-picker" } }, column: { component: { name: "fs-date-format", format: "YYYY-MM-DD" } }, valueBuilder(context: any) { console.log("time2,valueBuilder", context); }, }, }); // 此处演示自定义字段合并插件 const { registerMergeColumnPlugin } = useColumns(); registerMergeColumnPlugin({ name: "readonly-plugin", order: 1, handle: (columnProps: ColumnCompositionProps) => { // 你可以在此处做你自己的处理 // 比如你可以定义一个readonly的公共属性,处理该字段只读,不能编辑 if (columnProps.readonly) { // 合并column配置 merge(columnProps, { form: { show: false }, viewForm: { show: true }, }); } return columnProps; }, }); //默认宽度,支持自动拖动调整列宽 registerMergeColumnPlugin({ name: "resize-column-plugin", order: 2, handle: (columnProps: ColumnCompositionProps) => { if (!columnProps.column) { columnProps.column = {}; } columnProps.column.resizable = true; const savedColumnWidth = columnSizeSaver.get(columnProps.key as string); if (savedColumnWidth) { columnProps.column.width = savedColumnWidth; } else if (columnProps.column.width == null) { columnProps.column.width = 200; } else if (typeof columnProps.column?.width === "string" && columnProps.column.width.indexOf("px") > -1) { columnProps.column.width = parseInt(columnProps.column.width.replace("px", "")); } return columnProps; }, }); registerMergeColumnPlugin({ name: "reset-values-format-colors", order: 10, handle: (columnProps: ColumnCompositionProps) => { // 你可以在此处做你自己的处理 // 比如你可以定义一个readonly的公共属性,处理该字段只读,不能编辑 if (columnProps.column?.component?.name === "fs-values-format") { // 合并column配置 if (!columnProps.column.component.autoColors) { columnProps.column.component.autoColors = ["green", "cyan", "blue", "purple", "geekblue"]; } } return columnProps; }, }); } export default { install, }; ================================================ FILE: packages/ui/certd-client/src/plugin/iconfont/iconfont.js ================================================ !(function (t) { var e, c, o, n, l, a = '', i = (i = document.getElementsByTagName("script"))[i.length - 1].getAttribute("data-injectcss"), h = function (t, e) { e.parentNode.insertBefore(t, e); }; if (i && !t.__iconfont__svg__cssinject__) { t.__iconfont__svg__cssinject__ = !0; try { document.write(""); } catch (t) { console && console.log(t); } } function s() { l || ((l = !0), o()); } function d() { try { n.documentElement.doScroll("left"); } catch (t) { return void setTimeout(d, 50); } s(); } (e = function () { var t, e = document.createElement("div"); (e.innerHTML = a), (a = null), (e = e.getElementsByTagName("svg")[0]) && (e.setAttribute("aria-hidden", "true"), (e.style.position = "absolute"), (e.style.width = 0), (e.style.height = 0), (e.style.overflow = "hidden"), (e = e), (t = document.body).firstChild ? h(e, t.firstChild) : t.appendChild(e)); }), document.addEventListener ? ~["complete", "loaded", "interactive"].indexOf(document.readyState) ? setTimeout(e, 0) : ((c = function () { document.removeEventListener("DOMContentLoaded", c, !1), e(); }), document.addEventListener("DOMContentLoaded", c, !1)) : document.attachEvent && ((o = e), (n = t.document), (l = !1), d(), (n.onreadystatechange = function () { "complete" == n.readyState && ((n.onreadystatechange = null), s()); })); })(window); ================================================ FILE: packages/ui/certd-client/src/plugin/iconfont/index.ts ================================================ import "./iconfont.js"; ================================================ FILE: packages/ui/certd-client/src/plugin/iconify/index.ts ================================================ // import "@iconify/iconify"; // import "@purge-icons/generated"; ================================================ FILE: packages/ui/certd-client/src/plugin/index.ts ================================================ import "./iconify"; import "./iconfont"; import FastCrud from "./fast-crud"; import permission from "./permission"; import { App } from "vue"; import "./validator/index.js"; function install(app: App, options: any = {}) { app.use(FastCrud, options); app.use(permission); } export default { install, }; ================================================ FILE: packages/ui/certd-client/src/plugin/permission/api.ts ================================================ import { request } from "/src/api/service"; export async function getPermissions() { const ret = await request({ url: "/sys/authority/user/permissions", method: "post", }); // 如果使用你自己的后端,需要在此处将返回结果改造为本模块需要的结构 // 结构详情,请参考示例中打印的日志 ”获取权限数据成功:{...}“ (实际上就是“资源管理”页面中列出来的数据) return ret; } ================================================ FILE: packages/ui/certd-client/src/plugin/permission/directive/index.ts ================================================ import permission from "./permission.js"; import permissionUtil from "../util.permission"; const install = function (app: any) { app.directive("permission", permission); app.config.globalProperties.$hasPermissions = permissionUtil.hasPermissions; }; export default { install, ...permission, }; ================================================ FILE: packages/ui/certd-client/src/plugin/permission/directive/permission.ts ================================================ import permissionUtil from "../util.permission"; export default { mounted(el: any, binding: any, vnode: any) { const { value } = binding; const hasPermission = permissionUtil.hasPermissions(value); if (!hasPermission) { el.parentNode && el.parentNode.removeChild(el); } }, }; ================================================ FILE: packages/ui/certd-client/src/plugin/permission/errors.ts ================================================ export class NoPermissionError extends Error { constructor(message?: string) { super(message || "对不起,您没有权限执行此操作"); } } ================================================ FILE: packages/ui/certd-client/src/plugin/permission/hook.ts ================================================ import router from "/src/router"; import { useUserStore } from "/@/store/user"; import { usePermissionStore } from "./store.permission"; import util from "./util.permission"; import { message } from "ant-design-vue"; import NProgress from "nprogress"; export function registerRouterHook() { // 注册路由beforeEach钩子,在第一次加载路由页面时,加载权限 router.beforeEach(async (to, from) => { const permissionStore = usePermissionStore(); if (permissionStore.isInited) { if (to.meta.permission) { //校验权限 // @ts-ignore if (!util.hasPermissions(to.meta.permission)) { //没有权限 message.warn("对不起,您没有权限"); //throw new Error("对不起,您没有权限"); NProgress.done(); return false; } } return true; } const userStore = useUserStore(); const token = userStore.getToken; if (!token || token === "undefined") { return true; } // 初始化权限列表 try { console.log("permission is enabled"); await permissionStore.loadFromRemote(); console.log("PM load success"); return { ...to, replace: true }; } catch (e) { console.error("加载动态路由失败", e); return false; } }); } ================================================ FILE: packages/ui/certd-client/src/plugin/permission/index.ts ================================================ import permissionDirective from "./directive/index.js"; import { registerRouterHook } from "./hook"; import util from "./util.permission"; export * from "./use-crud-permission"; export * from "./errors"; export function usePermission() { return { ...util, }; } export default { install(app: any) { // 开启权限模块 // 注册v-permission指令, 用于控制按钮权限 app.use(permissionDirective); // 注册路由钩子 // 通过路由守卫,在登录成功后拦截路由,从后台加载权限数据 // 然后将权限数据转化为菜单和路由,添加到系统中 registerRouterHook(); }, }; ================================================ FILE: packages/ui/certd-client/src/plugin/permission/store.permission.ts ================================================ import { defineStore } from "pinia"; // import { useResourceStore } from "/src/store/modules/resource"; import { getPermissions } from "./api"; import { mitter } from "/@/utils/util.mitt"; import { env } from "/@/utils/util.env"; import { useAccessStore } from "/@/vben/stores"; import { eachTree } from "/@/utils/util.tree"; import util from "/@/plugin/permission/util.permission"; //监听注销事件 mitter.on("app.logout", () => { const permissionStore = usePermissionStore(); permissionStore.clear(); const accessStore = useAccessStore(); accessStore.setIsAccessChecked(false); }); mitter.on("app.login", () => { const accessStore = useAccessStore(); accessStore.setIsAccessChecked(false); const permissionStore = usePermissionStore(); permissionStore.clear(); // const accessStore = useAccessStore(); // accessStore.setAccessCode([]); // permissionStore.init(); }); interface PermissionState { permissions: []; inited: boolean; } /** * 构建权限码列表 * @param menuTree * @param permissionList * @returns {*} */ export function formatPermissions(menuTree: Array, permissionList: any[] = []) { if (menuTree == null) { menuTree = []; } menuTree.forEach((item: any) => { if (item.permission) { // @ts-ignore permissionList.push(item.permission); } if (item.children != null && item.children.length > 0) { formatPermissions(item.children, permissionList); } }); return permissionList; } export const usePermissionStore = defineStore({ id: "app.permission", state: (): PermissionState => ({ permissions: [], inited: false, }), getters: { // @ts-ignore getPermissions() { // @ts-ignore return this.permissions; }, // @ts-ignore isInited() { // @ts-ignore return this.inited; }, }, actions: { init({ permissions }: any) { this.permissions = permissions; this.inited = true; }, clear() { this.permissions = []; this.inited = false; }, resolve(resourceTree: any) { const permissions = formatPermissions(resourceTree); this.init({ permissions }); //过滤没有权限的菜单 const accessStore = useAccessStore(); accessStore.setAccessCodes(permissions); }, async loadFromRemote() { let permissionTree = []; if (env.PM_ENABLED === "false") { console.warn("当前权限模块未开启,权限列表为空"); } else { //开启了权限模块,向后台请求权限列表 const data = await getPermissions(); if (data != null) { permissionTree = data; } else { console.warn("当前获取到的权限列表为空"); } } this.resolve(permissionTree); }, }, }); ================================================ FILE: packages/ui/certd-client/src/plugin/permission/use-crud-permission.ts ================================================ import { usePermission } from "/@/plugin/permission"; import { merge as LodashMerge } from "lodash-es"; export type UseCrudPermissionExtraProps = { hasActionPermission: (action: string) => boolean; }; export type UseCrudPermissionExtra = (props: UseCrudPermissionExtraProps) => any; export type UseCrudPermissionCompProps = { prefix: string; extra?: UseCrudPermissionExtra; [key: string]: any; }; export type UseCrudPermissionProps = { permission: string | UseCrudPermissionCompProps; }; /** * 设置按钮动作权限 * @param permission {prefix,extra} */ export function useCrudPermission({ permission }: UseCrudPermissionProps) { const { hasPermissions } = usePermission(); const prefix = permission instanceof Object ? permission.prefix : permission; //根据权限显示按钮 function hasActionPermission(action: string) { if (!prefix) { return true; } return hasPermissions(prefix + ":" + action); } function buildCrudPermission(): any { if (permission == null) { return {}; } let extra = {}; if (permission instanceof Object) { extra = permission.extra; if (permission.extra && permission.extra instanceof Function) { extra = permission.extra({ hasActionPermission }); } } return LodashMerge( { actionbar: { buttons: { add: { show: hasActionPermission("add") }, }, }, rowHandle: { buttons: { edit: { show: hasActionPermission("edit") }, remove: { show: hasActionPermission("remove") }, view: { show: hasActionPermission("view") }, }, }, }, extra ); } function merge(userOptions: any) { const permissionOptions = buildCrudPermission(); LodashMerge(permissionOptions, userOptions); return permissionOptions; } return { merge, buildCrudPermission, hasActionPermission }; } ================================================ FILE: packages/ui/certd-client/src/plugin/permission/util.permission.ts ================================================ import { usePermissionStore } from "./store.permission"; import { NoPermissionError } from "./errors"; import { message } from "ant-design-vue"; const util = { hasPermissions: (value: string | string[]): boolean => { let need: string[] = []; if (typeof value === "string") { need.push(value); } else if (value && value instanceof Array && value.length > 0) { need = need.concat(value); } if (need.length === 0) { throw new Error('need permissions! Like "sys:user:view" '); } const permissionStore = usePermissionStore(); const userPermissionList = permissionStore.getPermissions; return userPermissionList.some((permission: any) => { if (permission === "*") { return true; } return need.includes(permission); }); }, requirePermissions: (value: any) => { if (!util.hasPermissions(value)) { message.error("对不起,您没有权限执行此操作"); throw new NoPermissionError(); } }, }; export default util; ================================================ FILE: packages/ui/certd-client/src/plugin/validator/__tests__/validator.spec.ts ================================================ import { describe, expect, it } from "vitest"; import { isDomain, isFilePath } from "/@/plugin/validator"; describe("domain_validator", () => { it("ok", () => { const value = ["a.cc.com", "*.zz.com", "a.cc.com"]; const v = isDomain({}, value); expect(v).to.be.true; }); it("allowDotStart", () => { let value = ["&.cc.com"]; function test() { return isDomain({ allowDotStart: true }, value); } expect(test).to.throw(Error, "域名有误:&.cc.com,请输入正确的域名"); value = ["a,cc.com"]; expect(test).to.throw(Error, "域名有误:a,cc.com,请输入正确的域名"); value = ["&cc.com"]; expect(test).to.throw(Error, "域名有误:&cc.com,请输入正确的域名"); value = [".cc.com"]; expect(test()).to.be.true; }); it("default", () => { let value = ["&.cc.com"]; function test() { return isDomain({ allowDotStart: false }, value); } expect(test).to.throw(Error, "域名有误:&.cc.com,请输入正确的域名"); value = ["&cc.com"]; expect(test).to.throw(Error, "域名有误:&cc.com,请输入正确的域名"); value = ["a,cc.com"]; expect(test).to.throw(Error, "域名有误:a,cc.com,请输入正确的域名"); value = [".cc.com"]; expect(test).to.throw(Error, "域名有误:.cc.com,请输入正确的域名"); }); it("isFilePath", () => { let value = "/a/$/bc"; function test() { return isFilePath({}, value); } expect(test()).to.be.true; value = "/a/&/bc"; expect(test()).to.be.true; //*?“<>|等特殊字符 value = "/a/&/b>c.txt"; const errorMessage = '文件名不能包含*?"<>|等特殊字符'; expect(test).to.throw(Error, errorMessage); value = "/a/&/b|等特殊符号 if (!/^[^*?"<>|]*$/.test(value)) { throw new Error(`文件名不能包含*?"<>|等特殊字符`); } return true; } Validator.register("filepath", isFilePath); ================================================ FILE: packages/ui/certd-client/src/router/access.ts ================================================ import type { ComponentRecordType, GenerateMenuAndRoutesOptions } from "/@/vben/types"; import { generateAccessible } from "/@/vben/access"; import { preferences } from "/@/vben/preferences"; import { BasicLayout, IFrameView } from "/@/vben/layouts"; const forbiddenComponent = () => import("#/views/_core/fallback/forbidden.vue"); async function generateAccess(options: GenerateMenuAndRoutesOptions) { const pageMap: ComponentRecordType = import.meta.glob("../views/**/*.vue"); const layoutMap: ComponentRecordType = { BasicLayout, IFrameView, } as any; return await generateAccessible(preferences.app.accessMode, { ...options, // fetchMenuListAsync: async () => { // message.loading({ // content: `${$t("common.loadingMenu")}...`, // duration: 1.5 // }); // return await getAllMenusApi(); // }, // 可以指定没有权限跳转403页面 forbiddenComponent, // 如果 route.meta.menuVisibleWithForbidden = true layoutMap, pageMap, }); } export { generateAccess }; ================================================ FILE: packages/ui/certd-client/src/router/guard.ts ================================================ import type { Router } from "vue-router"; import { DEFAULT_HOME_PATH, LOGIN_PATH } from "/@/vben/constants"; import { preferences } from "/@/vben/preferences"; import { useAccessStore } from "/@/vben/stores"; import { generateMenus, startProgress, stopProgress } from "/@/vben/utils"; import { frameworkRoutes } from "/@/router/resolve"; import { useSettingStore } from "/@/store/settings"; import { usePermissionStore } from "/@/plugin/permission/store.permission"; import util from "/@/plugin/permission/util.permission"; import { useUserStore } from "/@/store/user"; function buildAccessedMenus(menus: any) { if (menus == null) { return; } const list: any = []; for (const sub of menus) { if (sub.meta?.permission != null) { if (!util.hasPermissions(sub.meta.permission)) { continue; } } const item: any = { ...sub, }; list.push(item); if (sub.children && sub.children.length > 0) { item.children = buildAccessedMenus(sub.children); } } return list; } /** * 通用守卫配置 * @param router */ export function setupCommonGuard(router: Router) { // 记录已经加载的页面 const loadedPaths = new Set(); router.beforeEach(async to => { const settingStore = useSettingStore(); await settingStore.initOnce(); to.meta.loaded = loadedPaths.has(to.path); // 页面加载进度条 if (!to.meta.loaded && preferences.transition.progress) { startProgress(); } return true; }); router.afterEach(to => { // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行 loadedPaths.add(to.path); // 关闭页面加载进度条 if (preferences.transition.progress) { stopProgress(); } }); } /** * 权限访问守卫配置 * @param router */ function setupAccessGuard(router: Router) { router.beforeEach(async (to, from) => { if (to.matched && to.matched.length > 2) { to.matched.splice(1, to.matched.length - 2); } const accessStore = useAccessStore(); // 是否已经生成过动态路由 if (!accessStore.isAccessChecked) { if (accessStore.accessToken) { //如果已登录 const permissionStore = usePermissionStore(); await permissionStore.loadFromRemote(); const userStore = useUserStore(); await userStore.getUserInfoAction(); } const settingsStore = useSettingStore(); const headerMenus: any[] = settingsStore.getHeaderMenus; let allMenus = await generateMenus(frameworkRoutes[0].children, router); allMenus = allMenus.concat(headerMenus); const accessibleMenus = buildAccessedMenus(allMenus); accessStore.setAccessRoutes(frameworkRoutes); accessStore.setAccessMenus(accessibleMenus); accessStore.setIsAccessChecked(true); } // 基本路由,这些路由不需要进入权限拦截 const needAuth = to.matched.some(r => { return r.meta?.auth || r.meta?.permission; }); if (!needAuth) { return true; } if (!accessStore.accessToken) { // 没有访问权限,跳转登录页面 if (to.fullPath !== LOGIN_PATH) { return { path: LOGIN_PATH, // 如不需要,直接删除 query query: to.fullPath === DEFAULT_HOME_PATH ? {} : { redirect: encodeURIComponent(to.fullPath) }, // 携带当前跳转的页面,登录后重新跳转该页面 replace: true, }; } return true; } }); } /** * 项目守卫配置 * @param router */ function createRouterGuard(router: Router) { /** 通用 */ setupCommonGuard(router); /** 权限访问 */ setupAccessGuard(router); } export { createRouterGuard }; ================================================ FILE: packages/ui/certd-client/src/router/index.ts ================================================ import { createRouter, createWebHashHistory } from "vue-router"; // 进度条 import "nprogress/nprogress.css"; import { routes } from "./resolve"; import { createRouterGuard } from "/@/router/guard"; const router = createRouter({ history: createWebHashHistory(), routes, }); /** */ createRouterGuard(router); export default router; // // /** // * 路由拦截 // */ // router.beforeEach(async (to, from, next) => { // // 进度条 // NProgress.start(); // const settingStore = useSettingStore(); // await settingStore.initOnce(); // const resourceStore = useResourceStore(); // resourceStore.init(); // // 修复三级以上路由页面无法缓存的问题 // if (to.matched && to.matched.length > 2) { // to.matched.splice(1, to.matched.length - 2); // } // // 验证当前路由所有的匹配中是否需要有登录验证的 // if ( // to.matched.some((r) => { // return r.meta?.auth || r.meta?.permission; // }) // ) { // const userStore = useUserStore(); // // 这里暂时将cookie里是否存有token作为验证是否登录的条件 // // 请根据自身业务需要修改 // const token = userStore.getToken; // if (token) { // next(); // } else { // // 没有登录的时候跳转到登录界面 // // 携带上登陆成功之后需要跳转的页面完整路径 // resourceStore.clear(); // next({ // name: "login", // query: { // redirect: to.fullPath // } // }); // // https://github.com/d2-projects/d2-admin/issues/138 // NProgress.done(); // } // } else { // // 不需要身份校验 直接通过 // next(); // } // }); // // router.afterEach((to: any) => { // // 进度条 // NProgress.done(); // // 多页控制 打开新的页面 // const pageStore = usePageStore(); // // for (const item of to.matched) { // // pageStore.keepAlivePush(item.name); // // } // pageStore.open(to); // // 更改标题 // const settingStore = useSettingStore(); // site.title(to.meta.title, settingStore.siteInfo.title); ================================================ FILE: packages/ui/certd-client/src/router/resolve.ts ================================================ import LayoutPass from "/src/layout/layout-pass.vue"; import { cloneDeep } from "lodash-es"; import { outsideResource } from "./source/outside"; import { headerResource } from "./source/header"; import { frameworkResource } from "./source/framework"; const modules = import.meta.glob("/src/views/**/*.vue"); let index = 0; function transformOneResource(resource: any, parent: any) { let menu: any = null; if (resource.meta == null) { resource.meta = {}; } const meta = resource.meta; meta.title = meta.title ?? resource.title ?? "未命名"; if (resource.title == null) { resource.title = meta.title; } if (meta.isMenu === false) { menu = null; } else { menu = cloneDeep(resource); delete menu.component; if (menu.path?.startsWith("/")) { menu.fullPath = menu.path; } else { menu.fullPath = (parent?.fullPath || "") + "/" + menu.path; } } let route; if (meta.isRoute === false || resource.path == null) { //没有route route = null; } else { route = cloneDeep(resource); if (route.component && typeof route.component === "string") { const path = "/src/views" + route.component; route.component = modules[path]; } if (route.component == null) { route.component = LayoutPass; } if (route?.meta?.keepAlive !== true) { if (route.meta == null) { route.meta = {}; } route.meta.keepAlive = false; } } if (resource.children) { const { menus, routes } = buildMenusAndRouters(resource.children, resource); if (menu) { menu.children = menus; } if (route) { route.children = routes; } } return { menu, route, }; } export const buildMenusAndRouters = (resources: any, parent: any = null) => { const routes: Array = []; const menus: Array = []; for (const item of resources) { const { menu, route } = transformOneResource(item, parent); if (menu) { menus.push(menu); } if (route) { routes.push(route); } } setIndex(menus); return { routes, menus, }; }; function setIndex(menus: any) { for (const menu of menus) { menu.index = "index_" + index; index++; if (menu.children && menu.children.length > 0) { setIndex(menu.children); } } } function findMenus(menus: any, condition: any) { const list: any = []; for (const menu of menus) { if (condition(menu)) { list.push(menu); } if (menu.children && menu.children.length > 0) { const subList = findMenus(menu.children, condition); for (const item of subList) { list.push(item); } } } return list; } function filterMenus(menus: any, condition: any) { const list = menus.filter((item: any) => { return condition(item); }); for (const item of list) { if (item.children && item.children.length > 0) { item.children = filterMenus(item.children, condition); } } return list; } function flatChildren(list: any, children: any) { for (const child of children) { list.push(child); if (child.children && child.children.length > 0) { flatChildren(list, child.children); } child.children = null; } } function flatSubRouters(routers: any) { for (const router of routers) { const children: Array = []; if (router.children && router.children.length > 0) { flatChildren(children, router.children); } router.children = children; } return routers; } const frameworkRet = buildMenusAndRouters(frameworkResource); const outsideRet = buildMenusAndRouters(outsideResource); const headerRet = buildMenusAndRouters(headerResource); const outsideRoutes = outsideRet.routes; const frameworkRoutes = frameworkRet.routes; const routes = [...outsideRoutes, ...frameworkRoutes]; const frameworkMenus = frameworkRet.menus; const headerMenus = headerRet.menus; export { routes, outsideRoutes, frameworkRoutes, frameworkMenus, headerMenus, findMenus, filterMenus }; ================================================ FILE: packages/ui/certd-client/src/router/source/framework.ts ================================================ import LayoutBasic from "/@/layout/layout-basic.vue"; import type { RouteRecordRaw } from "vue-router"; import i18n from "/@/locales/i18n"; import { mergeRouteModules } from "/@/vben/utils"; const dynamicRouteFiles = import.meta.glob("./modules/**/*.ts*", { eager: true, }); /** 动态路由 */ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles); export const frameworkResource = [ { title: "certd.framework.title", name: "root", path: "/", redirect: "/index", component: LayoutBasic, meta: { icon: "ion:accessibility", hideInBreadcrumb: true, }, children: [ { title: "certd.framework.home", name: "index", path: "/index", component: "/framework/home/index.vue", meta: { fixedAside: true, showOnHeader: false, icon: "ion:home-outline", auth: true, }, }, // @ts-ignore ...dynamicRoutes, ], }, ]; console.assert(frameworkResource.length === 1, "frameworkResource数组长度只能为1,你只能配置framework路由的子路由"); ================================================ FILE: packages/ui/certd-client/src/router/source/header.ts ================================================ import i18n from "/@/locales/i18n"; export const headerResource = [ { title: "certd.helpDoc", path: "https://certd.docmirror.cn", meta: { icon: "ion:document-text-outline", }, }, { title: "certd.source", name: "source", key: "source", meta: { icon: "ion:git-branch-outline", }, children: [ { title: "certd.github", path: "https://github.com/certd/certd", meta: { icon: "ion:logo-github", }, }, { title: "certd.gitee", path: "https://gitee.com/certd/certd", meta: { icon: "ion:logo-octocat", }, }, ], }, ]; ================================================ FILE: packages/ui/certd-client/src/router/source/modules/about.tsx ================================================ import { IFrameView } from "/@/vben/layouts"; import { useSettingStore } from "/@/store/settings"; import { computed } from "vue"; import TutorialButton from "/@/components/tutorial/index.vue"; import i18n from "/@/locales/i18n"; export const aboutResource = [ { title: "certd.dashboard.helpDoc", name: "document", path: "/about/doc", component: IFrameView, meta: { icon: "lucide:book-open-text", link: "https://certd.docmirror.cn", title: "certd.dashboard.helpDoc", order: 9999, show: () => { const settingStore = useSettingStore(); return !settingStore.isComm; }, }, }, ]; export default aboutResource; ================================================ FILE: packages/ui/certd-client/src/router/source/modules/certd.ts ================================================ import { useSettingStore } from "/@/store/settings"; import aboutResource from "/@/router/source/modules/about"; import i18n from "/@/locales/i18n"; export const certdResources = [ { title: "certd.title", name: "CertdRoot", path: "/certd", redirect: "/certd/pipeline", meta: { icon: "ion:key-outline", auth: true, order: 0, }, children: [ { title: "certd.pipeline", name: "PipelineManager", path: "/certd/pipeline", component: "/certd/pipeline/index.vue", meta: { icon: "ion:analytics-sharp", keepAlive: true, }, }, { title: "certd.pipelineEdit", name: "PipelineEdit", path: "/certd/pipeline/detail", component: "/certd/pipeline/detail.vue", meta: { isMenu: false, }, }, { title: "certd.history", name: "PipelineHistory", path: "/certd/history", component: "/certd/history/index.vue", meta: { icon: "ion:timer-outline", keepAlive: true, }, }, { title: "certd.template.title", name: "PipelineTemplate", path: "/certd/pipeline/template", component: "/certd/pipeline/template/index.vue", meta: { isMenu: true, icon: "ion:duplicate-outline", }, }, { title: "certd.template.edit", name: "PipelineTemplateEdit", path: "/certd/pipeline/template/edit", component: "/certd/pipeline/template/edit.vue", meta: { isMenu: false, }, }, { title: "certd.template.importCreate", name: "PipelineTemplateImport", path: "/certd/pipeline/template/import", component: "/certd/pipeline/template/import/index.vue", meta: { isMenu: false, }, }, { title: "certd.certStore", name: "CertStore", path: "/certd/monitor/cert", component: "/certd/monitor/cert/index.vue", meta: { icon: "ion:shield-checkmark-outline", auth: true, isMenu: true, keepAlive: true, }, }, { title: "certd.siteMonitor", name: "SiteCertMonitor", path: "/certd/monitor/site", component: "/certd/monitor/site/index.vue", meta: { icon: "ion:videocam-outline", auth: true, keepAlive: true, }, }, { title: "certd.settings", name: "MineSetting", path: "/certd/setting", redirect: "/certd/access", meta: { icon: "ion:settings-outline", auth: true, keepAlive: true, }, children: [ { title: "certd.accessManager", name: "AccessManager", path: "/certd/access", component: "/certd/access/index.vue", meta: { icon: "ion:disc-outline", auth: true, keepAlive: true, }, }, { title: "certd.domain.domainManager", name: "DomainManager", path: "/certd/cert/domain", component: "/certd/cert/domain/index.vue", meta: { icon: "ion:globe-outline", auth: true, keepAlive: true, }, }, { title: "certd.cnameRecord", name: "CnameRecord", path: "/certd/cname/record", component: "/certd/cname/record/index.vue", meta: { icon: "ion:link-outline", auth: true, keepAlive: true, }, }, { title: "certd.subDomain", name: "SubDomain", path: "/certd/pipeline/subDomain", component: "/certd/pipeline/sub-domain/index.vue", meta: { icon: "material-symbols:approval-delegation-outline", auth: true, keepAlive: true, }, }, { title: "certd.pipelineGroup", name: "PipelineGroupManager", path: "/certd/pipeline/group", component: "/certd/pipeline/group/index.vue", meta: { icon: "mdi:format-list-group", auth: true, keepAlive: true, }, }, { title: "certd.openKey", name: "OpenKey", path: "/certd/open/openkey", component: "/certd/open/openkey/index.vue", meta: { icon: "hugeicons:api", auth: true, keepAlive: true, }, }, { title: "certd.notification", name: "NotificationManager", path: "/certd/notification", component: "/certd/notification/index.vue", meta: { icon: "ion:megaphone-outline", auth: true, keepAlive: true, }, }, { title: "certd.siteMonitorSetting", name: "SiteMonitorSetting", path: "/certd/monitor/setting", component: "/certd/monitor/site/setting/index.vue", meta: { icon: "ion:videocam-outline", auth: true, isMenu: true, }, }, { title: "certd.userSecurity", name: "UserSecurity", path: "/certd/mine/security", component: "/certd/mine/security/index.vue", meta: { icon: "fluent:shield-keyhole-16-regular", auth: true, isMenu: true, }, }, { title: "certd.userProfile", name: "UserProfile", path: "/certd/mine/user-profile", component: "/certd/mine/user-profile.vue", meta: { icon: "ion:person-outline", auth: true, isMenu: false, }, }, ], }, { title: "certd.suite", name: "SuiteProduct", path: "/certd/suite", redirect: "/certd/suite/mine", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm && settingStore.isSuiteEnabled; }, icon: "ion:cart-outline", auth: true, }, children: [ { title: "certd.mySuite", name: "MySuite", path: "/certd/suite/mine", component: "/certd/suite/mine/index.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:gift-outline", auth: true, }, }, { title: "certd.suiteBuy", name: "SuiteProductBuy", path: "/certd/suite/buy", component: "/certd/suite/buy.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:cart-outline", auth: true, }, }, { title: "certd.myTrade", name: "MyTrade", path: "/certd/trade", component: "/certd/trade/index.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:bag-check-outline", auth: true, keepAlive: true, }, }, { title: "certd.paymentReturn", name: "PaymentReturn", path: "/certd/payment/return/:type", component: "/certd/payment/return.vue", meta: { icon: "ant-design:pay-circle-outlined", auth: false, isMenu: false, }, }, ], }, ], }, ]; export default certdResources; ================================================ FILE: packages/ui/certd-client/src/router/source/modules/sys.ts ================================================ import LayoutPass from "/@/layout/layout-pass.vue"; import { useSettingStore } from "/@/store/settings"; import aboutResource from "/@/router/source/modules/about"; import i18n from "/@/locales/i18n"; export const sysResources = [ { title: "certd.sysResources.sysRoot", name: "SysRoot", path: "/sys", redirect: "/sys/settings", meta: { icon: "ion:settings-outline", permission: "sys:settings:view", order: 10, }, children: [ { title: "certd.sysResources.sysConsole", name: "SysConsole", path: "/sys/console", component: "/sys/console/index.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:speedometer-outline", permission: "sys:auth:user:view", }, }, { title: "certd.sysResources.sysSettings", name: "SysSettings", path: "/sys/settings", component: "/sys/settings/index.vue", meta: { icon: "ion:settings-outline", permission: "sys:settings:view", }, }, { title: "certd.sysResources.cnameSetting", name: "CnameSetting", path: "/sys/cname/provider", component: "/sys/cname/provider/index.vue", meta: { icon: "ion:earth-outline", permission: "sys:settings:view", keepAlive: true, }, }, { title: "certd.sysResources.emailSetting", name: "EmailSetting", path: "/sys/settings/email", component: "/sys/settings/email/index.vue", meta: { permission: "sys:settings:view", icon: "ion:mail-outline", auth: true, }, }, { title: "certd.sysResources.siteSetting", name: "SiteSetting", path: "/sys/site", component: "/sys/site/index.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:document-text-outline", permission: "sys:settings:view", }, }, { title: "certd.sysResources.headerMenus", name: "HeaderMenus", path: "/sys/settings/header-menus", component: "/sys/settings/header-menus/index.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:menu", permission: "sys:settings:view", keepAlive: true, }, }, { title: "certd.sysResources.sysAccess", name: "SysAccess", path: "/sys/access", component: "/sys/access/index.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:disc-outline", permission: "sys:settings:view", keepAlive: true, }, }, { title: "certd.sysResources.sysPlugin", name: "SysPlugin", path: "/sys/plugin", component: "/sys/plugin/index.vue", meta: { icon: "ion:extension-puzzle-outline", permission: "sys:settings:view", keepAlive: true, }, }, { title: "certd.sysResources.sysPluginEdit", name: "SysPluginEdit", path: "/sys/plugin/edit", component: "/sys/plugin/edit.vue", meta: { isMenu: false, icon: "ion:extension-puzzle", permission: "sys:settings:view", keepAlive: true, }, }, { title: "certd.sysResources.sysPluginConfig", name: "SysPluginConfig", path: "/sys/plugin/config", component: "/sys/plugin/config.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:extension-puzzle", permission: "sys:settings:view", }, }, { title: "certd.sysResources.accountBind", name: "AccountBind", path: "/sys/account", component: "/sys/account/index.vue", meta: { icon: "ion:golf-outline", permission: "sys:settings:view", keepAlive: true, }, }, { title: "certd.sysResources.permissionManager", name: "PermissionManager", path: "/sys/authority/permission", component: "/sys/authority/permission/index.vue", meta: { icon: "ion:list-outline", permission: "sys:auth:per:view", keepAlive: true, }, }, { title: "certd.sysResources.roleManager", name: "RoleManager", path: "/sys/authority/role", component: "/sys/authority/role/index.vue", meta: { icon: "ion:people-outline", permission: "sys:auth:role:view", keepAlive: true, }, }, { title: "certd.sysResources.userManager", name: "UserManager", path: "/sys/authority/user", component: "/sys/authority/user/index.vue", meta: { icon: "ion:person-outline", permission: "sys:auth:user:view", keepAlive: true, }, }, { title: "certd.sysResources.suiteManager", name: "SuiteManager", path: "/sys/suite", redirect: "/sys/suite/setting", meta: { icon: "ion:cart-outline", permission: "sys:settings:edit", show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, keepAlive: true, }, children: [ { title: "certd.sysResources.suiteSetting", name: "SuiteSetting", path: "/sys/suite/setting", component: "/sys/suite/setting/index.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:cart", permission: "sys:settings:edit", }, }, { title: "certd.sysResources.orderManager", name: "OrderManager", path: "/sys/suite/trade", component: "/sys/suite/trade/index.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:bag-check", permission: "sys:settings:edit", keepAlive: true, }, }, { title: "certd.sysResources.userSuites", name: "UserSuites", path: "/sys/suite/user-suite", component: "/sys/suite/user-suite/index.vue", meta: { show: () => { const settingStore = useSettingStore(); return settingStore.isComm; }, icon: "ion:gift-outline", auth: true, keepAlive: true, }, }, ], }, ], }, ]; export default sysResources; ================================================ FILE: packages/ui/certd-client/src/router/source/outside.ts ================================================ import LayoutOutside from "/src/layout/layout-outside.vue"; import Error404 from "/src/views/framework/error/404.vue"; const errorPage = [{ path: "/:pathMatch(.*)*", name: "not-found", component: Error404 }]; export const outsideResource = [ { title: "outside", name: "outside", path: "/outside", component: LayoutOutside, children: [ { meta: { title: "登录", }, name: "login", path: "/login", component: "/framework/login/index.vue", }, { meta: { title: "注册", }, name: "register", path: "/register", component: "/framework/register/index.vue", }, ], }, ...errorPage, ]; ================================================ FILE: packages/ui/certd-client/src/shims-vue.d.ts ================================================ declare module "*.vue" { import { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } ================================================ FILE: packages/ui/certd-client/src/store/plugin/api.plugin.ts ================================================ import { request } from "/src/api/service"; import * as _ from "lodash-es"; import { PluginConfigBean, PluginSysSetting } from "/@/views/sys/plugin/api"; const apiPrefix = "/pi/plugin"; const defaultInputDefine = { component: { name: "a-input", vModel: "modelValue", }, }; function initPlugins(plugins: any) { const checkedComponents = ["a-checkbox", "a-radio", "a-switch"]; for (const plugin of plugins) { for (const key in plugin.input) { const field = _.merge({}, defaultInputDefine, plugin.input[key]); const componentName = field.component.name; if (componentName.startsWith("a-")) { if (checkedComponents.includes(componentName)) { field.component.vModel = "checked"; } else { field.component.vModel = "value"; } } //嵌套对象 field.key = ["input", key]; if (field.required) { // delete field.required; if (field.rules == null) { field.rules = []; } field.rules.push({ required: true, message: "此项必填" }); } plugin.input[key] = field; } } } export async function GetList(query: any) { const plugins = await request({ url: apiPrefix + "/list", method: "post", params: query, }); initPlugins(plugins); return plugins; } export async function GetGroups(query: any) { const groups = await request({ url: apiPrefix + "/groups", method: "post", params: query, }); const plugins: any = []; for (const groupKey in groups) { plugins.push(...groups[groupKey].plugins); } initPlugins(plugins); return groups; } export async function GetPluginDefine(type: string) { const define = await request({ url: apiPrefix + "/getDefineByType", method: "post", data: { type, }, }); initPlugins([define]); return define; } export async function GetPluginConfig(req: { id?: number; name: string; type: string }): Promise { return await request({ url: apiPrefix + "/config", method: "post", data: req, }); } ================================================ FILE: packages/ui/certd-client/src/store/plugin/index.ts ================================================ import { defineStore } from "pinia"; import * as api from "./api.plugin"; import { DynamicType, FormItemProps } from "@fast-crud/fast-crud"; import { i18n } from "/src/locales/i18n"; interface PluginState { group?: PluginGroups; } export type PluginGroup = { key: string; title: string; desc?: string; order: number; icon: string; plugins: any[]; }; export type PluginDefine = { name: string; title: string; desc?: string; shortcut: any; input: { [key: string]: DynamicType; }; output: { [key: string]: any; }; }; export class PluginGroups { groups!: { [key: string]: PluginGroup }; map!: { [key: string]: PluginDefine }; t: any; constructor(groups: { [key: string]: PluginGroup }) { this.groups = groups; this.t = i18n.global.t; this.initGroup(groups); this.initMap(); } private initGroup(groups: { [p: string]: PluginGroup }) { const all: PluginGroup = { key: "all", title: this.t("certd.all"), order: 0, plugins: [], icon: "material-symbols:border-all-rounded", }; for (const key in groups) { all.plugins.push(...groups[key].plugins); } this.groups = { all, ...groups, }; } initMap() { const map: { [key: string]: PluginDefine } = {}; for (const key in this.groups) { const group = this.groups[key]; for (const plugin of group.plugins) { map[plugin.name] = plugin; } } this.map = map; } getGroups() { return this.groups; } get(name: string) { return this.map[name]; } getPreStepOutputOptions({ pipeline, currentStageIndex, currentTaskIndex, currentStepIndex, currentTask }: any) { const steps = this.collectionPreStepOutputs({ pipeline, currentStageIndex, currentTaskIndex, currentStepIndex, currentTask, }); const options: any[] = []; for (const step of steps) { const stepDefine = this.get(step.type); for (const key in stepDefine?.output) { const inputDefine = stepDefine.output[key]; options.push({ value: `step.${step.id}.${key}`, label: `${inputDefine.title}【from:${step.title}】`, type: step.type, valueType: inputDefine.type, key: key, }); } } return options; } collectionPreStepOutputs({ pipeline, currentStageIndex, currentTaskIndex, currentStepIndex, currentTask }: any) { const steps: any[] = []; // 开始放step for (let i = 0; i < currentStageIndex; i++) { const stage = pipeline.stages[i]; for (const task of stage.tasks) { for (const step of task.steps) { steps.push(step); } } } //当前阶段之前的task const currentStage = pipeline.stages[currentStageIndex]; for (let i = 0; i < currentTaskIndex; i++) { const task = currentStage.tasks[i]; for (const step of task.steps) { steps.push(step); } } //放当前任务下的step for (let i = 0; i < currentStepIndex; i++) { const step = currentTask.steps[i]; steps.push(step); } return steps; } } export const usePluginStore = defineStore({ id: "app.plugin", state: (): PluginState => ({ group: null, }), actions: { async reload() { const groups = await api.GetGroups({}); this.group = new PluginGroups(groups); }, async init() { if (!this.group) { await this.reload(); } return this.group; }, async getGroups(): Promise { await this.init(); return this.group as PluginGroups; }, async clear() { this.group = null; }, async getList(): Promise { await this.init(); return this.group.groups.all.plugins; }, async getPluginDefine(name: string): Promise { await this.init(); return this.group.get(name); }, async getPluginConfig(query: any) { return await api.GetPluginConfig(query); }, getPluginDefineSync(name: string) { return this.group.get(name); }, }, }); ================================================ FILE: packages/ui/certd-client/src/store/settings/api.basic.ts ================================================ import { request } from "/src/api/service"; export type SiteEnv = { agent?: { enabled?: boolean; contactText?: string; contactLink?: string; }; }; export type AppInfo = { version?: string; time?: number; deltaTime?: number; }; export type SiteInfo = { title?: string; slogan?: string; logo?: string; loginLogo?: string; icpNo?: string; licenseTo?: string; licenseToUrl?: string; }; export type PlusInfo = { vipType?: string; expireTime?: number; isPlus: boolean; isComm?: boolean; }; export type SysPublicSetting = { registerEnabled?: boolean; userValidTimeEnabled?: boolean; usernameRegisterEnabled?: boolean; mobileRegisterEnabled?: boolean; emailRegisterEnabled?: boolean; passwordLoginEnabled?: boolean; smsLoginEnabled?: boolean; limitUserPipelineCount?: number; managerOtherUserPipeline?: boolean; icpNo?: string; mpsNo?: string; robots?: boolean; aiChatEnabled?: boolean; showRunStrategy?: boolean; }; export type SuiteSetting = { enabled?: boolean; }; export type SysPrivateSetting = { httpProxy?: string; httpsProxy?: string; dnsResultOrder?: string; commonCnameEnabled?: boolean; sms?: { type?: string; config?: any; }; }; export type SysInstallInfo = { siteId: string; }; export type MenuItem = { id: string; title: string; icon?: string; path?: string; children?: MenuItem[]; }; export type HeaderMenus = { menus: MenuItem[]; }; export type AllSettings = { sysPublic: SysPublicSetting; installInfo: SysInstallInfo; plusInfo: PlusInfo; siteInfo: SiteInfo; siteEnv: SiteEnv; headerMenus: HeaderMenus; suiteSetting: SuiteSetting; app: AppInfo; }; export async function loadAllSettings(): Promise { return await request({ url: "/basic/settings/all", method: "get", }); } export async function bindUrl(data: any): Promise { return await request({ url: "/sys/plus/bindUrl", method: "post", data, }); } export async function sendSmsCode(data: any): Promise { return await request({ url: "/basic/code/sendSmsCode", method: "post", data, }); } export async function sendEmailCode(data: any): Promise { return await request({ url: "/basic/code/sendEmailCode", method: "post", data, }); } export async function getProductInfo(): Promise { return await request({ url: "/basic/settings/productInfo", method: "get", showErrorNotify: false, }); } ================================================ FILE: packages/ui/certd-client/src/store/settings/index.ts ================================================ import { defineStore } from "pinia"; import { Modal, notification } from "ant-design-vue"; import * as _ from "lodash-es"; import * as basicApi from "./api.basic"; import { AppInfo, HeaderMenus, PlusInfo, SiteEnv, SiteInfo, SuiteSetting, SysInstallInfo, SysPublicSetting } from "./api.basic"; import { useUserStore } from "../user"; import { mitter } from "/@/utils/util.mitt"; import { env } from "/@/utils/util.env"; import { updatePreferences } from "/@/vben/preferences"; import { useTitle } from "@vueuse/core"; import { utils } from "/@/utils"; import { cloneDeep, merge } from "lodash-es"; import { useI18n } from "/src/locales"; export interface SettingState { sysPublic?: SysPublicSetting; installInfo?: { siteId: string; installTime?: number; bindUserId?: number; bindUrl?: string; accountServerBaseUrl?: string; appKey?: string; }; siteInfo: SiteInfo; plusInfo?: PlusInfo; siteEnv?: SiteEnv; headerMenus?: HeaderMenus; inited?: boolean; suiteSetting?: SuiteSetting; app: { version?: string; time?: number; deltaTime?: number; }; productInfo: { notice?: string; plus: { name: string; price: number; price3: number; tooltip?: string; }; comm: { name: string; price: number; price3: number; tooltip?: string; }; }; } const defaultSiteInfo: SiteInfo = { title: env.TITLE || "Certd", slogan: env.SLOGAN || "让你的证书永不过期", logo: env.LOGO || "/static/images/logo/logo.svg", loginLogo: env.LOGIN_LOGO || "/static/images/logo/rect-block.svg", licenseTo: "", licenseToUrl: "", }; export const useSettingStore = defineStore({ id: "app.setting", state: (): SettingState => ({ plusInfo: { isPlus: false, vipType: "free", isComm: false, }, sysPublic: { registerEnabled: false, managerOtherUserPipeline: false, icpNo: env.ICP_NO || "", }, installInfo: { siteId: "", bindUserId: null, bindUrl: "", accountServerBaseUrl: "", appKey: "", }, siteInfo: defaultSiteInfo, siteEnv: { agent: { enabled: undefined, contactText: "", contactLink: "", }, }, headerMenus: { menus: [], }, suiteSetting: { enabled: false }, inited: false, app: { version: "", time: 0, deltaTime: 0, }, productInfo: { notice: "", plus: { name: "专业版", price: 29.9, price3: 89.9, }, comm: { name: "商业版", price: 399, price3: 899, }, }, }), getters: { getSysPublic(): SysPublicSetting { return this.sysPublic; }, getInstallInfo(): SysInstallInfo { return this.installInfo; }, isPlus(): boolean { return this.plusInfo?.isPlus && this.plusInfo?.expireTime > new Date().getTime(); }, isComm(): boolean { return this.plusInfo?.isComm && this.plusInfo?.expireTime > new Date().getTime(); }, isAgent(): boolean { return this.siteEnv?.agent?.enabled === true; }, isCommOrAgent() { return this.isComm || this.isAgent; }, vipLabel(): string { const { t } = useI18n(); const vipLabelMap: any = { free: t("vip.label.free"), plus: t("vip.label.plus"), comm: t("vip.label.comm"), }; return vipLabelMap[this.plusInfo?.vipType || "free"]; }, getHeaderMenus(): any[] { // @ts-ignore let menus = this.headerMenus?.menus || []; menus = cloneDeep(menus); return utils.tree.treeMap(menus, (menu: any) => { return { ...menu, name: menu.title, path: menu.path ?? "/" + menu.title, meta: { title: menu.title, icon: menu.icon, link: menu.path, order: 99999, }, }; }); }, isSuiteEnabled(): boolean { // @ts-ignore return this.suiteSetting?.enabled === true; }, }, actions: { checkPlus() { if (!this.isPlus) { notification.warn({ message: "此为专业版功能,请先升级到专业版", }); throw new Error("此为专业版功能,请升级到专业版"); } }, async loadSysSettings() { const allSettings = await basicApi.loadAllSettings(); _.merge(this.sysPublic, allSettings.sysPublic || {}); _.merge(this.installInfo, allSettings.installInfo || {}); _.merge(this.siteEnv, allSettings.siteEnv || {}); _.merge(this.plusInfo, allSettings.plusInfo || {}); _.merge(this.headerMenus, allSettings.headerMenus || {}); _.merge(this.suiteSetting, allSettings.suiteSetting || {}); //@ts-ignore this.initSiteInfo(allSettings.siteInfo || {}); this.initAppInfo(allSettings.app || {}); }, initAppInfo(appInfo: AppInfo) { this.app.time = appInfo.time; this.app.version = appInfo.version; this.app.deltaTime = new Date().getTime() - this.app.time; }, initSiteInfo(siteInfo: SiteInfo) { //@ts-ignore if (this.isComm) { if (siteInfo.logo) { siteInfo.logo = `api/basic/file/download?key=${siteInfo.logo}`; } if (siteInfo.loginLogo) { siteInfo.loginLogo = `api/basic/file/download?key=${siteInfo.loginLogo}`; } } this.siteInfo = _.merge({}, defaultSiteInfo, siteInfo); if (this.siteInfo.logo) { updatePreferences({ logo: { source: this.siteInfo.logo, }, }); } if (this.siteInfo.title) { updatePreferences({ app: { name: this.siteInfo.title, }, }); useTitle(this.siteInfo.title); } }, getBaseUrl() { let url = window.location.href; //只要hash前面的部分 url = url.split("#")[0]; return url; }, async doBindUrl() { const url = this.getBaseUrl(); await basicApi.bindUrl({ url }); await this.loadSysSettings(); }, async checkUrlBound() { const userStore = useUserStore(); const settingStore = useSettingStore(); if (!userStore.isAdmin) { return; } const bindUrl = this.installInfo.bindUrl; if (!bindUrl) { //绑定url await this.doBindUrl(); } else { //检查当前url 是否与绑定的url一致 const url = window.location.href; if (!url.startsWith(bindUrl)) { Modal.confirm({ title: "URL地址有变化", content: "以后都用这个新地址访问本系统吗?", onOk: async () => { await this.doBindUrl(); }, okText: "是的,继续", cancelText: "不是,回到原来的地址", onCancel: () => { window.location.href = bindUrl; }, }); } } }, async loadProductInfo() { try { const productInfo = await basicApi.getProductInfo(); merge(this.productInfo, productInfo); } catch (e) { console.error(e); } }, async init() { await this.loadSysSettings(); }, async initOnce() { if (this.inited) { return; } await this.init(); this.loadProductInfo(); this.inited = true; }, }, }); mitter.on("app.login", async () => { await useSettingStore().init(); }); ================================================ FILE: packages/ui/certd-client/src/store/user/api.user.ts ================================================ import { request } from "/src/api/service"; export interface RegisterReq { username: string; password: string; confirmPassword: string; } /** * @description: Login interface parameters */ export interface LoginReq { username: string; password: string; } export interface SmsLoginReq { mobile: string; phoneCode: string; smsCode: string; randomStr: string; } export interface UserInfoRes { id: string | number; username: string; nickName: string; avatar?: string; roleIds: number[]; isWeak?: boolean; validTime?: number; status?: number; } export interface LoginRes { token: string; expire: number; } export async function register(user: RegisterReq): Promise { return await request({ url: "/register", method: "post", data: user, }); } export async function logout() { return await request({ url: "/logout", method: "post", }); } export async function login(data: LoginReq): Promise { //如果开启了登录与权限模块,则真实登录 return await request({ url: "/login", method: "post", data, }); } export async function loginBySms(data: SmsLoginReq): Promise { //如果开启了登录与权限模块,则真实登录 return await request({ url: "/loginBySms", method: "post", data, }); } export async function mine(): Promise { return await request({ url: "/mine/info", method: "post", }); } export async function loginByTwoFactor(data: any) { return await request({ url: "/loginByTwoFactor", method: "post", data, }); } ================================================ FILE: packages/ui/certd-client/src/store/user/index.ts ================================================ import { defineStore } from "pinia"; import router from "../../router"; // @ts-ignore import { LocalStorage } from "/src/utils/util.storage"; // @ts-ignore import * as UserApi from "./api.user"; import { RegisterReq, SmsLoginReq } from "./api.user"; // @ts-ignore import { LoginReq, UserInfoRes } from "/@/store/user/api.user"; import { message, Modal, notification } from "ant-design-vue"; import { useI18n } from "vue-i18n"; import { mitter } from "/src/utils/util.mitt"; import { resetAllStores, useAccessStore } from "/@/vben/stores"; import { useUserStore as vbenUserStore } from "/@/vben/stores/modules/user"; interface UserState { userInfo: Nullable; token?: string; } const USER_INFO_KEY = "USER_INFO"; const TOKEN_KEY = "TOKEN"; export const useUserStore = defineStore({ id: "app.user", state: (): UserState => ({ // user info userInfo: null, // token token: undefined, }), getters: { getUserInfo(): UserInfoRes { return this.userInfo || LocalStorage.get(USER_INFO_KEY) || {}; }, getToken(): string { return this.token || LocalStorage.get(TOKEN_KEY); }, isAdmin(): boolean { return this.getUserInfo.roleIds?.includes(1) || this.getUserInfo.id === 1; }, }, actions: { setToken(token: string, expire: number) { this.token = token; const accessStore = useAccessStore(); accessStore.setAccessToken(token); LocalStorage.set(TOKEN_KEY, this.token, expire); }, setUserInfo(info: UserInfoRes) { this.userInfo = info; const userStore = vbenUserStore(); userStore.setUserInfo(info as any); LocalStorage.set(USER_INFO_KEY, info); }, resetState() { this.userInfo = null; this.token = ""; LocalStorage.remove(TOKEN_KEY); LocalStorage.remove(USER_INFO_KEY); }, async register(user: RegisterReq) { await UserApi.register(user); notification.success({ message: "注册成功,请登录", }); await router.replace("/login"); }, /** * @description: login */ async login(loginType: string, params: LoginReq | SmsLoginReq): Promise { let loginRes: any = null; if (loginType === "sms") { loginRes = await UserApi.loginBySms(params as SmsLoginReq); } else { loginRes = await UserApi.login(params as LoginReq); } return await this.onLoginSuccess(loginRes); }, async loginByTwoFactor(form: any) { const loginRes = await UserApi.loginByTwoFactor(form); return await this.onLoginSuccess(loginRes); }, async getUserInfoAction(): Promise { const userInfo = await UserApi.mine(); this.setUserInfo(userInfo); return userInfo; }, async loadUserInfo() { await this.getUserInfoAction(); }, async onLoginSuccess(loginData: any) { const { token, expire } = loginData; // save token this.setToken(token, expire); // get user info // await this.getUserInfoAction(); // const userInfo = await this.getUserInfoAction(); mitter.emit("app.login", { ...loginData }); await router.replace("/"); }, /** * @description: logout */ async logout(goLogin = true, from401 = false) { this.resetState(); resetAllStores(); if (!from401) { await UserApi.logout(); //主要是清空cookie } goLogin && router.push("/login"); mitter.emit("app.logout"); }, /** * @description: Confirm before logging out */ confirmLoginOut() { const { t } = useI18n(); Modal.confirm({ iconType: "warning", title: t("app.login.logoutTip"), content: t("app.login.logoutMessage"), onOk: async () => { await this.logout(true); }, }); }, }, }); ================================================ FILE: packages/ui/certd-client/src/style/antdv4.less ================================================ .ant-layout .ant-layout-sider { background-color: #ebf1f6; } .ant-layout .ant-layout-header { background-color: #ebf1f6; } .fs-multiple-page-control-group .fs-multiple-page-control-content { overflow-y: hidden; } .fs-multiple-page-control-group .fs-multiple-page-control-btn .ant-btn { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .ant-menu-horizontal { border-bottom: 0; } .fs-framework .header-menu { overflow-y: hidden; } .ant-btn.ant-btn-icon-only { padding-inline: revert; } .fs-form-wrapper .fs-form-header { padding-right: 30px !important; } .fs-values-format .fs-tag { display: inline-flex; .fs-icon { margin-right: 3px; } } .fs-search .ant-row { } //适配手机端 .ant-tour { max-width: 90vw; } .fs-page { .fs-page-header { background-color: hsl(var(--card)); } .fs-crud-table { background-color: hsl(var(--card)); } } footer { background-color: hsl(var(--card)) !important; } .ant-select-multiple .ant-select-selection-item-remove { display: flex; align-items: center; } .ant-progress.ant-progress-show-info .ant-progress-outer { margin-inline-end: calc(-3em - 8px); padding-inline-end: calc(3em + 8px); } .ant-progress .ant-progress-text{ width:3em; } ================================================ FILE: packages/ui/certd-client/src/style/certd.less ================================================ .fs-iconify.fs-icon { display: inline-flex; justify-content: center; align-items: center; font-size: 16px; } .fs-icon { font-size: 16px; } .ant-btn { display: inline-flex; justify-content: center; align-items: center; .fs-iconify { margin-right: 5px; } } .cd-icon-button { display: inline-flex; justify-content: center; align-items: center; cursor: pointer; } .cd-flex { display: flex; justify-content: left; align-items: center; } .cd-flex-inline { display: inline-flex; justify-content: left; align-items: center; } .ant-drawer-content { &.fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; background: #fff; } } .icon-button { cursor: pointer; font-size: 22px; } .ant-drawer { .ant-drawer-header-title { display: flex; align-items: center; .ant-drawer-title { flex: 1; display: flex; align-items: center; justify-content: space-between; } } } .settings-form { width: 800px; margin: 20px; } .fs-crud-table { .ant-table-body { height: 60vh; table { } } } body a { color: #1890ff; &:hover { color: #40a9ff; } } span.fs-icon-svg { display: inline-flex; align-items: center; } .ant-btn .fs-icon:last-child { margin-right: 0px; } .fs-iconify fs-icon { svg { vertical-align: 0 !important; } } .fs-button { span { &:first-child { margin-right: 5px; } &:last-child { margin-left: 5px; } } .fs-icon, .fs-button-icon { margin: 0 !important; } } ================================================ FILE: packages/ui/certd-client/src/style/common.less ================================================ //@import "./theme/index.less"; //@import "./theme/default.less"; @import "./scroll.less"; @import "./transition.less"; @import "./fix-windicss.less"; @import "./antdv4.less"; @import "./certd.less"; html, body { margin: 0; padding: 0; height: 100%; width: 100%; box-sizing: border-box; } div#app { height: 100%; } h1, h2, h3, h4, h5, h6 { margin-bottom: 0; margin-top: 0; } .fs-desc { font-size: 12px; color: #888888; margin-left: 5px; margin-right: 5px; } .ant-btn-link { height: 24px; } .ant-input-affix-wrapper { padding: 4px 11px; } .anticon { vertical-align: 0 !important; } .pointer { cursor: pointer; } .flex-center { display: flex; justify-content: center; align-items: center; } .flex-vc { align-items: center; } .flex-vb { align-items: baseline; } .flex-o { display: flex !important; align-items: center; } .flex-baseline { display: flex !important; align-items: baseline; } .flex-between { display: flex; justify-content: space-between; align-items: baseline; } .flex { display: flex; } .flex-inline { display: inline-flex; align-items: center; } .flex-1 { flex: 1; } .flex-0 { flex: 0; } .flex-col { display: flex; flex-direction: column; } .align-left { text-align: left; } .align-right { text-align: right; } .scroll-y { overflow-y: auto; } .m-0 { margin: 0 !important; } .m-2 { margin: 2px !important; } .m-3 { margin: 3px !important; } .m-5 { margin: 5px !important; } .m-10 { margin: 10px !important; } .m-20 { margin: 20px !important; } .mb-2 { margin-bottom: 2px !important; } .mb-5 { margin-bottom: 5px !important; } .ml-5 { margin-left: 5px !important; } .ml-10 { margin-left: 10px !important; } .ml-20 { margin-left: 20px !important; } .ml-15 { margin-left: 15px !important; } .mr-5 { margin-right: 5px !important; } .mr-10 { margin-right: 10px !important; } .mr-20 { margin-right: 20px !important; } .mr-15 { margin-right: 15px !important; } .mt-5 { margin-top: 5px !important; } .mt-10 { margin-top: 10px !important; } .mb-10 { margin-bottom: 10px !important; } .p-5 { padding: 5px !important; } .p-10 { padding: 10px !important; } .p-20 { padding: 20px !important; } .ellipsis { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .w-100 { width: 100%; } .h-100 { height: 100%; } .overflow-hidden { overflow: hidden; } .block-header { margin: 3px; padding-top: 15px; padding-bottom: 3px; border-bottom: 1px solid #dedede; } .color-plus { color: #c5913f; } .color-blue { color: #1890ff; } .color-red { color: red; } .color-green { color: green; } .color-gray { color: gray; } .iconify { //font-size: 16px; } .icon-box { display: inline-flex; align-items: center; justify-content: center; .fs-icon { display: flex; align-items: center; justify-content: center; } } .need-plus { color: #c5913f !important; } .deleted { color: #c7c7c7; //删除线 text-decoration: line-through; } .cursor-move { cursor: move !important; } .cursor-pointer { cursor: pointer; } .helper { color: #aeaeae; font-size: 12px; margin-top: 3px; margin-bottom: 3px; &.error { color: #ff4d4f; } } .fs-copyable { display: inline-flex; .text { flex: 1; } .copy-button { position: relative !important; /* right: 0; */ } } .fs-16 { font-size: 16px; } .w-50\% { width: 50%; } .ant-drawer-content-wrapper { max-width: 90vw; } .block-title { font-size: 14px; padding: 10px; color: #6e6e6e; } ================================================ FILE: packages/ui/certd-client/src/style/fix-windicss.less ================================================ img.ant-image-preview-img { display: initial; } ================================================ FILE: packages/ui/certd-client/src/style/scroll.less ================================================ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { width: 8px; background: rgba(#101f1c, 0.1); -webkit-border-radius: 2em; -moz-border-radius: 2em; border-radius: 2em; } ::-webkit-scrollbar-thumb { // background-color: rgba(#101F1C, 0.5); background-clip: padding-box; min-height: 28px; -webkit-border-radius: 2em; -moz-border-radius: 2em; border-radius: 2em; background-color: #b3b3b3; box-shadow: 0px 1px 1px #eee inset; } ::-webkit-scrollbar-thumb:hover { //background-color: rgba(#101F1C, 1); } ================================================ FILE: packages/ui/certd-client/src/style/tailwind.less ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: packages/ui/certd-client/src/style/transition.less ================================================ //.v-enter-from, //.v-leave-to { // opacity: 0; //} // //.v-leave-from, //.v-enter-to { // opacity: 1; //} // 过渡动画 横向渐变 .fade-transverse-leave-active, .fade-transverse-enter-active { transition: all 0.5s; } .fade-transverse-enter-from { opacity: 0; transform: translateX(-30px); } .fade-transverse-leave-to { opacity: 0; transform: translateX(30px); } // 过渡动画 缩放渐变 .fade-scale-leave-active, .fade-scale-enter-active { transition: all 0.3s; } .fade-scale-enter { opacity: 0; transform: scale(1.2); } .fade-scale-leave-to { opacity: 0; transform: scale(0.8); } ================================================ FILE: packages/ui/certd-client/src/style.css ================================================ /*占位,勿删*/ ================================================ FILE: packages/ui/certd-client/src/types/global.d.ts ================================================ import type { ComponentRenderProxy, VNode, ComponentPublicInstance, FunctionalComponent, PropType as VuePropType } from "vue"; declare global { const __APP_INFO__: { pkg: { name: string; version: string; dependencies: Recordable; devDependencies: Recordable; }; lastBuildTime: string; }; declare interface Window { // Global vue app instance __APP__: App; } // vue declare type PropType = VuePropType; export type Writable = { -readonly [P in keyof T]: T[P]; }; declare type Nullable = T | null; declare type NonNullable = T extends null | undefined ? never : T; declare type Recordable = Record; declare type ReadonlyRecordable = { readonly [key: string]: T; }; declare type Indexable = { [key: string]: T; }; declare type DeepPartial = { [P in keyof T]?: DeepPartial; }; declare type TimeoutHandle = ReturnType; declare type IntervalHandle = ReturnType; declare interface ChangeEvent extends Event { target: HTMLInputElement; } declare interface WheelEvent { path?: EventTarget[]; } interface ImportMetaEnv extends ViteEnv { __: unknown; } declare interface ViteEnv { VITE_PORT: number; VITE_USE_MOCK: boolean; VITE_USE_PWA: boolean; VITE_PUBLIC_PATH: string; VITE_PROXY: [string, string][]; VITE_GLOB_APP_TITLE: string; VITE_GLOB_APP_SHORT_NAME: string; VITE_USE_CDN: boolean; VITE_DROP_CONSOLE: boolean; VITE_BUILD_COMPRESS: "gzip" | "brotli" | "none"; VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean; VITE_LEGACY: boolean; VITE_USE_IMAGEMIN: boolean; VITE_GENERATE_UI: string; } declare function parseInt(s: string | number, radix?: number): number; declare function parseFloat(string: string | number): number; namespace JSX { // tslint:disable no-empty-interface type Element = VNode; // tslint:disable no-empty-interface type ElementClass = ComponentRenderProxy; interface ElementAttributesProperty { $props: any; } interface IntrinsicElements { [elem: string]: any; } interface IntrinsicAttributes { [elem: string]: any; } } } declare module "vue" { export type JSXComponent = { new (): ComponentPublicInstance } | FunctionalComponent; } ================================================ FILE: packages/ui/certd-client/src/use/use-modal.ts ================================================ import { inject } from "vue"; import type { ModalStaticFunctions } from "ant-design-vue/es/modal/confirm"; import { ModalFuncWithRef } from "ant-design-vue/es/modal/useModal"; export function useModal(): ModalStaticFunctions { return inject("modal"); } ================================================ FILE: packages/ui/certd-client/src/use/use-refrence.tsx ================================================ import * as _ from "lodash-es"; import { asyncCompute, compute } from "@fast-crud/fast-crud"; import { computed } from "vue"; export type MergeScriptContext = { compute: typeof compute; asyncCompute: typeof asyncCompute; computed: typeof computed; }; export function useReference(formItem: any) { if (formItem.mergeScript) { const ctx = { compute, asyncCompute, computed, }; const script = formItem.mergeScript; const func = new Function("ctx", script); const merged = func(ctx); _.merge(formItem, merged); delete formItem.mergeScript; } //helper if (formItem.helper && typeof formItem.helper === "string") { //正则表达式替换 [name](url) 成 let helper = formItem.helper.replace(/\[(.*)\]\((.*)\)/g, '$1'); helper = helper.replace(/\n/g, "
"); formItem.helper = { render: () => { return
; }, }; } } ================================================ FILE: packages/ui/certd-client/src/utils/index.ts ================================================ import * as envs from "./util.env"; import * as sites from "./util.site"; import * as storages from "./util.storage"; import commons from "./util.common"; import * as mitt from "./util.mitt"; import { routerUtils } from "./util.router"; import { treeUtils } from "./util.tree"; import { hashUtils } from "./util.hash"; import { amountUtils } from "./util.amount"; import { cache } from "./util.cache"; export const util = { ...envs, ...sites, ...storages, ...commons, ...mitt, router: routerUtils, tree: treeUtils, hash: hashUtils, amount: amountUtils, cache, }; export const utils = util; ================================================ FILE: packages/ui/certd-client/src/utils/util.amount.ts ================================================ export const amountUtils = { toCent(amount: number): number { return parseInt((amount * 100).toFixed(0)); }, toYuan(amount: number): number { return parseFloat((amount / 100).toFixed(2)); }, }; ================================================ FILE: packages/ui/certd-client/src/utils/util.cache.ts ================================================ export class Cache { bucket: Record = {}; async get(key: string) { return this.bucket[key]; } async set(key: string, value: any, ttl?: number) { this.bucket[key] = value; } async del(key: string) { delete this.bucket[key]; } } export const cache = new Cache(); ================================================ FILE: packages/ui/certd-client/src/utils/util.common.ts ================================================ import { isArray } from "lodash-es"; export default { arrayToMap(array: any) { if (!array) { return {}; } if (!isArray(array)) { return array; } const map: any = {}; for (const item of array) { if (item.key) { map[item.key] = item; } } return map; }, mapToArray(map: any) { if (!map) { return []; } if (isArray(map)) { return map; } const array: any = []; for (const key in map) { const item = map[key]; item.key = key; array.push(item); } return array; }, async sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); }, maxLength(str?: string, length = 100) { if (str) { return str.length > length ? str.slice(0, length) + "..." : str; } return ""; }, transformLink(desc: string = "") { if (!desc) { return ""; } return desc.replace(/\[(.*)\]\((.*)\)/g, '$1'); }, randomString(length: number) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }, }; ================================================ FILE: packages/ui/certd-client/src/utils/util.env.ts ================================================ import { forEach } from "lodash-es"; export function getEnvValue(key: string) { // @ts-ignore return import.meta.env["VITE_APP_" + key]; } export class EnvConfig { MODE: string = import.meta.env.MODE; API: string = import.meta.env.VITE_APP_API; STORAGE: string = import.meta.env.VITE_APP_STORAGE; TITLE: string = import.meta.env.VITE_APP_TITLE; SLOGAN: string = import.meta.env.VITE_APP_SLOGAN; LOGO: string = import.meta.env.VITE_APP_LOGO; LOGIN_LOGO: string = import.meta.env.VITE_APP_LOGIN_LOGO; ICP_NO: string = import.meta.env.VITE_APP_ICP_NO; COPYRIGHT_YEAR: string = import.meta.env.VITE_APP_COPYRIGHT_YEAR; COPYRIGHT_NAME: string = import.meta.env.VITE_APP_COPYRIGHT_NAME; COPYRIGHT_URL: string = import.meta.env.VITE_APP_COPYRIGHT_URL; PM_ENABLED: string = import.meta.env.VITE_APP_PM_ENABLED; init(env: any) { for (const key in this) { if (this.hasOwnProperty(key)) { this[key] = env[key]; } } } get(key: string, defaultValue: string) { //@ts-ignore return this[key] ?? defaultValue; } isDev() { return this.MODE === "development" || this.MODE === "debug"; } isProd() { return this.MODE === "production"; } } export const env = new EnvConfig(); ================================================ FILE: packages/ui/certd-client/src/utils/util.hash.ts ================================================ export const hashUtils = { md5(data: string) { throw new Error("Not implemented"); }, }; ================================================ FILE: packages/ui/certd-client/src/utils/util.mitt.ts ================================================ import mitt from "mitt"; export const mitter = mitt(); ================================================ FILE: packages/ui/certd-client/src/utils/util.router.ts ================================================ import router from "/@/router"; async function open(path: any) { if (path == null) { return; } if (path.startsWith("http://") || path.startsWith("https://")) { window.open(path); return; } try { const navigationResult = await router.push(path); if (navigationResult) { // 导航被阻止 } else { // 导航成功 (包括重新导航的情况) } } catch (e) { console.error("导航失败", e); } } export const routerUtils = { open, }; ================================================ FILE: packages/ui/certd-client/src/utils/util.site.ts ================================================ import { env } from "./util.env"; export const site = { /** * @description 更新标题 * @param titleText */ title: function (titleText: string, baseTitle?: string) { const processTitle = baseTitle || env.TITLE || "Certd"; window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`; }, }; ================================================ FILE: packages/ui/certd-client/src/utils/util.storage.ts ================================================ import { env } from "./util.env"; function isNullOrUnDef(value: any) { return value == null; } const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7; export interface CreateStorageParams { prefixKey: string; storage: Storage; timeout?: number; } /** *Cache class *Construction parameters can be passed into sessionStorage, localStorage, * @class Cache * @example */ export class WebStorage { private storage: Storage; private prefixKey?: string; private timeout?: number; /** * * @param option */ constructor(option: Partial) { this.storage = option.storage ?? localStorage; this.prefixKey = option.prefixKey ?? getStorageShortName(); this.timeout = option.timeout ?? DEFAULT_CACHE_TIME; } private getKey(key: string) { return `${this.prefixKey}${key}`.toUpperCase(); } /** * * Set cache * @param {string} key * @param {*} value * @param expire * @expire Expiration time in seconds * @memberof Cache */ set(key: string, value: any, expire: number | undefined = this.timeout) { const stringData = JSON.stringify({ value, time: Date.now(), expire: expire != null ? new Date().getTime() + expire * 1000 : null, }); this.storage.setItem(this.getKey(key), stringData); } /** *Read cache * @param {string} key * @param def * @memberof Cache */ get(key: string, def: any = null): any { const val = this.storage.getItem(this.getKey(key)); if (!val) return def; try { const data = JSON.parse(val); const { value, expire } = data; if (isNullOrUnDef(expire) || expire >= new Date().getTime()) { return value; } this.remove(key); } catch (e) { return def; } } /** * Delete cache based on key * @param {string} key * @memberof Cache */ remove(key: string) { this.storage.removeItem(this.getKey(key)); } /** * Delete all caches of this instance */ clear(): void { this.storage.clear(); } } export const createStorage = (option: Partial = {}): WebStorage => { return new WebStorage(option); }; export type Options = Partial; function getStorageShortName() { return env.MODE + "_" + env.get("STORAGE", "certd") + "_"; } export const createSessionStorage = (options: Options = {}): WebStorage => { return createStorage({ storage: sessionStorage, ...options }); }; export const createLocalStorage = (options: Options = {}): WebStorage => { return createStorage({ storage: localStorage, timeout: DEFAULT_CACHE_TIME, ...options }); }; export const SessionStorage = createSessionStorage(); export const LocalStorage = createLocalStorage(); export default WebStorage; ================================================ FILE: packages/ui/certd-client/src/utils/util.tree.ts ================================================ export function eachTree(tree: any[], callback: (item: any) => void) { tree.forEach(item => { callback(item); if (item.children) { eachTree(item.children, callback); } }); } export function treeMap(tree: any[], mapFunc: (item: any) => {}) { return tree.map((item: any) => { const newItem: any = mapFunc(item); if (item.children) { newItem.children = treeMap(item.children, mapFunc); } return newItem; }); } export const treeUtils = { eachTree, treeMap, }; ================================================ FILE: packages/ui/certd-client/src/vben/access/access-control.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/access/accessible.ts ================================================ import type { AccessModeType, GenerateMenuAndRoutesOptions, RouteRecordRaw } from "/@/vben/types"; import { cloneDeep, generateMenus, generateRoutesByBackend, generateRoutesByFrontend, mapTree } from "/@/vben/utils"; async function generateAccessible(mode: AccessModeType, options: GenerateMenuAndRoutesOptions) { const { router } = options; options.routes = cloneDeep(options.routes); // 生成路由 const accessibleRoutes = await generateRoutes(mode, options); const root = router.getRoutes().find((item: any) => item.path === "/"); // 动态添加到router实例内 accessibleRoutes.forEach(route => { if (root && !route.meta?.noBasicLayout) { // 为了兼容之前的版本用法,如果包含子路由,则将component移除,以免出现多层BasicLayout // 如果你的项目已经跟进了本次修改,移除了所有自定义菜单首级的BasicLayout,可以将这段if代码删除 if (route.children && route.children.length > 0) { delete route.component; } root.children?.push(route); } else { router.addRoute(route); } }); if (root) { if (root.name) { router.removeRoute(root.name); } router.addRoute(root); } // 生成菜单 const accessibleMenus = await generateMenus(accessibleRoutes, options.router); return { accessibleMenus, accessibleRoutes }; } /** * Generate routes * @param mode * @param options */ async function generateRoutes(mode: AccessModeType, options: GenerateMenuAndRoutesOptions) { const { forbiddenComponent, roles, routes } = options; let resultRoutes: RouteRecordRaw[] = routes; switch (mode) { case "backend": { resultRoutes = await generateRoutesByBackend(options); break; } case "frontend": { resultRoutes = await generateRoutesByFrontend(routes, roles || [], forbiddenComponent); break; } } /** * 调整路由树,做以下处理: * 1. 对未添加redirect的路由添加redirect */ resultRoutes = mapTree(resultRoutes, route => { // 如果有redirect或者没有子路由,则直接返回 if (route.redirect || !route.children || route.children.length === 0) { return route; } const firstChild = route.children[0]; // 如果子路由不是以/开头,则直接返回,这种情况需要计算全部父级的path才能得出正确的path,这里不做处理 if (!firstChild?.path || !firstChild.path.startsWith("/")) { return route; } route.redirect = firstChild.path; return route; }); return resultRoutes; } export { generateAccessible }; ================================================ FILE: packages/ui/certd-client/src/vben/access/directive.ts ================================================ /** * Global authority directive * Used for fine-grained control of component permissions * @Example v-access:role="[ROLE_NAME]" or v-access:role="ROLE_NAME" * @Example v-access:code="[ROLE_CODE]" or v-access:code="ROLE_CODE" */ import type { App, Directive, DirectiveBinding } from "vue"; import { useAccess } from "./use-access"; function isAccessible(el: Element, binding: DirectiveBinding) { const { accessMode, hasAccessByCodes, hasAccessByRoles } = useAccess(); const value = binding.value; if (!value) return; const authMethod = accessMode.value === "frontend" && binding.arg === "role" ? hasAccessByRoles : hasAccessByCodes; const values = Array.isArray(value) ? value : [value]; if (!authMethod(values)) { el?.remove(); } } const mounted = (el: Element, binding: DirectiveBinding) => { isAccessible(el, binding); }; const authDirective: Directive = { mounted, }; export function registerAccessDirective(app: App) { app.directive("access", authDirective); } ================================================ FILE: packages/ui/certd-client/src/vben/access/index.ts ================================================ export { default as AccessControl } from "./access-control.vue"; export * from "./accessible"; export * from "./directive"; export * from "./use-access"; ================================================ FILE: packages/ui/certd-client/src/vben/access/use-access.ts ================================================ import { computed } from "vue"; import { preferences, updatePreferences } from "@vben/preferences"; import { useAccessStore, useUserStore } from "@vben/stores"; function useAccess() { const accessStore = useAccessStore(); const userStore = useUserStore(); const accessMode = computed(() => { return preferences.app.accessMode; }); /** * 基于角色判断是否有权限 * @description: Determine whether there is permission,The role is judged by the user's role * @param roles */ function hasAccessByRoles(roles: string[]) { const userRoleSet = new Set(userStore.userRoles); const intersection = roles.filter(item => userRoleSet.has(item)); return intersection.length > 0; } /** * 基于权限码判断是否有权限 * @description: Determine whether there is permission,The permission code is judged by the user's permission code * @param codes */ function hasAccessByCodes(codes: string[]) { const userCodesSet = new Set(accessStore.accessCodes); const intersection = codes.filter(item => userCodesSet.has(item)); return intersection.length > 0; } async function toggleAccessMode() { updatePreferences({ app: { accessMode: preferences.app.accessMode === "frontend" ? "backend" : "frontend", }, }); } return { accessMode, hasAccessByCodes, hasAccessByRoles, toggleAccessMode, }; } export { useAccess }; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/api-component/api-component.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/api-component/index.ts ================================================ export { default as ApiComponent } from "./api-component.vue"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/hooks/useCaptchaPoints.ts ================================================ import type { CaptchaPoint } from "../types"; import { reactive } from "vue"; export function useCaptchaPoints() { const points = reactive([]); function addPoint(point: CaptchaPoint) { points.push(point); } function clearPoints() { points.splice(0, points.length); } return { addPoint, clearPoints, points, }; } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/index.ts ================================================ export { default as PointSelectionCaptcha } from "./point-selection-captcha/index.vue"; export { default as PointSelectionCaptchaCard } from "./point-selection-captcha/index.vue"; export { default as SliderCaptcha } from "./slider-captcha/index.vue"; export { default as SliderRotateCaptcha } from "./slider-rotate-captcha/index.vue"; export type * from "./types"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/point-selection-captcha/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/point-selection-captcha/point-selection-captcha-card.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/slider-captcha/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/slider-captcha/slider-captcha-action.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/slider-captcha/slider-captcha-bar.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/slider-captcha/slider-captcha-content.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/slider-rotate-captcha/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/captcha/types.ts ================================================ import type { CSSProperties } from "vue"; import type { ClassType } from "/@/vben/types"; export interface CaptchaData { /** * x */ x: number; /** * y */ y: number; /** * 时间戳 */ t: number; } export interface CaptchaPoint extends CaptchaData { /** * 数据索引 */ i: number; } export interface PointSelectionCaptchaCardProps { /** * 验证码图片 */ captchaImage: string; /** * 验证码图片高度 * @default '220px' */ height?: number | string; /** * 水平内边距 * @default '12px' */ paddingX?: number | string; /** * 垂直内边距 * @default '16px' */ paddingY?: number | string; /** * 标题 * @default '请按图依次点击' */ title?: string; /** * 验证码图片宽度 * @default '300px' */ width?: number | string; } export interface PointSelectionCaptchaProps extends PointSelectionCaptchaCardProps { /** * 是否展示确定按钮 * @default false */ showConfirm?: boolean; /** * 提示图片 * @default '' */ hintImage?: string; /** * 提示文本 * @default '' */ hintText?: string; } export interface SliderCaptchaProps { class?: ClassType; /** * @description 滑块的样式 * @default {} */ actionStyle?: CSSProperties; /** * @description 滑块条的样式 * @default {} */ barStyle?: CSSProperties; /** * @description 内容的样式 * @default {} */ contentStyle?: CSSProperties; /** * @description 组件的样式 * @default {} */ wrapperStyle?: CSSProperties; /** * @description 是否作为插槽使用,用于联动组件,可参考旋转校验组件 * @default false */ isSlot?: boolean; /** * @description 验证成功的提示 * @default '验证通过' */ successText?: string; /** * @description 提示文字 * @default '请按住滑块拖动' */ text?: string; } export interface SliderRotateCaptchaProps { /** * @description 旋转的角度 * @default 20 */ diffDegree?: number; /** * @description 图片的宽度 * @default 260 */ imageSize?: number; /** * @description 图片的样式 * @default {} */ imageWrapperStyle?: CSSProperties; /** * @description 最大旋转角度 * @default 270 */ maxDegree?: number; /** * @description 最小旋转角度 * @default 90 */ minDegree?: number; /** * @description 图片的地址 */ src?: string; /** * @description 默认提示文本 */ defaultTip?: string; } export interface CaptchaVerifyPassingData { isPassing: boolean; time: number | string; } export interface SliderCaptchaActionType { resume: () => void; } export interface SliderRotateVerifyPassingData { event: MouseEvent | TouchEvent; moveDistance: number; moveX: number; } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/col-page/col-page.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/col-page/index.ts ================================================ export { default as ColPage } from "./col-page.vue"; export * from "./types"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/col-page/types.ts ================================================ import type { PageProps } from "../page/types"; export interface ColPageProps extends PageProps { /** * 左侧宽度 * @default 30 */ leftWidth?: number; leftMinWidth?: number; leftMaxWidth?: number; leftCollapsedWidth?: number; leftCollapsible?: boolean; /** * 右侧宽度 * @default 70 */ rightWidth?: number; rightMinWidth?: number; rightCollapsedWidth?: number; rightMaxWidth?: number; rightCollapsible?: boolean; resizable?: boolean; splitLine?: boolean; splitHandle?: boolean; } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/count-to/count-to.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/count-to/index.ts ================================================ export { default as CountTo } from "./count-to.vue"; export * from "./types"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/count-to/types.ts ================================================ import type { CubicBezierPoints, EasingFunction } from "@vueuse/core"; import type { StyleValue } from "vue"; import { TransitionPresets as TransitionPresetsData } from "@vueuse/core"; export type TransitionPresets = keyof typeof TransitionPresetsData; export const TransitionPresetsKeys = Object.keys(TransitionPresetsData) as TransitionPresets[]; export interface CountToProps { /** 初始值 */ startVal?: number; /** 当前值 */ endVal: number; /** 是否禁用动画 */ disabled?: boolean; /** 延迟动画开始的时间 */ delay?: number; /** 持续时间 */ duration?: number; /** 小数位数 */ decimals?: number; /** 小数点 */ decimal?: string; /** 分隔符 */ separator?: string; /** 前缀 */ prefix?: string; /** 后缀 */ suffix?: string; /** 过渡效果 */ transition?: CubicBezierPoints | EasingFunction | TransitionPresets; /** 整数部分的类名 */ mainClass?: string; /** 小数部分的类名 */ decimalClass?: string; /** 前缀部分的类名 */ prefixClass?: string; /** 后缀部分的类名 */ suffixClass?: string; /** 整数部分的样式 */ mainStyle?: StyleValue; /** 小数部分的样式 */ decimalStyle?: StyleValue; /** 前缀部分的样式 */ prefixStyle?: StyleValue; /** 后缀部分的样式 */ suffixStyle?: StyleValue; } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/ellipsis-text/ellipsis-text.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/ellipsis-text/index.ts ================================================ export { default as EllipsisText } from "./ellipsis-text.vue"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/icon-picker/icon-picker.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/icon-picker/icons.ts ================================================ import type { Recordable } from "/@/vben/types"; /** * 一个缓存对象,在不刷新页面时,无需重复请求远程接口 */ export const ICONS_MAP: Recordable = {}; interface IconifyResponse { prefix: string; total: number; title: string; uncategorized?: string[]; categories?: Recordable; aliases?: Recordable; } const PENDING_REQUESTS: Recordable> = {}; /** * 通过Iconify接口获取图标集数据。 * 同一时间多个图标选择器同时请求同一个图标集时,实际上只会发起一次请求(所有请求共享同一份结果)。 * 请求结果会被缓存,刷新页面前同一个图标集不会再次请求 * @param prefix 图标集名称 * @returns 图标集中包含的所有图标名称 */ export async function fetchIconsData(prefix: string): Promise { if (Reflect.has(ICONS_MAP, prefix) && ICONS_MAP[prefix]) { return ICONS_MAP[prefix]; } if (Reflect.has(PENDING_REQUESTS, prefix) && PENDING_REQUESTS[prefix]) { return PENDING_REQUESTS[prefix]; } PENDING_REQUESTS[prefix] = (async () => { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1000 * 10); const response: IconifyResponse = await fetch(`https://api.iconify.design/collection?prefix=${prefix}`, { signal: controller.signal }).then(res => res.json()); clearTimeout(timeoutId); const list = response.uncategorized || []; if (response.categories) { for (const category in response.categories) { list.push(...(response.categories[category] || [])); } } ICONS_MAP[prefix] = list.map(v => `${prefix}:${v}`); } catch (error) { console.error(`Failed to fetch icons for prefix ${prefix}:`, error); return [] as string[]; } return ICONS_MAP[prefix]; })(); return PENDING_REQUESTS[prefix]; } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/icon-picker/index.ts ================================================ export { default as IconPicker } from "./icon-picker.vue"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/index.ts ================================================ export * from "./api-component"; export * from "./captcha"; export * from "./col-page"; export * from "./count-to"; export * from "./ellipsis-text"; export * from "./icon-picker"; export * from "./json-viewer"; export * from "./loading"; export * from "./page"; export * from "./resize"; export * from "./tippy"; export * from "/@/vben/form-ui"; export * from "/@/vben/popup-ui"; // 给文档用 export { VbenButton, VbenButtonGroup, VbenCheckButtonGroup, VbenCountToAnimator, VbenInputPassword, VbenLoading, VbenPinInput, VbenSpinner } from "/@/vben/shadcn-ui"; export { globalShareState } from "/@/vben/shared/global-state"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/json-viewer/index.ts ================================================ export { default as JsonViewer } from "./index.vue"; export * from "./types"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/json-viewer/index.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/json-viewer/style.scss ================================================ .default-json-theme { font-family: Consolas, Menlo, Courier, monospace; font-size: 14px; color: hsl(var(--foreground)); white-space: nowrap; background: hsl(var(--background)); &.jv-container.boxed { border: 1px solid hsl(var(--border)); } .jv-ellipsis { display: inline-block; padding: 0 4px 2px; font-size: 0.9em; line-height: 0.9; color: hsl(var(--secondary-foreground)); vertical-align: 2px; cursor: pointer; user-select: none; background-color: hsl(var(--secondary)); border-radius: 3px; } .jv-button { color: hsl(var(--primary)); } .jv-key { color: hsl(var(--heavy-foreground)); } .jv-item { &.jv-array { color: hsl(var(--heavy-foreground)); } &.jv-boolean { color: hsl(var(--red-400)); } &.jv-function { color: hsl(var(--destructive-foreground)); } &.jv-number { color: hsl(var(--info-foreground)); } &.jv-number-float { color: hsl(var(--info-foreground)); } &.jv-number-integer { color: hsl(var(--info-foreground)); } &.jv-object { color: hsl(var(--accent-darker)); } &.jv-undefined { color: hsl(var(--secondary-foreground)); } &.jv-string { color: hsl(var(--primary)); word-break: break-word; white-space: normal; } } &.jv-container .jv-code { padding: 10px; &.boxed:not(.open) { padding-bottom: 20px; margin-bottom: 10px; } &.open { padding-bottom: 10px; } .jv-toggle { &::before { padding: 0 2px; border-radius: 2px; } &:hover { &::before { background: hsl(var(--accent-foreground)); } } } } } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/json-viewer/types.ts ================================================ export interface JsonViewerProps { /** 要展示的结构数据 */ value: any; /** 展开深度 */ expandDepth?: number; /** 是否可复制 */ copyable?: boolean; /** 是否排序 */ sort?: boolean; /** 显示边框 */ boxed?: boolean; /** 主题 */ theme?: string; /** 是否展开 */ expanded?: boolean; /** 时间格式化函数 */ timeformat?: (time: Date | number | string) => string; /** 预览模式 */ previewMode?: boolean; /** 显示数组索引 */ showArrayIndex?: boolean; /** 显示双引号 */ showDoubleQuotes?: boolean; } export interface JsonViewerAction { action: string; text: string; trigger: HTMLElement; } export interface JsonViewerValue { value: any; path: string; depth: number; el: HTMLElement; } export interface JsonViewerToggle { /** 鼠标事件 */ event: MouseEvent; /** 当前展开状态 */ open: boolean; } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/loading/directive.ts ================================================ import type { App, Directive, DirectiveBinding } from "vue"; import { h, render } from "vue"; import { VbenLoading, VbenSpinner } from "/@/vben/shadcn-ui"; import { isString } from "/@/vben/shared/utils"; const LOADING_INSTANCE_KEY = Symbol("loading"); const SPINNER_INSTANCE_KEY = Symbol("spinner"); const CLASS_NAME_RELATIVE = "spinner-parent--relative"; const loadingDirective: Directive = { mounted(el, binding) { const instance = h(VbenLoading, getOptions(binding)); render(instance, el); el.classList.add(CLASS_NAME_RELATIVE); el[LOADING_INSTANCE_KEY] = instance; }, unmounted(el) { const instance = el[LOADING_INSTANCE_KEY]; el.classList.remove(CLASS_NAME_RELATIVE); render(null, el); instance.el.remove(); el[LOADING_INSTANCE_KEY] = null; }, updated(el, binding) { const instance = el[LOADING_INSTANCE_KEY]; const options = getOptions(binding); if (options && instance?.component) { try { Object.keys(options).forEach(key => { instance.component.props[key] = options[key]; }); instance.component.update(); } catch (error) { console.error("Failed to update loading component in directive:", error); } } }, }; function getOptions(binding: DirectiveBinding) { if (binding.value === undefined) { return { spinning: true }; } else if (typeof binding.value === "boolean") { return { spinning: binding.value }; } else { return { ...binding.value }; } } const spinningDirective: Directive = { mounted(el, binding) { const instance = h(VbenSpinner, getOptions(binding)); render(instance, el); el.classList.add(CLASS_NAME_RELATIVE); el[SPINNER_INSTANCE_KEY] = instance; }, unmounted(el) { const instance = el[SPINNER_INSTANCE_KEY]; el.classList.remove(CLASS_NAME_RELATIVE); render(null, el); instance.el.remove(); el[SPINNER_INSTANCE_KEY] = null; }, updated(el, binding) { const instance = el[SPINNER_INSTANCE_KEY]; const options = getOptions(binding); if (options && instance?.component) { try { Object.keys(options).forEach(key => { instance.component.props[key] = options[key]; }); instance.component.update(); } catch (error) { console.error("Failed to update spinner component in directive:", error); } } }, }; type loadingDirectiveParams = { /** 是否注册loading指令。如果提供一个string,则将指令注册为指定的名称 */ loading?: boolean | string; /** 是否注册spinning指令。如果提供一个string,则将指令注册为指定的名称 */ spinning?: boolean | string; }; /** * 注册loading指令 * @param app * @param params */ export function registerLoadingDirective(app: App, params?: loadingDirectiveParams) { // 注入一个样式供指令使用,确保容器是相对定位 const style = document.createElement("style"); style.id = CLASS_NAME_RELATIVE; style.innerHTML = ` .${CLASS_NAME_RELATIVE} { position: relative !important; } `; document.head.append(style); if (params?.loading !== false) { app.directive(isString(params?.loading) ? params.loading : "loading", loadingDirective); } if (params?.spinning !== false) { app.directive(isString(params?.spinning) ? params.spinning : "spinning", spinningDirective); } } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/loading/index.ts ================================================ export * from "./directive"; export { default as Loading } from "./loading.vue"; export { default as Spinner } from "./spinner.vue"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/loading/loading.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/loading/spinner.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/page/__tests__/page.test.ts ================================================ import { mount } from "@vue/test-utils"; import { describe, expect, it } from "vitest"; import { Page } from ".."; describe("page.vue", () => { it("renders title when passed", () => { const wrapper = mount(Page, { props: { title: "Test Title", }, }); expect(wrapper.text()).toContain("Test Title"); }); it("renders description when passed", () => { const wrapper = mount(Page, { props: { description: "Test Description", }, }); expect(wrapper.text()).toContain("Test Description"); }); it("renders default slot content", () => { const wrapper = mount(Page, { slots: { default: "

Default Slot Content

", }, }); expect(wrapper.html()).toContain("

Default Slot Content

"); }); it("renders footer slot when showFooter is true", () => { const wrapper = mount(Page, { props: { showFooter: true, }, slots: { footer: "

Footer Slot Content

", }, }); expect(wrapper.html()).toContain("

Footer Slot Content

"); }); it("applies the custom contentClass", () => { const wrapper = mount(Page, { props: { contentClass: "custom-class", }, }); const contentDiv = wrapper.find(".p-4"); expect(contentDiv.classes()).toContain("custom-class"); }); it("does not render title slot if title prop is provided", () => { const wrapper = mount(Page, { props: { title: "Test Title", }, slots: { title: "

Title Slot Content

", }, }); expect(wrapper.text()).toContain("Title Slot Content"); expect(wrapper.html()).not.toContain("Test Title"); }); it("does not render description slot if description prop is provided", () => { const wrapper = mount(Page, { props: { description: "Test Description", }, slots: { description: "

Description Slot Content

", }, }); expect(wrapper.text()).toContain("Description Slot Content"); expect(wrapper.html()).not.toContain("Test Description"); }); }); ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/page/index.ts ================================================ export { default as Page } from "./page.vue"; export * from "./types"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/page/page.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/page/types.ts ================================================ export interface PageProps { title?: string; description?: string; contentClass?: string; /** * 根据content可见高度自适应 */ autoContentHeight?: boolean; headerClass?: string; footerClass?: string; } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/resize/index.ts ================================================ export { default as VResize } from "./resize.vue"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/resize/resize.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/tippy/directive.ts ================================================ import type { ComputedRef, Directive } from "vue"; import { useTippy } from "vue-tippy"; export default function useTippyDirective(isDark: ComputedRef) { const directive: Directive = { mounted(el, binding, vnode) { const opts = typeof binding.value === "string" ? { content: binding.value } : binding.value || {}; const modifiers = Object.keys(binding.modifiers || {}); const placement = modifiers.find(modifier => modifier !== "arrow"); const withArrow = modifiers.includes("arrow"); if (placement) { opts.placement = opts.placement || placement; } if (withArrow) { opts.arrow = opts.arrow === undefined ? true : opts.arrow; } if (vnode.props && vnode.props.onTippyShow) { opts.onShow = function (...args: any[]) { return vnode.props?.onTippyShow(...args); }; } if (vnode.props && vnode.props.onTippyShown) { opts.onShown = function (...args: any[]) { return vnode.props?.onTippyShown(...args); }; } if (vnode.props && vnode.props.onTippyHidden) { opts.onHidden = function (...args: any[]) { return vnode.props?.onTippyHidden(...args); }; } if (vnode.props && vnode.props.onTippyHide) { opts.onHide = function (...args: any[]) { return vnode.props?.onTippyHide(...args); }; } if (vnode.props && vnode.props.onTippyMount) { opts.onMount = function (...args: any[]) { return vnode.props?.onTippyMount(...args); }; } if (el.getAttribute("title") && !opts.content) { opts.content = el.getAttribute("title"); el.removeAttribute("title"); } if (el.getAttribute("content") && !opts.content) { opts.content = el.getAttribute("content"); } useTippy(el, opts); }, unmounted(el) { if (el.$tippy) { el.$tippy.destroy(); } else if (el._tippy) { el._tippy.destroy(); } }, updated(el, binding) { const opts = typeof binding.value === "string" ? { content: binding.value, theme: isDark.value ? "" : "light" } : Object.assign({ theme: isDark.value ? "" : "light" }, binding.value); if (el.getAttribute("title") && !opts.content) { opts.content = el.getAttribute("title"); el.removeAttribute("title"); } if (el.getAttribute("content") && !opts.content) { opts.content = el.getAttribute("content"); } if (el.$tippy) { el.$tippy.setProps(opts || {}); } else if (el._tippy) { el._tippy.setProps(opts || {}); } }, }; return directive; } ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/components/tippy/index.ts ================================================ import type { DefaultProps, Props } from "tippy.js"; import type { App, SetupContext } from "vue"; import { h, watchEffect } from "vue"; import { setDefaultProps, Tippy as TippyComponent } from "vue-tippy"; import { usePreferences } from "/@/vben/preferences"; import useTippyDirective from "./directive"; import "tippy.js/dist/tippy.css"; import "tippy.js/dist/backdrop.css"; import "tippy.js/themes/light.css"; import "tippy.js/animations/scale.css"; import "tippy.js/animations/shift-toward.css"; import "tippy.js/animations/shift-away.css"; import "tippy.js/animations/perspective.css"; const { isDark } = usePreferences(); export type TippyProps = Partial< Props & { animation?: "fade" | "perspective" | "scale" | "shift-away" | "shift-toward" | boolean; theme?: "auto" | "dark" | "light"; } >; export function initTippy(app: App, options?: DefaultProps) { setDefaultProps({ allowHTML: true, delay: [500, 200], theme: isDark.value ? "" : "light", ...options, }); if (!options || !Reflect.has(options, "theme") || options.theme === "auto") { watchEffect(() => { setDefaultProps({ theme: isDark.value ? "" : "light" }); }); } app.directive("tippy", useTippyDirective(isDark)); } export const Tippy = (props: any, { attrs, slots }: SetupContext) => { let theme: string = (attrs.theme as string) ?? "auto"; if (theme === "auto") { theme = isDark.value ? "" : "light"; } if (theme === "dark") { theme = ""; } return h( TippyComponent, { ...props, ...attrs, theme, }, slots ); }; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/index.ts ================================================ export * from "./components"; export * from "./ui"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/about/about.ts ================================================ import type { Component } from "vue"; interface AboutProps { description?: string; name?: string; title?: string; } interface DescriptionItem { content: Component | string; title: string; } export type { AboutProps, DescriptionItem }; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/about/about.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/about/index.ts ================================================ export { default as About } from "./about.vue"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/auth-title.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/code-login.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/forget-password.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/index.ts ================================================ export { default as AuthenticationCodeLogin } from "./code-login.vue"; export { default as AuthenticationForgetPassword } from "./forget-password.vue"; export { default as AuthenticationLoginExpiredModal } from "./login-expired-modal.vue"; export { default as AuthenticationLogin } from "./login.vue"; export { default as AuthenticationQrCodeLogin } from "./qrcode-login.vue"; export { default as AuthenticationRegister } from "./register.vue"; export type { AuthenticationProps } from "./types"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/login-expired-modal.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/login.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/qrcode-login.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/register.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/third-party-login.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/authentication/types.ts ================================================ interface AuthenticationProps { /** * @zh_CN 验证码登录路径 */ codeLoginPath?: string; /** * @zh_CN 忘记密码路径 */ forgetPasswordPath?: string; /** * @zh_CN 是否处于加载处理状态 */ loading?: boolean; /** * @zh_CN 二维码登录路径 */ qrCodeLoginPath?: string; /** * @zh_CN 注册路径 */ registerPath?: string; /** * @zh_CN 是否显示验证码登录 */ showCodeLogin?: boolean; /** * @zh_CN 是否显示忘记密码 */ showForgetPassword?: boolean; /** * @zh_CN 是否显示二维码登录 */ showQrcodeLogin?: boolean; /** * @zh_CN 是否显示注册按钮 */ showRegister?: boolean; /** * @zh_CN 是否显示记住账号 */ showRememberMe?: boolean; /** * @zh_CN 是否显示第三方登录 */ showThirdPartyLogin?: boolean; /** * @zh_CN 登录框子标题 */ subTitle?: string; /** * @zh_CN 登录框标题 */ title?: string; /** * @zh_CN 提交按钮文本 */ submitButtonText?: string; } export type { AuthenticationProps }; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/analysis/analysis-chart-card.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/analysis/analysis-charts-tabs.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/analysis/analysis-overview.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/analysis/index.ts ================================================ export { default as AnalysisChartCard } from "./analysis-chart-card.vue"; export { default as AnalysisChartsTabs } from "./analysis-charts-tabs.vue"; export { default as AnalysisOverview } from "./analysis-overview.vue"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/index.ts ================================================ export * from "./analysis"; export type * from "./typing"; export * from "./workbench"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/typing.ts ================================================ import type { Component } from "vue"; interface AnalysisOverviewItem { icon: Component | string; title: string; totalTitle: string; totalValue: number; value: number; } interface WorkbenchProjectItem { color?: string; content: string; date: string; group: string; icon: Component | string; title: string; url?: string; } interface WorkbenchTrendItem { avatar: string; content: string; date: string; title: string; } interface WorkbenchTodoItem { completed: boolean; content: string; date: string; title: string; } interface WorkbenchQuickNavItem { color?: string; icon: Component | string; title: string; url?: string; } export type { AnalysisOverviewItem, WorkbenchProjectItem, WorkbenchQuickNavItem, WorkbenchTodoItem, WorkbenchTrendItem }; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/workbench/index.ts ================================================ export { default as WorkbenchHeader } from "./workbench-header.vue"; export { default as WorkbenchProject } from "./workbench-project.vue"; export { default as WorkbenchQuickNav } from "./workbench-quick-nav.vue"; export { default as WorkbenchTodo } from "./workbench-todo.vue"; export { default as WorkbenchTrends } from "./workbench-trends.vue"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/workbench/workbench-header.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/workbench/workbench-project.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/workbench/workbench-quick-nav.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/workbench/workbench-todo.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/dashboard/workbench/workbench-trends.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/fallback/fallback.ts ================================================ interface FallbackProps { /** * 描述 */ description?: string; /** * @zh_CN 首页路由地址 * @default / */ homePath?: string; /** * @zh_CN 默认显示的图片 * @default pageNotFoundSvg */ image?: string; /** * @zh_CN 内置类型 */ status?: "403" | "404" | "500" | "coming-soon" | "offline"; /** * @zh_CN 页面提示语 */ title?: string; } export type { FallbackProps }; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/fallback/fallback.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/fallback/icons/icon-403.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/fallback/icons/icon-404.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/fallback/icons/icon-500.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/fallback/icons/icon-coming-soon.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/fallback/icons/icon-offline.vue ================================================ ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/fallback/index.ts ================================================ export type * from "./fallback"; export { default as Fallback } from "./fallback.vue"; ================================================ FILE: packages/ui/certd-client/src/vben/common-ui/ui/index.ts ================================================ export * from "./about"; export * from "./authentication"; export * from "./dashboard"; export * from "./fallback"; ================================================ FILE: packages/ui/certd-client/src/vben/composables/__tests__/use-sortable.test.ts ================================================ import type { SortableOptions } from "sortablejs"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { useSortable } from "../use-sortable"; describe("useSortable", () => { beforeEach(() => { vi.mock("sortablejs/modular/sortable.complete.esm.js", () => ({ default: { create: vi.fn(), }, })); }); it("should call Sortable.create with the correct options", async () => { // Create a mock element const mockElement = document.createElement("div") as HTMLDivElement; // Define custom options const customOptions: SortableOptions = { group: "test-group", sort: false, }; // Use the useSortable function const { initializeSortable } = useSortable(mockElement, customOptions); // Initialize sortable await initializeSortable(); // Import sortablejs to access the mocked create function const Sortable = await import("sortablejs/modular/sortable.complete.esm.js"); // Verify that Sortable.create was called with the correct parameters expect(Sortable.default.create).toHaveBeenCalledTimes(1); expect(Sortable.default.create).toHaveBeenCalledWith( mockElement, expect.objectContaining({ animation: 300, delay: 400, delayOnTouchOnly: true, ...customOptions, }) ); }); }); ================================================ FILE: packages/ui/certd-client/src/vben/composables/index.ts ================================================ export * from "./use-is-mobile"; export * from "./use-layout-style"; export * from "./use-namespace"; export * from "./use-priority-value"; export * from "./use-scroll-lock"; export * from "./use-simple-locale"; export * from "./use-sortable"; export { useEmitAsProps, useForwardExpose, useForwardProps, useForwardPropsEmits } from "radix-vue"; ================================================ FILE: packages/ui/certd-client/src/vben/composables/use-is-mobile.ts ================================================ import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"; export function useIsMobile() { const breakpoints = useBreakpoints(breakpointsTailwind); const isMobile = breakpoints.smaller("md"); return { isMobile }; } ================================================ FILE: packages/ui/certd-client/src/vben/composables/use-layout-style.ts ================================================ import type { CSSProperties } from "vue"; import type { VisibleDomRect } from "/@/vben/shared/utils"; import { computed, onMounted, onUnmounted, ref } from "vue"; import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT, CSS_VARIABLE_LAYOUT_CONTENT_WIDTH, CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT, CSS_VARIABLE_LAYOUT_HEADER_HEIGHT } from "/@/vben/shared/constants"; import { getElementVisibleRect } from "/@/vben/shared/utils"; import { useCssVar, useDebounceFn } from "@vueuse/core"; /** * @zh_CN content style */ export function useLayoutContentStyle() { let resizeObserver: null | ResizeObserver = null; const contentElement = ref(null); const visibleDomRect = ref(null); const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT); const contentWidth = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_WIDTH); const overlayStyle = computed((): CSSProperties => { const { height, left, top, width } = visibleDomRect.value ?? {}; return { height: `${height}px`, left: `${left}px`, position: "fixed", top: `${top}px`, width: `${width}px`, zIndex: 150, }; }); const debouncedCalcHeight = useDebounceFn((_entries: ResizeObserverEntry[]) => { visibleDomRect.value = getElementVisibleRect(contentElement.value); contentHeight.value = `${visibleDomRect.value.height}px`; contentWidth.value = `${visibleDomRect.value.width}px`; }, 16); onMounted(() => { if (contentElement.value && !resizeObserver) { resizeObserver = new ResizeObserver(debouncedCalcHeight); resizeObserver.observe(contentElement.value); } }); onUnmounted(() => { resizeObserver?.disconnect(); resizeObserver = null; }); return { contentElement, overlayStyle, visibleDomRect }; } export function useLayoutHeaderStyle() { const headerHeight = useCssVar(CSS_VARIABLE_LAYOUT_HEADER_HEIGHT); return { getLayoutHeaderHeight: () => { return Number.parseInt(`${headerHeight.value}`, 10); }, setLayoutHeaderHeight: (height: number) => { headerHeight.value = `${height}px`; }, }; } export function useLayoutFooterStyle() { const footerHeight = useCssVar(CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT); return { getLayoutFooterHeight: () => { return Number.parseInt(`${footerHeight.value}`, 10); }, setLayoutFooterHeight: (height: number) => { footerHeight.value = `${height}px`; }, }; } ================================================ FILE: packages/ui/certd-client/src/vben/composables/use-namespace.ts ================================================ import { DEFAULT_NAMESPACE } from "../shared/constants"; /** * @see copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-namespace/index.ts */ const statePrefix = "is-"; const _bem = (namespace: string, block: string, blockSuffix: string, element: string, modifier: string) => { let cls = `${namespace}-${block}`; if (blockSuffix) { cls += `-${blockSuffix}`; } if (element) { cls += `__${element}`; } if (modifier) { cls += `--${modifier}`; } return cls; }; const is: { (name: string): string; // eslint-disable-next-line @typescript-eslint/unified-signatures (name: string, state: boolean | undefined): string; } = (name: string, ...args: [] | [boolean | undefined]) => { const state = args.length > 0 ? args[0] : true; return name && state ? `${statePrefix}${name}` : ""; }; const useNamespace = (block: string) => { const namespace = DEFAULT_NAMESPACE; const b = (blockSuffix = "") => _bem(namespace, block, blockSuffix, "", ""); const e = (element?: string) => (element ? _bem(namespace, block, "", element, "") : ""); const m = (modifier?: string) => (modifier ? _bem(namespace, block, "", "", modifier) : ""); const be = (blockSuffix?: string, element?: string) => (blockSuffix && element ? _bem(namespace, block, blockSuffix, element, "") : ""); const em = (element?: string, modifier?: string) => (element && modifier ? _bem(namespace, block, "", element, modifier) : ""); const bm = (blockSuffix?: string, modifier?: string) => (blockSuffix && modifier ? _bem(namespace, block, blockSuffix, "", modifier) : ""); const bem = (blockSuffix?: string, element?: string, modifier?: string) => (blockSuffix && element && modifier ? _bem(namespace, block, blockSuffix, element, modifier) : ""); // for css var // --el-xxx: value; const cssVar = (object: Record) => { const styles: Record = {}; for (const key in object) { if (object[key]) { styles[`--${namespace}-${key}`] = object[key]; } } return styles; }; // with block const cssVarBlock = (object: Record) => { const styles: Record = {}; for (const key in object) { if (object[key]) { styles[`--${namespace}-${block}-${key}`] = object[key]; } } return styles; }; const cssVarName = (name: string) => `--${namespace}-${name}`; const cssVarBlockName = (name: string) => `--${namespace}-${block}-${name}`; return { b, be, bem, bm, // css cssVar, cssVarBlock, cssVarBlockName, cssVarName, e, em, is, m, namespace, }; }; type UseNamespaceReturn = ReturnType; export type { UseNamespaceReturn }; export { useNamespace }; ================================================ FILE: packages/ui/certd-client/src/vben/composables/use-priority-value.ts ================================================ import type { ComputedRef, Ref } from "vue"; import { computed, getCurrentInstance, unref, useAttrs, useSlots } from "vue"; import { getFirstNonNullOrUndefined, kebabToCamelCase } from "../shared/utils"; /** * 依次从插槽、attrs、props、state 中获取值 * @param key * @param props * @param state */ export function usePriorityValue, S extends Record, K extends keyof T = keyof T>(key: K, props: T, state: Readonly>> | undefined) { const instance = getCurrentInstance(); const slots = useSlots(); const attrs = useAttrs() as T; const value = computed((): T[K] => { // props不管有没有传,都会有默认值,会影响这里的顺序, // 通过判断原始props是否有值来判断是否传入 const rawProps = (instance?.vnode?.props || {}) as T; const standardRawProps = {} as T; for (const [key, value] of Object.entries(rawProps)) { standardRawProps[kebabToCamelCase(key) as K] = value; } const propsKey = standardRawProps?.[key] === undefined ? undefined : props[key]; // slot可以关闭 return getFirstNonNullOrUndefined(slots[key as string], attrs[key], propsKey, state?.value?.[key as keyof S]) as T[K]; }); return value; } /** * 批量获取state中的值(每个值都是ref) * @param props * @param state */ export function usePriorityValues, S extends Ref> = Readonly, NoInfer>>>(props: T, state: S | undefined) { const result: { [K in keyof T]: ComputedRef } = {} as never; (Object.keys(props) as (keyof T)[]).forEach(key => { result[key] = usePriorityValue(key as keyof typeof props, props, state); }); return result; } /** * 批量获取state中的值(集中在一个computed,用于透传) * @param props * @param state */ export function useForwardPriorityValues, S extends Ref> = Readonly, NoInfer>>>(props: T, state: S | undefined) { const computedResult: { [K in keyof T]: ComputedRef } = {} as never; (Object.keys(props) as (keyof T)[]).forEach(key => { computedResult[key] = usePriorityValue(key as keyof typeof props, props, state); }); return computed(() => { const unwrapResult: Record = {}; Object.keys(props).forEach(key => { unwrapResult[key] = unref(computedResult[key]); }); return unwrapResult as { [K in keyof T]: T[K] }; }); } ================================================ FILE: packages/ui/certd-client/src/vben/composables/use-scroll-lock.ts ================================================ import { getScrollbarWidth, needsScrollbar } from "../shared/utils"; import { useScrollLock as _useScrollLock, tryOnBeforeUnmount, tryOnMounted } from "@vueuse/core"; export const SCROLL_FIXED_CLASS = `_scroll__fixed_`; export function useScrollLock() { const isLocked = _useScrollLock(document.body); const scrollbarWidth = getScrollbarWidth(); tryOnMounted(() => { if (!needsScrollbar()) { return; } document.body.style.paddingRight = `${scrollbarWidth}px`; const layoutFixedNodes = document.querySelectorAll(`.${SCROLL_FIXED_CLASS}`); const nodes = [...layoutFixedNodes]; if (nodes.length > 0) { nodes.forEach(node => { node.dataset.transition = node.style.transition; node.style.transition = "none"; node.style.paddingRight = `${scrollbarWidth}px`; }); } isLocked.value = true; }); tryOnBeforeUnmount(() => { if (!needsScrollbar()) { return; } isLocked.value = false; const layoutFixedNodes = document.querySelectorAll(`.${SCROLL_FIXED_CLASS}`); const nodes = [...layoutFixedNodes]; if (nodes.length > 0) { nodes.forEach(node => { node.style.paddingRight = ""; requestAnimationFrame(() => { node.style.transition = node.dataset.transition || ""; }); }); } document.body.style.paddingRight = ""; }); } ================================================ FILE: packages/ui/certd-client/src/vben/composables/use-simple-locale/README.md ================================================ # Simple i18n Simple i18 implementation ================================================ FILE: packages/ui/certd-client/src/vben/composables/use-simple-locale/index.ts ================================================ import type { Locale } from "./messages"; import { computed, ref } from "vue"; import { createSharedComposable } from "@vueuse/core"; import { getMessages } from "./messages"; export const useSimpleLocale = createSharedComposable(() => { const currentLocale = ref("zh-CN"); const setSimpleLocale = (locale: Locale) => { currentLocale.value = locale; }; const $t = computed(() => { const localeMessages = getMessages(currentLocale.value); return (key: string) => { return localeMessages[key] || key; }; }); return { $t, currentLocale, setSimpleLocale, }; }); ================================================ FILE: packages/ui/certd-client/src/vben/composables/use-simple-locale/messages.ts ================================================ export type Locale = "en-US" | "zh-CN"; export const messages: Record> = { "en-US": { cancel: "Cancel", collapse: "Collapse", confirm: "Confirm", expand: "Expand", reset: "Reset", submit: "Submit", }, "zh-CN": { cancel: "取消", collapse: "收起", confirm: "确认", expand: "展开", reset: "重置", submit: "提交", }, }; export const getMessages = (locale: Locale) => messages[locale]; ================================================ FILE: packages/ui/certd-client/src/vben/composables/use-sortable.ts ================================================ import type { SortableOptions } from "sortablejs"; import type Sortable from "sortablejs"; function useSortable(sortableContainer: T, options: SortableOptions = {}) { const initializeSortable = async () => { const Sortable = await import( // @ts-expect-error - This is a dynamic import "sortablejs/modular/sortable.complete.esm.js" ); const sortable = Sortable?.default?.create?.(sortableContainer, { animation: 300, delay: 400, delayOnTouchOnly: true, ...options, }); return sortable as Sortable; }; return { initializeSortable, }; } export { useSortable }; export type { Sortable }; ================================================ FILE: packages/ui/certd-client/src/vben/constants/core.ts ================================================ /** * @zh_CN 登录页面 url 地址 */ export const LOGIN_PATH = "/login"; /** * @zh_CN 默认首页地址 */ export const DEFAULT_HOME_PATH = "/"; export interface LanguageOption { label: string; value: "en-US" | "zh-CN"; } /** * Supported languages */ export const SUPPORT_LANGUAGES: LanguageOption[] = [ { label: "简体中文", value: "zh-CN", }, { label: "English", value: "en-US", }, ]; ================================================ FILE: packages/ui/certd-client/src/vben/constants/globals.ts ================================================ /** layout content 组件的高度 */ export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`; /** layout content 组件的宽度 */ export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`; /** layout header 组件的高度 */ export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`; /** layout footer 组件的高度 */ export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`; /** 内容区域的组件ID */ export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`; /** * @zh_CN 默认命名空间 */ export const DEFAULT_NAMESPACE = "vben"; ================================================ FILE: packages/ui/certd-client/src/vben/constants/index.ts ================================================ export * from "./globals"; export * from "./vben"; export * from "./core"; ================================================ FILE: packages/ui/certd-client/src/vben/constants/vben.ts ================================================ /** * @zh_CN GITHUB 仓库地址 */ export const VBEN_GITHUB_URL = "https://github.com/vbenjs/vue-vben-admin"; /** * @zh_CN 文档地址 */ export const VBEN_DOC_URL = "https://doc.vben.pro"; /** * @zh_CN Vben Logo */ export const VBEN_LOGO_URL = "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp"; /** * @zh_CN Vben Admin 首页地址 */ export const VBEN_PREVIEW_URL = "https://www.vben.pro"; export const VBEN_ELE_PREVIEW_URL = "https://ele.vben.pro"; export const VBEN_NAIVE_PREVIEW_URL = "https://naive.vben.pro"; export const VBEN_ANT_PREVIEW_URL = "https://ant.vben.pro"; ================================================ FILE: packages/ui/certd-client/src/vben/design/css/global.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { *, ::after, ::before { @apply border-border; box-sizing: border-box; border-style: solid; border-width: 0; } html { @apply text-foreground bg-background font-sans text-[100%]; font-variation-settings: normal; line-height: 1.5; text-size-adjust: 100%; font-synthesis-weight: none; scroll-behavior: smooth; text-rendering: optimizelegibility; -webkit-tap-highlight-color: transparent; /* -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; */ } #app, body, html { @apply size-full; /* scrollbar-gutter: stable; */ } body { min-height: 100vh; /* pointer-events: auto !important; */ /* overflow: overlay; */ /* -webkit-font-smoothing: antialiased; */ /* -moz-osx-font-smoothing: grayscale; */ } a, a:active, a:hover, a:link, a:visited { @apply no-underline; } ::view-transition-new(root), ::view-transition-old(root) { @apply animate-none mix-blend-normal; } ::view-transition-old(root) { @apply z-[1]; } ::view-transition-new(root) { @apply z-[2147483646]; } html.dark::view-transition-old(root) { @apply z-[2147483646]; } html.dark::view-transition-new(root) { @apply z-[1]; } input::placeholder, textarea::placeholder { @apply opacity-100; } /* input:-webkit-autofill { @apply border-none; box-shadow: 0 0 0 1000px transparent inset; } */ input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { @apply m-0 appearance-none; } /* 只有非mac下才进行调整,mac下使用默认滚动条 */ html:not([data-platform="macOs"]) { ::-webkit-scrollbar { @apply h-[10px] w-[10px]; } ::-webkit-scrollbar-thumb { @apply bg-border rounded-sm border-none; } ::-webkit-scrollbar-track { @apply rounded-sm border-none bg-transparent shadow-none; } ::-webkit-scrollbar-button { @apply hidden; } } } @layer components { .flex-center { @apply flex items-center justify-center; } .flex-col-center { @apply flex flex-col items-center justify-center; } .outline-box { @apply outline-border relative cursor-pointer rounded-md p-1 outline outline-1; } .outline-box::after { @apply absolute left-1/2 top-1/2 z-20 h-0 w-[1px] rounded-sm opacity-0 outline outline-2 outline-transparent transition-all duration-300 content-[""]; } .outline-box.outline-box-active { @apply outline-primary outline outline-2; } .outline-box.outline-box-active::after { display: none; } .outline-box:not(.outline-box-active):hover::after { @apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100; } .vben-link { @apply text-primary hover:text-primary-hover active:text-primary-active cursor-pointer; } .card-box { @apply bg-card text-card-foreground border-border rounded-xl border; } } html.invert-mode { @apply invert; } html.grayscale-mode { @apply grayscale; } ================================================ FILE: packages/ui/certd-client/src/vben/design/css/nprogress.css ================================================ /* Make clicks pass-through */ #nprogress { @apply pointer-events-none; } #nprogress .bar { @apply bg-primary fixed left-0 top-0 z-[1031] h-[2px] w-full; } /* Fancy blur effect */ #nprogress .peg { @apply absolute right-0 block h-full w-[100px]; box-shadow: 0 0 10px hsl(var(--primary)), 0 0 5px hsl(var(--primary)); opacity: 1; transform: rotate(3deg) translate(0, -4px); } /* Remove these to get rid of the spinner */ #nprogress .spinner { @apply fixed right-4 top-4 z-[1031] block; } #nprogress .spinner-icon { @apply border-t-primary border-l-primary size-4 rounded-full border-[2px] border-solid border-transparent; animation: nprogress-spinner 400ms linear infinite; } .nprogress-custom-parent { @apply relative overflow-hidden; } .nprogress-custom-parent #nprogress .spinner, .nprogress-custom-parent #nprogress .bar { @apply absolute; } @keyframes nprogress-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes nprogress-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ================================================ FILE: packages/ui/certd-client/src/vben/design/css/transition.css ================================================ .slide-up-enter-active, .slide-up-leave-active { transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1); } .slide-up-move { transition: transform 0.3s; } .slide-up-enter-from, .slide-up-leave-to { opacity: 0; transform: translateY(-15px); } .slide-down-enter-active, .slide-down-leave-active { transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1); } .slide-down-move { transition: transform 0.3s; } .slide-down-enter-from, .slide-down-leave-to { opacity: 0; transform: translateY(15px); } .slide-left-enter-active, .slide-left-leave-active { transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1); } .slide-left-move { transition: transform 0.3s; } .slide-left-enter-from, .slide-left-leave-to { opacity: 0; transform: translate(-15px); } .slide-right-enter-active, .slide-right-leave-active { transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1); } .slide-right-move { transition: transform 0.3s; } .slide-right-enter-from, .slide-right-leave-to { opacity: 0; transform: translate(15px); } .fade-transition-enter-active, .fade-transition-leave-active { transition: opacity 0.2s ease-in-out; } .fade-transition-enter-from, .fade-transition-leave-to { opacity: 0; } .fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease-in-out; } .fade-enter-from, .fade-leave-to { opacity: 0; } .fade-slide-leave-active, .fade-slide-enter-active { transition: all 0.3s; } .fade-slide-enter-from { opacity: 0; transform: translate(-30px); } .fade-slide-leave-to { opacity: 0; transform: translate(30px); } .fade-down-enter-active, .fade-down-leave-active { transition: opacity 0.25s, transform 0.3s; } .fade-down-enter-from { opacity: 0; transform: translateY(-10%); } .fade-down-leave-to { opacity: 0; transform: translateY(10%); } .fade-scale-leave-active, .fade-scale-enter-active { transition: all 0.28s; } .fade-scale-enter-from { opacity: 0; transform: scale(1.2); } .fade-scale-leave-to { opacity: 0; transform: scale(0.8); } .fade-up-enter-active, .fade-up-leave-active { transition: opacity 0.2s, transform 0.25s; } .fade-up-enter-from { opacity: 0; transform: translateY(10%); } .fade-up-leave-to { opacity: 0; transform: translateY(-10%); } @keyframes fade-slide { 0% { opacity: 0; transform: translate(-30px); } 50% { opacity: 1; } 100% { opacity: 0; transform: translate(30px); } } @keyframes fade { 0% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0; } } @keyframes fade-up { 0% { opacity: 0; transform: translateY(10%); } 50% { opacity: 1; } 100% { opacity: 0; transform: translateY(-10%); } } @keyframes fade-down { 0% { opacity: 0; transform: translateY(-10%); } 50% { opacity: 1; } 100% { opacity: 0; transform: translateY(10%); } } .fade-slow { animation: fade 3s infinite; } .fade-slide-slow { animation: fade-slide 3s infinite; } .fade-up-slow { animation: fade-up 3s infinite; } .fade-down-slow { animation: fade-down 3s infinite; } .collapse-transition { transition: 0.2s height ease-in-out, 0.2s padding-top ease-in-out, 0.2s padding-bottom ease-in-out; } .collapse-transition-leave-active, .collapse-transition-enter-active { transition: 0.2s max-height ease-in-out, 0.2s padding-top ease-in-out, 0.2s margin-top ease-in-out; } ================================================ FILE: packages/ui/certd-client/src/vben/design/css/ui.css ================================================ .side-content { animation-duration: 0.2s; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); } .side-content[data-side="top"] { animation-name: slide-up; } .side-content[data-side="bottom"] { animation-name: slide-down; } .side-content[data-side="left"] { animation-name: slide-left; } .side-content[data-side="right"] { animation-name: slide-right; } .breadcrumb-transition-enter-active { transition: transform 0.4s cubic-bezier(0.76, 0, 0.24, 1), opacity 0.4s cubic-bezier(0.76, 0, 0.24, 1); } .breadcrumb-transition-leave-active { display: none; } .breadcrumb-transition-enter-from { opacity: 0; transform: translateX(30px) skewX(-30deg); } @keyframes slide-down { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } @keyframes slide-left { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } } @keyframes slide-right { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } } @keyframes slide-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .z-popup { z-index: var(--popup-z-index); } ================================================ FILE: packages/ui/certd-client/src/vben/design/design-tokens/dark.css ================================================ .dark, .dark[data-theme="custom"], .dark[data-theme="default"] { /* Default background color of ...etc */ --background: 222.34deg 10.43% 12.27%; /* 主体区域背景色 */ --background-deep: 220deg 13.06% 9%; --foreground: 0 0% 95%; /* Background color for */ --card: 222.34deg 10.43% 12.27%; /* --card: 222.2 84% 4.9%; */ --card-foreground: 210 40% 98%; /* Background color for popovers such as , , */ /* --popover: 222.82deg 8.43% 12.27%; */ /* 弹出层的背景色与主题区域背景色太过接近 */ --popover: 0 0 14.2%; --popover-foreground: 210 40% 98%; /* Muted backgrounds such as , and */ /* --muted: 220deg 6.82% 17.25%; */ /* --muted-foreground: 215 20.2% 65.1%; */ --muted: 240 3.7% 15.9%; --muted-foreground: 240 5% 64.9%; /* 主题颜色 */ /* --primary: 245 82% 67%; */ --primary-foreground: 0 0% 98%; /* Used for destructive actions such as