Background
We can sync domain contacts from OpenSRS into the local SQLite database, but there's no path to push changes back to the registry. This means macaw is currently read-only with respect to contact information — you can see what's registered but can't fix a typo, update a phone number, or change an address without going directly to the OpenSRS reseller console.
Current State
The existing implementation is half of a round-trip:
src/db/contacts.rs has find_or_create_contact() for deduplication, but no update_contact() path
src/opensrs/xml.rs deserialize_domain_all_info() initializes contact variables to None at lines 282–285 but never actually populates them from the XML
src/sync/mod.rs maps contacts one-way: OpenSRS → local DB
What Needs to Be Built
1. Fix the incomplete contact XML deserializer (src/opensrs/xml.rs)
deserialize_domain_all_info() declares four contact variables that are always None. The parser needs state machine entries to handle contact_set → owner/admin/billing/tech → first_name/last_name/org_name/email/phone/fax/address1/address2/city/state/country/postal_code.
2. Add SET_CONTACT request/response types (src/opensrs/types.rs)
New structs for the OpenSRS CONTACT object SET action. The outbound contact fields use OpenSRS naming (org_name, state, country) which differs from the DB schema (organization, state_province, country_code) — mapping needs to be explicit and documented.
3. XML serialization for contact update requests (src/opensrs/xml.rs)
A serialize_set_contact_request() function following the same string-building pattern used for domain requests. The contact set block nests four contact types each with ~12 fields.
4. Add update_contact() to the DB layer (src/db/contacts.rs)
Currently find_or_create_contact() is the only entry point. We need a function that updates an existing contact record in place. Important: contacts are reusable across multiple domains via domain_contacts. Updating a shared contact changes data for every domain linked to it — callers need to decide whether to update in place or create a new contact record and re-link the domain.
5. New OpenSrsClient::update_domain_contacts() method (src/opensrs/client.rs)
Handles authentication, XML serialization, HTTP dispatch, and response parsing for the SET_CONTACT operation. Should accept a domain name and a ContactSet and return a result.
6. CLI subcommand
A new subcommand (e.g., macaw contacts update <domain>) that reads updated contact info (from a file, flags, or interactive prompt — TBD), calls the OpenSRS client, updates the local DB, and writes an audit log entry.
7. Audit logging
Per the security considerations in CLAUDE.md, all contact changes must write to audit_log with the old and new values as JSON snapshots.
Tricky Bits
Field name mapping between OpenSRS and the local schema:
| OpenSRS field |
DB column |
org_name |
organization |
state |
state_province |
country |
country_code |
Contact deduplication and shared contacts: The find_or_create_contact() function does exact-match deduplication, so the same contact record may be linked to multiple domains. An update workflow needs to handle this — probably by creating a new contact record and updating the domain_contacts link for the specific domain being changed, rather than mutating a shared record.
Acceptance Criteria
Related
src/db/contacts.rs — current read-only contact persistence
src/opensrs/xml.rs lines 282–285 — stub contact variables waiting to be wired up
src/sync/mod.rs — one-way sync that will need a complement for outbound changes
- OpenSRS XCP API docs,
CONTACT object, SET action
Background
We can sync domain contacts from OpenSRS into the local SQLite database, but there's no path to push changes back to the registry. This means macaw is currently read-only with respect to contact information — you can see what's registered but can't fix a typo, update a phone number, or change an address without going directly to the OpenSRS reseller console.
Current State
The existing implementation is half of a round-trip:
src/db/contacts.rshasfind_or_create_contact()for deduplication, but noupdate_contact()pathsrc/opensrs/xml.rsdeserialize_domain_all_info()initializes contact variables toNoneat lines 282–285 but never actually populates them from the XMLsrc/sync/mod.rsmaps contacts one-way: OpenSRS → local DBWhat Needs to Be Built
1. Fix the incomplete contact XML deserializer (
src/opensrs/xml.rs)deserialize_domain_all_info()declares four contact variables that are alwaysNone. The parser needs state machine entries to handlecontact_set → owner/admin/billing/tech → first_name/last_name/org_name/email/phone/fax/address1/address2/city/state/country/postal_code.2. Add
SET_CONTACTrequest/response types (src/opensrs/types.rs)New structs for the OpenSRS
CONTACTobjectSETaction. The outbound contact fields use OpenSRS naming (org_name,state,country) which differs from the DB schema (organization,state_province,country_code) — mapping needs to be explicit and documented.3. XML serialization for contact update requests (
src/opensrs/xml.rs)A
serialize_set_contact_request()function following the same string-building pattern used for domain requests. The contact set block nests four contact types each with ~12 fields.4. Add
update_contact()to the DB layer (src/db/contacts.rs)Currently
find_or_create_contact()is the only entry point. We need a function that updates an existing contact record in place. Important: contacts are reusable across multiple domains viadomain_contacts. Updating a shared contact changes data for every domain linked to it — callers need to decide whether to update in place or create a new contact record and re-link the domain.5. New
OpenSrsClient::update_domain_contacts()method (src/opensrs/client.rs)Handles authentication, XML serialization, HTTP dispatch, and response parsing for the
SET_CONTACToperation. Should accept a domain name and aContactSetand return a result.6. CLI subcommand
A new subcommand (e.g.,
macaw contacts update <domain>) that reads updated contact info (from a file, flags, or interactive prompt — TBD), calls the OpenSRS client, updates the local DB, and writes an audit log entry.7. Audit logging
Per the security considerations in
CLAUDE.md, all contact changes must write toaudit_logwith the old and new values as JSON snapshots.Tricky Bits
Field name mapping between OpenSRS and the local schema:
org_nameorganizationstatestate_provincecountrycountry_codeContact deduplication and shared contacts: The
find_or_create_contact()function does exact-match deduplication, so the same contact record may be linked to multiple domains. An update workflow needs to handle this — probably by creating a new contact record and updating thedomain_contactslink for the specific domain being changed, rather than mutating a shared record.Acceptance Criteria
deserialize_domain_all_info()correctly populates contact fields from OpenSRS XMLSET_CONTACTXML request serialization produces valid OpenSRS XMLaudit_loghas entries for every contact change with old/new JSON snapshotsRelated
src/db/contacts.rs— current read-only contact persistencesrc/opensrs/xml.rslines 282–285 — stub contact variables waiting to be wired upsrc/sync/mod.rs— one-way sync that will need a complement for outbound changesCONTACTobject,SETaction