package hostnames import ( "context" "encoding/json" "fmt" "strings" "github.com/go-errors/errors" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/api" ) func GetCustomHostnameConfig(ctx context.Context, projectRef string) (*api.V1GetHostnameConfigResponse, error) { resp, err := utils.GetSupabase().V1GetHostnameConfigWithResponse(ctx, projectRef) if err != nil { return nil, errors.Errorf("failed to get custom hostname: %w", err) } if resp.JSON200 == nil { return nil, errors.New("failed to get custom hostname config; received: " + string(resp.Body)) } return resp, nil } func VerifyCNAME(ctx context.Context, projectRef string, customHostname string) error { expectedEndpoint := fmt.Sprintf("%s.", utils.GetSupabaseHost(projectRef)) cname, err := utils.ResolveCNAME(ctx, customHostname) if err != nil { return errors.Errorf("expected custom hostname '%s' to have a CNAME record pointing to your project at '%s', but it failed to resolve: %w", customHostname, expectedEndpoint, err) } if cname != expectedEndpoint { return errors.Errorf("expected custom hostname '%s' to have a CNAME record pointing to your project at '%s', but it is currently set to '%s'", customHostname, expectedEndpoint, cname) } return nil } type RawResponse struct { Result struct { CustomOriginServer string `json:"custom_origin_server"` OwnershipVerification struct { Name string Type string Value string } `json:"ownership_verification"` Ssl struct { ValidationRecords []struct { Status string `json:"status"` TxtName string `json:"txt_name"` TxtValue string `json:"txt_value"` } `json:"validation_records"` ValidationErrors []struct { Message string `json:"message"` } `json:"validation_errors"` Status string `json:"status"` } } `json:"result"` } func serializeRawOutput(response *api.UpdateCustomHostnameResponse) (string, error) { output, err := json.MarshalIndent(response, "", " ") if err != nil { return "", errors.Errorf("failed to serialize json: %w", err) } return string(output), nil } func appendRawOutputIfNeeded(status string, response *api.UpdateCustomHostnameResponse, includeRawOutput bool) string { if !includeRawOutput { return status } rawOutput, err := serializeRawOutput(response) if err != nil { return fmt.Sprintf("%s\nFailed to serialize raw output: %+v\n", status, err) } return fmt.Sprintf("%s\nRaw output follows:\n%s\n", status, rawOutput) } func TranslateStatus(response *api.UpdateCustomHostnameResponse, includeRawOutput bool) (string, error) { if response.Status == api.N5ServicesReconfigured { return appendRawOutputIfNeeded(fmt.Sprintf("Custom hostname setup completed. Project is now accessible at %s.", response.CustomHostname), response, includeRawOutput), nil } if response.Status == api.N4OriginSetupCompleted { var res RawResponse rawBody, err := json.Marshal(response.Data) if err != nil { return "", errors.Errorf("failed to serialize body: %w", err) } err = json.Unmarshal(rawBody, &res) if err != nil { return "", errors.Errorf("failed to deserialize body: %w", err) } return appendRawOutputIfNeeded(fmt.Sprintf(`Custom hostname configuration complete, and ready for activation. Please ensure that your custom domain is set up as a CNAME record to your Supabase subdomain: %s CNAME -> %s`, response.CustomHostname, res.Result.CustomOriginServer), response, includeRawOutput), nil } if response.Status == api.N2Initiated { var res RawResponse rawBody, err := json.Marshal(response.Data) if err != nil { return "", errors.Errorf("failed to serialize body: %w", err) } err = json.Unmarshal(rawBody, &res) if err != nil { return "", errors.Errorf("failed to deserialize body: %w", err) } ssl := res.Result.Ssl.ValidationRecords if res.Result.Ssl.Status == "initializing" { return appendRawOutputIfNeeded("Custom hostname setup is being initialized; please request re-verification in a few seconds.\n", response, includeRawOutput), nil } if len(res.Result.Ssl.ValidationErrors) > 0 { var errorMessages []string for _, valError := range res.Result.Ssl.ValidationErrors { if strings.Contains(valError.Message, "caa_error") { return appendRawOutputIfNeeded("CAA mismatch; please remove any existing CAA records on your domain, or add one for \"digicert.com\"\n", response, includeRawOutput), nil } errorMessages = append(errorMessages, valError.Message) } valErrors := strings.Join(errorMessages, "\n\t- ") return appendRawOutputIfNeeded(fmt.Sprintf("SSL validation errors: \n\t- %s\n", valErrors), response, includeRawOutput), nil } if len(ssl) != 1 { return "", errors.Errorf("expected a single SSL verification record, received: %+v", ssl) } records := "" if ssl[0].TxtName != "" { records = fmt.Sprintf("%s\n\t%s TXT -> %s", records, ssl[0].TxtName, ssl[0].TxtValue) } status := fmt.Sprintf("Custom hostname verification in-progress; please configure the appropriate DNS entries and request re-verification.\n"+ "Required outstanding validation records: %s\n", records) return appendRawOutputIfNeeded(status, response, includeRawOutput), nil } return appendRawOutputIfNeeded("Custom hostname configuration not started.", response, includeRawOutput), nil }