From 56ff9e63442db48391870ef3bc131d790ad679b7 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sun, 23 Mar 2025 22:13:45 +0800 Subject: [PATCH] feat: add porkbun dns-01 applicant --- go.mod | 1 + go.sum | 2 + internal/applicant/providers.go | 17 ++++ internal/domain/access.go | 5 ++ internal/domain/provider.go | 6 ++ .../lego-providers/porkbun/porkbun.go | 38 +++++++++ .../lego-providers/powerdns/powerdns.go | 2 +- migrations/1742644800_upgrade.go | 83 ++++++++++++++++++- ui/public/imgs/providers/porkbun.svg | 1 + ui/src/components/access/AccessForm.tsx | 3 + .../access/AccessFormPorkbunConfig.tsx | 76 +++++++++++++++++ ui/src/domain/access.ts | 6 ++ ui/src/domain/provider.ts | 8 ++ ui/src/i18n/locales/en/nls.access.json | 6 ++ ui/src/i18n/locales/en/nls.provider.json | 3 + ui/src/i18n/locales/zh/nls.access.json | 6 ++ ui/src/i18n/locales/zh/nls.provider.json | 3 + 17 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 internal/pkg/core/applicant/acme-dns-01/lego-providers/porkbun/porkbun.go create mode 100644 ui/public/imgs/providers/porkbun.svg create mode 100644 ui/src/components/access/AccessFormPorkbunConfig.tsx diff --git a/go.mod b/go.mod index 190cc75c..8623edb0 100644 --- a/go.mod +++ b/go.mod @@ -106,6 +106,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/nrdcg/mailinabox v0.2.0 // indirect + github.com/nrdcg/porkbun v0.4.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/qiniu/dyn v1.3.0 // indirect github.com/qiniu/x v1.10.5 // indirect diff --git a/go.sum b/go.sum index 8c79203b..41ce2a0e 100644 --- a/go.sum +++ b/go.sum @@ -650,6 +650,8 @@ github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CF github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc= github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= +github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= +github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index dcfc1dde..5119f86d 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -25,6 +25,7 @@ import ( pNameDotCom "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namedotcom" pNameSilo "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namesilo" pNS1 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ns1" + pPorkbun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/porkbun" pPowerDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns" pRainYun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/rainyun" pTencentCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud" @@ -345,6 +346,22 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { return applicant, err } + case domain.ApplyDNSProviderTypePorkbun: + { + access := domain.AccessConfigForPorkbun{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pPorkbun.NewChallengeProvider(&pPorkbun.ChallengeProviderConfig{ + ApiKey: access.ApiKey, + SecretApiKey: access.SecretApiKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + } + case domain.ApplyDNSProviderTypePowerDNS: { access := domain.AccessConfigForPowerDNS{} diff --git a/internal/domain/access.go b/internal/domain/access.go index f19a0871..60726bd6 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -165,6 +165,11 @@ type AccessConfigForNS1 struct { ApiKey string `json:"apiKey"` } +type AccessConfigForPorkbun struct { + ApiKey string `json:"apiKey"` + SecretApiKey string `json:"secretApiKey"` +} + type AccessConfigForPowerDNS struct { ApiUrl string `json:"apiUrl"` ApiKey string `json:"apiKey"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index d79d5f59..7b3a8770 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -26,6 +26,7 @@ const ( AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud") AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留) AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留) + AccessProviderTypeDeSEC = AccessProviderType("desec") // deSEC(预留) AccessProviderTypeDNSLA = AccessProviderType("dnsla") AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") AccessProviderTypeDynv6 = AccessProviderType("dynv6") @@ -43,6 +44,7 @@ const ( AccessProviderTypeNameDotCom = AccessProviderType("namedotcom") AccessProviderTypeNameSilo = AccessProviderType("namesilo") AccessProviderTypeNS1 = AccessProviderType("ns1") + AccessProviderTypePorkbun = AccessProviderType("porkbun") // Porkbun(预留) AccessProviderTypePowerDNS = AccessProviderType("powerdns") AccessProviderTypeQiniu = AccessProviderType("qiniu") AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留) @@ -52,6 +54,7 @@ const ( AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud") AccessProviderTypeUCloud = AccessProviderType("ucloud") AccessProviderTypeUpyun = AccessProviderType("upyun") + AccessProviderTypeVercel = AccessProviderType("vercel") // Vercel(预留) AccessProviderTypeVolcEngine = AccessProviderType("volcengine") AccessProviderTypeWebhook = AccessProviderType("webhook") AccessProviderTypeWestcn = AccessProviderType("westcn") @@ -79,6 +82,7 @@ const ( ApplyDNSProviderTypeCloudflare = ApplyDNSProviderType("cloudflare") ApplyDNSProviderTypeClouDNS = ApplyDNSProviderType("cloudns") ApplyDNSProviderTypeCMCCCloud = ApplyDNSProviderType("cmcccloud") + ApplyDNSProviderTypeDeSEC = ApplyDNSProviderType("desec") ApplyDNSProviderTypeDNSLA = ApplyDNSProviderType("dnsla") ApplyDNSProviderTypeDynv6 = ApplyDNSProviderType("dynv6") ApplyDNSProviderTypeGcore = ApplyDNSProviderType("gcore") @@ -92,10 +96,12 @@ const ( ApplyDNSProviderTypeNameDotCom = ApplyDNSProviderType("namedotcom") ApplyDNSProviderTypeNameSilo = ApplyDNSProviderType("namesilo") ApplyDNSProviderTypeNS1 = ApplyDNSProviderType("ns1") + ApplyDNSProviderTypePorkbun = ApplyDNSProviderType("porkbun") ApplyDNSProviderTypePowerDNS = ApplyDNSProviderType("powerdns") ApplyDNSProviderTypeRainYun = ApplyDNSProviderType("rainyun") ApplyDNSProviderTypeTencentCloud = ApplyDNSProviderType("tencentcloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeTencentCloudDNS] ApplyDNSProviderTypeTencentCloudDNS = ApplyDNSProviderType("tencentcloud-dns") + ApplyDNSProviderTypeVercel = ApplyDNSProviderType("vercel") ApplyDNSProviderTypeVolcEngine = ApplyDNSProviderType("volcengine") // 兼容旧值,等同于 [ApplyDNSProviderTypeVolcEngineDNS] ApplyDNSProviderTypeVolcEngineDNS = ApplyDNSProviderType("volcengine-dns") ApplyDNSProviderTypeWestcn = ApplyDNSProviderType("westcn") diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/porkbun/porkbun.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/porkbun/porkbun.go new file mode 100644 index 00000000..ba60a791 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/porkbun/porkbun.go @@ -0,0 +1,38 @@ +package porkbun + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/providers/dns/porkbun" +) + +type ChallengeProviderConfig struct { + ApiKey string `json:"apiKey"` + SecretApiKey string `json:"secretApiKey"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := porkbun.NewDefaultConfig() + providerConfig.APIKey = config.ApiKey + providerConfig.SecretAPIKey = config.SecretApiKey + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := porkbun.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go index 3dc86d66..e5275efe 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go @@ -1,4 +1,4 @@ -package namesilo +package powerdns import ( "net/url" diff --git a/migrations/1742644800_upgrade.go b/migrations/1742644800_upgrade.go index 97fb2bc4..b28634a1 100644 --- a/migrations/1742644800_upgrade.go +++ b/migrations/1742644800_upgrade.go @@ -7,7 +7,7 @@ import ( func init() { m.Register(func(app core.App) error { - // create collection `workflow_run` + // update collection `workflow_run` { collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") if err != nil { @@ -37,7 +37,7 @@ func init() { } } - // create collection `workflow_output` + // update collection `workflow_output` { collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") if err != nil { @@ -63,7 +63,7 @@ func init() { } } - // create collection `workflow_logs` + // update collection `workflow_logs` { collection, err := app.FindCollectionByNameOrId("pbc_1682296116") if err != nil { @@ -107,6 +107,83 @@ func init() { } } + // update collection `access` + { + collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update field + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "hidden": false, + "id": "hwy7m03o", + "maxSelect": 1, + "name": "provider", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "1panel", + "acmehttpreq", + "akamai", + "aliyun", + "aws", + "azure", + "baiducloud", + "baishan", + "baotapanel", + "byteplus", + "cachefly", + "cdnfly", + "cloudflare", + "cloudns", + "cmcccloud", + "ctcccloud", + "cucccloud", + "desec", + "dnsla", + "dogecloud", + "dynv6", + "edgio", + "fastly", + "gname", + "gcore", + "godaddy", + "goedge", + "huaweicloud", + "jdcloud", + "k8s", + "local", + "namecheap", + "namedotcom", + "namesilo", + "ns1", + "porkbun", + "powerdns", + "qiniu", + "qingcloud", + "rainyun", + "safeline", + "ssh", + "tencentcloud", + "ucloud", + "upyun", + "vercel", + "volcengine", + "webhook", + "westcn" + ] + }`)); err != nil { + return err + } + + if err := app.Save(collection); err != nil { + return err + } + } + return nil }, func(app core.App) error { return nil diff --git a/ui/public/imgs/providers/porkbun.svg b/ui/public/imgs/providers/porkbun.svg new file mode 100644 index 00000000..096d03ac --- /dev/null +++ b/ui/public/imgs/providers/porkbun.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 63a66875..6a948b9a 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -38,6 +38,7 @@ import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig"; import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig"; import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig"; import AccessFormNS1Config from "./AccessFormNS1Config"; +import AccessFormPorkbunConfig from "./AccessFormPorkbunConfig"; import AccessFormPowerDNSConfig from "./AccessFormPowerDNSConfig"; import AccessFormQiniuConfig from "./AccessFormQiniuConfig"; import AccessFormRainYunConfig from "./AccessFormRainYunConfig"; @@ -160,6 +161,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.NS1: return ; + case ACCESS_PROVIDERS.PORKBUN: + return ; case ACCESS_PROVIDERS.POWERDNS: return ; case ACCESS_PROVIDERS.QINIU: diff --git a/ui/src/components/access/AccessFormPorkbunConfig.tsx b/ui/src/components/access/AccessFormPorkbunConfig.tsx new file mode 100644 index 00000000..20cbc38a --- /dev/null +++ b/ui/src/components/access/AccessFormPorkbunConfig.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForPorkbun } from "@/domain/access"; + +type AccessFormPorkbunConfigFieldValues = Nullish; + +export type AccessFormPorkbunConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormPorkbunConfigFieldValues; + onValuesChange?: (values: AccessFormPorkbunConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormPorkbunConfigFieldValues => { + return { + apiKey: "", + secretApiKey: "", + }; +}; + +const AccessFormPorkbunConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormPorkbunConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiKey: z + .string() + .min(1, t("access.form.porkbun_api_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim(), + secretApiKey: z + .string() + .min(1, t("access.form.porkbun_secret_api_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormPorkbunConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 8c011857..9c33ff4e 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -34,6 +34,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForNamecheap | AccessConfigForNameDotCom | AccessConfigForNameSilo + | AccessConfigForPorkbun | AccessConfigForPowerDNS | AccessConfigForQiniu | AccessConfigForRainYun @@ -190,6 +191,11 @@ export type AccessConfigForNS1 = { apiKey: string; }; +export type AccessConfigForPorkbun = { + apiKey: string; + secretApiKey: string; +}; + export type AccessConfigForPowerDNS = { apiUrl: string; apiKey: string; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index bbc1ce78..37a3f8a0 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -18,6 +18,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ CLOUDFLARE: "cloudflare", CLOUDNS: "cloudns", CMCCCLOUD: "cmcccloud", + DESEC: "desec", DNSLA: "dnsla", DOGECLOUD: "dogecloud", DYNV6: "dynv6", @@ -33,6 +34,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ NAMEDOTCOM: "namedotcom", NAMESILO: "namesilo", NS1: "ns1", + PORKBUN: "porkbun", POWERDNS: "powerdns", QINIU: "qiniu", RAINYUN: "rainyun", @@ -41,6 +43,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ TENCENTCLOUD: "tencentcloud", UCLOUD: "ucloud", UPYUN: "upyun", + VERCEL: "vercel", VOLCENGINE: "volcengine", WEBHOOK: "webhook", WESTCN: "westcn", @@ -105,6 +108,7 @@ export const accessProvidersMap: Maphttps://www.ibm.com/docs/en/ns1-connect?topic=introduction-using-api", + "access.form.porkbun_api_key.label": "Porkbun API key", + "access.form.porkbun_api_key.placeholder": "Please enter Porkbun API key", + "access.form.porkbun_api_key.tooltip": "For more information, see https://porkbun.com/api/json/v3/documentation", + "access.form.porkbun_secret_api_key.label": "Porkbun secret API key", + "access.form.porkbun_secret_api_key.placeholder": "Please enter Porkbun secret API key", + "access.form.porkbun_secret_api_key.tooltip": "For more information, see https://porkbun.com/api/json/v3/documentation", "access.form.powerdns_api_url.label": "PowerDNS API URL", "access.form.powerdns_api_url.placeholder": "Please enter PowerDNS API URL", "access.form.powerdns_api_url.tooltip": "For more information, see https://doc.powerdns.com/authoritative/http-api/index.html#endpoints-and-objects-in-the-api", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index cb1f434c..0b3e158e 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -44,6 +44,7 @@ "provider.cmcccloud": "China Mobile Cloud (ECloud)", "provider.ctcccloud": "China Telecom Cloud (State Cloud)", "provider.cucccloud": "China Unicom Cloud", + "provider.desec": "deSEC", "provider.dnsla": "DNS.LA", "provider.dogecloud": "Doge Cloud", "provider.dogecloud.cdn": "Doge Cloud - CDN (Content Delivery Network)", @@ -75,6 +76,7 @@ "provider.namedotcom": "Name.com", "provider.namesilo": "NameSilo", "provider.ns1": "NS1 (IBM NS1 Connect)", + "provider.porkbun": "Porkbun", "provider.powerdns": "PowerDNS", "provider.qiniu": "Qiniu", "provider.qiniu.cdn": "Qiniu - CDN (Content Delivery Network)", @@ -102,6 +104,7 @@ "provider.upyun": "UPYUN", "provider.upyun.cdn": "UPYUN - CDN (Content Delivery Network)", "provider.upyun.file": "UPYUN - File Storage", + "provider.vercel": "Vercel", "provider.volcengine": "Volcengine", "provider.volcengine.alb": "Volcengine - ALB (Application Load Balancer)", "provider.volcengine.cdn": "Volcengine - CDN (Content Delivery Network)", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 12c10595..4a5ead78 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -190,6 +190,12 @@ "access.form.ns1_api_key.label": "NS1 API Key", "access.form.ns1_api_key.placeholder": "请输入 NS1 API Key", "access.form.ns1_api_key.tooltip": "这是什么?请参阅 https://www.ibm.com/docs/zh/ns1-connect?topic=introduction-using-api", + "access.form.porkbun_api_key.label": "Porkbun API Key", + "access.form.porkbun_api_key.placeholder": "请输入 Porkbun API Key", + "access.form.porkbun_api_key.tooltip": "这是什么?请参阅 https://porkbun.com/api/json/v3/documentation", + "access.form.porkbun_secret_api_key.label": "Porkbun Secret API Key", + "access.form.porkbun_secret_api_key.placeholder": "请输入 Porkbun Secret API Key", + "access.form.porkbun_secret_api_key.tooltip": "这是什么?请参阅 https://porkbun.com/api/json/v3/documentation", "access.form.powerdns_api_url.label": "PowerDNS API URL", "access.form.powerdns_api_url.placeholder": "请输入 PowerDNS API URL", "access.form.powerdns_api_url.tooltip": "这是什么?请参阅 https://doc.powerdns.com/authoritative/http-api/index.html#endpoints-and-objects-in-the-api", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 1b8f4e24..d3086452 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -44,6 +44,7 @@ "provider.cmcccloud": "移动云", "provider.ctcccloud": "联通云", "provider.cucccloud": "天翼云", + "provider.desec": "deSEC", "provider.dnsla": "DNS.LA", "provider.dogecloud": "多吉云", "provider.dogecloud.cdn": "多吉云 - 内容分发网络 CDN", @@ -75,6 +76,7 @@ "provider.namedotcom": "Name.com", "provider.namesilo": "NameSilo", "provider.ns1": "NS1(IBM NS1 Connect)", + "provider.porkbun": "Porkbun", "provider.powerdns": "PowerDNS", "provider.qiniu": "七牛云", "provider.qiniu.cdn": "七牛云 - 内容分发网络 CDN", @@ -102,6 +104,7 @@ "provider.upyun": "又拍云", "provider.upyun.cdn": "又拍云 - 云分发 CDN", "provider.upyun.file": "又拍云 - 云存储", + "provider.vercel": "Vercel", "provider.volcengine": "火山引擎", "provider.volcengine.alb": "火山引擎 - 应用型负载均衡 ALB", "provider.volcengine.cdn": "火山引擎 - 内容分发网络 CDN",