diff --git a/go.mod b/go.mod index a84716a3..c2017e0b 100644 --- a/go.mod +++ b/go.mod @@ -40,10 +40,10 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 go.lumeweb.com/configmanager v0.3.30 go.lumeweb.com/ipfs-content v0.1.18 - go.lumeweb.com/ipfs-sdk v0.1.89 + go.lumeweb.com/ipfs-sdk v0.1.95 go.lumeweb.com/ipfs-sdk/dnsname v0.1.64 go.lumeweb.com/oauth v0.1.6 - go.lumeweb.com/portal-sdk v0.1.71 + go.lumeweb.com/portal-sdk v0.1.72 go.lumeweb.com/queryutil v0.3.19 go.sia.tech/core v0.21.7 go.sia.tech/indexd v0.4.4 diff --git a/go.sum b/go.sum index b3127240..7a0d9453 100644 --- a/go.sum +++ b/go.sum @@ -735,8 +735,8 @@ go.lumeweb.com/gswagger v0.20.12 h1:FNSrDAeMQVAGIogzJ4SQX+X8U/FMeWQ80yxdyfsM2/M= go.lumeweb.com/gswagger v0.20.12/go.mod h1:qz+9/8qstYy7WJ48VZJW/0FonlExw/Igv7vWGgp5uoU= go.lumeweb.com/ipfs-content v0.1.18 h1:Lq3/A+2kZcS4RK+bpdk+m4iJ95ZgpEa4DKhiH6bQWUg= go.lumeweb.com/ipfs-content v0.1.18/go.mod h1:idWCsfndMDCsE5LtU0ZBNaFTBAaiuIe50dce0SDJLxw= -go.lumeweb.com/ipfs-sdk v0.1.89 h1:BCivMn9vKEiQ7A9tqq2U/fyzc9J+lWZKJq7TnAl+GCo= -go.lumeweb.com/ipfs-sdk v0.1.89/go.mod h1:/ZKw5UlfGIxBBLTdX9Ixq/ObGVTgHjRGKwsNf8UHuPo= +go.lumeweb.com/ipfs-sdk v0.1.95 h1:4y3Oj6aTKLTWSzpdjizrQ7wJc/iXhza9MVuEhxbblyo= +go.lumeweb.com/ipfs-sdk v0.1.95/go.mod h1:+V8YkslX65rPa83BbNYLGnhK24i75wqFtXKyltJ6jV4= go.lumeweb.com/ipfs-sdk/dnsname v0.1.64 h1:RTBD+zoXHOYYKreNRoQhyLLKKWp/VgYfr7qojCzKjww= go.lumeweb.com/ipfs-sdk/dnsname v0.1.64/go.mod h1:1++6EWMiG/BC5UQjlIobId2YcWDVHjHEhdsEzeISKpQ= go.lumeweb.com/oauth v0.1.6 h1:6c6LrXxMwx5klbq3OshzXjkGwvYVjAQhjX2FxCAgqqg= @@ -747,8 +747,8 @@ go.lumeweb.com/portal-middleware v0.3.7 h1:kq4SZq4T/uhauqHehw2JaTbNJpPpE8/EQAC3R go.lumeweb.com/portal-middleware v0.3.7/go.mod h1:Yn9ZsFy5n3x2jUJ085M9B/aoZ+ANQV5QD/MAE0BpJXo= go.lumeweb.com/portal-router v0.7.7 h1:6mUGkG2BtHDygIPqRZs1sRkPLkz9l2wTXMUhZmdp24E= go.lumeweb.com/portal-router v0.7.7/go.mod h1:hl51sHDKcgw8yxJ6RraYBOI1Vnufq33FhKwfKiqCkVg= -go.lumeweb.com/portal-sdk v0.1.71 h1:wPnR2zufo96Ao/vML5f5P2uyK0mDTPW2JOsePuy/WdI= -go.lumeweb.com/portal-sdk v0.1.71/go.mod h1:KKxa2hV+44EI+0riJIjREvz7/sqgWGRTxtLUGgnADZg= +go.lumeweb.com/portal-sdk v0.1.72 h1:MqbLlNLfjFxOjCcgg7ArxOCqD8IV5AR+KKJ3droidhs= +go.lumeweb.com/portal-sdk v0.1.72/go.mod h1:Xrm8rbyHc5QZGmn6fxZGQbtChHPbVZzX1fVkqtcw+Io= go.lumeweb.com/queryutil v0.3.19 h1:GV+zKsyGJ+YYPZi0mqOocDAROCQruGdizrE8Aw8cNFk= go.lumeweb.com/queryutil v0.3.19/go.mod h1:YPrXdEelsjJNBPXrG4IiYp4aq5YUk/l4uXw8z2cSAuw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/internal/catalogops/admin_platform_domains.go b/internal/catalogops/admin_platform_domains.go index 6109295c..82ec4eaa 100644 --- a/internal/catalogops/admin_platform_domains.go +++ b/internal/catalogops/admin_platform_domains.go @@ -64,7 +64,7 @@ func adminPlatformDomainsRegister(d AdminDeps) catalog.Operation { Name: OpAdminPlatformDomainsRegister, Title: "Register a platform domain", Summary: "Register a platform-owned root domain", - Description: "Register a platform-owned root domain that users can claim free subdomains under, e.g. pinned.site. Requires admin privileges.", + Description: "Register a platform-owned root domain that users can claim free subdomains under, e.g. pinned.site. The namespace defaults to hns when omitted; pass icann for a traditional DNS root. Requires admin privileges.", Category: "admin", Safety: catalog.SafetyMutate, Interaction: catalog.InteractionAgentSafe, @@ -72,7 +72,8 @@ func adminPlatformDomainsRegister(d AdminDeps) catalog.Operation { Positional: "", Args: []catalog.OperationArg{ {Name: "domain", Type: catalog.ArgTypeString, Required: true, Help: "Platform root domain, e.g. pinned.site", PositionalOnly: true}, - {Name: "namespace", Type: catalog.ArgTypeString, Required: true, Help: "Domain namespace: icann, hns, etc."}, + // Optional: the backend defaults omitted namespaces to hns. + {Name: "namespace", Type: catalog.ArgTypeString, Required: false, Help: "Domain namespace (default hns): icann or hns", PositionalOnly: true}, // Nullable so an omitted flag leaves Enabled nil (backend default) // rather than forcing false; an explicit --enabled=false disables. {Name: "enabled", Type: catalog.ArgTypeNullableBool, Required: false, Help: "Enable the platform domain so users can claim subdomains under it"}, @@ -89,11 +90,12 @@ func adminPlatformDomainsRegister(d AdminDeps) catalog.Operation { if domain == "" { return nil, fmt.Errorf("admin_platform_domains_register: domain is required") } - namespace := catalog.StrArg(input, "namespace", "") - if namespace == "" { - return nil, fmt.Errorf("admin_platform_domains_register: namespace is required") + req := &admin.PlatformDomainRequest{Domain: domain} + // Namespace is optional: the backend defaults an omitted namespace + // to hns, so nil is passed through unchanged when not supplied. + if namespace := catalog.StrArg(input, "namespace", ""); namespace != "" { + req.Namespace = &namespace } - req := &admin.PlatformDomainRequest{Domain: domain, Namespace: namespace} if enabled := catalog.BoolArgPtr(input, "enabled"); enabled != nil { req.Enabled = enabled } diff --git a/internal/catalogops/websites.go b/internal/catalogops/websites.go index ded0e7b6..1b5f0e1e 100644 --- a/internal/catalogops/websites.go +++ b/internal/catalogops/websites.go @@ -115,6 +115,7 @@ func WebsitesOperations(d WebsitesDeps) []catalog.Operation { websitesDomainsVerify(d), websitesDomainsDNSRequirements(d), websitesDomainsDANERepublish(d), + websitesDomainsConvertOnChain(d), websitesDomainsUpdate(d), websitesPlatformDomainsList(d), websitesPlatformDomainAvailability(d), diff --git a/internal/catalogops/websites_convert_onchain_test.go b/internal/catalogops/websites_convert_onchain_test.go new file mode 100644 index 00000000..b86537e5 --- /dev/null +++ b/internal/catalogops/websites_convert_onchain_test.go @@ -0,0 +1,119 @@ +package catalogops + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + ipfs "go.lumeweb.com/ipfs-sdk" + + "go.lumeweb.com/pinner-cli/internal/core/config" + configmocks "go.lumeweb.com/pinner-cli/internal/core/config/mocks" + "go.lumeweb.com/pinner-cli/internal/core/websites" +) + +// convertOnChainCaptureService satisfies websites.Service for the +// websites_domains_convert_onchain operation: it serves a one-website list +// with one HNS binding and records the website/domain IDs the handler called +// the SDK with. +type convertOnChainService struct { + websites.Service + + websiteID string + domainID string + called bool +} + +func (f *convertOnChainService) RequireAuthenticated() error { return nil } + +func (f *convertOnChainService) List(_ context.Context, _ websites.ListOptions) ([]ipfs.WebsiteItem, error) { + return []ipfs.WebsiteItem{{Id: 7, Domain: "example.test", TargetType: "ipfs"}}, nil +} + +func (f *convertOnChainService) ListDomains(_ context.Context, _ string) ([]ipfs.DomainResponse, error) { + return []ipfs.DomainResponse{{ + Id: 3, + Domain: "acme", + Namespace: ipfs.DomainNamespaceHNS, + Status: new(ipfs.DomainResponseStatusOnchainManaged), + }}, nil +} + +func (f *convertOnChainService) ConvertDomainToOnChain(_ context.Context, websiteID, domainID string) (*ipfs.DomainResponse, error) { + f.called = true + f.websiteID = websiteID + f.domainID = domainID + return &ipfs.DomainResponse{Id: 3, Domain: "acme", Namespace: ipfs.DomainNamespaceHNS, Status: new(ipfs.DomainResponseStatusOnchainManaged)}, nil +} + +func convertOnChainDeps(t testing.TB, fake *convertOnChainService) WebsitesDeps { + return WebsitesDeps{ + CfgMgr: func() config.Manager { return configmocks.NewMockManager(t) }, + NewAuthenticated: func(_ config.Manager, _ bool, _ string) (websites.Service, error) { + return fake, nil + }, + ServiceFactory: func(_ config.Manager, _ bool, _ ...websites.Option) websites.Service { + return fake + }, + GetAuthToken: func() string { return "" }, + } +} + +// TestWebsitesDomainsConvertOnChain_RequiresConfirm guards the destructive +// gate: the one-way conversion refuses to run without confirm=true. +func TestWebsitesDomainsConvertOnChain_RequiresConfirm(t *testing.T) { + fake := &convertOnChainService{} + op := websitesDomainsConvertOnChain(convertOnChainDeps(t, fake)) + + _, err := op.Handler().Execute(context.Background(), map[string]any{ + "domain": "acme", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "confirmation is required") + require.False(t, fake.called, "the SDK conversion must never run without confirm") +} + +// TestWebsitesDomainsConvertOnChain_WithConfirm guards the happy path: the op +// resolves the binding by name, sends the website + numeric binding IDs to the +// SDK, and returns the (onchain_managed) domain response. +func TestWebsitesDomainsConvertOnChain_WithConfirm(t *testing.T) { + fake := &convertOnChainService{} + op := websitesDomainsConvertOnChain(convertOnChainDeps(t, fake)) + + result, err := op.Handler().Execute(context.Background(), map[string]any{ + "domain": "acme", + "confirm": true, + }) + require.NoError(t, err) + require.True(t, fake.called) + require.Equal(t, "7", fake.websiteID) + require.Equal(t, "3", fake.domainID) + + domain, ok := result.(*ipfs.DomainResponse) + require.True(t, ok) + require.NotNil(t, domain.Status) + require.Equal(t, ipfs.DomainResponseStatusOnchainManaged, *domain.Status) +} + +// TestWebsitesDomainsConvertOnChain_RegisteredCatalogOp guards that the op is +// part of the canonical websites catalog so both frontends (CLI + MCP) expose +// it — a missing entry here would silently drop the tool from one surface. +func TestWebsitesDomainsConvertOnChain_RegisteredCatalogOp(t *testing.T) { + d := WebsitesDeps{ + CfgMgr: func() config.Manager { return nil }, + NewAuthenticated: func(_ config.Manager, _ bool, _ string) (websites.Service, error) { + return nil, nil + }, + ServiceFactory: func(_ config.Manager, _ bool, _ ...websites.Option) websites.Service { + return nil + }, + GetAuthToken: func() string { return "" }, + } + names := make(map[string]bool) + for _, op := range WebsitesOperations(d) { + names[op.Name()] = true + } + require.True(t, names["websites_domains_convert_onchain"], + "websites_domains_convert_onchain must be in WebsitesOperations") +} diff --git a/internal/catalogops/websites_domains.go b/internal/catalogops/websites_domains.go index 4b1f70e7..c2370a59 100644 --- a/internal/catalogops/websites_domains.go +++ b/internal/catalogops/websites_domains.go @@ -327,6 +327,52 @@ func websitesDomainsDANERepublish(d WebsitesDeps) catalog.Operation { }) } +// websitesDomainsConvertOnChain is the `websites domains convert-onchain` +// operation. Returns *ipfs.DomainResponse. Reclassifies the binding as +// on-chain managed: the HNS name's DNS is served by an external contract +// (its NS record points at one), so the portal +// deletes its managed PowerDNS zone/DNSSEC and switches ownership verification +// to the TXT token. One-way; DANE/SSL state is retained. +func websitesDomainsConvertOnChain(d WebsitesDeps) catalog.Operation { + return catalog.NewOperation(catalog.OperationSpec{ + Name: "websites_domains_convert_onchain", + Title: "Convert a domain to on-chain managed", + Summary: "Reclassify a bound HNS domain as on-chain managed", + Description: "Reclassify a bound HNS domain as on-chain managed (status onchain_managed): the name's DNS is served by an external contract on the Handshake chain, so Pinner deletes its managed zone and DNSSEC and verifies ownership via a TXT token instead of delegation. DANE/SSL state is retained. ONE-WAY and irreversible as a hosting change: requires confirm=true. Only applies to HNS-namespace bindings; the backend refuses an already on-chain, ineligible, or zone-sharing binding. The domain argument can be the domain name or its numeric binding ID; the owning website is resolved automatically. Returns the updated domain object (status onchain_managed, delegation null).", + Category: "core", + Safety: catalog.SafetyDestructive, + Interaction: catalog.InteractionAgentSafe, + Visibility: catalog.VisibilityBoth, + Positional: "", + Args: []catalog.OperationArg{ + {Name: "domain", Type: catalog.ArgTypeString, Required: true, Help: "Domain name or binding ID to convert to on-chain managed"}, + {Name: "confirm", Type: catalog.ArgTypeBool, AgentRequired: true, Help: "Confirm the one-way conversion (deletes Pinner's managed zone/DNSSEC for the binding)", AgentHelp: "Must be true to convert the domain to on-chain managed; this drops Pinner's managed zone/DNSSEC and is one-way. Only a human sets this on confirmation; a model alone cannot confirm a destructive operation."}, + }, + Handler: handler(func(ctx context.Context, input map[string]any) (any, error) { + if !catalog.BoolArg(input, "confirm", false) { + return nil, fmt.Errorf("websites_domains_convert_onchain: confirmation is required — the conversion deletes Pinner's managed zone/DNSSEC for the binding and is one-way") + } + svc, svcErr := d.service(input) + if svcErr != nil { + return nil, svcErr + } + if err := svc.RequireAuthenticated(); err != nil { + return nil, err + } + domainArg := catalog.StrArg(input, "domain", "") + if domainArg == "" { + return nil, fmt.Errorf("websites_domains_convert_onchain: domain is required") + } + websiteID, domainID, err := websites.ResolveDomainBinding(ctx, svc, domainArg) + if err != nil { + return nil, err + } + // *ipfs.DomainResponse + return svc.ConvertDomainToOnChain(ctx, websiteID, domainID) + }), + }) +} + // websitesDomainsUpdate is the `websites domains update` operation. Returns // *ipfs.DomainResponse. // diff --git a/internal/catalogops/websites_domains_test.go b/internal/catalogops/websites_domains_test.go index 4665f455..2123c52f 100644 --- a/internal/catalogops/websites_domains_test.go +++ b/internal/catalogops/websites_domains_test.go @@ -111,7 +111,7 @@ func singleWebsiteFixture() *domainsService { return []ipfs.WebsiteItem{{Id: 7, Domain: "example.test"}}, nil }, listDomainsFn: func(_ context.Context, websiteID string) ([]ipfs.DomainResponse, error) { - return []ipfs.DomainResponse{{Id: 3, Domain: "example.test", Namespace: "icann"}}, nil + return []ipfs.DomainResponse{{Id: 3, Domain: "example.test", Namespace: ipfs.DomainNamespaceICANN}}, nil }, } } @@ -121,7 +121,7 @@ func TestWebsitesDomainsListResolvesWebsite(t *testing.T) { var gotWebsiteID string fake.listDomainsFn = func(_ context.Context, websiteID string) ([]ipfs.DomainResponse, error) { gotWebsiteID = websiteID - return []ipfs.DomainResponse{{Id: 3, Domain: "example.test", Namespace: "icann"}}, nil + return []ipfs.DomainResponse{{Id: 3, Domain: "example.test", Namespace: ipfs.DomainNamespaceICANN}}, nil } op := websitesDomainsList(domainsDeps(t, fake)) @@ -179,7 +179,7 @@ func TestWebsitesDomainsAddRejectsInvalidNamespace(t *testing.T) { var bindCalled bool fake.bindDomainFn = func(_ context.Context, websiteID string, req ipfs.DomainRequest) (*ipfs.DomainResponse, error) { bindCalled = true - return &ipfs.DomainResponse{Id: 9, Domain: req.Domain, Namespace: req.Namespace}, nil + return &ipfs.DomainResponse{Id: 9, Domain: req.Domain, Namespace: ipfs.DomainNamespace(req.Namespace)}, nil } op := websitesDomainsAdd(domainsDeps(t, fake)) @@ -199,7 +199,7 @@ func TestWebsitesDomainsAddValidNamespaceReachesBindDomain(t *testing.T) { var gotReq ipfs.DomainRequest fake.bindDomainFn = func(_ context.Context, websiteID string, req ipfs.DomainRequest) (*ipfs.DomainResponse, error) { gotReq = req - return &ipfs.DomainResponse{Id: 9, Domain: req.Domain, Namespace: req.Namespace}, nil + return &ipfs.DomainResponse{Id: 9, Domain: req.Domain, Namespace: ipfs.DomainNamespace(req.Namespace)}, nil } op := websitesDomainsAdd(domainsDeps(t, fake)) diff --git a/internal/cli/catalog_websites_wiring.go b/internal/cli/catalog_websites_wiring.go index da444c75..9511a196 100644 --- a/internal/cli/catalog_websites_wiring.go +++ b/internal/cli/catalog_websites_wiring.go @@ -319,7 +319,7 @@ func websitesActionAdapter(op catalog.Operation) cli.ActionFunc { } headers := []string{"Field", "Value"} rows := [][]string{ - {"Status", website.Ssl.Status}, + {"Status", string(website.Ssl.Status)}, {"Issued At", formatTimePtr(website.Ssl.IssuedAt)}, {"Last Updated", formatTimePtr(website.Ssl.LastUpdatedAt)}, } @@ -471,10 +471,10 @@ func renderWebsitesResult(_ context.Context, c *cli.Command, op catalog.Operatio } status := "" if d.Status != nil { - status = *d.Status + status = string(*d.Status) } return []string{ - fmt.Sprintf("%d", d.Id), d.Domain, d.Namespace, status, zoneName, + fmt.Sprintf("%d", d.Id), d.Domain, string(d.Namespace), status, zoneName, } }) output.PrintTable(headers, rows) @@ -572,7 +572,7 @@ func renderWebsitesResult(_ context.Context, c *cli.Command, op catalog.Operatio func renderDomainResponse(output Output, r *ipfs.DomainResponse) { status := "" if r.Status != nil { - status = *r.Status + status = string(*r.Status) } zoneName := "" if r.ZoneName != nil { @@ -581,7 +581,7 @@ func renderDomainResponse(output Output, r *ipfs.DomainResponse) { fields := []Field{ {"ID", fmt.Sprintf("%d", r.Id)}, {"Domain", r.Domain}, - {"Namespace", r.Namespace}, + {"Namespace", string(r.Namespace)}, {"Status", status}, {"Zone Name", zoneName}, // Surface the per-domain DNS hosting state so the user can verify @@ -603,7 +603,7 @@ func renderDomainResponse(output Output, r *ipfs.DomainResponse) { func renderDomainDANEResponse(output Output, r *ipfs.DomainDANERepublishResponse) { status := "" if r.Status != nil { - status = *r.Status + status = string(*r.Status) } ownerName := "" if r.OwnerName != nil { @@ -617,7 +617,7 @@ func renderDomainDANEResponse(output Output, r *ipfs.DomainDANERepublishResponse Fields: []Field{ {"ID", fmt.Sprintf("%d", r.Id)}, {"Domain", r.Domain}, - {"Namespace", r.Namespace}, + {"Namespace", string(r.Namespace)}, {"Status", status}, {"Owner Name", ownerName}, {"TLSA Record", tlsaRecord}, @@ -683,7 +683,7 @@ func renderWebsiteSSLStatusHuman(output Output, r *ipfs.WebsiteResponse) { } headers := []string{"Field", "Value"} rows := [][]string{ - {"Status", r.Ssl.Status}, + {"Status", string(r.Ssl.Status)}, {"Issued At", formatTimePtr(r.Ssl.IssuedAt)}, {"Last Updated", formatTimePtr(r.Ssl.LastUpdatedAt)}, } diff --git a/internal/cli/websites_domains.go b/internal/cli/websites_domains.go index df7fb24c..93b83b8a 100644 --- a/internal/cli/websites_domains.go +++ b/internal/cli/websites_domains.go @@ -22,11 +22,11 @@ func renderDomainDelegation(output Output, result *ipfs.DomainResponse, managed status := "" if result.Status != nil { - status = *result.Status + status = string(*result.Status) } fields := []Field{ {"Domain", result.Domain}, - {"Namespace", result.Namespace}, + {"Namespace", string(result.Namespace)}, {"Status", status}, } // Surface the explicit DNSSEC state (enabled/disabled/error) + reason so an @@ -39,10 +39,9 @@ func renderDomainDelegation(output Output, result *ipfs.DomainResponse, managed } output.PrintFields(FieldGroup{Fields: fields}) - if result.Delegation == nil { - output.Printfln("No delegation records are available for %s.", result.Domain) - return - } - + // The registry renders even without a delegation bundle: a nil Delegation is + // meaningful per-namespace (e.g. an HNS on-chain managed binding + // serves its DNS from an external contract and has no records to publish), + // so the driver owns the explanation instead of a generic miss here. defaultDelegationDriver.Render(output, result, managed) } diff --git a/internal/cli/websites_domains_delegation_common.go b/internal/cli/websites_domains_delegation_common.go index 138bc35f..6c022a61 100644 --- a/internal/cli/websites_domains_delegation_common.go +++ b/internal/cli/websites_domains_delegation_common.go @@ -6,6 +6,21 @@ import ( ipfs "go.lumeweb.com/ipfs-sdk" ) +// statusOf returns the domain response's status string, "" when unset. +func statusOf(r *ipfs.DomainResponse) ipfs.DomainResponseStatus { + if r == nil || r.Status == nil { + return "" + } + return *r.Status +} + +// statusOnchainManaged reports whether an HNS domain binding is on-chain +// managed: its DNS is served by an external contract, so there is no portal +// delegation to publish. +func statusOnchainManaged(r *ipfs.DomainResponse) bool { + return statusOf(r) == ipfs.DomainResponseStatusOnchainManaged +} + // renderDelegationNameservers prints the nameservers shortcut when present. func renderDelegationNameservers(output Output, d *ipfs.DNSDelegation) { if d.Nameservers == nil || len(*d.Nameservers) == 0 { diff --git a/internal/cli/websites_domains_delegation_generic.go b/internal/cli/websites_domains_delegation_generic.go index 12d9bd1b..eff83576 100644 --- a/internal/cli/websites_domains_delegation_generic.go +++ b/internal/cli/websites_domains_delegation_generic.go @@ -11,6 +11,13 @@ type genericDelegationDriver struct{} func (g *genericDelegationDriver) Render(output Output, result *ipfs.DomainResponse, managed bool) { d := result.Delegation + if d == nil { + // A namespace without a dedicated driver has no on-chain concept to + // explain; the absence of a bundle is simply nothing to publish. + output.Printfln("") + output.Printfln("No delegation records are available for %s.", result.Domain) + return + } printDelegationRecords(output, "Parent records", d.ParentRecords) if !managed { printDelegationRecords(output, "Authoritative records", d.AuthoritativeRecords) diff --git a/internal/cli/websites_domains_delegation_hns.go b/internal/cli/websites_domains_delegation_hns.go index 284b8def..734198b6 100644 --- a/internal/cli/websites_domains_delegation_hns.go +++ b/internal/cli/websites_domains_delegation_hns.go @@ -17,9 +17,32 @@ type hnsDelegationDriver struct{} func (h *hnsDelegationDriver) Render(output Output, result *ipfs.DomainResponse, managed bool) { d := result.Delegation + // On-chain managed or otherwise delegation-less HNS binding: the name's + // DNS is served by an external contract (its NS record points at one), so + // the portal provisions no zone, DNSSEC, or delegation bundle — ownership + // is proven via a TXT token resolved through the HNS-aware resolver. There + // are no parent/authoritative records for the user to publish; publishing + // Pinner's delegation records would be wrong here. + if statusOnchainManaged(result) { + output.Printfln("") + output.Printfln("%s is on-chain managed: its DNS is served by an external", result.Domain) + output.Printfln("contract on the Handshake chain, not by a Pinner-managed zone.") + output.Printfln("No delegation records must be published — ownership is verified via a") + output.Printfln("TXT token through the HNS resolver. Manage the zone in your HNS wallet.") + return + } + + // Any other delegation-less HNS binding also has nothing to publish; the + // status header above already identifies the binding to the user. + if d == nil { + output.Printfln("") + output.Printfln("No delegation records are available for %s.", result.Domain) + return + } + parentTitle := "Parent records (publish in your HNS wallet)" - inline := d != nil && d.Mode != nil && *d.Mode == "inline" + inline := d.Mode != nil && *d.Mode == "inline" // Inline mode serves the authoritative side via Pinner's synthetic // nameserver names; the user only publishes the parent (SYNTH) records. diff --git a/internal/cli/websites_domains_delegation_icann.go b/internal/cli/websites_domains_delegation_icann.go index 4efdaa6d..a9310fad 100644 --- a/internal/cli/websites_domains_delegation_icann.go +++ b/internal/cli/websites_domains_delegation_icann.go @@ -12,6 +12,14 @@ type icannDelegationDriver struct{} func (i *icannDelegationDriver) Render(output Output, result *ipfs.DomainResponse, managed bool) { d := result.Delegation + if d == nil { + // An ICANN binding with no bundle means Pinner holds nothing for this + // name yet (e.g. not bound as a managed domain); there is nothing to + // publish from here. + output.Printfln("") + output.Printfln("No delegation records are available for %s.", result.Domain) + return + } parentTitle := "Parent records (configure at your registrar)" diff --git a/internal/cli/websites_domains_test.go b/internal/cli/websites_domains_test.go index 1b320516..1db40bfd 100644 --- a/internal/cli/websites_domains_test.go +++ b/internal/cli/websites_domains_test.go @@ -13,7 +13,7 @@ func TestRenderDomainDelegation(t *testing.T) { t.Run("renders no delegation message when nil", func(t *testing.T) { output := newTestOutput() renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "mydomain.hns", Namespace: "hns", Status: strPtr("delegated"), + Id: 1, Domain: "mydomain.hns", Namespace: ipfs.DomainNamespaceHNS, Status: new(ipfs.DomainResponseStatusActive), }, false) // exercises the nil-delegation branch without asserting exact text }) @@ -21,15 +21,15 @@ func TestRenderDomainDelegation(t *testing.T) { t.Run("renders records with typed helper", func(t *testing.T) { output := newTestOutput() renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "mydomain.hns", Namespace: "hns", Status: strPtr("delegated"), + Id: 1, Domain: "mydomain.hns", Namespace: ipfs.DomainNamespaceHNS, Status: new(ipfs.DomainResponseStatusActive), Delegation: &ipfs.DNSDelegation{ - Mode: strPtr("delegated"), - Dnssec: strPtr("enabled"), + Mode: new("delegated"), + Dnssec: new("enabled"), ParentRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("ns1.lumeweb,ns2.lumeweb")}, + {Type: "NS", Value: new("ns1.lumeweb,ns2.lumeweb")}, }, AuthoritativeRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "TLSA", Value: strPtr("_443._tcp.mydomain. 3 1 1 ")}, + {Type: "TLSA", Value: new("_443._tcp.mydomain. 3 1 1 ")}, }, }, }, false) @@ -41,14 +41,14 @@ func TestRenderDomainDelegation(t *testing.T) { output := NewOutputFormatter(false, false, false, false) output.SetWriter(&buf) renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "mydomain.hns", Namespace: "hns", Status: strPtr("delegated"), + Id: 1, Domain: "mydomain.hns", Namespace: ipfs.DomainNamespaceHNS, Status: new(ipfs.DomainResponseStatusActive), Delegation: &ipfs.DNSDelegation{ - Mode: strPtr("inline"), + Mode: new("inline"), ParentRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "SYNTH4", Value: strPtr("hns-626f7578e5.rec.ns1.lumeweb")}, + {Type: "SYNTH4", Value: new("hns-626f7578e5.rec.ns1.lumeweb")}, }, AuthoritativeRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("hns-626f7578e5.rec.ns1.lumeweb")}, + {Type: "NS", Value: new("hns-626f7578e5.rec.ns1.lumeweb")}, }, }, }, false) @@ -65,14 +65,14 @@ func TestRenderDomainDelegation(t *testing.T) { output := NewOutputFormatter(false, false, false, false) output.SetWriter(&buf) renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "mydomain.hns", Namespace: "hns", Status: strPtr("delegated"), + Id: 1, Domain: "mydomain.hns", Namespace: ipfs.DomainNamespaceHNS, Status: new(ipfs.DomainResponseStatusActive), Delegation: &ipfs.DNSDelegation{ - Mode: strPtr("inline"), + Mode: new("inline"), ParentRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "SYNTH4", Value: strPtr("hns-626f7578e5.rec.ns1.lumeweb")}, + {Type: "SYNTH4", Value: new("hns-626f7578e5.rec.ns1.lumeweb")}, }, AuthoritativeRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("hns-626f7578e5.rec.ns1.lumeweb")}, + {Type: "NS", Value: new("hns-626f7578e5.rec.ns1.lumeweb")}, }, }, }, true) @@ -84,7 +84,7 @@ func TestRenderDomainDelegation(t *testing.T) { t.Run("icann driver renders registrar wording and nameservers", func(t *testing.T) { output := newTestOutput() renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "mydomain.com", Namespace: "icann", Status: strPtr("delegated"), + Id: 1, Domain: "mydomain.com", Namespace: ipfs.DomainNamespaceICANN, Status: new(ipfs.DomainResponseStatusActive), Delegation: &ipfs.DNSDelegation{ Nameservers: &[]string{"ns1.example.com", "ns2.example.com"}, }, @@ -95,14 +95,14 @@ func TestRenderDomainDelegation(t *testing.T) { t.Run("unknown namespace falls back to generic driver", func(t *testing.T) { output := newTestOutput() renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "mydomain.eth", Namespace: "ens", Status: strPtr("delegated"), + Id: 1, Domain: "mydomain.eth", Namespace: "ens", Status: new(ipfs.DomainResponseStatusActive), Delegation: &ipfs.DNSDelegation{ - Mode: strPtr("delegated"), + Mode: new("delegated"), ParentRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("ns1.lumeweb")}, + {Type: "NS", Value: new("ns1.lumeweb")}, }, AuthoritativeRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "TLSA", Value: strPtr("_443._tcp.mydomain.eth. 3 1 1 ")}, + {Type: "TLSA", Value: new("_443._tcp.mydomain.eth. 3 1 1 ")}, }, }, }, false) @@ -118,13 +118,13 @@ func TestRenderDomainDelegation(t *testing.T) { digest := "c35938688953467518f2a9c613b8a32da647595912a67fa9cf47e41b593831d5" dsValue := "lumeweb DS 44451 13 2 " + digest renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "lumeweb", Namespace: "hns", Status: strPtr("delegated"), + Id: 1, Domain: "lumeweb", Namespace: ipfs.DomainNamespaceHNS, Status: new(ipfs.DomainResponseStatusActive), Delegation: &ipfs.DNSDelegation{ - Mode: strPtr("delegated"), - Dnssec: strPtr("enabled"), + Mode: new("delegated"), + Dnssec: new("enabled"), ParentRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("ns1.pinner.xyz,ns2.pinner.xyz")}, - {Type: "DS", Value: strPtr(dsValue)}, + {Type: "NS", Value: new("ns1.pinner.xyz,ns2.pinner.xyz")}, + {Type: "DS", Value: new(dsValue)}, }, }, }, false) @@ -150,15 +150,15 @@ func TestRenderDomainDelegation(t *testing.T) { output := NewOutputFormatter(false, false, false, false) output.SetWriter(&buf) renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "mydomain.hns", Namespace: "hns", Status: strPtr("delegated"), + Id: 1, Domain: "mydomain.hns", Namespace: ipfs.DomainNamespaceHNS, Status: new(ipfs.DomainResponseStatusActive), Delegation: &ipfs.DNSDelegation{ - Mode: strPtr("delegated"), + Mode: new("delegated"), ParentRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("ns1.pinner.xyz")}, - {Type: "DS", Value: strPtr("44451 13 2 c359")}, + {Type: "NS", Value: new("ns1.pinner.xyz")}, + {Type: "DS", Value: new("44451 13 2 c359")}, }, AuthoritativeRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("nsx.pinner.xyz")}, + {Type: "NS", Value: new("nsx.pinner.xyz")}, }, }, }, true) @@ -180,13 +180,13 @@ func TestRenderDomainDelegation(t *testing.T) { output := NewOutputFormatter(false, false, false, false) output.SetWriter(&buf) renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "mydomain.com", Namespace: "icann", Status: strPtr("delegated"), + Id: 1, Domain: "mydomain.com", Namespace: ipfs.DomainNamespaceICANN, Status: new(ipfs.DomainResponseStatusActive), Delegation: &ipfs.DNSDelegation{ ParentRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("ns1.pinner.xyz")}, + {Type: "NS", Value: new("ns1.pinner.xyz")}, }, AuthoritativeRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "TLSA", Value: strPtr("_443._tcp.mydomain.com. 3 1 1 ")}, + {Type: "TLSA", Value: new("_443._tcp.mydomain.com. 3 1 1 ")}, }, }, }, true) @@ -197,19 +197,42 @@ func TestRenderDomainDelegation(t *testing.T) { assert.NotContains(t, out, "TLSA") }) + t.Run("onchain managed hns explains the contract-served DNS and publishes nothing", func(t *testing.T) { + var buf bytes.Buffer + output := NewOutputFormatter(false, false, false, false) + output.SetWriter(&buf) + // On-chain managed: the HNS name's DNS is served by an external + // contract, so the backend returns status onchain_managed with NO + // delegation bundle. + renderDomainDelegation(output, &ipfs.DomainResponse{ + Id: 1, Domain: "mydomain", Namespace: ipfs.DomainNamespaceHNS, Status: new(ipfs.DomainResponseStatusOnchainManaged), + }, false) + out := buf.String() + // The driver must not crash on a nil Delegation and must explain the + // on-chain hosting shape instead of rendering a record table. + assert.Contains(t, out, "on-chain managed") + assert.Contains(t, out, "external") + assert.Contains(t, out, "TXT token") + // No record tables and no delegation publishing instructions: none of + // it applies to a contract-served name. + assert.NotContains(t, out, "Parent records") + assert.NotContains(t, out, "Authoritative records") + assert.NotContains(t, out, "Publish the records") + }) + t.Run("self-managed hns shows authoritative records", func(t *testing.T) { var buf bytes.Buffer output := NewOutputFormatter(false, false, false, false) output.SetWriter(&buf) renderDomainDelegation(output, &ipfs.DomainResponse{ - Id: 1, Domain: "mydomain.hns", Namespace: "hns", Status: strPtr("delegated"), + Id: 1, Domain: "mydomain.hns", Namespace: ipfs.DomainNamespaceHNS, Status: new(ipfs.DomainResponseStatusActive), Delegation: &ipfs.DNSDelegation{ - Mode: strPtr("delegated"), + Mode: new("delegated"), ParentRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("ns1.lumeweb")}, + {Type: "NS", Value: new("ns1.lumeweb")}, }, AuthoritativeRecords: &[]ipfs.DNSDelegationRecord{ - {Type: "NS", Value: strPtr("ns.eigen.lumeweb")}, + {Type: "NS", Value: new("ns.eigen.lumeweb")}, }, }, }, false) diff --git a/internal/cli/websites_domains_wizard.go b/internal/cli/websites_domains_wizard.go index 71b905de..fe9a940d 100644 --- a/internal/cli/websites_domains_wizard.go +++ b/internal/cli/websites_domains_wizard.go @@ -374,7 +374,7 @@ func runDomainsWizard(ctx context.Context, cmd *cli.Command, output Output) erro output.Printfln("") status := "" if domain.Status != nil { - status = *domain.Status + status = string(*domain.Status) } zoneName := "" if domain.ZoneName != nil { @@ -385,7 +385,7 @@ func runDomainsWizard(ctx context.Context, cmd *cli.Command, output Output) erro {"Website", w.WebsiteDomain()}, {"ID", strconv.Itoa(domain.Id)}, {"Domain", domain.Domain}, - {"Namespace", domain.Namespace}, + {"Namespace", string(domain.Namespace)}, {"Status", status}, {"Zone Name", zoneName}, }, diff --git a/internal/cli/websites_domains_wizard_pterm.go b/internal/cli/websites_domains_wizard_pterm.go index aea08b37..8d233b76 100644 --- a/internal/cli/websites_domains_wizard_pterm.go +++ b/internal/cli/websites_domains_wizard_pterm.go @@ -70,13 +70,13 @@ func (ui *PTermDomainsUI) ShowCompletion() error { if domain := ui.wizard.Result(); domain != nil { status := "" if domain.Status != nil { - status = *domain.Status + status = string(*domain.Status) } msg += "The domain has been bound to your website.\n\n" msg += "Domain details:\n" msg += " • Website: " + ui.wizard.WebsiteDomain() + "\n" msg += " • Domain: " + domain.Domain + "\n" - msg += " • Namespace:" + domain.Namespace + "\n" + msg += " • Namespace:" + string(domain.Namespace) + "\n" msg += " • Status: " + status + "\n\n" msg += "Next steps:\n" msg += " • View delegation: pinner websites domains dns-requirements " + domain.Domain + "\n" @@ -256,7 +256,7 @@ func (ui *PTermDomainsUI) ExecuteVerifyStep(ctx context.Context, w *DomainAddWiz result := w.Result() status := "" if result != nil && result.Status != nil { - status = *result.Status + status = string(*result.Status) } if valid { @@ -265,7 +265,7 @@ func (ui *PTermDomainsUI) ExecuteVerifyStep(ctx context.Context, w *DomainAddWiz ui.output.PrintFields(FieldGroup{ Fields: []Field{ {"Domain", result.Domain}, - {"Namespace", result.Namespace}, + {"Namespace", string(result.Namespace)}, {"Status", status}, }, }) @@ -277,7 +277,7 @@ func (ui *PTermDomainsUI) ExecuteVerifyStep(ctx context.Context, w *DomainAddWiz // bound domain for display so the retry/give-up message below never // dereferences a nil result. if result == nil { - result = &ipfs.DomainResponse{Domain: w.Domain(), Namespace: w.Namespace()} + result = &ipfs.DomainResponse{Domain: w.Domain(), Namespace: ipfs.DomainNamespace(w.Namespace())} } w.SetVerifyAttempts(w.VerifyAttempts() + 1) @@ -295,11 +295,13 @@ func (ui *PTermDomainsUI) ExecuteVerifyStep(ctx context.Context, w *DomainAddWiz return nil } -// domainStatusIsValid reports whether a domain status string represents a -// fully validated/active domain binding. -func domainStatusIsValid(status string) bool { +// domainStatusIsValid reports whether a domain binding status represents a +// fully validated/active binding. On-chain managed counts: an on-chain +// managed HNS name is verified via the namespace TXT token, so it needs no +// delegation wait and is as valid as active. +func domainStatusIsValid(status ipfs.DomainResponseStatus) bool { switch status { - case "active", "validated", "verified": + case ipfs.DomainResponseStatusActive, ipfs.DomainResponseStatusOnchainManaged: return true default: return false diff --git a/internal/cli/websites_domains_wizard_test.go b/internal/cli/websites_domains_wizard_test.go index 1c2cceb1..376c7c3a 100644 --- a/internal/cli/websites_domains_wizard_test.go +++ b/internal/cli/websites_domains_wizard_test.go @@ -25,26 +25,25 @@ func TestDomainAddWizard_Run(t *testing.T) { } cfgMgr.EXPECT().Config().Return(cfg).Maybe() - status := "active" + status := new(ipfs.DomainResponseStatusActive) result := &ipfs.DomainResponse{ Id: 42, Domain: "staging.example.com", - Namespace: "icann", - Status: &status, + Namespace: ipfs.DomainNamespaceICANN, + Status: status, } - delegStatus := "active" delegResult := &ipfs.DomainResponse{ Id: 42, Domain: "staging.example.com", - Namespace: "icann", - Status: &delegStatus, + Namespace: ipfs.DomainNamespaceICANN, + Status: new(ipfs.DomainResponseStatusActive), } - verifyStatus := "active" + verifyResult := &ipfs.DomainResponse{ Id: 42, Domain: "staging.example.com", - Namespace: "icann", - Status: &verifyStatus, + Namespace: ipfs.DomainNamespaceICANN, + Status: new(ipfs.DomainResponseStatusActive), } mockWebsitesSvc := &mockWebsitesServiceForCLI{ @@ -87,30 +86,29 @@ func TestDomainAddWizard_Run(t *testing.T) { } cfgMgr.EXPECT().Config().Return(cfg).Maybe() - status := "active" mockWebsitesSvc := &mockWebsitesServiceForCLI{ BindDomainFn: func(_ context.Context, _ string, req ipfs.DomainRequest) (*ipfs.DomainResponse, error) { return &ipfs.DomainResponse{ Id: 7, Domain: req.Domain, - Namespace: req.Namespace, - Status: &status, + Namespace: ipfs.DomainNamespace(req.Namespace), + Status: new(ipfs.DomainResponseStatusActive), }, nil }, GetDomainDNSRequirementsFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { return &ipfs.DomainResponse{ Id: 7, Domain: "mydomain", - Namespace: "hns", - Status: &status, + Namespace: ipfs.DomainNamespaceHNS, + Status: new(ipfs.DomainResponseStatusActive), }, nil }, VerifyDomainFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { return &ipfs.DomainResponse{ Id: 7, Domain: "mydomain", - Namespace: "hns", - Status: &status, + Namespace: ipfs.DomainNamespaceHNS, + Status: new(ipfs.DomainResponseStatusActive), }, nil }, } @@ -138,16 +136,16 @@ func TestDomainAddWizard_Run(t *testing.T) { } cfgMgr.EXPECT().Config().Return(cfg).Maybe() - status := "active" + status := new(ipfs.DomainResponseStatusActive) mockWebsitesSvc := &mockWebsitesServiceForCLI{ BindDomainFn: func(_ context.Context, _ string, _ ipfs.DomainRequest) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "x.com", Namespace: "icann", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "x.com", Namespace: ipfs.DomainNamespaceICANN, Status: status}, nil }, GetDomainDNSRequirementsFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "x.com", Namespace: "icann", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "x.com", Namespace: ipfs.DomainNamespaceICANN, Status: status}, nil }, VerifyDomainFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "x.com", Namespace: "icann", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "x.com", Namespace: ipfs.DomainNamespaceICANN, Status: status}, nil }, } @@ -233,16 +231,16 @@ func TestDomainAddWizard_Run(t *testing.T) { cfg := &config.Config{AuthToken: "test-token", Secure: true} cfgMgr.EXPECT().Config().Return(cfg).Maybe() - status := "active" + status := new(ipfs.DomainResponseStatusActive) mockWebsitesSvc := &mockWebsitesServiceForCLI{ BindDomainFn: func(_ context.Context, _ string, _ ipfs.DomainRequest) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "mydomain", Namespace: "hns", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "mydomain", Namespace: ipfs.DomainNamespaceHNS, Status: status}, nil }, GetDomainDNSRequirementsFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { return nil, nil }, VerifyDomainFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "mydomain", Namespace: "hns", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "mydomain", Namespace: ipfs.DomainNamespaceHNS, Status: status}, nil }, } @@ -265,16 +263,16 @@ func TestDomainAddWizard_Run(t *testing.T) { cfg := &config.Config{AuthToken: "test-token", Secure: true} cfgMgr.EXPECT().Config().Return(cfg).Maybe() - status := "active" + status := new(ipfs.DomainResponseStatusActive) mockWebsitesSvc := &mockWebsitesServiceForCLI{ BindDomainFn: func(_ context.Context, _ string, _ ipfs.DomainRequest) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: status}, nil }, GetDomainDNSRequirementsFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: status}, nil }, VerifyDomainFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: status}, nil }, } @@ -298,13 +296,13 @@ func TestDomainAddWizard_Run(t *testing.T) { cfg := &config.Config{AuthToken: "test-token", Secure: true} cfgMgr.EXPECT().Config().Return(cfg).Maybe() - status := "active" + status := new(ipfs.DomainResponseStatusActive) mockWebsitesSvc := &mockWebsitesServiceForCLI{ BindDomainFn: func(_ context.Context, _ string, _ ipfs.DomainRequest) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: status}, nil }, GetDomainDNSRequirementsFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &status}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: status}, nil }, VerifyDomainFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { return nil, nil @@ -376,7 +374,7 @@ func TestDomainAddWizard_Accessors(t *testing.T) { w.SetWebsiteDomain("example.com") w.SetDomain("staging.example.com") w.SetNamespace("icann") - w.SetResult(&ipfs.DomainResponse{Id: 9, Domain: "staging.example.com", Namespace: "icann"}) + w.SetResult(&ipfs.DomainResponse{Id: 9, Domain: "staging.example.com", Namespace: ipfs.DomainNamespaceICANN}) w.SetVerifyRetry(true) require.Equal(t, "5", w.WebsiteID()) @@ -431,10 +429,10 @@ func TestDomainWizardPTermExecuteVerifyStep(t *testing.T) { // BindDomain returns a valid "active" status, but VerifyDomain returns // (nil, nil); a stale bind status must NOT be treated as verified. - active := "active" + active := new(ipfs.DomainResponseStatusActive) mockWebsitesSvc := &mockWebsitesServiceForCLI{ BindDomainFn: func(_ context.Context, _ string, _ ipfs.DomainRequest) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &active}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: active}, nil }, VerifyDomainFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { return nil, nil @@ -444,7 +442,7 @@ func TestDomainWizardPTermExecuteVerifyStep(t *testing.T) { ui := NewPTermDomainsUI(newOutput) w := NewDomainAddWizard(mockWebsitesSvc, cfgMgr, ui, newOutput) ui.SetWizard(w) - w.SetResult(&ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &active}) + w.SetResult(&ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: active}) err := ui.ExecuteVerifyStep(context.Background(), w) @@ -461,20 +459,20 @@ func TestDomainWizardPTermExecuteVerifyStep(t *testing.T) { cfg := &config.Config{AuthToken: "test-token", Secure: true} cfgMgr.EXPECT().Config().Return(cfg).Maybe() - active := "active" + active := new(ipfs.DomainResponseStatusActive) mockWebsitesSvc := &mockWebsitesServiceForCLI{ BindDomainFn: func(_ context.Context, _ string, _ ipfs.DomainRequest) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &active}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: active}, nil }, VerifyDomainFn: func(_ context.Context, _, _ string) (*ipfs.DomainResponse, error) { - return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &active}, nil + return &ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: active}, nil }, } ui := NewPTermDomainsUI(newOutput) w := NewDomainAddWizard(mockWebsitesSvc, cfgMgr, ui, newOutput) ui.SetWizard(w) - w.SetResult(&ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: "icann", Status: &active}) + w.SetResult(&ipfs.DomainResponse{Id: 1, Domain: "s.com", Namespace: ipfs.DomainNamespaceICANN, Status: active}) err := ui.ExecuteVerifyStep(context.Background(), w) diff --git a/internal/cli/websites_handler_test.go b/internal/cli/websites_handler_test.go index 0a0de700..8b545253 100644 --- a/internal/cli/websites_handler_test.go +++ b/internal/cli/websites_handler_test.go @@ -143,6 +143,10 @@ func (m *mockWebsitesHandlerService) RepublishDANE(ctx context.Context, websiteI return nil, nil } +func (m *mockWebsitesHandlerService) ConvertDomainToOnChain(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainResponse, error) { + return nil, nil +} + func (m *mockWebsitesHandlerService) UpdateDomain(ctx context.Context, websiteID string, domainID string, req ipfs.DomainUpdateRequest) (*ipfs.DomainResponse, error) { return nil, nil } diff --git a/internal/cli/websites_required_records_test.go b/internal/cli/websites_required_records_test.go index e8784a0f..93737aa3 100644 --- a/internal/cli/websites_required_records_test.go +++ b/internal/cli/websites_required_records_test.go @@ -49,7 +49,7 @@ func TestBuildRequiredRecords(t *testing.T) { ValidationToken: "token123", TargetType: "ipfs", TargetHash: "QmXxx", - GatewayDomain: strPtr("gateway.pinner.xyz"), + GatewayDomain: new("gateway.pinner.xyz"), }, want: []map[string]string{ {"name": "example.com", "type": "TXT", "value": "token123"}, @@ -77,7 +77,7 @@ func TestBuildRequiredRecords(t *testing.T) { Domain: "example.com", DnsHostingEnabled: false, ValidationToken: "token123", - ValidationRecordHost: strPtr("pinner-verify.example.com"), + ValidationRecordHost: new("pinner-verify.example.com"), TargetType: "ipfs", TargetHash: "QmXxx", }, @@ -108,7 +108,7 @@ func TestBuildRequiredRecords(t *testing.T) { ValidationToken: "token123", TargetType: "ipfs", TargetHash: "QmXxx", - GatewayDomain: strPtr(""), + GatewayDomain: new(""), }, want: []map[string]string{ {"name": "example.com", "type": "TXT", "value": "token123"}, @@ -121,7 +121,7 @@ func TestBuildRequiredRecords(t *testing.T) { Domain: "example.com", DnsHostingEnabled: false, ValidationToken: "token123", - ValidationRecordHost: strPtr(""), + ValidationRecordHost: new(""), TargetType: "ipfs", TargetHash: "QmXxx", }, @@ -140,6 +140,3 @@ func TestBuildRequiredRecords(t *testing.T) { } } -func strPtr(s string) *string { - return &s -} diff --git a/internal/cli/websites_ssl.go b/internal/cli/websites_ssl.go index 7f7ec727..9cd4198d 100644 --- a/internal/cli/websites_ssl.go +++ b/internal/cli/websites_ssl.go @@ -41,7 +41,7 @@ func websitesSSLStatus(ctx context.Context, cmd websitesCommandGetter, output Ou headers := []string{"Field", "Value"} rows := [][]string{ - {"Status", website.Ssl.Status}, + {"Status", string(website.Ssl.Status)}, {"Issued At", formatTimePtr(website.Ssl.IssuedAt)}, {"Last Updated", formatTimePtr(website.Ssl.LastUpdatedAt)}, } @@ -73,7 +73,7 @@ func websitesSSLStatus(ctx context.Context, cmd websitesCommandGetter, output Ou headers := []string{"Field", "Value"} rows := [][]string{ - {"Status", website.Ssl.Status}, + {"Status", string(website.Ssl.Status)}, {"Issued At", formatTimePtr(website.Ssl.IssuedAt)}, {"Last Updated", formatTimePtr(website.Ssl.LastUpdatedAt)}, } diff --git a/internal/cli/websites_test.go b/internal/cli/websites_test.go index 9186ceee..b06aff78 100644 --- a/internal/cli/websites_test.go +++ b/internal/cli/websites_test.go @@ -31,6 +31,7 @@ type mockWebsitesServiceForCLI struct { VerifyDomainFn func(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainResponse, error) GetDomainDNSRequirementsFn func(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainResponse, error) RepublishDANEFn func(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainDANERepublishResponse, error) + ConvertDomainToOnChainFn func(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainResponse, error) UpdateDomainFn func(ctx context.Context, websiteID string, domainID string, req ipfs.DomainUpdateRequest) (*ipfs.DomainResponse, error) CheckPlatformAvailabilityFn func(ctx context.Context, label string) (*ipfs.PlatformAvailabilityResponse, error) ListPlatformDomainsFn func(ctx context.Context) (*ipfs.PlatformDomainListResponse, error) @@ -190,6 +191,13 @@ func (m *mockWebsitesServiceForCLI) RepublishDANE(ctx context.Context, websiteID return nil, nil } +func (m *mockWebsitesServiceForCLI) ConvertDomainToOnChain(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainResponse, error) { + if m.ConvertDomainToOnChainFn != nil { + return m.ConvertDomainToOnChainFn(ctx, websiteID, domainID) + } + return nil, nil +} + func (m *mockWebsitesServiceForCLI) UpdateDomain(ctx context.Context, websiteID string, domainID string, req ipfs.DomainUpdateRequest) (*ipfs.DomainResponse, error) { if m.UpdateDomainFn != nil { return m.UpdateDomainFn(ctx, websiteID, domainID, req) @@ -1464,7 +1472,7 @@ func websitesSSLStatusWithService(ctx context.Context, cmd websitesCommandGetter headers := []string{"Field", "Value"} rows := [][]string{ - {"Status", website.Ssl.Status}, + {"Status", string(website.Ssl.Status)}, {"Issued At", formatTimePtr(website.Ssl.IssuedAt)}, {"Last Updated", formatTimePtr(website.Ssl.LastUpdatedAt)}, } @@ -1776,7 +1784,6 @@ func TestStripValidationPrefix(t *testing.T) { } func TestValidationRecordValue(t *testing.T) { - strPtr := func(s string) *string { return &s } tests := []struct { name string @@ -1787,7 +1794,7 @@ func TestValidationRecordValue(t *testing.T) { name: "derives key from validation record host", website: &ipfs.WebsiteItem{ Domain: "example.com", - ValidationRecordHost: strPtr("pinner-verify.example.com"), + ValidationRecordHost: new("pinner-verify.example.com"), ValidationToken: "abc123", }, expected: "pinner-verify=abc123", @@ -1796,7 +1803,7 @@ func TestValidationRecordValue(t *testing.T) { name: "custom server key", website: &ipfs.WebsiteItem{ Domain: "example.com", - ValidationRecordHost: strPtr("lumeweb-verify.example.com"), + ValidationRecordHost: new("lumeweb-verify.example.com"), ValidationToken: "abc123", }, expected: "lumeweb-verify=abc123", @@ -1805,7 +1812,7 @@ func TestValidationRecordValue(t *testing.T) { name: "token already carries key= prefix is not doubled", website: &ipfs.WebsiteItem{ Domain: "example.com", - ValidationRecordHost: strPtr("pinner-verify.example.com"), + ValidationRecordHost: new("pinner-verify.example.com"), ValidationToken: "pinner-verify=abc123", }, expected: "pinner-verify=abc123", @@ -1814,7 +1821,7 @@ func TestValidationRecordValue(t *testing.T) { name: "token with foreign prefix is preserved unchanged (no rewrite)", website: &ipfs.WebsiteItem{ Domain: "example.com", - ValidationRecordHost: strPtr("pinner-verify.example.com"), + ValidationRecordHost: new("pinner-verify.example.com"), ValidationToken: "lumeweb-verify=abc123", }, expected: "pinner-verify=lumeweb-verify=abc123", @@ -1823,7 +1830,7 @@ func TestValidationRecordValue(t *testing.T) { name: "token containing equals as content is not corrupted", website: &ipfs.WebsiteItem{ Domain: "example.com", - ValidationRecordHost: strPtr("pinner-verify.example.com"), + ValidationRecordHost: new("pinner-verify.example.com"), ValidationToken: "abc=def==", }, expected: "pinner-verify=abc=def==", @@ -1832,7 +1839,7 @@ func TestValidationRecordValue(t *testing.T) { name: "token matching derived key without equals is not stripped", website: &ipfs.WebsiteItem{ Domain: "example.com", - ValidationRecordHost: strPtr("pinner-verify.example.com"), + ValidationRecordHost: new("pinner-verify.example.com"), ValidationToken: "pinner-verifyvalue", }, expected: "pinner-verify=pinner-verifyvalue", @@ -1849,7 +1856,7 @@ func TestValidationRecordValue(t *testing.T) { name: "empty validation record host falls back to bare token", website: &ipfs.WebsiteItem{ Domain: "example.com", - ValidationRecordHost: strPtr(""), + ValidationRecordHost: new(""), ValidationToken: "abc123", }, expected: "abc123", diff --git a/internal/core/websites/service.go b/internal/core/websites/service.go index 4429c1f8..a100abb2 100644 --- a/internal/core/websites/service.go +++ b/internal/core/websites/service.go @@ -40,6 +40,14 @@ type Service interface { GetDomainDNSRequirements(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainResponse, error) RepublishDANE(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainDANERepublishResponse, error) + // ConvertDomainToOnChain reclassifies a bound HNS domain as on-chain + // managed: the name's DNS is served by an external contract, so the portal + // deletes its managed zone/DNSSEC and switches ownership verification to + // the TXT token. One-way; DANE/SSL state is retained. Refused with an + // error when the binding is already on-chain, not eligible, or shares a + // zone. + ConvertDomainToOnChain(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainResponse, error) + // UpdateDomain updates a bound domain's per-domain DNS control - whether the // portal manages DNS hosting for this binding (dns_hosting_enabled) and/or // promotes the binding to primary. Omitted fields are left unchanged. @@ -412,6 +420,21 @@ func (s *service) UpdateDomain(ctx context.Context, websiteID string, domainID s return svc.UpdateDomain(ctx, websiteID, domainID, req) } +// ConvertDomainToOnChain reclassifies a bound HNS domain as on-chain managed. +// Mirrors RepublishDANE: auth + SDK delegation only, no business +// logic. An "already on-chain" 422 from the backend is idempotent success +// (the SDK returns the current domain state for it). +func (s *service) ConvertDomainToOnChain(ctx context.Context, websiteID string, domainID string) (*ipfs.DomainResponse, error) { + if err := s.RequireAuthenticated(); err != nil { + return nil, err + } + svc, err := s.requireService() + if err != nil { + return nil, err + } + return svc.ConvertDomainToOnChain(ctx, websiteID, domainID) +} + // ListPlatformDomains lists the platform (free-subdomain) roots available // for websites. func (s *service) ListPlatformDomains(ctx context.Context) (*ipfs.PlatformDomainListResponse, error) { diff --git a/internal/mcp/agent_guide.go b/internal/mcp/agent_guide.go index b554a9df..c1cf8de7 100644 --- a/internal/mcp/agent_guide.go +++ b/internal/mcp/agent_guide.go @@ -315,7 +315,7 @@ func publishDomainDecision() *toolforge.GuideDecisionBuilder { Detail(publishCidLead.Then(htmlRootClause). Static("Call websites_create with {\"cid\": \"\", \"website\": \"\"}. The domain is used directly as a custom domain (not a platform subdomain). Read pinner://websites//dns-requirements for DNS records to publish. If dns_hosting=true (managed), DNS is reconciled asynchronously — validation may report the old CID right after the update; that is reconciliation lag, not failure, so re-call websites_validate without starting a new flow. If self-managed, publish the _dnslink TXT and validation TXT before calling websites_validate."). Then(cdnDeployNoticeClause). - Static("For a Handshake (alt-root) name such as acme/ (i.e. the user has a Handshake domain, not an ICANN TLD domain), pass {\"namespace\": \"hns\"} alongside the website so the site binds under the HNS namespace. Read pinner://websites//dns-requirements for the HNS delegation bundle and publish the parent NS/DS/GLUE records on-chain in the HNS wallet; with managed DNS the authoritative side is handled for you.")), + Then(hnsNamespaceClause)), ) } diff --git a/internal/mcp/agent_guide_test.go b/internal/mcp/agent_guide_test.go index 6ac7a5e8..4d934700 100644 --- a/internal/mcp/agent_guide_test.go +++ b/internal/mcp/agent_guide_test.go @@ -68,7 +68,6 @@ func TestAgentGuideDescriptorIsDirectVisible(t *testing.T) { // only that subset so an agent is never directed to a failing mode (e.g. // source.mode=path from a remote HTTP host, which Kody flagged). func TestAgentGuideModesMatchProfile(t *testing.T) { - strPtr := func(p hostenv.PlatformProfile) *hostenv.PlatformProfile { return &p } cases := []struct { name string @@ -79,43 +78,43 @@ func TestAgentGuideModesMatchProfile(t *testing.T) { }{ { name: "stdio generic advertises only path", - profile: strPtr(hostenv.ProfileStdioGeneric), + profile: new(hostenv.ProfileStdioGeneric), mustHave: []string{"source.mode=path"}, mustNot: []string{"source.mode=mint", "source.mode=url/data"}, }, { name: "http generic advertises only mint", - profile: strPtr(hostenv.ProfileHTTPGeneric), + profile: new(hostenv.ProfileHTTPGeneric), mustHave: []string{"source.mode=mint"}, mustNot: []string{"source.mode=path", "source.mode=url/data"}, }, { name: "grok http advertises only mint", - profile: strPtr(hostenv.ProfileGrokHTTP), + profile: new(hostenv.ProfileGrokHTTP), mustHave: []string{"source.mode=mint"}, mustNot: []string{"source.mode=path", "source.mode=url/data"}, }, { name: "openai tunnel advertises url/data fallback", - profile: strPtr(hostenv.ProfileOpenAITunnel), + profile: new(hostenv.ProfileOpenAITunnel), mustHave: []string{"source.mode=url/data"}, mustNot: []string{"source.mode=path", "source.mode=mint"}, }, { name: "openai http advertises mint fallback", - profile: strPtr(hostenv.ProfileOpenAIHTTP), + profile: new(hostenv.ProfileOpenAIHTTP), mustHave: []string{"source.mode=mint"}, mustNot: []string{"source.mode=path", "source.mode=url/data"}, }, { name: "claude web advertises mint transport source", - profile: strPtr(hostenv.ProfileClaudeHTTP), + profile: new(hostenv.ProfileClaudeHTTP), mustHave: []string{"source.mode=mint"}, mustNot: []string{"source.mode=path", "source.mode=url/data"}, }, { name: "claude desktop advertises only path", - profile: strPtr(hostenv.ProfileStdioMCPApps), + profile: new(hostenv.ProfileStdioMCPApps), mustHave: []string{"source.mode=path"}, mustNot: []string{"source.mode=mint", "source.mode=url/data"}, }, @@ -265,21 +264,18 @@ func flowStepsContain(f GuideFlow, s string) bool { return false } -// strPtr dereferences a profile value into a pointer for buildAgentGuide. -func strPtr(p hostenv.PlatformProfile) *hostenv.PlatformProfile { return &p } - // TestAgentGuideSummaryNeverPrefersFileForGrok regresses the shared-compiler // residue: a host without a `file` parameter must never see a "prefer the file // parameter when your host has one" clause, which previously let Grok invent a // {download_url, file_id} even after the schema dropped `file`. Grok's summary // must instead say it has no file parameter and lead with mint + PUT + poll. func TestAgentGuideSummaryNeverPrefersFileForGrok(t *testing.T) { - grok := buildAgentGuide(strPtr(hostenv.ProfileGrokHTTP)) + grok := buildAgentGuide(new(hostenv.ProfileGrokHTTP)) require.NotContains(t, grok.Summary, "prefer the `file` parameter") require.Contains(t, grok.Summary, "no `file` parameter") require.Contains(t, grok.Summary, "upload_data", "Grok must be told not to call upload_data") - openai := buildAgentGuide(strPtr(hostenv.ProfileOpenAITunnel)) + openai := buildAgentGuide(new(hostenv.ProfileOpenAITunnel)) require.Contains(t, openai.Summary, "prefer the `file` parameter") require.NotContains(t, openai.Summary, "no `file` parameter") } @@ -291,9 +287,9 @@ func TestAgentGuideSummaryNeverPrefersFileForGrok(t *testing.T) { // step their schema/flow never returns. func TestAgentGuideStepsAreFeatureGated(t *testing.T) { mintFlows := []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileHTTPGeneric), - strPtr(hostenv.ProfileGrokHTTP), - strPtr(hostenv.ProfileOpenAIHTTP), + new(hostenv.ProfileHTTPGeneric), + new(hostenv.ProfileGrokHTTP), + new(hostenv.ProfileOpenAIHTTP), } for _, p := range mintFlows { f := guideFlowByName(t, buildAgentGuide(p), "upload") @@ -303,8 +299,8 @@ func TestAgentGuideStepsAreFeatureGated(t *testing.T) { } nonMintFlows := []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileStdioGeneric), // path - strPtr(hostenv.ProfileOpenAITunnel), // url/data relay + new(hostenv.ProfileStdioGeneric), // path + new(hostenv.ProfileOpenAITunnel), // url/data relay } for _, p := range nonMintFlows { f := guideFlowByName(t, buildAgentGuide(p), "upload") @@ -328,9 +324,9 @@ func TestAgentGuideStepsAreFeatureGated(t *testing.T) { // On non-mint transports both host PUT steps are absent. func TestAgentGuideNamesHostPUTOnMintSteps(t *testing.T) { mintProfiles := []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileHTTPGeneric), - strPtr(hostenv.ProfileGrokHTTP), - strPtr(hostenv.ProfileOpenAIHTTP), + new(hostenv.ProfileHTTPGeneric), + new(hostenv.ProfileGrokHTTP), + new(hostenv.ProfileOpenAIHTTP), } for _, p := range mintProfiles { up := guideFlowByName(t, buildAgentGuide(p), "upload") @@ -343,8 +339,8 @@ func TestAgentGuideNamesHostPUTOnMintSteps(t *testing.T) { } nonMintProfiles := []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileStdioGeneric), // path - strPtr(hostenv.ProfileOpenAITunnel), // url/data relay + new(hostenv.ProfileStdioGeneric), // path + new(hostenv.ProfileOpenAITunnel), // url/data relay } for _, p := range nonMintProfiles { for _, flowName := range []string{"upload", "vault_upload"} { @@ -360,10 +356,10 @@ func TestAgentGuideNamesHostPUTOnMintSteps(t *testing.T) { // the generic/label/custom-domain decision a deterministic agent needs. func TestAgentGuidePublishBranchesPerProfile(t *testing.T) { for _, p := range []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileStdioGeneric), - strPtr(hostenv.ProfileHTTPGeneric), - strPtr(hostenv.ProfileGrokHTTP), - strPtr(hostenv.ProfileOpenAITunnel), + new(hostenv.ProfileStdioGeneric), + new(hostenv.ProfileHTTPGeneric), + new(hostenv.ProfileGrokHTTP), + new(hostenv.ProfileOpenAITunnel), } { pub := guideFlowByName(t, buildAgentGuide(p), "publish_website") require.NotNil(t, pub.Decision, "publish_website must be a decision flow for %s", p.Transport) @@ -387,10 +383,10 @@ func TestAgentGuidePublishBranchesPerProfile(t *testing.T) { // the user is set to expect the site to not be reachable at its URL immediately. func TestAgentGuideCDNDeployNotice(t *testing.T) { for _, p := range []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileStdioGeneric), - strPtr(hostenv.ProfileHTTPGeneric), - strPtr(hostenv.ProfileGrokHTTP), - strPtr(hostenv.ProfileOpenAITunnel), + new(hostenv.ProfileStdioGeneric), + new(hostenv.ProfileHTTPGeneric), + new(hostenv.ProfileGrokHTTP), + new(hostenv.ProfileOpenAITunnel), } { guide := buildAgentGuide(p) @@ -414,6 +410,38 @@ func TestAgentGuideCDNDeployNotice(t *testing.T) { } } +// TestAgentGuideHNSOnchainBranches verifies the custom-domain publish branch +// carries the Handshake namespace guidance with BOTH HNS hosting shapes: the +// on-chain managed outcome (status onchain_managed → no records to +// publish, never re-convert) and the native HNS delegation flow (publish +// parent records; convert-onchain exists for migration, one-way + confirm). +func TestAgentGuideHNSOnchainBranches(t *testing.T) { + for _, p := range []*hostenv.PlatformProfile{ + new(hostenv.ProfileStdioGeneric), + new(hostenv.ProfileHTTPGeneric), + } { + guide := buildAgentGuide(p) + pub := guideFlowByName(t, guide, "publish_website") + require.NotNil(t, pub.Decision, "publish_website must be a decision flow for %s", p.Transport) + var domain *GuideDecision + for _, br := range pub.Decision.Branches { + if domain == nil && br.Next != nil { + domain = br.Next + } + } + require.NotNil(t, domain, "publish_website must nest the domain decision for %s", p.Transport) + custom := domain.Branches[len(domain.Branches)-1] + require.Contains(t, custom.Detail, "namespace\": \"hns", + "custom-domain branch must name the hns namespace for %s", p.Transport) + require.Contains(t, custom.Detail, "onchain_managed", + "custom-domain branch must explain the onchain_managed outcome for %s", p.Transport) + require.Contains(t, custom.Detail, "websites_domains_convert_onchain", + "custom-domain branch must name the convert-onchain operation for %s", p.Transport) + require.Contains(t, custom.Detail, "confirm=true", + "convert-onchain must be described as confirm-gated (one-way, destructive) for %s", p.Transport) + } +} + // TestAgentGuideSandboxFilesRouteToFileNotMint regresses audit F-002. On a host // with both FeatFileHostInput and mint (OpenAI HTTP, host_file_first), an // assistant-generated sandbox file must route to the `file` parameter; the mint @@ -421,7 +449,7 @@ func TestAgentGuideCDNDeployNotice(t *testing.T) { // agent-local" files (which host_file_first requires on `file`). func TestAgentGuideSandboxFilesRouteToFileNotMint(t *testing.T) { p := hostenv.ProfileOpenAIHTTP - guide := buildAgentGuide(strPtr(p)) + guide := buildAgentGuide(new(p)) for _, flowName := range []string{"upload", "vault_upload"} { f := guideFlowByName(t, guide, flowName) require.NotNil(t, f.Decision, "%s must expose a byte-route decision", flowName) @@ -456,10 +484,10 @@ func TestAgentGuideSandboxFilesRouteToFileNotMint(t *testing.T) { // agent-only hosts. func TestAgentGuideMCPAppsRuleScoped(t *testing.T) { guiProfiles := []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileStdioMCPApps), - strPtr(hostenv.ProfileClaudeHTTP), - strPtr(hostenv.ProfileOpenAIHTTP), - strPtr(hostenv.ProfileOpenAITunnel), + new(hostenv.ProfileStdioMCPApps), + new(hostenv.ProfileClaudeHTTP), + new(hostenv.ProfileOpenAIHTTP), + new(hostenv.ProfileOpenAITunnel), } for _, p := range guiProfiles { guide := buildAgentGuide(p) @@ -477,10 +505,10 @@ func TestAgentGuideMCPAppsRuleScoped(t *testing.T) { } agentProfiles := []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileStdioGeneric), - strPtr(hostenv.ProfileHTTPGeneric), - strPtr(hostenv.ProfileGrokHTTP), - strPtr(hostenv.ProfileGrokStdio), + new(hostenv.ProfileStdioGeneric), + new(hostenv.ProfileHTTPGeneric), + new(hostenv.ProfileGrokHTTP), + new(hostenv.ProfileGrokStdio), } for _, p := range agentProfiles { guide := buildAgentGuide(p) @@ -500,9 +528,9 @@ func TestAgentGuideMCPAppsRuleScoped(t *testing.T) { // publish_website. func TestAgentGuideWizardGuidanceIndependentOfElicitation(t *testing.T) { for _, p := range []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileGrokHTTP), - strPtr(hostenv.ProfileHTTPGeneric), - strPtr(hostenv.ProfileStdioGeneric), + new(hostenv.ProfileGrokHTTP), + new(hostenv.ProfileHTTPGeneric), + new(hostenv.ProfileStdioGeneric), } { require.False(t, p.Has(hostenv.FeatElicitation), "%s must lack FeatElicitation for this test", p.Transport) summary := buildAgentGuide(p).Summary @@ -520,9 +548,9 @@ func TestAgentGuideWizardGuidanceIndependentOfElicitation(t *testing.T) { // poll). The per-flow step lists confirm the same split structurally. func TestAgentGuideMintSummaryScopedByTool(t *testing.T) { for _, p := range []*hostenv.PlatformProfile{ - strPtr(hostenv.ProfileOpenAIHTTP), - strPtr(hostenv.ProfileHTTPGeneric), - strPtr(hostenv.ProfileGrokHTTP), + new(hostenv.ProfileOpenAIHTTP), + new(hostenv.ProfileHTTPGeneric), + new(hostenv.ProfileGrokHTTP), } { guide := buildAgentGuide(p) summary := guide.Summary diff --git a/internal/mcp/core/transfer/upload_http_test.go b/internal/mcp/core/transfer/upload_http_test.go index 361a8bb0..131e31b3 100644 --- a/internal/mcp/core/transfer/upload_http_test.go +++ b/internal/mcp/core/transfer/upload_http_test.go @@ -44,10 +44,24 @@ func TestUploadCredentialPropagatesToExecutor(t *testing.T) { require.Equal(t, http.StatusAccepted, resp.StatusCode) resp.Body.Close() - v := got.Load().(struct { + // The PUT returns 202 while the upload task executes on its own + // goroutine, so the executor result races these assertions. + var v struct { body string jwt string - }) + } + require.Eventually(t, func() bool { + loaded, ok := got.Load().(struct { + body string + jwt string + }) + if !ok { + return false + } + v = loaded + return true + }, 5*time.Second, 10*time.Millisecond, "executor must have run and stored its result") + require.Equal(t, "credentialed body", v.body) require.Equal(t, "portal.jwt.upload", v.jwt, "executor must receive the mint-time JWT via credctx") } @@ -78,10 +92,24 @@ func TestUploadNoCredentialIsEmpty(t *testing.T) { require.Equal(t, http.StatusAccepted, resp.StatusCode) resp.Body.Close() - v := got.Load().(struct { + // The PUT returns 202 while the upload task executes on its own + // goroutine, so the executor result races these assertions. + var v struct { body string jwt string - }) + } + require.Eventually(t, func() bool { + loaded, ok := got.Load().(struct { + body string + jwt string + }) + if !ok { + return false + } + v = loaded + return true + }, 5*time.Second, 10*time.Millisecond, "executor must have run and stored its result") + require.Equal(t, "plain body", v.body) require.Equal(t, "", v.jwt, "no JWT minted => credctx.From must be empty") } diff --git a/internal/mcp/grok_upload_chooser_test.go b/internal/mcp/grok_upload_chooser_test.go index f3d1702d..5b443a11 100644 --- a/internal/mcp/grok_upload_chooser_test.go +++ b/internal/mcp/grok_upload_chooser_test.go @@ -201,7 +201,7 @@ func TestCapabilitiesUploadToolsListsRelayTools(t *testing.T) { // detail prose, so a steps-first model on Grok can produce a CID from mint // (upload_file), a public URL (upload_url), or raw bytes (upload_data). func TestAgentGuideByteRouteChooserInSteps(t *testing.T) { - grok := buildAgentGuide(strPtr(hostenv.ProfileGrokHTTP)) + grok := buildAgentGuide(new(hostenv.ProfileGrokHTTP)) upload := guideFlowByName(t, grok, "upload") require.Equal(t, []string{"capabilities"}, upload.Steps, "upload flow leads with capabilities") @@ -254,7 +254,7 @@ func TestCapabilitiesLeadInNamesThreeFields(t *testing.T) { // agree. func TestVaultByteRouteTransportGated(t *testing.T) { // Grok (HTTP, mint-only): only the mint vault branch; no url/data claim. - grok := buildAgentGuide(strPtr(hostenv.ProfileGrokHTTP)) + grok := buildAgentGuide(new(hostenv.ProfileGrokHTTP)) grokVault := guideFlowByName(t, grok, "vault_upload") require.NotNil(t, grokVault.Decision) for _, br := range grokVault.Decision.Branches { @@ -266,7 +266,7 @@ func TestVaultByteRouteTransportGated(t *testing.T) { require.NotContains(t, grokVault.Detail, "via its own url/data source", "Grok must not claim vault_put_file url/data") // OpenAI tunnel: vault_put_file HAS url/data source modes, so those branches exist. - tunnel := buildAgentGuide(strPtr(hostenv.ProfileOpenAITunnel)) + tunnel := buildAgentGuide(new(hostenv.ProfileOpenAITunnel)) tunnelVault := guideFlowByName(t, tunnel, "vault_upload") require.NotNil(t, tunnelVault.Decision) sawURL, sawData := false, false @@ -289,7 +289,7 @@ func TestVaultByteRouteTransportGated(t *testing.T) { // Generic HTTP (mint-only, no relay tools): vault detail must NOT name // upload_url/upload_data — those tools don't exist on this host. - generic := buildAgentGuide(strPtr(hostenv.ProfileHTTPGeneric)) + generic := buildAgentGuide(new(hostenv.ProfileHTTPGeneric)) genericVault := guideFlowByName(t, generic, "vault_upload") require.NotNil(t, genericVault.Decision) require.Contains(t, genericVault.Detail, "materialize them to an agent-local file first", "generic HTTP vault detail must give the mint materialize path") @@ -298,7 +298,7 @@ func TestVaultByteRouteTransportGated(t *testing.T) { // Claude HTTP has FeatSourceData but NOT FeatSourceURL: vault detail must // name upload_data (registered) but NOT upload_url (not registered). - claude := buildAgentGuide(strPtr(hostenv.ProfileClaudeHTTP)) + claude := buildAgentGuide(new(hostenv.ProfileClaudeHTTP)) claudeVault := guideFlowByName(t, claude, "vault_upload") require.NotNil(t, claudeVault.Decision) require.NotContains(t, claudeVault.Detail, "upload_url", "Claude HTTP has no upload_url tool; vault detail must not name it") diff --git a/internal/mcp/guide_fragments.go b/internal/mcp/guide_fragments.go index a24d3376..1910b7e0 100644 --- a/internal/mcp/guide_fragments.go +++ b/internal/mcp/guide_fragments.go @@ -42,6 +42,23 @@ var reconcileNoSleep = toolforge.Static("On this host there is no sleep tool: if // caveat is needed. Shares the same decision (re-check rather than fail). var reconcilePlain = toolforge.Static("If validation is still pending or failing, treat it as reconciliation lag and re-call websites_validate after other work, without starting a new flow.") +// hnsNamespaceClause is the Handshake (alt-root) namespace guidance appended to +// custom-domain website flows whose domain may be an HNS name. It covers the +// two HNS hosting shapes: native HNS (portal builds the delegation bundle; +// the user publishes the parent NS/DS/GLUE records on-chain in the HNS wallet) +// and on-chain managed (the name's NS record points at an external +// contract, so the backend coerces the binding to status onchain_managed with +// no delegation records at all, and ownership is proven via a TXT token). +var hnsNamespaceClause = toolforge.Static("For a Handshake (alt-root) name such as acme/ (i.e. the user has a Handshake domain, not an ICANN TLD domain), pass {\"namespace\": \"hns\"} alongside the website so the site binds under the HNS namespace.").Then(hnsOnchainClause) + +// hnsOnchainClause tells the agent how to read and act on the two possible +// HNS bind outcomes in the same domain: status onchain_managed (no +// records to publish) versus the normal delegation flow. +var hnsOnchainClause = toolforge.Static("Read the created/bound domain's status next:").Sentences( + "On status onchain_managed the HNS name's DNS is served by an external contract on the Handshake chain, so there are NO delegation records to publish and pinner://websites//dns-requirements returns no parent/authoritative records — the site works and ownership is verified via a TXT token through the HNS resolver. Do not call websites_domains_convert_onchain (it is already on-chain managed) and do not wait for delegation.", + "On any other status (records_generated, waiting_delegation), read pinner://websites//dns-requirements for the HNS delegation bundle and publish the parent NS/DS/GLUE records on-chain in the HNS wallet; with managed DNS the authoritative side is handled for you. To migrate a portal-managed HNS name whose DNS now lives in an external contract, call websites_domains_convert_onchain — it is one-way and destructive (deletes Pinner's managed zone/DNSSEC), so it requires confirm=true.", +) + // siteBundleUpload composes the "how to upload a static site bundle" guidance // block. The upload call self-gates on FeatFileHostInput (a host that can hand // over a file object uploads via the `file` argument, others must convert a diff --git a/internal/mcp/prompttemplates/website_onboarding.tmpl b/internal/mcp/prompttemplates/website_onboarding.tmpl index 843aaa64..f0cfde7d 100644 --- a/internal/mcp/prompttemplates/website_onboarding.tmpl +++ b/internal/mcp/prompttemplates/website_onboarding.tmpl @@ -96,8 +96,12 @@ Determine how the user wants to address the website: {"domain": ""} (custom is inferred when a domain is given). If it is a Handshake (alt-root) name (e.g. acme/), also pass {"namespace": "hns"}; otherwise omit namespace (defaults to icann). - After create, read pinner://websites//dns-requirements — for HNS it - renders the records to publish on-chain in the HNS wallet (parent NS/DS/GLUE). + After create, check the created domain's status first (websites_domains_list): + status onchain_managed means the HNS name's DNS is served by an external + contract on the Handshake chain — there are NO delegation records to publish + and the site needs no DNS setup. Any other status: read + pinner://websites//dns-requirements — for HNS it renders the records + to publish on-chain in the HNS wallet (parent NS/DS/GLUE). {{end}} {{define "website_step_dns_mode_filled"}}Step: dns_mode diff --git a/internal/mcp/resources.go b/internal/mcp/resources.go index a5882d57..095b9e45 100644 --- a/internal/mcp/resources.go +++ b/internal/mcp/resources.go @@ -42,6 +42,10 @@ type DNSRequirements struct { Records []DNSRecord `json:"records"` Nameservers []string `json:"nameservers,omitempty"` GatewayDomain string `json:"gateway_domain,omitempty"` + // Notes are advisory annotations the agent must read before acting: they + // point at the per-domain delegation flow (where Handshake namespace and + // delegation state live), which this website-level view cannot see. + Notes []string `json:"notes,omitempty"` } // ValidationStatus holds the live validation result for a website. @@ -439,6 +443,15 @@ func buildDNSRequirements(website *ipfs.WebsiteItem) DNSRequirements { DNSHostingEnabled: website.DnsHostingEnabled, } + // Namespace-specific delegation (Handshake/HNS bundles, on-chain managed + // bindings that need NO records) lives on the domain binding, not the + // website. Point agents there instead of letting them publish website- + // level records against a domain the per-domain flow owns. + reqs.Notes = append(reqs.Notes, + "For a custom domain, the per-domain delegation bundle (and its namespace) is authoritative: call websites_domains_list to see the binding's status and websites_domains_dns_requirements for its records.", + "A Handshake (HNS) binding with status onchain_managed (DNS served by an external contract on the Handshake chain) requires NO records to be published — do not add website-level records for it.", + ) + if website.DnsHostingEnabled { // NS delegation: use gateway domain if known. if website.GatewayDomain != nil && *website.GatewayDomain != "" { diff --git a/internal/mcp/wizard/wizard_test.go b/internal/mcp/wizard/wizard_test.go index 8ca82180..b40c3720 100644 --- a/internal/mcp/wizard/wizard_test.go +++ b/internal/mcp/wizard/wizard_test.go @@ -13,7 +13,6 @@ import ( "testing" "time" - "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ipfs "go.lumeweb.com/ipfs-sdk" @@ -335,8 +334,8 @@ func (m *mockWebsitesSvc) BindDomain(ctx context.Context, websiteID string, req return &ipfs.DomainResponse{ Id: 1, Domain: req.Domain, - Namespace: req.Namespace, - Status: lo.ToPtr("pending"), + Namespace: ipfs.DomainNamespace(req.Namespace), + Status: new(ipfs.DomainResponseStatusRecordsGenerated), }, nil } @@ -344,8 +343,8 @@ func (m *mockWebsitesSvc) GetDomainDNSRequirements(_ context.Context, _, _ strin return &ipfs.DomainResponse{ Id: 1, Domain: "example.com", - Namespace: "icann", - Status: lo.ToPtr("active"), + Namespace: ipfs.DomainNamespaceICANN, + Status: new(ipfs.DomainResponseStatusActive), }, nil } @@ -397,8 +396,8 @@ func (m *mockWebsitesSvc) VerifyDomain(_ context.Context, _, _ string) (*ipfs.Do return &ipfs.DomainResponse{ Id: 1, Domain: "example.com", - Namespace: "icann", - Status: lo.ToPtr("active"), + Namespace: ipfs.DomainNamespaceICANN, + Status: new(ipfs.DomainResponseStatusActive), }, nil } diff --git a/internal/mcptest/ipfs/server.gen.go b/internal/mcptest/ipfs/server.gen.go index ab3ff41a..c2c8401e 100644 --- a/internal/mcptest/ipfs/server.gen.go +++ b/internal/mcptest/ipfs/server.gen.go @@ -15,6 +15,108 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) +// Defines values for DomainDANERepublishResponseNamespace. +const ( + DomainDANERepublishResponseNamespaceHns DomainDANERepublishResponseNamespace = "hns" + DomainDANERepublishResponseNamespaceIcann DomainDANERepublishResponseNamespace = "icann" +) + +// Valid indicates whether the value is a known member of the DomainDANERepublishResponseNamespace enum. +func (e DomainDANERepublishResponseNamespace) Valid() bool { + switch e { + case DomainDANERepublishResponseNamespaceHns: + return true + case DomainDANERepublishResponseNamespaceIcann: + return true + default: + return false + } +} + +// Defines values for DomainDANERepublishResponseStatus. +const ( + DomainDANERepublishResponseStatusActive DomainDANERepublishResponseStatus = "active" + DomainDANERepublishResponseStatusDraft DomainDANERepublishResponseStatus = "draft" + DomainDANERepublishResponseStatusError DomainDANERepublishResponseStatus = "error" + DomainDANERepublishResponseStatusOnchainManaged DomainDANERepublishResponseStatus = "onchain_managed" + DomainDANERepublishResponseStatusRecordsGenerated DomainDANERepublishResponseStatus = "records_generated" + DomainDANERepublishResponseStatusSelfHosted DomainDANERepublishResponseStatus = "self_hosted" + DomainDANERepublishResponseStatusWaitingDelegation DomainDANERepublishResponseStatus = "waiting_delegation" +) + +// Valid indicates whether the value is a known member of the DomainDANERepublishResponseStatus enum. +func (e DomainDANERepublishResponseStatus) Valid() bool { + switch e { + case DomainDANERepublishResponseStatusActive: + return true + case DomainDANERepublishResponseStatusDraft: + return true + case DomainDANERepublishResponseStatusError: + return true + case DomainDANERepublishResponseStatusOnchainManaged: + return true + case DomainDANERepublishResponseStatusRecordsGenerated: + return true + case DomainDANERepublishResponseStatusSelfHosted: + return true + case DomainDANERepublishResponseStatusWaitingDelegation: + return true + default: + return false + } +} + +// Defines values for DomainResponseNamespace. +const ( + DomainResponseNamespaceHns DomainResponseNamespace = "hns" + DomainResponseNamespaceIcann DomainResponseNamespace = "icann" +) + +// Valid indicates whether the value is a known member of the DomainResponseNamespace enum. +func (e DomainResponseNamespace) Valid() bool { + switch e { + case DomainResponseNamespaceHns: + return true + case DomainResponseNamespaceIcann: + return true + default: + return false + } +} + +// Defines values for DomainResponseStatus. +const ( + DomainResponseStatusActive DomainResponseStatus = "active" + DomainResponseStatusDraft DomainResponseStatus = "draft" + DomainResponseStatusError DomainResponseStatus = "error" + DomainResponseStatusOnchainManaged DomainResponseStatus = "onchain_managed" + DomainResponseStatusRecordsGenerated DomainResponseStatus = "records_generated" + DomainResponseStatusSelfHosted DomainResponseStatus = "self_hosted" + DomainResponseStatusWaitingDelegation DomainResponseStatus = "waiting_delegation" +) + +// Valid indicates whether the value is a known member of the DomainResponseStatus enum. +func (e DomainResponseStatus) Valid() bool { + switch e { + case DomainResponseStatusActive: + return true + case DomainResponseStatusDraft: + return true + case DomainResponseStatusError: + return true + case DomainResponseStatusOnchainManaged: + return true + case DomainResponseStatusRecordsGenerated: + return true + case DomainResponseStatusSelfHosted: + return true + case DomainResponseStatusWaitingDelegation: + return true + default: + return false + } +} + // Defines values for GatewayWebsiteResponseNamespace. const ( Hns GatewayWebsiteResponseNamespace = "hns" @@ -33,27 +135,69 @@ func (e GatewayWebsiteResponseNamespace) Valid() bool { } } +// Defines values for SSLStatusInfoStatus. +const ( + SSLStatusInfoStatusFailed SSLStatusInfoStatus = "failed" + SSLStatusInfoStatusIssuing SSLStatusInfoStatus = "issuing" + SSLStatusInfoStatusPending SSLStatusInfoStatus = "pending" + SSLStatusInfoStatusReady SSLStatusInfoStatus = "ready" +) + +// Valid indicates whether the value is a known member of the SSLStatusInfoStatus enum. +func (e SSLStatusInfoStatus) Valid() bool { + switch e { + case SSLStatusInfoStatusFailed: + return true + case SSLStatusInfoStatusIssuing: + return true + case SSLStatusInfoStatusPending: + return true + case SSLStatusInfoStatusReady: + return true + default: + return false + } +} + // Defines values for UploadResultResponseStatus. const ( - Completed UploadResultResponseStatus = "completed" - Duplicate UploadResultResponseStatus = "duplicate" - Failed UploadResultResponseStatus = "failed" - Pending UploadResultResponseStatus = "pending" - Processing UploadResultResponseStatus = "processing" + UploadResultResponseStatusCompleted UploadResultResponseStatus = "completed" + UploadResultResponseStatusDuplicate UploadResultResponseStatus = "duplicate" + UploadResultResponseStatusFailed UploadResultResponseStatus = "failed" + UploadResultResponseStatusPending UploadResultResponseStatus = "pending" + UploadResultResponseStatusProcessing UploadResultResponseStatus = "processing" ) // Valid indicates whether the value is a known member of the UploadResultResponseStatus enum. func (e UploadResultResponseStatus) Valid() bool { switch e { - case Completed: + case UploadResultResponseStatusCompleted: + return true + case UploadResultResponseStatusDuplicate: return true - case Duplicate: + case UploadResultResponseStatusFailed: return true - case Failed: + case UploadResultResponseStatusPending: return true - case Pending: + case UploadResultResponseStatusProcessing: + return true + default: + return false + } +} + +// Defines values for WebsiteChangeEventEventType. +const ( + Published WebsiteChangeEventEventType = "published" + Removed WebsiteChangeEventEventType = "removed" +) + +// Valid indicates whether the value is a known member of the WebsiteChangeEventEventType enum. +func (e WebsiteChangeEventEventType) Valid() bool { + switch e { + case Published: return true - case Processing: + case Removed: return true default: return false @@ -152,19 +296,25 @@ type DNSDelegationRecord struct { // DomainDANERepublishResponse defines model for DomainDANERepublishResponse. type DomainDANERepublishResponse struct { - Delegation *DNSDelegation `json:"delegation,omitempty"` - Domain string `json:"domain"` - GatewayHost *string `json:"gateway_host,omitempty"` - Id int `json:"id"` - Namespace string `json:"namespace"` - OwnerName *string `json:"owner_name,omitempty"` - Ssl *SSLStatusInfo `json:"ssl,omitempty"` - Status *string `json:"status,omitempty"` - TlsaRdata *string `json:"tlsa_rdata,omitempty"` - TlsaRecord *string `json:"tlsa_record,omitempty"` - ZoneName *string `json:"zone_name,omitempty"` + Delegation *DNSDelegation `json:"delegation,omitempty"` + Domain string `json:"domain"` + GatewayHost *string `json:"gateway_host,omitempty"` + Id int `json:"id"` + Namespace DomainDANERepublishResponseNamespace `json:"namespace"` + OwnerName *string `json:"owner_name,omitempty"` + Ssl *SSLStatusInfo `json:"ssl,omitempty"` + Status *DomainDANERepublishResponseStatus `json:"status,omitempty"` + TlsaRdata *string `json:"tlsa_rdata,omitempty"` + TlsaRecord *string `json:"tlsa_record,omitempty"` + ZoneName *string `json:"zone_name,omitempty"` } +// DomainDANERepublishResponseNamespace defines model for DomainDANERepublishResponse.Namespace. +type DomainDANERepublishResponseNamespace string + +// DomainDANERepublishResponseStatus defines model for DomainDANERepublishResponse.Status. +type DomainDANERepublishResponseStatus string + // DomainListResponse defines model for DomainListResponse. type DomainListResponse struct { Data []DomainResponse `json:"data"` @@ -173,24 +323,35 @@ type DomainListResponse struct { // DomainRequest defines model for DomainRequest. type DomainRequest struct { - Config *map[string]interface{} `json:"config,omitempty"` - Domain string `json:"domain"` - Namespace string `json:"namespace"` + Config *map[string]interface{} `json:"config,omitempty"` + DnsHostingEnabled *bool `json:"dns_hosting_enabled,omitempty"` + Domain string `json:"domain"` + Generate *bool `json:"generate,omitempty"` + Label *string `json:"label,omitempty"` + Namespace string `json:"namespace"` + PlatformDomain *string `json:"platform_domain,omitempty"` + PlatformNamespace *string `json:"platform_namespace,omitempty"` } // DomainResponse defines model for DomainResponse. type DomainResponse struct { - Delegation *DNSDelegation `json:"delegation,omitempty"` - DnsHostingEnabled bool `json:"dns_hosting_enabled"` - Domain string `json:"domain"` - GatewayHost *string `json:"gateway_host,omitempty"` - Id int `json:"id"` - Namespace string `json:"namespace"` - Ssl *SSLStatusInfo `json:"ssl,omitempty"` - Status *string `json:"status,omitempty"` - ZoneName *string `json:"zone_name,omitempty"` + Delegation *DNSDelegation `json:"delegation,omitempty"` + DnsHostingEnabled bool `json:"dns_hosting_enabled"` + Domain string `json:"domain"` + GatewayHost *string `json:"gateway_host,omitempty"` + Id int `json:"id"` + Namespace DomainResponseNamespace `json:"namespace"` + Ssl *SSLStatusInfo `json:"ssl,omitempty"` + Status *DomainResponseStatus `json:"status,omitempty"` + ZoneName *string `json:"zone_name,omitempty"` } +// DomainResponseNamespace defines model for DomainResponse.Namespace. +type DomainResponseNamespace string + +// DomainResponseStatus defines model for DomainResponse.Status. +type DomainResponseStatus string + // DomainUpdateRequest defines model for DomainUpdateRequest. type DomainUpdateRequest struct { DnsHostingEnabled *bool `json:"dns_hosting_enabled,omitempty"` @@ -358,15 +519,49 @@ type PingResponse struct { Status string `json:"status"` } +// PlatformAvailabilityResponse defines model for PlatformAvailabilityResponse. +type PlatformAvailabilityResponse struct { + Label string `json:"label"` + Results []PlatformAvailabilityResult `json:"results"` +} + +// PlatformAvailabilityResult defines model for PlatformAvailabilityResult. +type PlatformAvailabilityResult struct { + Available bool `json:"available"` + Namespace string `json:"namespace"` + PlatformDomain string `json:"platform_domain"` +} + +// PlatformDomainListResponse defines model for PlatformDomainListResponse. +type PlatformDomainListResponse struct { + Data []PlatformDomainResponse `json:"data"` + Total int `json:"total"` +} + +// PlatformDomainResponse defines model for PlatformDomainResponse. +type PlatformDomainResponse struct { + Domain string `json:"domain"` + Enabled bool `json:"enabled"` + Id int `json:"id"` + Namespace string `json:"namespace"` + ZoneId int `json:"zone_id"` +} + // PostUploadResponse defines model for PostUploadResponse. type PostUploadResponse struct { CID string `json:"CID"` } +// RecordDeleteRequest defines model for RecordDeleteRequest. +type RecordDeleteRequest struct { + Content *string `json:"content,omitempty"` +} + // RecordIdentifier defines model for RecordIdentifier. type RecordIdentifier struct { - Name string `json:"name"` - Type string `json:"type"` + Content *string `json:"content,omitempty"` + Name string `json:"name"` + Type string `json:"type"` } // RecordRequest defines model for RecordRequest. @@ -382,6 +577,7 @@ type RecordRequest struct { type RecordResponse struct { Content string `json:"content"` Disabled bool `json:"disabled"` + Id string `json:"id"` Name string `json:"name"` Ttl int `json:"ttl"` Type string `json:"type"` @@ -404,12 +600,15 @@ type RecordResult struct { // SSLStatusInfo defines model for SSLStatusInfo. type SSLStatusInfo struct { - Error *string `json:"error,omitempty"` - IssuedAt *time.Time `json:"issued_at,omitempty"` - LastUpdatedAt *time.Time `json:"last_updated_at,omitempty"` - Status string `json:"status"` + Error *string `json:"error,omitempty"` + IssuedAt *time.Time `json:"issued_at,omitempty"` + LastUpdatedAt *time.Time `json:"last_updated_at,omitempty"` + Status SSLStatusInfoStatus `json:"status"` } +// SSLStatusInfoStatus defines model for SSLStatusInfo.Status. +type SSLStatusInfoStatus string + // SSLStatusUpdateRequest defines model for SSLStatusUpdateRequest. type SSLStatusUpdateRequest struct { Error *string `json:"error,omitempty"` @@ -443,6 +642,27 @@ type ValidationResponse struct { Valid bool `json:"valid"` } +// WebsiteChangeEvent defines model for WebsiteChangeEvent. +type WebsiteChangeEvent struct { + Cid *string `json:"cid,omitempty"` + CreatedAt time.Time `json:"created_at"` + Domain string `json:"domain"` + EventType WebsiteChangeEventEventType `json:"event_type"` + Id int `json:"id"` + UserId *int `json:"user_id,omitempty"` + WebsiteId *int `json:"website_id,omitempty"` +} + +// WebsiteChangeEventEventType defines model for WebsiteChangeEvent.EventType. +type WebsiteChangeEventEventType string + +// WebsiteChangesResponse defines model for WebsiteChangesResponse. +type WebsiteChangesResponse struct { + Events []WebsiteChangeEvent `json:"events"` + HighWaterMark int `json:"high_water_mark"` + Truncated bool `json:"truncated"` +} + // WebsiteConfigResponse defines model for WebsiteConfigResponse. type WebsiteConfigResponse struct { GatewayDomain *string `json:"gateway_domain,omitempty"` @@ -454,7 +674,6 @@ type WebsiteItem struct { ActiveCid *string `json:"active_cid,omitempty"` Created time.Time `json:"created"` DnsHostingEnabled bool `json:"dns_hosting_enabled"` - DnsZoneId *int `json:"dns_zone_id,omitempty"` Domain string `json:"domain"` Expired bool `json:"expired"` GatewayDomain *string `json:"gateway_domain,omitempty"` @@ -470,6 +689,7 @@ type WebsiteItem struct { ValidationExpiresAt *time.Time `json:"validation_expires_at,omitempty"` ValidationRecordHost *string `json:"validation_record_host,omitempty"` ValidationToken string `json:"validation_token"` + ZoneId *int `json:"zone_id,omitempty"` } // WebsiteItemResponse defines model for WebsiteItemResponse. @@ -481,8 +701,12 @@ type WebsiteItemResponse struct { // WebsiteRequest defines model for WebsiteRequest. type WebsiteRequest struct { DnsHostingEnabled *bool `json:"dns_hosting_enabled,omitempty"` - Domain string `json:"domain"` + Domain *string `json:"domain,omitempty"` + Generate *bool `json:"generate,omitempty"` + Label *string `json:"label,omitempty"` Namespace *string `json:"namespace,omitempty"` + PlatformDomain *string `json:"platform_domain,omitempty"` + PlatformNamespace *string `json:"platform_namespace,omitempty"` TargetHash string `json:"target_hash"` TargetType string `json:"target_type"` } @@ -492,7 +716,6 @@ type WebsiteResponse struct { ActiveCid *string `json:"active_cid,omitempty"` Created time.Time `json:"created"` DnsHostingEnabled bool `json:"dns_hosting_enabled"` - DnsZoneId *int `json:"dns_zone_id,omitempty"` Domain string `json:"domain"` Expired bool `json:"expired"` GatewayDomain *string `json:"gateway_domain,omitempty"` @@ -508,6 +731,7 @@ type WebsiteResponse struct { ValidationExpiresAt *time.Time `json:"validation_expires_at,omitempty"` ValidationRecordHost *string `json:"validation_record_host,omitempty"` ValidationToken string `json:"validation_token"` + ZoneId *int `json:"zone_id,omitempty"` } // WebsiteUpdateRequest defines model for WebsiteUpdateRequest. @@ -619,12 +843,129 @@ type PatchApiUploadTusIdParams struct { UploadOffset map[string]interface{} `json:"Upload-Offset"` } +// GetApiWebsitesParams defines parameters for GetApiWebsites. +type GetApiWebsitesParams struct { + // UnderscoreEnd Ending index of the items to return (exclusive). Defaults to 10. + UnderscoreEnd *int `form:"_end,omitempty" json:"_end,omitempty"` + + // UnderscoreOrder Comma-separated list of sort orders ('asc' or 'desc') corresponding to _sort fields. Defaults to 'asc'. + UnderscoreOrder *string `form:"_order,omitempty" json:"_order,omitempty"` + + // UnderscoreSort Comma-separated list of fields to sort by. Available fields: target_type, status + UnderscoreSort *string `form:"_sort,omitempty" json:"_sort,omitempty"` + + // UnderscoreStart Starting index of the items to return (0-based). Defaults to 0. + UnderscoreStart *int `form:"_start,omitempty" json:"_start,omitempty"` + + // DomainContains Filter by domain contains + DomainContains *string `form:"domain_contains,omitempty" json:"domain_contains,omitempty"` + + // DomainEndswith Filter by domain endswith + DomainEndswith *string `form:"domain_endswith,omitempty" json:"domain_endswith,omitempty"` + + // DomainEq Filter by domain eq + DomainEq *string `form:"domain_eq,omitempty" json:"domain_eq,omitempty"` + + // DomainNe Filter by domain ne + DomainNe *string `form:"domain_ne,omitempty" json:"domain_ne,omitempty"` + + // DomainStartswith Filter by domain startswith + DomainStartswith *string `form:"domain_startswith,omitempty" json:"domain_startswith,omitempty"` + + // FiltersDomainContains Filter by domain contains + FiltersDomainContains *string `form:"filters[domain][contains],omitempty" json:"filters[domain][contains],omitempty"` + + // FiltersDomainEndswith Filter by domain endswith + FiltersDomainEndswith *string `form:"filters[domain][endswith],omitempty" json:"filters[domain][endswith],omitempty"` + + // FiltersDomainEq Filter by domain eq + FiltersDomainEq *string `form:"filters[domain][eq],omitempty" json:"filters[domain][eq],omitempty"` + + // FiltersDomainNe Filter by domain ne + FiltersDomainNe *string `form:"filters[domain][ne],omitempty" json:"filters[domain][ne],omitempty"` + + // FiltersDomainStartswith Filter by domain startswith + FiltersDomainStartswith *string `form:"filters[domain][startswith],omitempty" json:"filters[domain][startswith],omitempty"` + + // FiltersStatusContains Filter by status contains + FiltersStatusContains *string `form:"filters[status][contains],omitempty" json:"filters[status][contains],omitempty"` + + // FiltersStatusEndswith Filter by status endswith + FiltersStatusEndswith *string `form:"filters[status][endswith],omitempty" json:"filters[status][endswith],omitempty"` + + // FiltersStatusEq Filter by status eq + FiltersStatusEq *string `form:"filters[status][eq],omitempty" json:"filters[status][eq],omitempty"` + + // FiltersStatusNe Filter by status ne + FiltersStatusNe *string `form:"filters[status][ne],omitempty" json:"filters[status][ne],omitempty"` + + // FiltersStatusStartswith Filter by status startswith + FiltersStatusStartswith *string `form:"filters[status][startswith],omitempty" json:"filters[status][startswith],omitempty"` + + // FiltersTargetTypeContains Filter by target_type contains + FiltersTargetTypeContains *string `form:"filters[target_type][contains],omitempty" json:"filters[target_type][contains],omitempty"` + + // FiltersTargetTypeEndswith Filter by target_type endswith + FiltersTargetTypeEndswith *string `form:"filters[target_type][endswith],omitempty" json:"filters[target_type][endswith],omitempty"` + + // FiltersTargetTypeEq Filter by target_type eq + FiltersTargetTypeEq *string `form:"filters[target_type][eq],omitempty" json:"filters[target_type][eq],omitempty"` + + // FiltersTargetTypeNe Filter by target_type ne + FiltersTargetTypeNe *string `form:"filters[target_type][ne],omitempty" json:"filters[target_type][ne],omitempty"` + + // FiltersTargetTypeStartswith Filter by target_type startswith + FiltersTargetTypeStartswith *string `form:"filters[target_type][startswith],omitempty" json:"filters[target_type][startswith],omitempty"` + + // StatusContains Filter by status contains + StatusContains *string `form:"status_contains,omitempty" json:"status_contains,omitempty"` + + // StatusEndswith Filter by status endswith + StatusEndswith *string `form:"status_endswith,omitempty" json:"status_endswith,omitempty"` + + // StatusEq Filter by status eq + StatusEq *string `form:"status_eq,omitempty" json:"status_eq,omitempty"` + + // StatusNe Filter by status ne + StatusNe *string `form:"status_ne,omitempty" json:"status_ne,omitempty"` + + // StatusStartswith Filter by status startswith + StatusStartswith *string `form:"status_startswith,omitempty" json:"status_startswith,omitempty"` + + // TargetTypeContains Filter by target_type contains + TargetTypeContains *string `form:"target_type_contains,omitempty" json:"target_type_contains,omitempty"` + + // TargetTypeEndswith Filter by target_type endswith + TargetTypeEndswith *string `form:"target_type_endswith,omitempty" json:"target_type_endswith,omitempty"` + + // TargetTypeEq Filter by target_type eq + TargetTypeEq *string `form:"target_type_eq,omitempty" json:"target_type_eq,omitempty"` + + // TargetTypeNe Filter by target_type ne + TargetTypeNe *string `form:"target_type_ne,omitempty" json:"target_type_ne,omitempty"` + + // TargetTypeStartswith Filter by target_type startswith + TargetTypeStartswith *string `form:"target_type_startswith,omitempty" json:"target_type_startswith,omitempty"` +} + +// GetApiWebsitesPlatformDomainsAvailabilityParams defines parameters for GetApiWebsitesPlatformDomainsAvailability. +type GetApiWebsitesPlatformDomainsAvailabilityParams struct { + // Label Candidate subdomain label + Label *string `form:"label,omitempty" json:"label,omitempty"` +} + // GetInternalDnsCertDomainParams defines parameters for GetInternalDnsCertDomain. type GetInternalDnsCertDomainParams struct { // Namespace Namespace: hns or icann Namespace *string `form:"namespace,omitempty" json:"namespace,omitempty"` } +// GetInternalWebsitesChangesParams defines parameters for GetInternalWebsitesChanges. +type GetInternalWebsitesChangesParams struct { + // After Durable event cursor to resume after. Returns all events with id > after. Omit to start from the beginning of the retained window. + After *string `form:"after,omitempty" json:"after,omitempty"` +} + // GetPinsParams defines parameters for GetPins. type GetPinsParams struct { // After Filter for pins created after this ISO 8601 timestamp. Example: 2024-01-01T00:00:00Z @@ -670,6 +1011,9 @@ type PostApiDnsZonesIdRecordsBulkJSONRequestBody = BulkRecordRequest // PostApiDnsZonesIdRecordsBulkDeleteJSONRequestBody defines body for PostApiDnsZonesIdRecordsBulkDelete for application/json ContentType. type PostApiDnsZonesIdRecordsBulkDeleteJSONRequestBody = BulkDeleteRequest +// DeleteApiDnsZonesIdRecordsNameTypeJSONRequestBody defines body for DeleteApiDnsZonesIdRecordsNameType for application/json ContentType. +type DeleteApiDnsZonesIdRecordsNameTypeJSONRequestBody = RecordDeleteRequest + // PutApiDnsZonesIdRecordsNameTypeJSONRequestBody defines body for PutApiDnsZonesIdRecordsNameType for application/json ContentType. type PutApiDnsZonesIdRecordsNameTypeJSONRequestBody = RecordRequest @@ -821,13 +1165,19 @@ type ServerInterface interface { PatchApiUploadTusId(w http.ResponseWriter, r *http.Request, id string, params PatchApiUploadTusIdParams) // List websites // (GET /api/websites) - GetApiWebsites(w http.ResponseWriter, r *http.Request) + GetApiWebsites(w http.ResponseWriter, r *http.Request, params GetApiWebsitesParams) // Create website // (POST /api/websites) PostApiWebsites(w http.ResponseWriter, r *http.Request) // Get website hosting configuration // (GET /api/websites/config) GetApiWebsitesConfig(w http.ResponseWriter, r *http.Request) + // List supported platform domains + // (GET /api/websites/platform-domains) + GetApiWebsitesPlatformDomains(w http.ResponseWriter, r *http.Request) + // Check platform subdomain availability + // (GET /api/websites/platform-domains/availability) + GetApiWebsitesPlatformDomainsAvailability(w http.ResponseWriter, r *http.Request, params GetApiWebsitesPlatformDomainsAvailabilityParams) // Get SSL status // (GET /api/websites/{domain}/ssl-status) GetApiWebsitesDomainSslStatus(w http.ResponseWriter, r *http.Request, domain string) @@ -858,6 +1208,9 @@ type ServerInterface interface { // Get domain DNS delegation requirements // (GET /api/websites/{id}/domains/{domain_id}/dns-requirements) GetApiWebsitesIdDomainsDomainIdDnsRequirements(w http.ResponseWriter, r *http.Request, id string, domainId string) + // Reclassify domain as on-chain managed + // (POST /api/websites/{id}/domains/{domain_id}/onchain) + PostApiWebsitesIdDomainsDomainIdOnchain(w http.ResponseWriter, r *http.Request, id string, domainId string) // Verify domain delegation // (POST /api/websites/{id}/domains/{domain_id}/verify) PostApiWebsitesIdDomainsDomainIdVerify(w http.ResponseWriter, r *http.Request, id string, domainId string) @@ -876,6 +1229,9 @@ type ServerInterface interface { // Health check // (GET /internal/ping) GetInternalPing(w http.ResponseWriter, r *http.Request) + // Reconcile website changes + // (GET /internal/websites/changes) + GetInternalWebsitesChanges(w http.ResponseWriter, r *http.Request, params GetInternalWebsitesChangesParams) // Stream website events via SSE // (GET /internal/websites/events) GetInternalWebsitesEvents(w http.ResponseWriter, r *http.Request) @@ -2105,92 +2461,587 @@ func (siw *ServerInterfaceWrapper) PatchApiUploadTusId(w http.ResponseWriter, r // GetApiWebsites operation middleware func (siw *ServerInterfaceWrapper) GetApiWebsites(w http.ResponseWriter, r *http.Request) { - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetApiWebsites(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} + var err error + _ = err -// PostApiWebsites operation middleware -func (siw *ServerInterfaceWrapper) PostApiWebsites(w http.ResponseWriter, r *http.Request) { + // Parameter object where we will unmarshal all parameters from the context + var params GetApiWebsitesParams - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.PostApiWebsites(w, r) - })) + // ------------- Optional query parameter "_end" ------------- - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) + err = runtime.BindQueryParameterWithOptions("form", true, false, "_end", r.URL.Query(), ¶ms.UnderscoreEnd, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "_end"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "_end", Err: err}) + } + return } - handler.ServeHTTP(w, r) -} + // ------------- Optional query parameter "_order" ------------- -// GetApiWebsitesConfig operation middleware -func (siw *ServerInterfaceWrapper) GetApiWebsitesConfig(w http.ResponseWriter, r *http.Request) { + err = runtime.BindQueryParameterWithOptions("form", true, false, "_order", r.URL.Query(), ¶ms.UnderscoreOrder, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "_order"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "_order", Err: err}) + } + return + } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetApiWebsitesConfig(w, r) - })) + // ------------- Optional query parameter "_sort" ------------- - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) + err = runtime.BindQueryParameterWithOptions("form", true, false, "_sort", r.URL.Query(), ¶ms.UnderscoreSort, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "_sort"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "_sort", Err: err}) + } + return } - handler.ServeHTTP(w, r) -} - -// GetApiWebsitesDomainSslStatus operation middleware -func (siw *ServerInterfaceWrapper) GetApiWebsitesDomainSslStatus(w http.ResponseWriter, r *http.Request) { + // ------------- Optional query parameter "_start" ------------- - var err error - _ = err + err = runtime.BindQueryParameterWithOptions("form", true, false, "_start", r.URL.Query(), ¶ms.UnderscoreStart, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "_start"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "_start", Err: err}) + } + return + } - // ------------- Path parameter "domain" ------------- - var domain string + // ------------- Optional query parameter "domain_contains" ------------- - err = runtime.BindStyledParameterWithOptions("simple", "domain", r.PathValue("domain"), &domain, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "domain_contains", r.URL.Query(), ¶ms.DomainContains, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "domain", Err: err}) + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "domain_contains"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "domain_contains", Err: err}) + } return } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetApiWebsitesDomainSslStatus(w, r, domain) - })) + // ------------- Optional query parameter "domain_endswith" ------------- - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) + err = runtime.BindQueryParameterWithOptions("form", true, false, "domain_endswith", r.URL.Query(), ¶ms.DomainEndswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "domain_endswith"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "domain_endswith", Err: err}) + } + return } - handler.ServeHTTP(w, r) -} - -// DeleteApiWebsitesId operation middleware -func (siw *ServerInterfaceWrapper) DeleteApiWebsitesId(w http.ResponseWriter, r *http.Request) { + // ------------- Optional query parameter "domain_eq" ------------- - var err error - _ = err + err = runtime.BindQueryParameterWithOptions("form", true, false, "domain_eq", r.URL.Query(), ¶ms.DomainEq, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "domain_eq"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "domain_eq", Err: err}) + } + return + } - // ------------- Path parameter "id" ------------- - var id string + // ------------- Optional query parameter "domain_ne" ------------- - err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + err = runtime.BindQueryParameterWithOptions("form", true, false, "domain_ne", r.URL.Query(), ¶ms.DomainNe, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "domain_ne"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "domain_ne", Err: err}) + } return } - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.DeleteApiWebsitesId(w, r, id) - })) + // ------------- Optional query parameter "domain_startswith" ------------- - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) + err = runtime.BindQueryParameterWithOptions("form", true, false, "domain_startswith", r.URL.Query(), ¶ms.DomainStartswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "domain_startswith"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "domain_startswith", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[domain][contains]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[domain][contains]", r.URL.Query(), ¶ms.FiltersDomainContains, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[domain][contains]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[domain][contains]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[domain][endswith]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[domain][endswith]", r.URL.Query(), ¶ms.FiltersDomainEndswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[domain][endswith]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[domain][endswith]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[domain][eq]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[domain][eq]", r.URL.Query(), ¶ms.FiltersDomainEq, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[domain][eq]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[domain][eq]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[domain][ne]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[domain][ne]", r.URL.Query(), ¶ms.FiltersDomainNe, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[domain][ne]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[domain][ne]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[domain][startswith]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[domain][startswith]", r.URL.Query(), ¶ms.FiltersDomainStartswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[domain][startswith]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[domain][startswith]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[status][contains]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[status][contains]", r.URL.Query(), ¶ms.FiltersStatusContains, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[status][contains]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[status][contains]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[status][endswith]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[status][endswith]", r.URL.Query(), ¶ms.FiltersStatusEndswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[status][endswith]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[status][endswith]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[status][eq]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[status][eq]", r.URL.Query(), ¶ms.FiltersStatusEq, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[status][eq]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[status][eq]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[status][ne]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[status][ne]", r.URL.Query(), ¶ms.FiltersStatusNe, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[status][ne]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[status][ne]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[status][startswith]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[status][startswith]", r.URL.Query(), ¶ms.FiltersStatusStartswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[status][startswith]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[status][startswith]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[target_type][contains]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[target_type][contains]", r.URL.Query(), ¶ms.FiltersTargetTypeContains, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[target_type][contains]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[target_type][contains]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[target_type][endswith]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[target_type][endswith]", r.URL.Query(), ¶ms.FiltersTargetTypeEndswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[target_type][endswith]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[target_type][endswith]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[target_type][eq]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[target_type][eq]", r.URL.Query(), ¶ms.FiltersTargetTypeEq, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[target_type][eq]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[target_type][eq]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[target_type][ne]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[target_type][ne]", r.URL.Query(), ¶ms.FiltersTargetTypeNe, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[target_type][ne]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[target_type][ne]", Err: err}) + } + return + } + + // ------------- Optional query parameter "filters[target_type][startswith]" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filters[target_type][startswith]", r.URL.Query(), ¶ms.FiltersTargetTypeStartswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "filters[target_type][startswith]"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filters[target_type][startswith]", Err: err}) + } + return + } + + // ------------- Optional query parameter "status_contains" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "status_contains", r.URL.Query(), ¶ms.StatusContains, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "status_contains"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "status_contains", Err: err}) + } + return + } + + // ------------- Optional query parameter "status_endswith" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "status_endswith", r.URL.Query(), ¶ms.StatusEndswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "status_endswith"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "status_endswith", Err: err}) + } + return + } + + // ------------- Optional query parameter "status_eq" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "status_eq", r.URL.Query(), ¶ms.StatusEq, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "status_eq"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "status_eq", Err: err}) + } + return + } + + // ------------- Optional query parameter "status_ne" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "status_ne", r.URL.Query(), ¶ms.StatusNe, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "status_ne"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "status_ne", Err: err}) + } + return + } + + // ------------- Optional query parameter "status_startswith" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "status_startswith", r.URL.Query(), ¶ms.StatusStartswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "status_startswith"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "status_startswith", Err: err}) + } + return + } + + // ------------- Optional query parameter "target_type_contains" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "target_type_contains", r.URL.Query(), ¶ms.TargetTypeContains, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "target_type_contains"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "target_type_contains", Err: err}) + } + return + } + + // ------------- Optional query parameter "target_type_endswith" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "target_type_endswith", r.URL.Query(), ¶ms.TargetTypeEndswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "target_type_endswith"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "target_type_endswith", Err: err}) + } + return + } + + // ------------- Optional query parameter "target_type_eq" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "target_type_eq", r.URL.Query(), ¶ms.TargetTypeEq, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "target_type_eq"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "target_type_eq", Err: err}) + } + return + } + + // ------------- Optional query parameter "target_type_ne" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "target_type_ne", r.URL.Query(), ¶ms.TargetTypeNe, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "target_type_ne"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "target_type_ne", Err: err}) + } + return + } + + // ------------- Optional query parameter "target_type_startswith" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "target_type_startswith", r.URL.Query(), ¶ms.TargetTypeStartswith, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "target_type_startswith"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "target_type_startswith", Err: err}) + } + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetApiWebsites(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostApiWebsites operation middleware +func (siw *ServerInterfaceWrapper) PostApiWebsites(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostApiWebsites(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetApiWebsitesConfig operation middleware +func (siw *ServerInterfaceWrapper) GetApiWebsitesConfig(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetApiWebsitesConfig(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetApiWebsitesPlatformDomains operation middleware +func (siw *ServerInterfaceWrapper) GetApiWebsitesPlatformDomains(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetApiWebsitesPlatformDomains(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetApiWebsitesPlatformDomainsAvailability operation middleware +func (siw *ServerInterfaceWrapper) GetApiWebsitesPlatformDomainsAvailability(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // Parameter object where we will unmarshal all parameters from the context + var params GetApiWebsitesPlatformDomainsAvailabilityParams + + // ------------- Optional query parameter "label" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "label", r.URL.Query(), ¶ms.Label, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "label"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "label", Err: err}) + } + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetApiWebsitesPlatformDomainsAvailability(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetApiWebsitesDomainSslStatus operation middleware +func (siw *ServerInterfaceWrapper) GetApiWebsitesDomainSslStatus(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "domain" ------------- + var domain string + + err = runtime.BindStyledParameterWithOptions("simple", "domain", r.PathValue("domain"), &domain, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "domain", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetApiWebsitesDomainSslStatus(w, r, domain) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeleteApiWebsitesId operation middleware +func (siw *ServerInterfaceWrapper) DeleteApiWebsitesId(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteApiWebsitesId(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) } handler.ServeHTTP(w, r) @@ -2440,6 +3291,41 @@ func (siw *ServerInterfaceWrapper) GetApiWebsitesIdDomainsDomainIdDnsRequirement handler.ServeHTTP(w, r) } +// PostApiWebsitesIdDomainsDomainIdOnchain operation middleware +func (siw *ServerInterfaceWrapper) PostApiWebsitesIdDomainsDomainIdOnchain(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + // ------------- Path parameter "domain_id" ------------- + var domainId string + + err = runtime.BindStyledParameterWithOptions("simple", "domain_id", r.PathValue("domain_id"), &domainId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "domain_id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostApiWebsitesIdDomainsDomainIdOnchain(w, r, id, domainId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // PostApiWebsitesIdDomainsDomainIdVerify operation middleware func (siw *ServerInterfaceWrapper) PostApiWebsitesIdDomainsDomainIdVerify(w http.ResponseWriter, r *http.Request) { @@ -2585,6 +3471,39 @@ func (siw *ServerInterfaceWrapper) GetInternalPing(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } +// GetInternalWebsitesChanges operation middleware +func (siw *ServerInterfaceWrapper) GetInternalWebsitesChanges(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // Parameter object where we will unmarshal all parameters from the context + var params GetInternalWebsitesChangesParams + + // ------------- Optional query parameter "after" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "after", r.URL.Query(), ¶ms.After, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + var requiredError *runtime.RequiredParameterError + if errors.As(err, &requiredError) { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "after"}) + } else { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "after", Err: err}) + } + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetInternalWebsitesChanges(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetInternalWebsitesEvents operation middleware func (siw *ServerInterfaceWrapper) GetInternalWebsitesEvents(w http.ResponseWriter, r *http.Request) { @@ -3222,6 +4141,8 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/websites", wrapper.GetApiWebsites) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/websites", wrapper.PostApiWebsites) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/websites/config", wrapper.GetApiWebsitesConfig) + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/websites/platform-domains", wrapper.GetApiWebsitesPlatformDomains) + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/websites/platform-domains/availability", wrapper.GetApiWebsitesPlatformDomainsAvailability) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/websites/{domain}/ssl-status", wrapper.GetApiWebsitesDomainSslStatus) m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/websites/{id}", wrapper.DeleteApiWebsitesId) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/websites/{id}", wrapper.GetApiWebsitesId) @@ -3232,12 +4153,14 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodPatch+" "+options.BaseURL+"/api/websites/{id}/domains/{domain_id}", wrapper.PatchApiWebsitesIdDomainsDomainId) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/websites/{id}/domains/{domain_id}/dane/republish", wrapper.PostApiWebsitesIdDomainsDomainIdDaneRepublish) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/websites/{id}/domains/{domain_id}/dns-requirements", wrapper.GetApiWebsitesIdDomainsDomainIdDnsRequirements) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/websites/{id}/domains/{domain_id}/onchain", wrapper.PostApiWebsitesIdDomainsDomainIdOnchain) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/websites/{id}/domains/{domain_id}/verify", wrapper.PostApiWebsitesIdDomainsDomainIdVerify) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/websites/{id}/validate", wrapper.PostApiWebsitesIdValidate) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/internal/dns/cert", wrapper.PostInternalDnsCert) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/internal/dns/cert/{domain}", wrapper.GetInternalDnsCertDomain) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/internal/dns/tlsa", wrapper.PostInternalDnsTlsa) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/internal/ping", wrapper.GetInternalPing) + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/internal/websites/changes", wrapper.GetInternalWebsitesChanges) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/internal/websites/events", wrapper.GetInternalWebsitesEvents) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/internal/websites/{domain}", wrapper.GetInternalWebsitesDomain) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/internal/websites/{domain}/ssl-status", wrapper.PostInternalWebsitesDomainSslStatus) diff --git a/internal/mcptest/ipfs/serverStub.gen.go b/internal/mcptest/ipfs/serverStub.gen.go index c792402a..6e32c7ec 100644 --- a/internal/mcptest/ipfs/serverStub.gen.go +++ b/internal/mcptest/ipfs/serverStub.gen.go @@ -156,7 +156,7 @@ func (serverStub) PatchApiUploadTusId(w http.ResponseWriter, r *http.Request, id http.Error(w, "not implemented", http.StatusNotImplemented) } -func (serverStub) GetApiWebsites(w http.ResponseWriter, r *http.Request) { +func (serverStub) GetApiWebsites(w http.ResponseWriter, r *http.Request, params GetApiWebsitesParams) { http.Error(w, "not implemented", http.StatusNotImplemented) } @@ -168,6 +168,14 @@ func (serverStub) GetApiWebsitesConfig(w http.ResponseWriter, r *http.Request) { http.Error(w, "not implemented", http.StatusNotImplemented) } +func (serverStub) GetApiWebsitesPlatformDomains(w http.ResponseWriter, r *http.Request) { + http.Error(w, "not implemented", http.StatusNotImplemented) +} + +func (serverStub) GetApiWebsitesPlatformDomainsAvailability(w http.ResponseWriter, r *http.Request, params GetApiWebsitesPlatformDomainsAvailabilityParams) { + http.Error(w, "not implemented", http.StatusNotImplemented) +} + func (serverStub) GetApiWebsitesDomainSslStatus(w http.ResponseWriter, r *http.Request, domain string) { http.Error(w, "not implemented", http.StatusNotImplemented) } @@ -208,6 +216,10 @@ func (serverStub) GetApiWebsitesIdDomainsDomainIdDnsRequirements(w http.Response http.Error(w, "not implemented", http.StatusNotImplemented) } +func (serverStub) PostApiWebsitesIdDomainsDomainIdOnchain(w http.ResponseWriter, r *http.Request, id string, domainId string) { + http.Error(w, "not implemented", http.StatusNotImplemented) +} + func (serverStub) PostApiWebsitesIdDomainsDomainIdVerify(w http.ResponseWriter, r *http.Request, id string, domainId string) { http.Error(w, "not implemented", http.StatusNotImplemented) } @@ -232,6 +244,10 @@ func (serverStub) GetInternalPing(w http.ResponseWriter, r *http.Request) { http.Error(w, "not implemented", http.StatusNotImplemented) } +func (serverStub) GetInternalWebsitesChanges(w http.ResponseWriter, r *http.Request, params GetInternalWebsitesChangesParams) { + http.Error(w, "not implemented", http.StatusNotImplemented) +} + func (serverStub) GetInternalWebsitesEvents(w http.ResponseWriter, r *http.Request) { http.Error(w, "not implemented", http.StatusNotImplemented) } diff --git a/internal/mcptest/ipfs/websites.go b/internal/mcptest/ipfs/websites.go index 348a80a9..6db22d6e 100644 --- a/internal/mcptest/ipfs/websites.go +++ b/internal/mcptest/ipfs/websites.go @@ -61,7 +61,7 @@ func (w *websiteSite) toResponse() WebsiteResponse { ActiveCid: w.ActiveCid, Created: w.Created, DnsHostingEnabled: w.DnsHostingEnabled, - DnsZoneId: w.DnsZoneId, + ZoneId: w.DnsZoneId, Domain: w.Domain, Expired: w.Expired, GatewayDomain: w.GatewayDomain, @@ -87,7 +87,7 @@ func (w *websiteSite) toItem() WebsiteItem { ActiveCid: r.ActiveCid, Created: r.Created, DnsHostingEnabled: r.DnsHostingEnabled, - DnsZoneId: r.DnsZoneId, + ZoneId: r.ZoneId, Domain: r.Domain, Expired: r.Expired, GatewayDomain: r.GatewayDomain, @@ -202,7 +202,7 @@ func (s *Server) domainByID(w *websiteSite, domainIDParam string) (*websiteDomai // GetApiWebsites lists websites for the authenticated user // (GET /api/websites). -func (s *Server) GetApiWebsites(w http.ResponseWriter, r *http.Request) { +func (s *Server) GetApiWebsites(w http.ResponseWriter, r *http.Request, params GetApiWebsitesParams) { if !s.authorized(r) { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "not authenticated"}) return @@ -228,7 +228,7 @@ func (s *Server) PostApiWebsites(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } - if body.Domain == "" { + if body.Domain == nil || *body.Domain == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "domain is required"}) return } @@ -254,7 +254,7 @@ func (s *Server) PostApiWebsites(w http.ResponseWriter, r *http.Request) { ActiveCid: &body.TargetHash, Created: now, DnsHostingEnabled: dnsHosting, - Domain: body.Domain, + Domain: *body.Domain, GatewayDomain: &host, Id: s.websiteSeq, IsSubdomain: false, @@ -266,7 +266,7 @@ func (s *Server) PostApiWebsites(w http.ResponseWriter, r *http.Request) { ValidationToken: "tok-" + strconv.Itoa(s.websiteSeq), Domains: []*websiteDomain{}, } - ws.Domains = append(ws.Domains, s.newDomainLocked(ws, body.Domain, namespace, true)) + ws.Domains = append(ws.Domains, s.newDomainLocked(ws, *body.Domain, namespace, true)) s.websites[ws.Id] = ws resp := ws.toResponse() s.mu.Unlock() @@ -437,6 +437,62 @@ func (s *Server) PostApiWebsitesIdValidate(w http.ResponseWriter, r *http.Reques }) } + +// PostApiWebsitesIdDomainsDomainIdOnchain converts a bound domain to on-chain +// managed (POST /api/websites/{id}/domains/{domain_id}/onchain). Mirrors the +// portal's ConvertToOnChain contract: a non-HNS binding or a name whose NS +// does not point at an external contract is refused with 422 "is not yet +// on-chain managed"; an already on-chain binding is refused with 422 "domain +// is already on-chain managed" (the SDK treats that as idempotent success). +// On success the binding is re-marked onchain_managed, DNS hosting drops to +// false, the delegation bundle is dropped, and the owning website re-arms to +// pending_validation. +func (s *Server) PostApiWebsitesIdDomainsDomainIdOnchain(w http.ResponseWriter, r *http.Request, id string, domainId string) { + if !s.authorized(r) { + writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "not authenticated"}) + return + } + ws, ok := s.websiteByID(id) + if !ok { + writeNotFound(w) + return + } + d, ok := s.domainByID(ws, domainId) + if !ok { + writeNotFound(w) + return + } + + status := "" + if d.Status != nil { + status = *d.Status + } + if status == "onchain_managed" { + apiError(w, http.StatusUnprocessableEntity, "validation_failed", + "domain is already on-chain managed: \""+d.Domain+"\"") + return + } + // The fake's Inspect is deterministic: only HNS bindings are eligible. + if d.Namespace != "hns" { + apiError(w, http.StatusUnprocessableEntity, "validation_failed", + "domain is not yet on-chain managed: \""+d.Domain+"\"; its NS record does not point at an external contract") + return + } + + s.mu.Lock() + now := "pending_validation" + onchain := "onchain_managed" + ws.Status = now + ws.Updated = time.Now().UTC() + d.DnsHostingEnabled = false + d.Delegation = nil + d.GatewayHost = nil + d.ZoneName = nil + d.Status = &onchain + s.mu.Unlock() + writeJSON(w, http.StatusOK, d.toResponse()) +} + // GetApiWebsitesIdDomains lists the domains bound to a website // (GET /api/websites/{id}/domains). func (s *Server) GetApiWebsitesIdDomains(w http.ResponseWriter, r *http.Request, id string) { @@ -471,9 +527,9 @@ func (d *websiteDomain) toResponse() DomainResponse { Domain: d.Domain, GatewayHost: d.GatewayHost, Id: d.Id, - Namespace: d.Namespace, + Namespace: DomainResponseNamespace(d.Namespace), Ssl: ssl, - Status: d.Status, + Status: domainStatusPtr(d.Status), ZoneName: d.ZoneName, } } @@ -486,16 +542,49 @@ func (d *websiteDomain) toRepublishResponse() DomainDANERepublishResponse { Domain: d.Domain, GatewayHost: d.GatewayHost, Id: d.Id, - Namespace: d.Namespace, + Namespace: DomainDANERepublishResponseNamespace(d.Namespace), OwnerName: d.OwnerName, Ssl: d.Ssl, - Status: d.Status, + Status: domainDANERepublishStatusPtr(d.Status), TlsaRdata: &rdata, TlsaRecord: &tlsa, ZoneName: d.ZoneName, } } + +// domainStatusPtr converts the fake's stored *string domain status into the +// typed DomainResponseStatus pointer the wire contract carries now that the +// status is an enum. nil stays nil. +func domainStatusPtr(s *string) *DomainResponseStatus { + if s == nil { + return nil + } + v := DomainResponseStatus(*s) + return &v +} + +// domainDANERepublishStatusPtr is domainStatusPtr for the DANE-republish +// response's enum type. +func domainDANERepublishStatusPtr(s *string) *DomainDANERepublishResponseStatus { + if s == nil { + return nil + } + v := DomainDANERepublishResponseStatus(*s) + return &v +} + +// apiError writes the portal's JSON error envelope ({"error":{"reason", +// "details"}}) with the given status code. +func apiError(w http.ResponseWriter, status int, reason, details string) { + writeJSON(w, status, ErrorResponse{ + Error: ErrorDetail{ + Reason: reason, + Details: &details, + }, + }) +} + // PostApiWebsitesIdDomains binds a domain to a website // (POST /api/websites/{id}/domains). func (s *Server) PostApiWebsitesIdDomains(w http.ResponseWriter, r *http.Request, id string) { diff --git a/internal/mcptest/onchain_e2e_test.go b/internal/mcptest/onchain_e2e_test.go new file mode 100644 index 00000000..88ad9ff5 --- /dev/null +++ b/internal/mcptest/onchain_e2e_test.go @@ -0,0 +1,80 @@ +package mcptest + +import ( + "context" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + + ipfs "go.lumeweb.com/ipfs-sdk" +) + +// TestConvertDomainToOnChainE2E drives the on-chain-managed conversion through +// the real ipfs-sdk client against the fake server: the happy path flips an +// HNS binding to onchain_managed (dns hosting dropped, delegation removed), +// the "already on-chain" 422 is reported as idempotent success returning the +// current state, and a non-HNS binding is refused as ineligible. +func TestConvertDomainToOnChainE2E(t *testing.T) { + srv := New() + ws := srv.IPFS().SeedWebsite("example.test", "QmYwAPJzv5CZsnAzt8auVZRnXbW7Z5k7pZNeRp4cQ3vJdH", "ipfs") + tok := srv.Seed("e2e@example.com", "E2E", "Test") + ts := srv.Start() + defer ts.Close() + + client, err := ipfs.NewClient(ts.URL, tok) + require.NoError(t, err, "construct ipfs-sdk client") + websites := client.Websites() + ctx := context.Background() + websiteID := intToString(ws.Id) + + // Bind an HNS domain and convert it to on-chain managed. + hns, err := websites.BindDomain(ctx, websiteID, ipfs.DomainRequest{ + Domain: "acme", + Namespace: "hns", + }) + require.NoError(t, err, "bind hns domain via sdk against fake") + + converted, err := websites.ConvertDomainToOnChain(ctx, websiteID, intToString(hns.Id)) + require.NoError(t, err, "convert hns domain to on-chain managed via sdk against fake") + require.NotNil(t, converted.Status) + require.Equal(t, ipfs.DomainResponseStatusOnchainManaged, *converted.Status) + require.False(t, converted.DnsHostingEnabled, "on-chain managed bindings drop DNS hosting") + require.Nil(t, converted.Delegation, "on-chain managed bindings carry no delegation bundle") + + // The persisted state (not just the response) reflects the conversion. + listed, err := websites.ListDomains(ctx, websiteID) + require.NoError(t, err) + for _, d := range listed { + if d.Id == hns.Id { + require.NotNil(t, d.Status) + require.Equal(t, ipfs.DomainResponseStatusOnchainManaged, *d.Status) + require.Nil(t, d.Delegation) + } + } + + // Re-convert hits "already on-chain managed" — the SDK reports that as + // idempotent success with the current state. + again, err := websites.ConvertDomainToOnChain(ctx, websiteID, intToString(hns.Id)) + require.NoError(t, err, "converting an already on-chain managed domain is idempotent success") + require.NotNil(t, again.Status) + require.Equal(t, ipfs.DomainResponseStatusOnchainManaged, *again.Status) + + // A non-HNS binding is ineligible: conversion is refused. + icann, err := websites.BindDomain(ctx, websiteID, ipfs.DomainRequest{ + Domain: "other.example", + Namespace: "icann", + }) + require.NoError(t, err, "bind icann domain via sdk against fake") + + // The SDK maps every 422 on this op to one generic refusal message; the + // distinguishing details stay in the response body. + _, err = websites.ConvertDomainToOnChain(ctx, websiteID, intToString(icann.Id)) + require.Error(t, err, "converting a non-HNS domain must be refused") + require.Contains(t, err.Error(), "not eligible for on-chain conversion") +} + +// intToString keeps the call sites readable (path params are strings). +func intToString(i int) string { + return strconv.Itoa(i) +} diff --git a/internal/mcptest/specs/ipfs.yaml b/internal/mcptest/specs/ipfs.yaml index 1255a34a..c370cd97 100644 --- a/internal/mcptest/specs/ipfs.yaml +++ b/internal/mcptest/specs/ipfs.yaml @@ -201,12 +201,23 @@ components: id: type: integer namespace: + enum: + - icann + - hns type: string owner_name: type: string ssl: $ref: '#/components/schemas/SSLStatusInfo' status: + enum: + - draft + - records_generated + - waiting_delegation + - active + - self_hosted + - error + - onchain_managed type: string tlsa_rdata: type: string @@ -237,10 +248,20 @@ components: properties: config: type: object + dns_hosting_enabled: + type: boolean domain: type: string + generate: + type: boolean + label: + type: string namespace: type: string + platform_domain: + type: string + platform_namespace: + type: string required: - domain - namespace @@ -259,10 +280,21 @@ components: id: type: integer namespace: + enum: + - icann + - hns type: string ssl: $ref: '#/components/schemas/SSLStatusInfo' status: + enum: + - draft + - records_generated + - waiting_delegation + - active + - self_hosted + - error + - onchain_managed type: string zone_name: type: string @@ -629,6 +661,66 @@ components: required: - status type: object + PlatformAvailabilityResponse: + additionalProperties: false + properties: + label: + type: string + results: + items: + $ref: '#/components/schemas/PlatformAvailabilityResult' + type: array + required: + - label + - results + type: object + PlatformAvailabilityResult: + additionalProperties: false + properties: + available: + type: boolean + namespace: + type: string + platform_domain: + type: string + required: + - platform_domain + - namespace + - available + type: object + PlatformDomainListResponse: + additionalProperties: false + properties: + data: + items: + $ref: '#/components/schemas/PlatformDomainResponse' + type: array + total: + type: integer + required: + - data + - total + type: object + PlatformDomainResponse: + additionalProperties: false + properties: + domain: + type: string + enabled: + type: boolean + id: + type: integer + namespace: + type: string + zone_id: + type: integer + required: + - id + - domain + - namespace + - zone_id + - enabled + type: object PostUploadResponse: additionalProperties: false properties: @@ -637,9 +729,17 @@ components: required: - CID type: object + RecordDeleteRequest: + additionalProperties: false + properties: + content: + type: string + type: object RecordIdentifier: additionalProperties: false properties: + content: + type: string name: type: string type: @@ -673,6 +773,8 @@ components: type: string disabled: type: boolean + id: + type: string name: type: string ttl: @@ -683,6 +785,7 @@ components: type: integer required: - zone_id + - id - name - type - content @@ -730,6 +833,11 @@ components: format: date-time type: string status: + enum: + - pending + - issuing + - ready + - failed type: string required: - status @@ -800,6 +908,49 @@ components: - message - checked_at type: object + WebsiteChangeEvent: + additionalProperties: false + properties: + cid: + type: string + created_at: + format: date-time + type: string + domain: + type: string + event_type: + enum: + - published + - removed + type: string + id: + type: integer + user_id: + type: integer + website_id: + type: integer + required: + - id + - event_type + - domain + - created_at + type: object + WebsiteChangesResponse: + additionalProperties: false + properties: + events: + items: + $ref: '#/components/schemas/WebsiteChangeEvent' + type: array + high_water_mark: + type: integer + truncated: + type: boolean + required: + - events + - high_water_mark + - truncated + type: object WebsiteConfigResponse: additionalProperties: false properties: @@ -820,8 +971,6 @@ components: type: string dns_hosting_enabled: type: boolean - dns_zone_id: - type: integer domain: type: string expired: @@ -855,6 +1004,8 @@ components: type: string validation_token: type: string + zone_id: + type: integer required: - id - domain @@ -888,14 +1039,21 @@ components: type: boolean domain: type: string + generate: + type: boolean + label: + type: string namespace: type: string + platform_domain: + type: string + platform_namespace: + type: string target_hash: type: string target_type: type: string required: - - domain - target_type - target_hash type: object @@ -909,8 +1067,6 @@ components: type: string dns_hosting_enabled: type: boolean - dns_zone_id: - type: integer domain: type: string expired: @@ -944,6 +1100,8 @@ components: type: string validation_token: type: string + zone_id: + type: integer required: - id - domain @@ -1753,6 +1911,14 @@ paths: required: true schema: type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RecordDeleteRequest' + description: 'Optional content selector: when set, only the record with this + content is deleted; when the body is absent the whole RRSet is deleted, + and a present body with empty content is rejected' responses: "204": description: Record deleted @@ -3613,6 +3779,181 @@ paths: Returns paginated list of website configurations with status, domain, and target information. Supports filtering by status and other criteria. See also:.* + parameters: + - description: Ending index of the items to return (exclusive). Defaults to + 10. + in: query + name: _end + schema: + type: integer + - description: Comma-separated list of sort orders ('asc' or 'desc') corresponding + to _sort fields. Defaults to 'asc'. + in: query + name: _order + schema: + type: string + - description: 'Comma-separated list of fields to sort by. Available fields: + target_type, status' + in: query + name: _sort + schema: + type: string + - description: Starting index of the items to return (0-based). Defaults to + 0. + in: query + name: _start + schema: + type: integer + - description: Filter by domain contains + in: query + name: domain_contains + schema: + type: string + - description: Filter by domain endswith + in: query + name: domain_endswith + schema: + type: string + - description: Filter by domain eq + in: query + name: domain_eq + schema: + type: string + - description: Filter by domain ne + in: query + name: domain_ne + schema: + type: string + - description: Filter by domain startswith + in: query + name: domain_startswith + schema: + type: string + - description: Filter by domain contains + in: query + name: filters[domain][contains] + schema: + type: string + - description: Filter by domain endswith + in: query + name: filters[domain][endswith] + schema: + type: string + - description: Filter by domain eq + in: query + name: filters[domain][eq] + schema: + type: string + - description: Filter by domain ne + in: query + name: filters[domain][ne] + schema: + type: string + - description: Filter by domain startswith + in: query + name: filters[domain][startswith] + schema: + type: string + - description: Filter by status contains + in: query + name: filters[status][contains] + schema: + type: string + - description: Filter by status endswith + in: query + name: filters[status][endswith] + schema: + type: string + - description: Filter by status eq + in: query + name: filters[status][eq] + schema: + type: string + - description: Filter by status ne + in: query + name: filters[status][ne] + schema: + type: string + - description: Filter by status startswith + in: query + name: filters[status][startswith] + schema: + type: string + - description: Filter by target_type contains + in: query + name: filters[target_type][contains] + schema: + type: string + - description: Filter by target_type endswith + in: query + name: filters[target_type][endswith] + schema: + type: string + - description: Filter by target_type eq + in: query + name: filters[target_type][eq] + schema: + type: string + - description: Filter by target_type ne + in: query + name: filters[target_type][ne] + schema: + type: string + - description: Filter by target_type startswith + in: query + name: filters[target_type][startswith] + schema: + type: string + - description: Filter by status contains + in: query + name: status_contains + schema: + type: string + - description: Filter by status endswith + in: query + name: status_endswith + schema: + type: string + - description: Filter by status eq + in: query + name: status_eq + schema: + type: string + - description: Filter by status ne + in: query + name: status_ne + schema: + type: string + - description: Filter by status startswith + in: query + name: status_startswith + schema: + type: string + - description: Filter by target_type contains + in: query + name: target_type_contains + schema: + type: string + - description: Filter by target_type endswith + in: query + name: target_type_endswith + schema: + type: string + - description: Filter by target_type eq + in: query + name: target_type_eq + schema: + type: string + - description: Filter by target_type ne + in: query + name: target_type_ne + schema: + type: string + - description: Filter by target_type startswith + in: query + name: target_type_startswith + schema: + type: string responses: "200": content: @@ -3620,6 +3961,11 @@ paths: schema: $ref: '#/components/schemas/WebsiteItemResponse' description: List of websites + headers: + X-Total-Count: + description: Total number of items + schema: + type: string "400": content: application/json: @@ -3944,7 +4290,7 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - description: Bad request + description: Invalid website ID or request body "401": content: application/json: @@ -3962,19 +4308,20 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - description: Not found + description: Website not found "422": content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - description: Validation failed + description: Invalid target (CID/IPNS) or DNS hosting value; target CID + not pinned (CID_NOT_PINNED) or IPNS key not found (IPNS_KEY_NOT_FOUND) "500": content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - description: Internal server error + description: Internal server error occurred summary: Update website tags: - Websites @@ -4362,6 +4709,72 @@ paths: tags: - Websites - Domains + /api/websites/{id}/domains/{domain_id}/onchain: + post: + description: Reclassifies a bound domain as on-chain managed (e.g. a Handshake + HIP-5 name whose NS now points at an external contract). Deletes the portal-managed + PowerDNS zone and DNSSEC for the binding and switches ownership verification + to the TXT token; DANE/SSL state is retained. One-way. + parameters: + - description: Domain ID + in: path + name: domain_id + required: true + schema: + type: string + - description: Website ID + in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DomainResponse' + description: Converted domain + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Bad request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Forbidden + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Not found + "422": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Validation failed + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Internal server error + summary: Reclassify domain as on-chain managed + tags: + - Websites + - Domains /api/websites/{id}/domains/{domain_id}/verify: post: description: Triggers verification of domain delegation. @@ -4454,7 +4867,7 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - description: Bad request + description: Invalid website ID format "401": content: application/json: @@ -4472,19 +4885,20 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - description: Not found + description: Website not found "422": content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - description: Validation failed + description: 'DNS validation failed: verification TXT record not published + or domain does not resolve (DNS_VALIDATION_FAILED)' "500": content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - description: Internal server error + description: Internal server error occurred summary: Validate website DNS tags: - Websites @@ -4542,6 +4956,116 @@ paths: summary: Get website hosting configuration tags: - Websites + /api/websites/platform-domains: + get: + description: Lists the platform-owned root domains that are enabled and available + for users to claim free subdomains under. Returns the standard paginated list + shape ({data, total}), ordered by domain. Only enabled roots are returned. + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformDomainListResponse' + description: List of supported platform domains + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Bad request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Forbidden + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Not found + "422": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Validation failed + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Internal server error + summary: List supported platform domains + tags: + - Websites + - PlatformDomains + /api/websites/platform-domains/availability: + get: + description: Checks, for a candidate label, whether it is claimable on each + enabled platform (free-subdomain) root. Returns one result per root. Auth-required; + only probes platform-owned roots, never user-managed zones. + parameters: + - description: Candidate subdomain label + in: query + name: label + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformAvailabilityResponse' + description: Availability by root + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Bad request + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Forbidden + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Not found + "422": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Validation failed + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Internal server error + summary: Check platform subdomain availability + tags: + - Websites + - PlatformDomains /internal/dns/cert: post: description: Internal endpoint for Caddy to push a cert and receive computed @@ -4972,11 +5496,73 @@ paths: summary: Get website status for gateway tags: - Gateway + /internal/websites/changes: + get: + description: |- + Returns durable website lifecycle changes (published, removed) after the + supplied cursor, in ascending durable-ID order, together with the current + replay high-water mark. The gateway calls this after an SSE reconnect to + discover domains it missed during the gap without relying on visitor traffic. + Requires X-Gateway-Secret header for authentication. + parameters: + - description: Durable event cursor to resume after. Returns all events with + id > after. Omit to start from the beginning of the retained window. + in: query + name: after + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/WebsiteChangesResponse' + description: Reconciled changes + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Invalid after cursor + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Forbidden + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Not found + "422": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Validation failed + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Internal server error occurred + summary: Reconcile website changes + tags: + - Gateway /internal/websites/events: get: description: |- Server-Sent Events endpoint for gateway-to-portal communication. Streams website lifecycle events (published, removed) in real time. + Supports Last-Event-ID replay of durable events after a reconnect. Requires X-Gateway-Secret header for authentication. responses: "200":