From 3eeb363971a7a097b47b0d41f0dfc4af2b88cfb5 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Fri, 4 Sep 2026 13:48:36 +0000 Subject: [PATCH 1/2] feat(websites): support on-chain managed HNS domains end to end Brings the CLI and MCP surfaces up to the backend's on-chain managed HNS support: new websites_domains_convert_onchain operation, onchain_managed aware status handling, delegation rendering that explains contract-served DNS, agent-guide and prompt guidance, and a refreshed test double (spec resynced from ipfs-sdk v0.1.95, onchain endpoint implemented, E2E test). Bumps ipfs-sdk to v0.1.95 and portal-sdk to v0.1.72. --- go.mod | 4 +- go.sum | 8 +- internal/catalogops/admin_platform_domains.go | 14 +- internal/catalogops/websites.go | 1 + .../websites_convert_onchain_test.go | 119 ++ internal/catalogops/websites_domains.go | 46 + internal/catalogops/websites_domains_test.go | 8 +- internal/cli/catalog_websites_wiring.go | 16 +- internal/cli/websites_domains.go | 13 +- .../cli/websites_domains_delegation_common.go | 15 + .../websites_domains_delegation_generic.go | 7 + .../cli/websites_domains_delegation_hns.go | 25 +- .../cli/websites_domains_delegation_icann.go | 8 + internal/cli/websites_domains_test.go | 95 +- internal/cli/websites_domains_wizard.go | 4 +- internal/cli/websites_domains_wizard_pterm.go | 20 +- internal/cli/websites_domains_wizard_test.go | 74 +- internal/cli/websites_handler_test.go | 4 + .../cli/websites_required_records_test.go | 11 +- internal/cli/websites_ssl.go | 4 +- internal/cli/websites_test.go | 25 +- internal/core/websites/service.go | 23 + internal/mcp/agent_guide.go | 2 +- internal/mcp/agent_guide_test.go | 120 +- internal/mcp/grok_upload_chooser_test.go | 10 +- internal/mcp/guide_fragments.go | 17 + .../prompttemplates/website_onboarding.tmpl | 8 +- internal/mcp/resources.go | 13 + internal/mcp/wizard/wizard_test.go | 13 +- internal/mcptest/ipfs/server.gen.go | 1125 +++++++++++++++-- internal/mcptest/ipfs/serverStub.gen.go | 18 +- internal/mcptest/ipfs/websites.go | 109 +- internal/mcptest/onchain_e2e_test.go | 80 ++ internal/mcptest/specs/ipfs.yaml | 612 ++++++++- 34 files changed, 2350 insertions(+), 321 deletions(-) create mode 100644 internal/catalogops/websites_convert_onchain_test.go create mode 100644 internal/mcptest/onchain_e2e_test.go 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/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": From 6bb3e35ea862800ac648be033f607447ec13e801 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Fri, 4 Sep 2026 14:06:37 +0000 Subject: [PATCH 2/2] fix(transfer): wait for async upload executor before asserting in tests The HTTP upload tests asserted the executor's atomic.Value result immediately after the PUT returned 202 Accepted, but the upload task runs on its own goroutine. Under runner scheduling pressure the assert could win the race and panic on a nil Load. Wait with require.Eventually instead of type-asserting blind. --- .../mcp/core/transfer/upload_http_test.go | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) 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") }