diff --git a/internal/adapter/cloudflare/client.go b/internal/adapter/cloudflare/client.go index a5e41f2..69c1115 100644 --- a/internal/adapter/cloudflare/client.go +++ b/internal/adapter/cloudflare/client.go @@ -55,6 +55,9 @@ func (c *Client) CreateRecord(ctx context.Context, record domain.DNSRecord) (*do "ttl": record.TTL, "proxied": record.Proxied, } + if record.Priority > 0 { + body["priority"] = record.Priority + } resp, err := c.doRequest(ctx, "POST", fmt.Sprintf("/zones/%s/dns_records", c.zoneID), body) if err != nil { @@ -84,6 +87,9 @@ func (c *Client) UpdateRecord(ctx context.Context, recordID string, record domai "ttl": record.TTL, "proxied": record.Proxied, } + if record.Priority > 0 { + body["priority"] = record.Priority + } resp, err := c.doRequest(ctx, "PUT", fmt.Sprintf("/zones/%s/dns_records/%s", c.zoneID, recordID), body) if err != nil { diff --git a/internal/adapter/notify/provisioner.go b/internal/adapter/notify/provisioner.go index 61f2037..0df698a 100644 --- a/internal/adapter/notify/provisioner.go +++ b/internal/adapter/notify/provisioner.go @@ -129,21 +129,21 @@ func (p *Provisioner) CreateProjectNotify(ctx context.Context, projectID, slug s } // 8. Add DNS records for DKIM/SPF (non-fatal). - // Resend returns record names relative to the registered domain; build FQDNs for Cloudflare. - // Cloudflare's normalizeName handles FQDNs ending in the zone name correctly. + // rec.Name is relative to the zone apex (e.g., "resend._domainkey.mail.slug"). + // Cloudflare's normalizeName appends ".baseDomain" to build the FQDN. if p.dns != nil && len(dnsRecords) > 0 { for _, rec := range dnsRecords { - fqdn := rec.Name + "." + host dnsRec := domain.DNSRecord{ - Type: rec.Record, - Name: fqdn, - Content: rec.Value, - TTL: 1, + Type: rec.DNSType, + Name: rec.Name, + Content: rec.Value, + TTL: 1, + Priority: rec.Priority, } if _, upsertErr := p.dns.UpsertRecord(ctx, dnsRec); upsertErr != nil { p.logger.Warn("failed to upsert notify DNS record", - "name", fqdn, - "record", rec.Record, + "name", rec.Name, + "type", rec.DNSType, "project_id", projectID, "error", upsertErr, ) @@ -341,18 +341,20 @@ func (p *Provisioner) ProvisionNotifyDomain(ctx context.Context, projectID, host p.logger.Info("resend domain created", "host", host, "domain_id", resendDomainID, "project_id", projectID) // Step 8: Add DKIM/SPF DNS records (non-fatal). + // rec.Name is relative to the zone apex (e.g., "resend._domainkey.mail.slug"). + // Cloudflare's normalizeName appends ".baseDomain" to build the FQDN. if p.dns != nil && len(dnsRecords) > 0 { for _, rec := range dnsRecords { - fqdn := rec.Name + "." + host if _, upsertErr := p.dns.UpsertRecord(ctx, domain.DNSRecord{ - Type: rec.Record, - Name: fqdn, - Content: rec.Value, - TTL: 1, + Type: rec.DNSType, + Name: rec.Name, + Content: rec.Value, + TTL: 1, + Priority: rec.Priority, }); upsertErr != nil { p.logger.Warn("failed to upsert notify DNS record", - "name", fqdn, - "record", rec.Record, + "name", rec.Name, + "type", rec.DNSType, "project_id", projectID, "error", upsertErr, ) diff --git a/internal/adapter/notify/resend_client.go b/internal/adapter/notify/resend_client.go index 57c6d5e..e6f0eb3 100644 --- a/internal/adapter/notify/resend_client.go +++ b/internal/adapter/notify/resend_client.go @@ -28,12 +28,16 @@ type resendClient struct { } // resendDNSRecord is a DNS record returned by Resend after domain creation. -// The "record" JSON field contains the DNS record type (e.g., "TXT", "MX"). +// The "record" field is a Resend semantic label ("DKIM", "SPF"). +// The "type" field is the actual DNS record type ("TXT", "MX", "CNAME"). +// The "name" field is the subdomain relative to the zone apex (e.g., "resend._domainkey.mail.project-name"), +// NOT relative to the registered domain — append "." + baseDomain to get the FQDN. type resendDNSRecord struct { - Record string `json:"record"` // DNS record type: "TXT", "MX", "CNAME" - Name string `json:"name"` // relative name (e.g., "resend._domainkey") - Value string `json:"value"` // record content - Priority int `json:"priority,omitempty"` + Record string `json:"record"` // Resend semantic label: "DKIM", "SPF" + DNSType string `json:"type"` // actual DNS type: "TXT", "MX", "CNAME" + Name string `json:"name"` // subdomain relative to zone apex + Value string `json:"value"` // record content + Priority int `json:"priority,omitempty"` // for MX records } // resendCreateDomainResponse is the shape returned by POST /domains. diff --git a/internal/domain/dns.go b/internal/domain/dns.go index 43bb262..7a862c8 100644 --- a/internal/domain/dns.go +++ b/internal/domain/dns.go @@ -3,12 +3,13 @@ package domain // DNSRecord represents a DNS record in a zone. type DNSRecord struct { - ID string // Provider-specific ID - Type string // A, AAAA, CNAME, TXT, etc. - Name string // Subdomain or @ for root - Content string // IP address or target - TTL int // TTL in seconds, 1 = auto - Proxied bool // Cloudflare proxy enabled + ID string // Provider-specific ID + Type string // A, AAAA, CNAME, TXT, MX, etc. + Name string // Subdomain or @ for root + Content string // IP address or target + TTL int // TTL in seconds, 1 = auto + Proxied bool // Cloudflare proxy enabled + Priority int // MX/SRV record priority (0 = not set) } // DNSRecordType constants for common record types.