carlos / platform Public

console: domains — the product surface for what routes always were

Claim writes apps/<app>/account.json (account = the claiming address;
truth originates in the console and travels through the bucket), the app
page backfills a missing manifest on the owner's first view and never
clobbers an existing one, and the routes table becomes the Domains table:
Domain/Type/DNS/TLS, em-dash unknowns, and a point-your-DNS-here guidance
row only under custom domains that aren't pointing yet. PLATFORM_EDGE_IPS
(the agent's own variable) feeds the guidance addresses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell b63baaefbd43ac25630ccb877e6f96d93b7b1d44 parent 32189f0
6 files changed, +306 −22
  • cmd/console/main.go +16 −0
  • internal/console/server.go +72 −2
  • internal/console/server_test.go +149 −1
  • internal/console/templates/app.html +12 −11
  • internal/console/view.go +52 −3
  • internal/console/view_test.go +5 −5
diff --git a/cmd/console/main.go b/cmd/console/main.go
index 76583271b..0f4f8e077 100644
--- a/cmd/console/main.go
+++ b/cmd/console/main.go
@@ -126,6 +126,7 @@ func serve() {
Releases: openStore(),
ReleasePub: pub,
Key: key,
+ EdgeIPs: edgeIPs(),
}, st)
if err != nil {
log.Fatal(err)
@@ -160,6 +161,21 @@ func openStore() release.Store {
return nil
}
+// edgeIPs parses PLATFORM_EDGE_IPS (comma-separated) — the SAME variable the
+// agent's domains reconcile loop reads, so the console's point-your-DNS-here
+// guidance and the agent's DNS health checks can never name different
+// addresses. Unset is legal: guidance falls back to naming "this
+// deployment's edge IP" in prose.
+func edgeIPs() []string {
+ var out []string
+ for _, ip := range strings.Split(os.Getenv("PLATFORM_EDGE_IPS"), ",") {
+ if ip = strings.TrimSpace(ip); ip != "" {
+ out = append(out, ip)
+ }
+ }
+ return out
+}
+
func envOr(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
diff --git a/internal/console/server.go b/internal/console/server.go
index d63ff66d6..0a09465e6 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -10,6 +10,7 @@ import (
"crypto/ed25519"
"crypto/subtle"
"embed"
+ "encoding/json"
"errors"
"fmt"
"html/template"
@@ -95,6 +96,11 @@ type Config struct {
// AppScheme is the scheme apps' public hosts are reached on — "https"
// (the default) everywhere but tests.
AppScheme string
+ // EdgeIPs is this deployment's edge address list (PLATFORM_EDGE_IPS,
+ // read at console start) — surfaced in the Domains section's
+ // point-your-DNS-here guidance. nil is legal: guidance then says
+ // "this deployment's edge IP" instead of inventing an address.
+ EdgeIPs []string
}
// Server is the console's http.Handler.
@@ -499,7 +505,7 @@ func (s *Server) handleApps(w http.ResponseWriter, r *http.Request) {
return
}
if len(apps) > 0 {
- views, err := Assemble(s.cfg.Registry, s.cfg.Releases, s.cfg.ReleasePub, apps)
+ views, err := Assemble(s.cfg.Registry, s.cfg.Releases, s.cfg.ReleasePub, apps, s.cfg.EdgeIPs)
if err != nil {
log.Printf("console: assemble: %v", err)
s.renderMessage(w, r, http.StatusInternalServerError, "Error", "Could not read platform state. Please try again.")
@@ -541,7 +547,11 @@ func (s *Server) handleApp(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
- views, err := Assemble(s.cfg.Registry, s.cfg.Releases, s.cfg.ReleasePub, []string{app})
+ // Backfill the account manifest for apps claimed before the domains
+ // feature existed (or CLI-granted ones): best-effort, on the OWNER's own
+ // first view — ownedApp above already proved the viewer owns this app.
+ s.ensureAccountManifest(r.Context(), app, addr)
+ views, err := Assemble(s.cfg.Registry, s.cfg.Releases, s.cfg.ReleasePub, []string{app}, s.cfg.EdgeIPs)
if err != nil || len(views) != 1 {
log.Printf("console: assemble %s: %v", app, err)
s.renderMessage(w, r, http.StatusInternalServerError, "Error", "Could not read platform state. Please try again.")
@@ -575,6 +585,59 @@ func (s *Server) audit(app, addr, action, detail string) {
}
}
+// writeAccountManifest lands apps/<app>/account.json in the deployment
+// bucket: {"account": <addr>, "owner": <addr>} — the account name IS the
+// claiming address for now (one account per user), and the agent's domains
+// reconcile pass materializes it into the registry from here. Overwrite-safe
+// put (status.PublishFleet's read-etag-then-Put idiom): the store's Put is
+// CAS-only, and a backfill racing another of the owner's own tabs must
+// retry, not fail.
+func (s *Server) writeAccountManifest(ctx context.Context, app, addr string) error {
+ key := "apps/" + app + "/account.json"
+ body, err := json.Marshal(map[string]string{"account": addr, "owner": addr})
+ if err != nil {
+ return err
+ }
+ put := func() error {
+ etag := ""
+ if _, cur, err := s.cfg.Releases.Get(ctx, key); err == nil {
+ etag = cur
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ _, err := s.cfg.Releases.Put(ctx, key, body, etag)
+ return err
+ }
+ if err := put(); errors.Is(err, release.ErrCASConflict) {
+ return put() // one retry on a lost race; a second loss surfaces
+ } else if err != nil {
+ return err
+ }
+ return nil
+}
+
+// ensureAccountManifest is the view-time backfill: write the manifest only
+// when none exists. An existing manifest is account truth someone already
+// wrote (possibly by hand, possibly naming a different account) — a page
+// view must never clobber it. Best-effort: failures are logged, never fail
+// the page.
+func (s *Server) ensureAccountManifest(ctx context.Context, app, addr string) {
+ key := "apps/" + app + "/account.json"
+ _, _, err := s.cfg.Releases.Get(ctx, key)
+ if err == nil {
+ return // already there — nothing to backfill
+ }
+ if !errors.Is(err, os.ErrNotExist) {
+ log.Printf("console: account manifest %s: %v", key, err)
+ return
+ }
+ if err := s.writeAccountManifest(ctx, app, addr); err != nil {
+ log.Printf("console: backfill account manifest %s: %v", key, err)
+ return
+ }
+ log.Printf("console: backfilled account manifest for %s (owner %s)", app, addr)
+}
+
// handleNewApp claims an app name — once, ever. A name is refused when the
// apps table has it, when ANY owner exists for it (CLI-granted apps stay
// claim-proof), or when the release store already holds anything under
@@ -634,6 +697,13 @@ func (s *Server) handleNewApp(w http.ResponseWriter, r *http.Request) {
fail("Created the app, but granting you ownership failed — ask an operator.")
return
}
+ // The account manifest: claim is where account truth originates, and it
+ // travels through the bucket to the agent's reconcile pass. Best-effort —
+ // the claim already succeeded, and the app-page backfill is the safety
+ // net if this write loses to a network hiccup.
+ if err := s.writeAccountManifest(ctx, name, addr); err != nil {
+ log.Printf("console: account manifest for %s: %v", name, err)
+ }
s.audit(name, addr, "create", "created app "+name)
s.setFlash(w, "ok", "Created app "+name+" — it's yours.")
http.Redirect(w, r, "/apps/"+url.PathEscape(name), http.StatusSeeOther)
diff --git a/internal/console/server_test.go b/internal/console/server_test.go
index c0cecb579..7a7aa6a06 100644
--- a/internal/console/server_test.go
+++ b/internal/console/server_test.go
@@ -3,6 +3,7 @@ package console
import (
"context"
"crypto/ed25519"
+ "encoding/json"
"errors"
"io"
"net/http"
@@ -233,7 +234,7 @@ func TestConsoleE2E(t *testing.T) {
t.Fatalf("/apps/kass = %d", status)
}
for _, want := range []string{
- "kass.carlosapp.dev", "docs.kass.carlosapp.dev", "instance", "static",
+ "Domains", "kass.carlosapp.dev", "docs.kass.carlosapp.dev", "custom",
"ready", "v1", "stable", "canary/x", "v2",
"bakes until ~", "<td>live</td>", "approximation",
} {
@@ -829,6 +830,10 @@ func TestPromotionsDisabledWithoutKey(t *testing.T) {
if !strings.Contains(body, "Promotions disabled: no signing key on this host.") {
t.Fatal("keyless console is missing the disabled note")
}
+ // The Domains section is a read — keyless (read-only) mode still renders it.
+ if !strings.Contains(body, "Domains") || !strings.Contains(body, "kass.carlosapp.dev") {
+ t.Fatal("keyless console is missing the Domains section")
+ }
if strings.Contains(body, `action="/apps/kass/promote"`) || strings.Contains(body, `action="/apps/kass/rollback"`) ||
strings.Contains(body, `action="/apps/kass/canary"`) || strings.Contains(body, `action="/apps/kass/canary/retire"`) {
t.Fatal("keyless console rendered mutation forms")
@@ -857,6 +862,149 @@ func TestPromotionsDisabledWithoutKey(t *testing.T) {
}
}
+// ---- domains ----------------------------------------------------------------
+
+// TestClaimWritesAccountManifest: claiming an app writes the account
+// manifest into the deployment bucket — the console originates account
+// truth, and it travels through the bucket (never a console→registry write).
+func TestClaimWritesAccountManifest(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+ csrf := sessionToken(t, fx, c)
+
+ if status, loc, _ := postForm(t, c, fx.ts.URL+"/apps/new", url.Values{"csrf": {csrf}, "name": {"zephyr"}}); status != http.StatusSeeOther || loc != "/apps/zephyr" {
+ t.Fatalf("claim = %d -> %q", status, loc)
+ }
+ body, _, err := fx.rel.Get(context.Background(), "apps/zephyr/account.json")
+ if err != nil {
+ t.Fatalf("claim wrote no account manifest: %v", err)
+ }
+ var m struct{ Account, Owner string }
+ if err := json.Unmarshal(body, &m); err != nil {
+ t.Fatalf("manifest not JSON: %v (%q)", err, body)
+ }
+ if m.Account != "paul@keymail.dev" || m.Owner != "paul@keymail.dev" {
+ t.Errorf("manifest = %+v, want the claiming address as account and owner", m)
+ }
+}
+
+// TestAppPageBackfillsAccountManifest: an already-claimed app with no
+// manifest (claimed before this feature shipped, or CLI-granted) gets one
+// written on the owner's first page view — and an existing manifest is
+// never overwritten by a view.
+func TestAppPageBackfillsAccountManifest(t *testing.T) {
+ fx := newConsoleFixture(t)
+ c := signInPaul(t, fx)
+
+ if _, _, err := fx.rel.Get(context.Background(), "apps/kass/account.json"); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("fixture already has a manifest: %v", err)
+ }
+ if status, _, _ := get(t, c, fx.ts.URL+"/apps/kass"); status != 200 {
+ t.Fatalf("/apps/kass = %d", status)
+ }
+ body, _, err := fx.rel.Get(context.Background(), "apps/kass/account.json")
+ if err != nil {
+ t.Fatalf("view did not backfill the manifest: %v", err)
+ }
+ if !strings.Contains(string(body), "paul@keymail.dev") {
+ t.Errorf("backfilled manifest = %q", body)
+ }
+
+ // A pre-existing manifest is account truth someone already wrote —
+ // a page view must not clobber it.
+ custom := []byte(`{"account":"someteam","owner":"paul@keymail.dev"}`)
+ if _, err := fx.rel.Put(context.Background(), "apps/zephyr/account.json", custom, ""); err != nil {
+ t.Fatal(err)
+ }
+ if err := fx.st.Grant("paul@keymail.dev", "zephyr"); err != nil {
+ t.Fatal(err)
+ }
+ if status, _, _ := get(t, c, fx.ts.URL+"/apps/zephyr"); status != 200 {
+ t.Fatalf("/apps/zephyr = %d", status)
+ }
+ body, _, err = fx.rel.Get(context.Background(), "apps/zephyr/account.json")
+ if err != nil || string(body) != string(custom) {
+ t.Errorf("view clobbered an existing manifest: %q %v", body, err)
+ }
+}
+
+// TestDomainViewStates is the pure Domains-row builder over route fixtures:
+// type from the platform flag, unknown DNS as "" (rendered em-dash),
+// guidance ONLY for non-pointing custom domains, canary labeling preserved.
+func TestDomainViewStates(t *testing.T) {
+ edge := []string{"203.0.113.7", "198.51.100.9"}
+ renew := time.Date(2026, 10, 12, 3, 0, 0, 0, time.UTC).Unix()
+ cases := []struct {
+ name string
+ rt registry.Route
+ ips []string
+ want DomainView
+ }{
+ {"platform pointing ready with cert",
+ registry.Route{Host: "kass.bab.oncarlos.com", Platform: true, Status: "ready",
+ DNSStatus: "pointing", CertExpiresAt: renew},
+ edge,
+ DomainView{Domain: "kass.bab.oncarlos.com", Type: "platform", DNS: "pointing",
+ TLS: "ready · renews 2026-10-12"}},
+ {"custom elsewhere gets guidance",
+ registry.Route{Host: "kass.training", Status: "ready", DNSStatus: "elsewhere"},
+ edge,
+ DomainView{Domain: "kass.training", Type: "custom", DNS: "elsewhere", TLS: "ready",
+ Guidance: "Point an A record for kass.training at 203.0.113.7, 198.51.100.9. Checks run every few minutes."}},
+ {"custom unknown: em-dash DNS, still guided",
+ registry.Route{Host: "kass.training", Status: "issuing"},
+ edge,
+ DomainView{Domain: "kass.training", Type: "custom", DNS: "", TLS: "issuing",
+ Guidance: "Point an A record for kass.training at 203.0.113.7, 198.51.100.9. Checks run every few minutes."}},
+ {"custom pointing: no guidance",
+ registry.Route{Host: "kass.training", Status: "ready", DNSStatus: "pointing"},
+ edge,
+ DomainView{Domain: "kass.training", Type: "custom", DNS: "pointing", TLS: "ready"}},
+ {"platform elsewhere: never guided (we made this domain; guidance is for THEIR dns)",
+ registry.Route{Host: "kass.bab.oncarlos.com", Platform: true, Status: "ready", DNSStatus: "elsewhere"},
+ edge,
+ DomainView{Domain: "kass.bab.oncarlos.com", Type: "platform", DNS: "elsewhere", TLS: "ready"}},
+ {"canary row keeps its label",
+ registry.Route{Host: "rc.kass.bab.oncarlos.com", Platform: true, Status: "ready",
+ DNSStatus: "pointing", Channel: "canary/rc", Canary: true},
+ edge,
+ DomainView{Domain: "rc.kass.bab.oncarlos.com", Type: "platform · canary/rc", DNS: "pointing", TLS: "ready"}},
+ {"no known edge IPs: guidance still helps, without inventing an address",
+ registry.Route{Host: "kass.training", Status: "ready", DNSStatus: "elsewhere"},
+ nil,
+ DomainView{Domain: "kass.training", Type: "custom", DNS: "elsewhere", TLS: "ready",
+ Guidance: "Point an A record for kass.training at this deployment's edge IP. Checks run every few minutes."}},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ if got := domainView(c.rt, c.ips); got != c.want {
+ t.Errorf("domainView:\n got %+v\n want %+v", got, c.want)
+ }
+ })
+ }
+}
+
+// TestAppPageRendersDomains: the app page's Domains table renders from
+// registry state — platform + custom rows, em-dash unknowns, and the
+// guidance row beneath a non-pointing custom domain.
+func TestAppPageRendersDomains(t *testing.T) {
+ fx := newConsoleFixture(t)
+ fx.addRoute(t, registry.Route{Host: "kass.bab.carlosapp.dev", Kind: "instance",
+ Status: "ready", App: "kass", Platform: true, DNSStatus: "pointing", Account: "paul@keymail.dev"})
+ c := signInPaul(t, fx)
+ _, _, body := get(t, c, fx.ts.URL+"/apps/kass")
+ for _, want := range []string{
+ "Domains", "kass.bab.carlosapp.dev", "platform", "pointing",
+ "kass.carlosapp.dev", "custom", "—",
+ "Point an A record for kass.carlosapp.dev at this deployment&#39;s edge IP.",
+ "Checks run every few minutes.",
+ } {
+ if !strings.Contains(body, want) {
+ t.Fatalf("/apps/kass missing %q in:\n%s", want, body)
+ }
+ }
+}
+
// seedFleetJSON writes a small status/fleet.json fixture through the real
// production path (one box snapshot, then BuildFleet + PublishFleet reading
// the fixture registry's routes) rather than hand-rolling JSON — so the test
diff --git a/internal/console/templates/app.html b/internal/console/templates/app.html
index c821cedd2..deef1e511 100644
--- a/internal/console/templates/app.html
+++ b/internal/console/templates/app.html
@@ -3,24 +3,25 @@
{{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}}
{{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}}
-<h2>Routes</h2>
-{{if .Detail.Routes}}
+<h2>Domains</h2>
+{{if .Detail.Domains}}
<div class="tablewrap">
<table>
- <tr><th>Host</th><th>Kind</th><th>Status</th><th>Version</th><th>Channel</th><th>Hibernate</th></tr>
- {{range .Detail.Routes}}
+ <tr><th>Domain</th><th>Type</th><th>DNS</th><th>TLS</th></tr>
+ {{range .Detail.Domains}}
<tr>
- <td><code>{{.Host}}</code></td>
- <td>{{.Kind}}</td>
- <td>{{.Status}}</td>
- <td>{{if .Version}}<code>{{.Version}}</code>{{else}}—{{end}}</td>
- <td>{{.Channel}}</td>
- <td>{{if .Hibernate}}yes{{else}}—{{end}}</td>
+ <td><code>{{.Domain}}</code></td>
+ <td>{{.Type}}</td>
+ <td>{{if .DNS}}{{.DNS}}{{else}}<span title="unknown — not checked yet">—</span>{{end}}</td>
+ <td>{{if .TLS}}{{.TLS}}{{else}}<span title="unknown">—</span>{{end}}</td>
</tr>
+ {{if .Guidance}}
+ <tr><td colspan="4" class="small">{{.Guidance}}</td></tr>
+ {{end}}
{{end}}
</table>
</div>
-{{else}}<p class="small">No routes yet.</p>{{end}}
+{{else}}<p class="small">No domains yet — the first shipped route brings one.</p>{{end}}
{{if and .Instances .Instances.Reporting}}
<h2>Instances</h2>
diff --git a/internal/console/view.go b/internal/console/view.go
index fa1ab7b73..2ca4e5bf8 100644
--- a/internal/console/view.go
+++ b/internal/console/view.go
@@ -24,10 +24,55 @@ import (
type AppView struct {
App string
Routes []RouteView
+ Domains []DomainView
Channels []ChannelView
Canaries []CanaryView
}
+// DomainView is one row of the app page's Domains table — the product
+// surface for what routes always were. Unknowns stay empty strings here and
+// render as the em-dash convention; Guidance is non-empty ONLY for a custom
+// domain that isn't pointing at this deployment yet.
+type DomainView struct {
+ Domain, Type, DNS, TLS string
+ Guidance string
+}
+
+// domainView folds one registry route into its Domains row. edgeIPs is the
+// console's PLATFORM_EDGE_IPS (nil = unknown; guidance then names "this
+// deployment's edge IP" rather than inventing an address).
+func domainView(rt registry.Route, edgeIPs []string) DomainView {
+ typ := "custom"
+ if rt.Platform {
+ typ = "platform"
+ }
+ // Canary labeling is preserved: a canary follower's row says so, right
+ // in its type cell, whichever type it is.
+ if strings.HasPrefix(rt.Channel, "canary/") {
+ typ += " · " + rt.Channel
+ }
+ // TLS: the route's status is the issuance signal (issuing/ready/failed);
+ // a known cert expiry adds the renew-by date.
+ tls := rt.Status
+ if tls == "" {
+ tls = "ready" // registry contract: empty status means ready
+ }
+ if rt.CertExpiresAt > 0 {
+ tls += " · renews " + time.Unix(rt.CertExpiresAt, 0).UTC().Format("2006-01-02")
+ }
+ dv := DomainView{Domain: rt.Host, Type: typ, DNS: rt.DNSStatus, TLS: tls}
+ // Guidance only for custom + non-pointing: platform domains are ours to
+ // fix, and a pointing custom domain needs no pointing instructions.
+ if !rt.Platform && rt.DNSStatus != "pointing" {
+ target := "this deployment's edge IP"
+ if len(edgeIPs) > 0 {
+ target = strings.Join(edgeIPs, ", ")
+ }
+ dv.Guidance = "Point an A record for " + rt.Host + " at " + target + ". Checks run every few minutes."
+ }
+ return dv
+}
+
// CanaryView is one canary on the app page: a canary/<name> channel plus
// the hosts (routes) currently following it. A canary with no hosts is a
// pointer waiting for a route — real, serving nothing yet.
@@ -96,9 +141,10 @@ func openRegistry(path string) (*registry.Registry, error) {
// Assemble builds the view for the given (owned) apps: routes from the
// registry at regPath, channels from the release store s verified against
-// pub. Every app in apps gets an AppView, even an empty one; per-channel
-// failures degrade to ChannelView.Err.
-func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, apps []string) ([]AppView, error) {
+// pub, Domains rows with guidance targeting edgeIPs (nil = unknown). Every
+// app in apps gets an AppView, even an empty one; per-channel failures
+// degrade to ChannelView.Err.
+func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, apps []string, edgeIPs []string) ([]AppView, error) {
ctx := context.Background()
reg, err := openRegistry(regPath)
if err != nil {
@@ -109,6 +155,7 @@ func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, apps []str
return nil, err
}
byApp := map[string][]RouteView{}
+ domainsByApp := map[string][]DomainView{}
for _, rt := range routes {
ch := rt.Channel
if ch == "" {
@@ -118,6 +165,7 @@ func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, apps []str
Host: rt.Host, Kind: rt.Kind, Status: rt.Status,
Version: rt.Version, Channel: ch, Hibernate: rt.Hibernate,
})
+ domainsByApp[rt.App] = append(domainsByApp[rt.App], domainView(rt, edgeIPs))
}
now := time.Now()
sorted := append([]string(nil), apps...)
@@ -133,6 +181,7 @@ func Assemble(regPath string, s release.Store, pub ed25519.PublicKey, apps []str
out = append(out, AppView{
App: app,
Routes: byApp[app], // List orders by host already
+ Domains: domainsByApp[app],
Channels: channels,
Canaries: assembleCanaries(channels, byApp[app]),
})
diff --git a/internal/console/view_test.go b/internal/console/view_test.go
index 66d3f00b5..17ec58918 100644
--- a/internal/console/view_test.go
+++ b/internal/console/view_test.go
@@ -108,7 +108,7 @@ func findChannel(t *testing.T, chans []ChannelView, name string) ChannelView {
func TestAssemble(t *testing.T) {
regPath, store, _, pub := buildFixtures(t)
- views, err := Assemble(regPath, store, pub, []string{"kass"})
+ views, err := Assemble(regPath, store, pub, []string{"kass"}, nil)
if err != nil {
t.Fatal(err)
}
@@ -164,7 +164,7 @@ func TestAssembleCanaries(t *testing.T) {
Status: "ready", App: "kass", Channel: "canary/x", Canary: true}); err != nil {
t.Fatal(err)
}
- views, err := Assemble(regPath, store, pub, []string{"kass"})
+ views, err := Assemble(regPath, store, pub, []string{"kass"}, nil)
if err != nil {
t.Fatal(err)
}
@@ -186,7 +186,7 @@ func TestAssembleCanaries(t *testing.T) {
func TestAssembleFiltersToGivenApps(t *testing.T) {
regPath, store, _, pub := buildFixtures(t)
- views, err := Assemble(regPath, store, pub, []string{"kass"})
+ views, err := Assemble(regPath, store, pub, []string{"kass"}, nil)
if err != nil {
t.Fatal(err)
}
@@ -204,7 +204,7 @@ func TestAssembleFiltersToGivenApps(t *testing.T) {
func TestAssembleEmptyAppStillListed(t *testing.T) {
regPath, store, _, pub := buildFixtures(t)
- views, err := Assemble(regPath, store, pub, []string{"ghost", "kass"})
+ views, err := Assemble(regPath, store, pub, []string{"ghost", "kass"}, nil)
if err != nil {
t.Fatal(err)
}
@@ -222,7 +222,7 @@ func TestAssembleDegradesBadChannel(t *testing.T) {
if _, err := store.Put(context.Background(), "apps/kass/channels/beta.json", []byte("not a signed pointer"), ""); err != nil {
t.Fatal(err)
}
- views, err := Assemble(regPath, store, pub, []string{"kass"})
+ views, err := Assemble(regPath, store, pub, []string{"kass"}, nil)
if err != nil {
t.Fatalf("Assemble must not fail on a bad channel: %v", err)
}