Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
async-trait = "0.1"
bytes = "1"
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
clap = { version = "4.5", features = ["std", "string", "derive"] }
cli-engine = { version = "0.10.2", features = ["pkce-auth"] }
clap = { version = "4.5", features = ["std", "string", "derive", "env"] }
cli-engine = { version = "0.10.3", features = ["pkce-auth"] }
dirs = "6"
domains-client = { path = "domains-client" }
email-client = { path = "email-client" }
Expand All @@ -53,7 +53,7 @@ phonenumber = "0.3"
progenitor-client = "0.14"
regex = { version = "1", features = ["std"] }
reqwest = { version = "0.13", default-features = false, features = ["json", "multipart", "form", "rustls"] }
schemars = { version = "1.2", features = ["derive"], optional = true }
schemars = { version = "1.2", features = ["derive"] }
self-replace = "1.5.0"
semver = { version = "1.0.28", default-features = false, features = ["std"] }
sha2 = "0.10"
Expand All @@ -79,7 +79,7 @@ tempfile = "3"
[features]
# Excludes `gddy spec` from default/release builds: it's only used to extract
# the CLI's command spec for the dev-portal, never by end users.
dev-portal-spec = ["dep:schemars"]
dev-portal-spec = []

[lints.rust]
unsafe_code = "deny"
Expand Down
2 changes: 1 addition & 1 deletion rust/src/dns/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
let domain = args.domain;
let record_type = args.record_type;
let name = args.name;
let domain_format = args.domain_format.form();
let domain_format = args.domain_format.form(&ctx)?;
validate_caa_fields(&record_type, &opts)
.map_err(crate::error::GddyError::validation)?;
validate_tlsa_fields(&record_type, &opts)
Expand Down
2 changes: 1 addition & 1 deletion rust/src/dns/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
let domain = args.domain;
let record_type = args.record_type;
let name = args.name;
let domain_format = args.domain_format.form();
let domain_format = args.domain_format.form(&ctx)?;

let debug = !ctx.middleware.debug.is_empty();
let client = make_client(&ctx).await?;
Expand Down
2 changes: 1 addition & 1 deletion rust/src/dns/guides/dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Deleting something that doesn't match is a no-op success (`deleted: 0`), not an

## International domain names

Domain names and record names can be typed either as Unicode (`münchen.de`) or in their encoded form (`xn--mnchen-3ya.de`); both reach the same domain. Output shows Unicode by default. Add `--punycode` to any DNS command to show the encoded form instead. Record values (the `--data` of a CNAME, MX or TXT record) are never converted; enter them exactly as they should be stored.
Domain names and record names can be typed either as Unicode (`münchen.de`) or in their encoded form (`xn--mnchen-3ya.de`); both reach the same domain. Output shows Unicode by default. Add `--punycode` to any DNS command to show the encoded form instead, or make that your default with `gddy config set punycode true` (undo it with `gddy config unset punycode`). Precedence is the `--punycode` flag, then the `GDDY_PUNYCODE` environment variable, then your saved setting, then Unicode, so `--punycode=false` turns the encoded form back off for one command. `gddy config list` shows the current value. Record values (the `--data` of a CNAME, MX or TXT record) are never converted; enter them exactly as they should be stored.

## Type-specific flags

Expand Down
3 changes: 2 additions & 1 deletion rust/src/dns/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ pub(super) fn command() -> RuntimeCommandSpec {
let mut out = build_list_output(&records).map_err(|e| {
CliCoreError::message(format!("failed to serialize DNS records: {e}"))
})?;
let domain_format = args.domain_format.form(&ctx)?;
for record in &mut out {
args.domain_format.form().convert_fields(record, &["name"]);
domain_format.convert_fields(record, &["name"]);
}
Ok(CommandResult::new(json!(out)))
},
Expand Down
2 changes: 1 addition & 1 deletion rust/src/dns/set/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
let domain = args.write.domain;
let record_type = args.write.record_type;
let name = args.write.name;
let domain_format = args.write.domain_format.form();
let domain_format = args.write.domain_format.form(&ctx)?;
let replace_conflicting = args.replace_conflicting_types;
validate_caa_fields(&record_type, &opts)
.map_err(crate::error::GddyError::validation)?;
Expand Down
14 changes: 13 additions & 1 deletion rust/src/domain/auto_renew.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use serde_json::json;
use super::common::{
api_error, format_operation_error, is_terminal_status, make_client, validate_domain_name,
};
use crate::idn::DomainFormArgs;
use crate::next_action::next_action;
use crate::output_schema::output_schema;
use crate::scopes::DOMAINS_DOMAIN_UPDATE;
Expand Down Expand Up @@ -42,6 +43,9 @@ struct AutoRenewSetArgs {
#[arg(value_name = "DOMAIN")]
domain: String,

#[command(flatten)]
domain_format: DomainFormArgs,

#[command(flatten)]
toggle: AutoRenewToggle,
}
Expand Down Expand Up @@ -77,6 +81,9 @@ pub(super) fn group() -> RuntimeGroupSpec {
// Clap's ArgGroup guarantees exactly one of --enable/--disable was passed.
let enabled = args.toggle.enable;
let debug = !ctx.middleware.debug.is_empty();
// Resolve the display form before the update is sent, so a bad
// preference can't hide the operation ID of a change already made.
let domain_format = args.domain_format.form(&ctx)?;

let client = make_client(&ctx).await?;
let idempotency_key = uuid::Uuid::new_v4().to_string();
Expand Down Expand Up @@ -120,14 +127,19 @@ pub(super) fn group() -> RuntimeGroupSpec {
result["status"] = json!(status);
}

domain_format.convert_fields(&mut result, &["domain"]);

let mut actions = Vec::new();
if is_terminal_status(&status) {
actions.push(
next_action(
"domain get <domain>",
"Confirm the updated auto-renew setting",
)
.with_param("domain", NextActionParam::value(domain)),
.with_param(
"domain",
NextActionParam::value(domain_format.render(&domain)),
),
);
} else if let Some(op_id) = operation_id.as_ref() {
actions.push(
Expand Down
2 changes: 1 addition & 1 deletion rust/src/domain/available.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
}
}

let domain_format = args.domain_format.form();
let domain_format = args.domain_format.form(&ctx)?;
domain_format.convert_fields(&mut result, &["domain"]);
let resolved_domain = domain_format.render(&resolved_domain);

Expand Down
2 changes: 1 addition & 1 deletion rust/src/domain/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
CliCoreError::message(format!("failed to serialize domain details: {e}"))
})?;
args.domain_format
.form()
.form(&ctx)?
.convert_fields(&mut value, &["domain", "nameServers"]);
Ok(CommandResult::new(value).with_next_actions(vec![
next_action("dns list <domain>", "View this domain's DNS records")
Expand Down
4 changes: 4 additions & 0 deletions rust/src/domain/guides/domain-purchase.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ token has expired or isn't found, just re-run `gddy domain quote`.

AI assistants: Before using `--agree` or `--confirm`, show the end user the exact price from `domain quote` (including any premium/registration fees) and each required legal agreement's title AND link (from `quote`'s `requiredAgreements`), and obtain their explicit confirmation to proceed. Do not summarize an agreement by name only — the user must be able to click through and read it. Do not infer confirmation from a general request to buy or register a domain.

## International domain names

You can type a name either as Unicode (`münchen.de`) or in its encoded form (`xn--mnchen-3ya.de`); both reach the same domain. Output shows Unicode by default. Add `--punycode` to any `domain` command that shows names to see the encoded form instead, or make that your default with `gddy config set punycode true` (undo it with `gddy config unset punycode`). Precedence is the `--punycode` flag, then the `GDDY_PUNYCODE` environment variable, then your saved setting, then Unicode, so `--punycode=false` turns the encoded form back off for one command.

## Contacts

A registration has four contact roles: **registrant**, **admin**, **billing**,
Expand Down
5 changes: 2 additions & 3 deletions rust/src/domain/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,9 @@ pub(super) fn command() -> RuntimeCommandSpec {
.map_err(|e| {
CliCoreError::message(format!("failed to serialize domain list: {e}"))
})?;
let domain_format = args.domain_format.form(&ctx)?;
for domain in &mut domains {
args.domain_format
.form()
.convert_fields(domain, &["domain", "nameServers"]);
domain_format.convert_fields(domain, &["domain", "nameServers"]);
}
Ok(CommandResult::new(json!(domains)).with_next_actions(vec![
next_action("dns list <domain>", "View a domain's DNS records")
Expand Down
4 changes: 3 additions & 1 deletion rust/src/domain/nameservers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ pub(super) fn group() -> RuntimeGroupSpec {
let domain = validate_domain_name(&args.domain)?;
let hosts = validate_nameserver_hosts(args.nameserver)?;
let debug = !ctx.middleware.debug.is_empty();
// Resolve the display form before the update is sent, so a bad
// preference can't hide the operation ID of a change already made.
let domain_format = args.domain_format.form(&ctx)?;

let client = make_client(&ctx).await?;
let body = types::NameServers(
Expand Down Expand Up @@ -93,7 +96,6 @@ pub(super) fn group() -> RuntimeGroupSpec {
if let Some(status) = op.status.as_ref() {
result["status"] = json!(status.to_string());
}
let domain_format = args.domain_format.form();
domain_format.convert_fields(&mut result, &["domain", "nameservers"]);
Ok(CommandResult::new(result).with_next_actions(vec![
next_action("domain get <domain>", "Confirm the updated nameservers").with_param(
Expand Down
2 changes: 1 addition & 1 deletion rust/src/domain/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ fn status_command() -> RuntimeCommandSpec {
.unwrap_or(operation_id);

let op_type = op.type_.as_ref().map(|t| t.to_string()).unwrap_or_default();
let domain_format = args.domain_format.form();
let domain_format = args.domain_format.form(&ctx)?;
let domain = domain_format.render(&domain);
let mut result = json!({
"operationId": op_id,
Expand Down
7 changes: 4 additions & 3 deletions rust/src/domain/purchase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ pub(super) fn command() -> RuntimeCommandSpec {
let agree = args.agree;
let confirm = args.confirm;
let debug = !ctx.middleware.debug.is_empty();
// Resolve the display form up front: a bad preference must fail
// before the registration is submitted, not after the charge.
let domain_format = args.domain_format.form(&ctx)?;

// Resolve auth *before* consuming the cached quote, so an auth
// failure fails locally rather than after the cache entry (and the
Expand Down Expand Up @@ -406,9 +409,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
);
}
}
args.domain_format
.form()
.convert_fields(&mut result, &["domain"]);
domain_format.convert_fields(&mut result, &["domain"]);
Ok(CommandResult::new(result).with_next_actions(actions))
},
)
Expand Down
11 changes: 6 additions & 5 deletions rust/src/domain/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ pub(super) fn command() -> RuntimeCommandSpec {
// build the profile. The token is bound to a hash of this quoted
// request, so what's quoted here is what `purchase` later registers.
let client = make_client(&ctx).await?;
// Resolve the display form before a quote is requested and cached,
// so a bad preference can't discard a token that was just issued.
let domain_format = args.domain_format.form(&ctx)?;
let profile = build_profile(privacy, renew_auto, &name_servers)?;
// Cache the exact profile we quote with: the token binds a hash of the
// domain/price/profile, so `purchase` must re-send this verbatim or the
Expand Down Expand Up @@ -410,17 +413,15 @@ pub(super) fn command() -> RuntimeCommandSpec {
// same next step `domain available` offers for a taken name.
// `domain suggest` accepts a seed domain, so the domain just
// quoted is a valid query to copy/paste directly.
let resolved_domain = args
.domain_format
.form()
.render(&quote.domain.clone().unwrap_or_else(|| domain.clone()));
let resolved_domain =
domain_format.render(&quote.domain.clone().unwrap_or_else(|| domain.clone()));
next_actions.push(
next_action("domain suggest <query>", "Find an available alternative")
.with_param("query", NextActionParam::value(resolved_domain)),
);
}

args.domain_format.form().convert_fields(&mut view, &["domain", "resolved.nameServers"]);
domain_format.convert_fields(&mut view, &["domain", "resolved.nameServers"]);
Ok(CommandResult::new(view).with_next_actions(next_actions))
},
)
Expand Down
2 changes: 1 addition & 1 deletion rust/src/domain/suggest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
Ok(r) => r.into_inner(),
Err(e) => return Err(api_error("domain suggestion", debug, e).await),
};
let domain_format = args.domain_format.form();
let domain_format = args.domain_format.form(&ctx)?;
let suggestions: Vec<serde_json::Value> = resp
.items
.iter()
Expand Down
14 changes: 13 additions & 1 deletion rust/src/domain/transfer_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use serde_json::json;
use super::common::{
api_error, format_operation_error, is_terminal_status, make_client, validate_domain_name,
};
use crate::idn::DomainFormArgs;
use crate::next_action::next_action;
use crate::output_schema::output_schema;
use crate::scopes::DOMAINS_DOMAIN_UPDATE;
Expand Down Expand Up @@ -43,6 +44,9 @@ struct TransferLockSetArgs {
#[arg(value_name = "DOMAIN")]
domain: String,

#[command(flatten)]
domain_format: DomainFormArgs,

#[command(flatten)]
toggle: TransferLockToggle,
}
Expand Down Expand Up @@ -81,6 +85,9 @@ pub(super) fn group() -> RuntimeGroupSpec {
// Clap's ArgGroup guarantees exactly one of --enable/--disable was passed.
let enabled = args.toggle.enable;
let debug = !ctx.middleware.debug.is_empty();
// Resolve the display form before the update is sent, so a bad
// preference can't hide the operation ID of a change already made.
let domain_format = args.domain_format.form(&ctx)?;

let client = make_client(&ctx).await?;
let idempotency_key = uuid::Uuid::new_v4().to_string();
Expand Down Expand Up @@ -124,14 +131,19 @@ pub(super) fn group() -> RuntimeGroupSpec {
result["status"] = json!(status);
}

domain_format.convert_fields(&mut result, &["domain"]);

let mut actions = Vec::new();
if is_terminal_status(&status) {
actions.push(
next_action(
"domain get <domain>",
"Confirm the updated transfer-lock setting",
)
.with_param("domain", NextActionParam::value(domain)),
.with_param(
"domain",
NextActionParam::value(domain_format.render(&domain)),
),
);
} else if let Some(op_id) = operation_id.as_ref() {
actions.push(
Expand Down
16 changes: 14 additions & 2 deletions rust/src/environments/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,25 @@ pub(crate) static ENV_LOCK: Mutex<()> = Mutex::const_new(());
/// value back if it was — even if a test panics. Restoring rather than
/// unconditionally removing keeps a var a developer happens to already
/// have set in their shell from leaking into the rest of the test run.
pub(super) struct EnvGuard {
pub(crate) struct EnvGuard {
key: &'static str,
prior: Option<String>,
}

impl EnvGuard {
pub(super) fn set(key: &'static str, value: &str) -> Self {
/// Removes `key` for the guard's lifetime, so a developer's own shell
/// value can't leak into the test.
pub(crate) fn unset(key: &'static str) -> Self {
let prior = std::env::var(key).ok();
// SAFETY: caller holds ENV_LOCK.
#[allow(unsafe_code)]
unsafe {
std::env::remove_var(key);
}
Self { key, prior }
}

pub(crate) fn set(key: &'static str, value: &str) -> Self {
let prior = std::env::var(key).ok();
// SAFETY: caller holds ENV_LOCK.
#[allow(unsafe_code)]
Expand Down
7 changes: 4 additions & 3 deletions rust/src/hosting/domain/attach.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ pub(super) fn command() -> RuntimeCommandSpec {
.with_output_schema::<HostingDomain>(),
|ctx, args: DomainAttachArgs| async move {
let app_id = args.app_id.clone();
// Resolve the display form before attaching, so a bad preference
// can't hide the details of an attachment that already succeeded.
let domain_format = args.domain_format.form(&ctx)?;
let client = make_client(&ctx, &[DOMAIN_WRITE]).await?;
let mut data = client
.attach_domain(&app_id, &args.hostname)
.await
.map_err(client_err)?;
args.domain_format
.form()
.convert_fields(&mut data, &["hostname"]);
domain_format.convert_fields(&mut data, &["hostname"]);
Ok(CommandResult::new(data).with_next_actions(vec![
next_action(
"hosting domain list --app-id <app-id>",
Expand Down
2 changes: 1 addition & 1 deletion rust/src/hosting/domain/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub(super) fn command() -> RuntimeCommandSpec {
.await
.map_err(client_err)?;
args.domain_format
.form()
.form(&ctx)?
.convert_fields(&mut data, &["hostname"]);
Ok(CommandResult::new(data).with_next_actions(vec![
next_action(
Expand Down
5 changes: 2 additions & 3 deletions rust/src/hosting/domain/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,9 @@ pub(super) fn command() -> RuntimeCommandSpec {
}
}

let domain_format = args.domain_format.form(&ctx)?;
for item in &mut all_items {
args.domain_format
.form()
.convert_fields(item, &["hostname"]);
domain_format.convert_fields(item, &["hostname"]);
}
Ok(CommandResult::new(json!(all_items)).with_next_actions(vec![
next_action(
Expand Down
Loading
Loading