feat(provider): new acme dns-01 provider: dynadot

This commit is contained in:
Rowan Chen
2026-05-22 13:52:51 +08:00
committed by GitHub
parent e36e0ae123
commit e4a4a1a9b5
17 changed files with 585 additions and 0 deletions
@@ -0,0 +1,27 @@
package certifiers
import (
"fmt"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core"
"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynadot"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDynadot, func(options *ProviderFactoryOptions) (core.ACMEChallenger, error) {
credentials := domain.AccessConfigForDynadot{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := dynadot.NewChallenger(&dynadot.ChallengerConfig{
ApiKey: credentials.ApiKey,
ApiSecret: credentials.ApiSecret,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return provider, err
})
}
+5
View File
@@ -225,6 +225,11 @@ type AccessConfigForDuckDNS struct {
Token string `json:"token"`
}
type AccessConfigForDynadot struct {
ApiKey string `json:"apiKey"`
ApiSecret string `json:"apiSecret"`
}
type AccessConfigForDynu struct {
ApiKey string `json:"apiKey"`
}
+2
View File
@@ -50,6 +50,7 @@ const (
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
AccessProviderTypeDokploy = AccessProviderType("dokploy")
AccessProviderTypeDuckDNS = AccessProviderType("duckdns")
AccessProviderTypeDynadot = AccessProviderType("dynadot")
AccessProviderTypeDynu = AccessProviderType("dynu")
AccessProviderTypeDynv6 = AccessProviderType("dynv6")
AccessProviderTypeEmail = AccessProviderType("email")
@@ -193,6 +194,7 @@ const (
ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
ACMEDns01ProviderTypeDNSMadeEasy = ACMEDns01ProviderType(AccessProviderTypeDNSMadeEasy)
ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS)
ACMEDns01ProviderTypeDynadot = ACMEDns01ProviderType(AccessProviderTypeDynadot)
ACMEDns01ProviderTypeDynu = ACMEDns01ProviderType(AccessProviderTypeDynu)
ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
ACMEDns01ProviderTypeGandinet = ACMEDns01ProviderType(AccessProviderTypeGandinet)
@@ -0,0 +1,39 @@
package dynadot
import (
"fmt"
"time"
"github.com/certimate-go/certimate/pkg/core/certifier"
"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynadot/internal"
)
type ChallengerConfig struct {
ApiKey string `json:"apiKey"`
ApiSecret string `json:"apiSecret"`
DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int `json:"dnsTTL,omitempty"`
}
func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the acme challenge provider is nil")
}
providerConfig := internal.NewDefaultConfig()
providerConfig.APIKey = config.ApiKey
providerConfig.APISecret = config.ApiSecret
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = config.DnsTTL
}
provider, err := internal.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}
@@ -0,0 +1,154 @@
package internal
import (
"fmt"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/samber/lo"
dynadotsdk "github.com/certimate-go/certimate/pkg/sdk3rd/dynadot"
)
const (
envNamespace = "DYNADOT_"
EnvAPIKey = envNamespace + "API_KEY"
EnvAPISecret = envNamespace + "API_SECRET"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
APIKey string
APISecret string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
type DNSProvider struct {
config *Config
client *dynadotsdk.Client
}
type dnsRecordCacheEntry struct {
Zone string
SubHost string
Value string
}
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAPIKey, EnvAPISecret)
if err != nil {
return nil, fmt.Errorf("dynadot: %w", err)
}
config := NewDefaultConfig()
config.APIKey = values[EnvAPIKey]
config.APISecret = values[EnvAPISecret]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, fmt.Errorf("dynadot: the configuration of the DNS provider is nil")
}
client, err := dynadotsdk.NewClient(config.APIKey, config.APISecret)
if err != nil {
return nil, fmt.Errorf("dynadot: %w", err)
} else {
client.SetTimeout(config.HTTPTimeout)
}
return &DNSProvider{
config: config,
client: client,
}, nil
}
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("dynadot: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("dynadot: %w", err)
}
// REF: https://www.dynadot.com/domain/api-document (set_dns)
request := &dynadotsdk.SetDnsRequest{
SubList: []*dynadotsdk.DnsSubRecord{
{
SubHost: subDomain,
RecordType: "TXT",
RecordValue1: info.Value,
},
},
TTL: lo.ToPtr(int64(d.config.TTL)),
AddDnsToCurrentSetting: lo.ToPtr(true),
}
if _, err := d.client.SetDns(dns01.UnFqdn(authZone), request); err != nil {
return fmt.Errorf("dynadot: error when create record: %w", err)
}
return nil
}
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("dynadot: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("dynadot: %w", err)
}
// REF: https://www.dynadot.com/domain/api-document (remove_dns)
request := &dynadotsdk.RemoveDnsRequest{
SubList: []*dynadotsdk.DnsSubRecord{
{
SubHost: subDomain,
RecordType: "TXT",
RecordValue1: info.Value,
},
},
}
if _, err := d.client.RemoveDns(dns01.UnFqdn(authZone), request); err != nil {
return fmt.Errorf("dynadot: error when delete record: %w", err)
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
+40
View File
@@ -0,0 +1,40 @@
package dynadot
import (
"context"
"fmt"
"net/http"
)
type RemoveDnsRequest struct {
DnsMainList []*DnsMainRecord `json:"dns_main_list,omitempty"`
SubList []*DnsSubRecord `json:"sub_list,omitempty"`
}
type RemoveDnsResponse struct {
sdkResponseBase
}
func (c *Client) RemoveDns(domain string, req *RemoveDnsRequest) (*RemoveDnsResponse, error) {
return c.RemoveDnsWithContext(context.Background(), domain, req)
}
func (c *Client) RemoveDnsWithContext(ctx context.Context, domain string, req *RemoveDnsRequest) (*RemoveDnsResponse, error) {
if domain == "" {
return nil, fmt.Errorf("sdkerr: unset domain")
}
httpreq, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/restful/v2/domains/%s/records", domain), req)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &RemoveDnsResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+42
View File
@@ -0,0 +1,42 @@
package dynadot
import (
"context"
"fmt"
"net/http"
)
type SetDnsRequest struct {
DnsMainList []*DnsMainRecord `json:"dns_main_list,omitempty"`
SubList []*DnsSubRecord `json:"sub_list,omitempty"`
TTL *int64 `json:"ttl,omitempty"`
AddDnsToCurrentSetting *bool `json:"add_dns_to_current_setting,omitempty"`
}
type SetDnsResponse struct {
sdkResponseBase
}
func (c *Client) SetDns(domain string, req *SetDnsRequest) (*SetDnsResponse, error) {
return c.SetDnsWithContext(context.Background(), domain, req)
}
func (c *Client) SetDnsWithContext(ctx context.Context, domain string, req *SetDnsRequest) (*SetDnsResponse, error) {
if domain == "" {
return nil, fmt.Errorf("sdkerr: unset domain")
}
httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/restful/v2/domains/%s/records", domain), req)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &SetDnsResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+149
View File
@@ -0,0 +1,149 @@
package dynadot
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/go-resty/resty/v2"
"github.com/certimate-go/certimate/internal/app"
)
type Client struct {
apiKey string
apiSecret string
client *resty.Client
}
func NewClient(apiKey, apiSecret string) (*Client, error) {
if apiKey == "" {
return nil, fmt.Errorf("sdkerr: unset apiKey")
}
if apiSecret == "" {
return nil, fmt.Errorf("sdkerr: unset apiSecret")
}
client := resty.New().
SetBaseURL("https://api.dynadot.com").
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", app.AppUserAgent)
return &Client{
apiKey: apiKey,
apiSecret: apiSecret,
client: client,
}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) {
if method == "" {
return nil, fmt.Errorf("sdkerr: unset method")
}
if path == "" {
return nil, fmt.Errorf("sdkerr: unset path")
}
var body string
if params != nil {
jsonb, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("sdkerr: failed to marshal request body: %w", err)
}
body = string(jsonb)
}
req := c.client.R()
req.Method = method
req.URL = path
req.SetHeader("Authorization", "Bearer "+c.apiKey)
req.SetHeader("X-Signature", generateSignature(c.apiKey, c.apiSecret, path, "", body))
if body != "" {
req.SetBody(body)
}
return req, nil
}
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
// WARN:
// PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD.
// PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := c.doRequest(req)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &res)
}
return resp, err
}
if len(resp.Body()) != 0 {
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
} else {
// NOTE: Unlike most providers in this project (which use `0` for success),
// Dynadot mirrors HTTP-style status codes inside the response body and
// always returns `200` on success.
// REF: https://www.dynadot.com/domain/api-document
if tcode := res.GetCode(); tcode != 200 {
return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", tcode, res.GetMessage())
}
}
}
return resp, nil
}
// generateSignature builds the X-Signature header value as required by the
// Dynadot RESTful v2 API. It signs the concatenation of:
//
// apiKey + "\n" + fullPathAndQuery + "\n" + xRequestId + "\n" + requestBody
//
// using HMAC-SHA256 with the API secret as key, then encodes the result with
// standard Base64.
//
// REF: https://www.dynadot.com/domain/api-document
func generateSignature(apiKey, apiSecret, fullPathAndQuery, xRequestId, requestBody string) string {
var sb strings.Builder
sb.WriteString(apiKey)
sb.WriteString("\n")
sb.WriteString(fullPathAndQuery)
sb.WriteString("\n")
sb.WriteString(xRequestId)
sb.WriteString("\n")
sb.WriteString(requestBody)
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(sb.String()))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
+34
View File
@@ -0,0 +1,34 @@
package dynadot
type sdkResponse interface {
GetCode() int
GetMessage() string
}
type sdkResponseBase struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (r *sdkResponseBase) GetCode() int {
return r.Code
}
func (r *sdkResponseBase) GetMessage() string {
return r.Message
}
var _ sdkResponse = (*sdkResponseBase)(nil)
type DnsMainRecord struct {
RecordType string `json:"record_type"`
RecordValue1 string `json:"record_value1"`
RecordValue2 string `json:"record_value2,omitempty"`
}
type DnsSubRecord struct {
SubHost string `json:"sub_host"`
RecordType string `json:"record_type"`
RecordValue1 string `json:"record_value1"`
RecordValue2 string `json:"record_value2,omitempty"`
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 256 256" width="200" height="200"><path fill="rgb(4,18,65)" stroke="rgb(4,18,65)" stroke-width="1" opacity="1" d="M 0 0 L 256 0 L 256 256 L 0 256 L 0 0 Z M 152 49 L 145 51 L 129 59 L 74 89 L 88 115 L 88 117 Q 75 121 66 129 L 55 142 L 51 151 L 49 161 Q 49 179 57 190 Q 67 204 88 208 L 101 208 L 111 205 L 180 169 L 166 140 L 188 129 Q 201 120 205 102 L 205 85 Q 203 74 197 67 Q 189 56 177 51 Q 167 47 152 49 Z " /><path fill="rgb(4,18,65)" stroke="rgb(4,18,65)" stroke-width="1" opacity="1" d="M 155.5 65 Q 176.7 63.8 184 76.5 Q 189.5 83 189 95.5 L 186 105.5 L 181 113 L 158.5 126 L 151 111.5 L 146.5 106 L 133.5 99 L 122.5 99 L 103.5 108 L 97 96.5 L 97 94 L 155.5 65 Z " /><path fill="rgb(4,18,65)" stroke="rgb(4,18,65)" stroke-width="1" opacity="1" d="M 126.5 115 Q 133.4 115.6 137 119.5 Q 141.3 122.7 140 131.5 L 134.5 139 Q 131.5 142 124.5 141 L 118 138 L 115 132.5 L 115 123.5 L 120.5 117 L 126.5 115 Z " /><path fill="rgb(4,18,65)" stroke="rgb(4,18,65)" stroke-width="1" opacity="1" d="M 95.5 131 Q 97.8 130.3 97 132.5 Q 101.6 145.4 111.5 153 Q 118.2 159.3 133.5 157 L 150.5 148 L 158 162 L 103.5 190 Q 97.2 192.7 86.5 191 Q 76.6 187.9 71 180.5 Q 64.1 173.4 66 157.5 Q 68.6 146.6 76.5 141 L 95.5 131 Z " /><path fill="rgb(249,251,252)" stroke="rgb(249,251,252)" stroke-width="1" opacity="1" d="M 151.5 49 Q 167 47 176.5 51 Q 189.4 56.1 197 66.5 Q 203 73.5 205 84.5 L 205 101.5 Q 200.6 119.6 187.5 129 L 166 140 L 180 168.5 L 110.5 205 L 100.5 208 L 87.5 208 Q 66.9 204.1 57 189.5 Q 48.8 179.2 49 160.5 L 51 150.5 L 55 141.5 L 65.5 129 Q 75.2 121 88 117 L 88 114.5 L 74 88.5 L 128.5 59 L 144.5 51 L 151.5 49 Z M 156 65 L 97 94 L 97 97 L 104 108 L 123 99 L 134 99 L 147 106 L 151 112 L 159 126 L 181 113 L 186 106 L 189 96 Q 190 83 184 77 Q 177 64 156 65 Z M 127 115 L 121 117 L 115 124 L 115 133 L 118 138 L 125 141 Q 132 142 135 139 L 140 132 Q 141 123 137 120 Q 133 116 127 115 Z M 96 131 L 77 141 Q 69 147 66 158 Q 64 173 71 181 Q 77 188 87 191 Q 97 193 104 190 L 158 162 L 151 148 L 134 157 Q 118 159 112 153 Q 102 145 97 133 Q 98 130 96 131 Z " /></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -42,6 +42,7 @@ import AccessConfigFieldsProviderDNSMadeEasy from "./AccessConfigFieldsProviderD
import AccessConfigFieldsProviderDogeCloud from "./AccessConfigFieldsProviderDogeCloud";
import AccessConfigFieldsProviderDokploy from "./AccessConfigFieldsProviderDokploy";
import AccessConfigFieldsProviderDuckDNS from "./AccessConfigFieldsProviderDuckDNS";
import AccessConfigFieldsProviderDynadot from "./AccessConfigFieldsProviderDynadot";
import AccessConfigFieldsProviderDynu from "./AccessConfigFieldsProviderDynu";
import AccessConfigFieldsProviderDynv6 from "./AccessConfigFieldsProviderDynv6";
import AccessConfigFieldsProviderEmail from "./AccessConfigFieldsProviderEmail";
@@ -159,6 +160,7 @@ const providerComponentMap: Partial<Record<AccessProviderType, React.ComponentTy
[ACCESS_PROVIDERS.DOKPLOY]: AccessConfigFieldsProviderDokploy,
[ACCESS_PROVIDERS.DOGECLOUD]: AccessConfigFieldsProviderDogeCloud,
[ACCESS_PROVIDERS.DUCKDNS]: AccessConfigFieldsProviderDuckDNS,
[ACCESS_PROVIDERS.DYNADOT]: AccessConfigFieldsProviderDynadot,
[ACCESS_PROVIDERS.DYNU]: AccessConfigFieldsProviderDynu,
[ACCESS_PROVIDERS.DYNV6]: AccessConfigFieldsProviderDynv6,
[ACCESS_PROVIDERS.EMAIL]: AccessConfigFieldsProviderEmail,
@@ -0,0 +1,64 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderDynadot = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item
name={[parentNamePath, "apiKey"]}
initialValue={initialValues.apiKey}
label={t("access.form.dynadot_api_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.dynadot_api_key.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.dynadot_api_key.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "apiSecret"]}
initialValue={initialValues.apiSecret}
label={t("access.form.dynadot_api_secret.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.dynadot_api_secret.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.dynadot_api_secret.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
apiKey: "",
apiSecret: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
return z.object({
apiKey: z.string().nonempty(),
apiSecret: z.string().nonempty(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDynadot, {
getInitialValues,
getSchema,
});
export default _default;
+4
View File
@@ -55,6 +55,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
DOGECLOUD: "dogecloud",
DOKPLOY: "dokploy",
DUCKDNS: "duckdns",
DYNADOT: "dynadot",
DYNU: "dynu",
DYNV6: "dynv6",
EMAIL: "email",
@@ -218,6 +219,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.DNSEXIT, "provider.dnsexit", "/imgs/providers/dnsexit.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.DNSMADEEASY, "provider.dnsmadeeasy", "/imgs/providers/dnsmadeeasy.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.DUCKDNS, "provider.duckdns", "/imgs/providers/duckdns.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.DYNADOT, "provider.dynadot", "/imgs/providers/dynadot.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.DYNU, "provider.dynu", "/imgs/providers/dynu.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.DYNV6, "provider.dynv6", "/imgs/providers/dynv6.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.GANDINET, "provider.gandinet", "/imgs/providers/gandinet.svg", [ACCESS_USAGES.DNS]],
@@ -383,6 +385,7 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
DNSLA: `${ACCESS_PROVIDERS.DNSLA}`,
DNSMADEEASY: `${ACCESS_PROVIDERS.DNSMADEEASY}`,
DUCKDNS: `${ACCESS_PROVIDERS.DUCKDNS}`,
DYNADOT: `${ACCESS_PROVIDERS.DYNADOT}`,
DYNU: `${ACCESS_PROVIDERS.DYNU}`,
DYNV6: `${ACCESS_PROVIDERS.DYNV6}`,
GANDINET: `${ACCESS_PROVIDERS.GANDINET}`,
@@ -462,6 +465,7 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.DNSEXIT, "provider.dnsexit"],
[ACME_DNS01_PROVIDERS.DNSMADEEASY, "provider.dnsmadeeasy"],
[ACME_DNS01_PROVIDERS.DUCKDNS, "provider.duckdns"],
[ACME_DNS01_PROVIDERS.DYNADOT, "provider.dynadot"],
[ACME_DNS01_PROVIDERS.DYNU, "provider.dynu"],
[ACME_DNS01_PROVIDERS.DYNV6, "provider.dynv6"],
[ACME_DNS01_PROVIDERS.GANDINET, "provider.gandinet"],
+10
View File
@@ -498,6 +498,16 @@
"placeholder": "Please enter DuckDNS token",
"tooltip": "For more information, see <a href=\"https://www.duckdns.org/spec.jsp\" target=\"_blank\">https://www.duckdns.org/spec.jsp</a>"
},
"dynadot_api_key": {
"label": "Dynadot API key",
"placeholder": "Please enter Dynadot API key",
"tooltip": "For more information, see <a href=\"https://www.dynadot.com/domain/api-document\" target=\"_blank\">https://www.dynadot.com/domain/api-document</a>"
},
"dynadot_api_secret": {
"label": "Dynadot API secret",
"placeholder": "Please enter Dynadot API secret",
"tooltip": "For more information, see <a href=\"https://www.dynadot.com/domain/api-document\" target=\"_blank\">https://www.dynadot.com/domain/api-document</a>"
},
"dynu_api_key": {
"label": "Dynu API key",
"placeholder": "Please enter Dynu API key",
@@ -119,6 +119,7 @@
"dogecloud": "Doge Cloud",
"dokploy": "Dokploy",
"duckdns": "Duck DNS",
"dynadot": "Dynadot",
"dynu": "Dynu",
"dynv6": "dynv6",
"email": "Email (SMTP)",
+10
View File
@@ -496,6 +496,16 @@
"placeholder": "请输入 DuckDNS Token",
"tooltip": "这是什么?请参阅 <a href=\"https://www.duckdns.org/spec.jsp\" target=\"_blank\">https://www.duckdns.org/spec.jsp</a>"
},
"dynadot_api_key": {
"label": "Dynadot API Key",
"placeholder": "请输入 Dynadot API Key",
"tooltip": "这是什么?请参阅 <a href=\"https://www.dynadot.com/zh/domain/api-document\" target=\"_blank\">https://www.dynadot.com/zh/domain/api-document</a>"
},
"dynadot_api_secret": {
"label": "Dynadot API Secret",
"placeholder": "请输入 Dynadot API Secret",
"tooltip": "这是什么?请参阅 <a href=\"https://www.dynadot.com/zh/domain/api-document\" target=\"_blank\">https://www.dynadot.com/zh/domain/api-document</a>"
},
"dynu_api_key": {
"label": "Dynu API Key",
"placeholder": "请输入 Dynu API Key",
@@ -119,6 +119,7 @@
"dogecloud_cdn": "多吉云 - 内容分发网络 CDN",
"dokploy": "Dokploy",
"duckdns": "Duck DNS",
"dynadot": "Dynadot",
"dynu": "Dynu",
"dynv6": "dynv6",
"email": "邮件(SMTP)",