domain

package
v1.801.425 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package domain is Hanzo Domains: search a name, see the price, buy it from your prepaid wallet.

This is the REGISTRATION product. The price carries Hanzo's markup, the purchase is billed through the customer's prepaid wallet, and the domain is born pointing at Hanzo's own authoritative nameservers.

This is DISTINCT from Hanzo DNS (hanzoai/dns): DNS manages records for a domain you already control; Domains ACQUIRES the domain. After a purchase, Domains hands the new zone to hanzoai/dns and points the registrar's nameservers at it — the two products compose (buy here, manage records there).

Wholesale is resold from a registrar behind the Registrar interface (name.com Core API v4 today, apps/domain/namecom). The core here is transport-free: it orchestrates availability → price → authorize → register → provision-zone → capture → record over four interfaces (Registrar, Biller, Zones, Store), so the policy is unit-testable with no HTTP/registrar/billing backend. mount.go is the thin cloud adapter that binds the real backends and exposes /v1/domain/*.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInsufficientFunds — the org's prepaid balance cannot cover the price. → 402.
	ErrInsufficientFunds = errors.New("domain: insufficient balance for this purchase")
	// ErrUnavailable — the name is not purchasable (taken, reserved, or unsupported TLD). → 409.
	ErrUnavailable = errors.New("domain: name is not available to register")
	// ErrAlreadyOwned — this org already holds this domain. → 409.
	ErrAlreadyOwned = errors.New("domain: already registered to this org")
	// ErrNotOwned — a renew/transfer/manage on a domain this org does not hold. → 404/403.
	ErrNotOwned = errors.New("domain: not registered to this org")
	// ErrNotConfigured — the registrar has no credentials. → 503.
	ErrNotConfigured = errors.New("domain: registrar not configured")
)

Sentinels the orchestration returns; the HTTP adapter maps them to status codes.

Functions

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount wires Hanzo Domains onto the unified cloud binary as /v1/domain/*:

GET  /v1/domain/health                      registrar reachability (no auth)
GET  /v1/domain/search?q=&tld=              keyword search + alternate TLDs (priced)
GET  /v1/domain/availability?domain=a,b     exact-name availability + pricing
GET  /v1/domain/domains                     the org's registered domains
POST /v1/domain/register {domain,years,contacts?}   buy (billed)
POST /v1/domain/renew    {domain,years}             renew (billed)
POST /v1/domain/transfer {domain,authCode,years}    transfer-in (billed)

Every mutating route is org-scoped: a validated principal's org owns the purchase and is the ledger the charge lands on. The registrar's wholesale credentials come from the platform secret store (KMS) via the operator-injected env NAMECOM_USER / NAMECOM_TOKEN — never hard-coded, exactly as clients/sites reads CF_API_TOKEN.

Types

type Biller

type Biller interface {
	// Authorize returns ErrInsufficientFunds when the balance can't cover cents, nil
	// to proceed, or another error when the balance is unknown (fail-closed).
	Authorize(ctx context.Context, org string, cents int64) error
	// Capture records the debit against the org's ledger. ref is the idempotency /
	// attribution key (e.g. "domain:register:acme.ai").
	Capture(org string, cents int64, ref string)
}

Biller is the two-phase deposit→charge a purchase bills through. Authorize refuses when the org's prepaid balance cannot cover the marked-up price (BEFORE the registrar is touched); Capture debits it AFTER the registrar succeeds. Backed by cloud's ResourceMeter (Gate → Authorize, Meter → Capture).

type Config

type Config struct {
	Markup      Markup   // wholesale → sell
	Nameservers []string // Hanzo authoritative NS to point purchased domains at (fallback when Zones returns none)
	Env         string   // registrar env label (test/prod) — attribution only
}

Config tunes pricing and the DNS handoff.

type Markup

type Markup struct {
	Multiplier     float64 // e.g. 1.15 = +15% over wholesale; <1 is clamped to 1 (never sell below cost)
	MinMarginCents int64   // floor absolute margin over cost, e.g. 300 = at least $3
}

Markup turns a wholesale registrar cost into the price a customer pays. The multiplier is applied, then a minimum absolute margin is enforced (so a cheap TLD still clears fixed per-order cost), then the result is rounded UP to whole cents.

This is the ONE place a margin is added, mirroring cloud/clients/pricing's THIRD_PARTY_MARKUP multiplier — kept as data (Config) so it is tunable without code.

func (Markup) Sell

func (m Markup) Sell(costCents int64) int64

Sell returns the customer price in cents for a wholesale cost in cents. A non-positive cost yields 0 (free / unpriced — the caller treats it as not purchasable). The result is always ≥ cost (never sell below wholesale).

type MemStore

type MemStore struct {
	// contains filtered or unexported fields
}

MemStore is an in-memory Store: a concurrency-safe map keyed by (org, domain). It is the default ownership store — sufficient for a single-process deployment and every test. A multi-replica deployment swaps in a SQLite/Postgres-backed Store with the SAME interface (the clients/finance per-org ledger pattern); nothing else changes.

func NewMemStore

func NewMemStore() *MemStore

NewMemStore builds an empty in-memory store.

func (*MemStore) Get

func (s *MemStore) Get(org, domainName string) (Record, bool, error)

Get returns the record for (org, domain) and whether it exists.

func (*MemStore) ListByOrg

func (s *MemStore) ListByOrg(org string) ([]Record, error)

ListByOrg returns every domain the org holds, newest registration first.

func (*MemStore) Put

func (s *MemStore) Put(rec Record) error

Put upserts a record.

type Quote

type Quote struct {
	Domain            string `json:"domain"`
	Available         bool   `json:"available"`
	Premium           bool   `json:"premium,omitempty"`
	CostCents         int64  `json:"-"`                 // wholesale (internal — not exposed to customers)
	PriceCents        int64  `json:"priceCents"`        // sell (first-term registration)
	RenewalPriceCents int64  `json:"renewalPriceCents"` // sell (renewal)
	Currency          string `json:"currency"`
	TLD               string `json:"tld,omitempty"`
}

Quote is a priced availability result: the wholesale cost and the customer sell price (marked up), both in cents. It is what search/availability return and what a register bills against.

type Record

type Record struct {
	Org          string   `json:"org"`
	Domain       string   `json:"domain"`
	RegisteredAt int64    `json:"registeredAt"` // unix seconds
	ExpiresAt    string   `json:"expiresAt,omitempty"`
	PriceCents   int64    `json:"priceCents"` // what the customer paid (sell)
	CostCents    int64    `json:"costCents"`  // wholesale cost
	Nameservers  []string `json:"nameservers,omitempty"`
	Order        int64    `json:"order,omitempty"` // registrar order id
}

Record is the domain↔org ownership row Hanzo issues on a successful purchase.

type RegisterResult

type RegisterResult struct {
	Record Record `json:"record"`
	Quote  Quote  `json:"quote"`
}

RegisterResult is a successful purchase.

type Registrar

type Registrar interface {
	CheckAvailability(ctx context.Context, names ...string) (*namecom.SearchResponse, error)
	Search(ctx context.Context, keyword string, tldFilter ...string) (*namecom.SearchResponse, error)
	CreateDomain(ctx context.Context, req namecom.CreateDomainRequest) (*namecom.CreateDomainResponse, error)
	RenewDomain(ctx context.Context, domainName string, req namecom.RenewDomainRequest) (*namecom.RenewDomainResponse, error)
	SetNameservers(ctx context.Context, domainName string, nameservers []string) (*namecom.Domain, error)
	CreateTransfer(ctx context.Context, req namecom.TransferRequest) (*namecom.TransferResponse, error)
	Hello(ctx context.Context) (*namecom.HelloResponse, error)
	Configured() bool
}

Registrar is the wholesale registrar Hanzo resells. *namecom.Client satisfies it.

type RenewResult

type RenewResult struct {
	Record    Record `json:"record"`
	PaidCents int64  `json:"paidCents"`
}

RenewResult is a successful renewal.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the transport-free orchestrator.

func NewService

func NewService(reg Registrar, bill Biller, zones Zones, store Store, cfg Config) *Service

NewService builds the orchestrator. All four backends are required; pass a no-op Zones/Store in a context that doesn't need them.

func (*Service) Availability

func (s *Service) Availability(ctx context.Context, names ...string) ([]Quote, error)

Availability checks exact names and returns priced quotes (availability + pricing).

func (*Service) Configured

func (s *Service) Configured() bool

Configured reports whether the registrar has credentials.

func (*Service) Env

func (s *Service) Env() string

Env reports the registrar environment label (test/prod).

func (*Service) ListByOrg

func (s *Service) ListByOrg(org string) ([]Record, error)

ListByOrg returns the domains an org holds.

func (*Service) Register

func (s *Service) Register(ctx context.Context, org, domainName string, years int, contacts *namecom.Contacts) (*RegisterResult, error)

Register buys a domain for org: quote → guard → authorize (deposit) → provision the DNS zone → register at the registrar pointing at Hanzo nameservers → capture (charge) → record ownership. The customer is charged ONLY after the registrar confirms — a registrar failure leaves the balance untouched.

contacts is optional; when nil the registrar uses the reseller account's default WHOIS contacts. years defaults to 1.

func (*Service) Renew

func (s *Service) Renew(ctx context.Context, org, domainName string, years int) (*RenewResult, error)

Renew extends a domain this org owns: guard ownership → quote renewal → authorize → renew at the registrar → capture → update the record's expiry.

func (*Service) Search

func (s *Service) Search(ctx context.Context, keyword string, tldFilter ...string) ([]Quote, error)

Search runs a keyword search (with alternate-TLD suggestions) and returns priced quotes. tldFilter (optional) narrows the TLDs.

func (*Service) Transfer

func (s *Service) Transfer(ctx context.Context, org, domainName, authCode string, years int) (*RegisterResult, error)

Transfer starts an inbound transfer of a domain the customer owns elsewhere: quote (the transfer price is the registration price) → authorize → create the transfer → capture → record. authCode is the EPP auth code from the losing registrar.

type Store

type Store interface {
	Put(rec Record) error
	Get(org, domainName string) (Record, bool, error)
	ListByOrg(org string) ([]Record, error)
}

Store persists ownership records.

type Zones

type Zones interface {
	EnsureZone(ctx context.Context, org, domainName string) (nameservers []string, err error)
}

Zones ensures an authoritative DNS zone exists for a freshly-registered domain and returns the nameservers to point it at. Backed by hanzoai/dns.

Directories

Path Synopsis
Package namecom is a minimal client for the name.com Core API v4 — the wholesale registrar surface Hanzo Domains resells: check availability, price, register, renew, transfer, and set nameservers/contacts on a domain.
Package namecom is a minimal client for the name.com Core API v4 — the wholesale registrar surface Hanzo Domains resells: check availability, price, register, renew, transfer, and set nameservers/contacts on a domain.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL