From c5cf60464f5e0146f5a5368a48f739e31290a6ac Mon Sep 17 00:00:00 2001 From: Vishal T <54330666+vish727@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:32:49 -0400 Subject: [PATCH] feat: source field decides who may read a service nook's code A static nook's files reach every viewer's browser, so they are readable by anyone who can open it. A service nook's run code never reaches a browser, and should not become readable just because someone was shared the nook. source is "editors" (owner and editors) by default, or "viewers" to opt in to the static behaviour. Parse writes the default out explicitly, so a pull or an export of a service nook states who can read it rather than leaving it implied. nookd enforces this on /_nook/source, pull, and remix. --- manifest/manifest.go | 20 +++++++++++++++++++- manifest/manifest_test.go | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/manifest/manifest.go b/manifest/manifest.go index 4c7921f..f352e06 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -26,6 +26,7 @@ var ( hostRe = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$`) reserved = map[string]bool{"api": true, "www": true, "app": true, "admin": true, "static": true, "mail": true, "dashboard": true, "nook": true} types = map[string]bool{"static": true, "service": true} + sources = map[string]bool{"editors": true, "viewers": true} ) // A CronEntry runs `Run` inside the machine on `Schedule`, five-field cron in UTC. @@ -40,6 +41,9 @@ type Manifest struct { Entry string `json:"entry,omitempty"` // Service nooks only. See DESIGN.md ยง10b. + // Source is who may read the run code: "editors" (owner and editors, the default) or + // "viewers" (anyone who can open the nook, as a static nook's files always are). + Source string `json:"source,omitempty"` Run string `json:"run,omitempty"` Port int `json:"port,omitempty"` Secrets []string `json:"secrets,omitempty"` @@ -60,6 +64,10 @@ func Parse(b []byte) (*Manifest, error) { if m.Type == "static" && m.Entry == "" { m.Entry = "index.html" } + // Written out explicitly, so a pull or an export of a service nook states who can read it. + if m.Type == "service" && m.Source == "" { + m.Source = "editors" + } if err := m.Validate(); err != nil { return nil, err } @@ -85,7 +93,7 @@ func (m *Manifest) Validate() error { for _, f := range []struct { name string set bool - }{{"run", m.Run != ""}, {"port", m.Port != 0}, {"secrets", len(m.Secrets) > 0}, {"egress", len(m.Egress) > 0}, {"cron", len(m.Cron) > 0}} { + }{{"source", m.Source != ""}, {"run", m.Run != ""}, {"port", m.Port != 0}, {"secrets", len(m.Secrets) > 0}, {"egress", len(m.Egress) > 0}, {"cron", len(m.Cron) > 0}} { if f.set { return fmt.Errorf(`nook.json: %s only applies to a service nook; set "type": "service" or remove it`, f.name) } @@ -97,6 +105,9 @@ func (m *Manifest) validateService() error { if m.Entry != "" { return fmt.Errorf(`nook.json: entry is for static nooks; a service nook serves its own routes from run`) } + if m.Source != "" && !sources[m.Source] { + return fmt.Errorf(`nook.json: source %q must be "editors" (the default) or "viewers"`, m.Source) + } if err := command("run", m.Run); err != nil { return err } @@ -180,3 +191,10 @@ func command(field, s string) error { } return nil } + +// SourceVisibleToViewers says whether anyone who can open the nook may read its code. A static +// nook's files reach every viewer's browser anyway, so they always are; a service nook's do not, +// and stay with the owner and editors unless the manifest opts in. +func (m *Manifest) SourceVisibleToViewers() bool { + return !m.IsService() || m.Source == "viewers" +} diff --git a/manifest/manifest_test.go b/manifest/manifest_test.go index 673a8ee..1ee7004 100644 --- a/manifest/manifest_test.go +++ b/manifest/manifest_test.go @@ -21,6 +21,7 @@ func TestParseStatic(t *testing.T) { `{"name":"ok","secrets":["TOKEN"]}`, `{"name":"ok","egress":["example.com"]}`, `{"name":"ok","cron":[{"schedule":"* * * * *","run":"x"}]}`, + `{"name":"ok","source":"viewers"}`, } { if _, err := Parse([]byte(bad)); err == nil { t.Errorf("expected error for %s", bad) @@ -41,6 +42,10 @@ func TestParseService(t *testing.T) { if m.Entry != "" { t.Errorf("a service nook has no entry, got %q", m.Entry) } + // Unset means the run code stays with the owner and editors, and says so when written back. + if m.Source != "editors" || m.SourceVisibleToViewers() { + t.Errorf("source defaults closed, got %q", m.Source) + } if m.Egress[1] != "api.example.com" { t.Errorf("egress hosts are lowercased, got %q", m.Egress[1]) } @@ -68,6 +73,8 @@ func TestServiceValidation(t *testing.T) { "egress bare word": `{"name":"board","type":"service","run":"x","port":8000,"egress":["localhost"]}`, "cron schedule": `{"name":"board","type":"service","run":"x","port":8000,"cron":[{"schedule":"0 9 * *","run":"y"}]}`, "cron run blank": `{"name":"board","type":"service","run":"x","port":8000,"cron":[{"schedule":"* * * * *","run":""}]}`, + "source unknown": `{"name":"board","type":"service","run":"x","port":8000,"source":"everyone"}`, + "source public": `{"name":"board","type":"service","run":"x","port":8000,"source":"public"}`, } { if _, err := Parse([]byte(bad)); err == nil { t.Errorf("%s: expected an error for %s", name, bad) @@ -82,3 +89,18 @@ func TestServiceValidation(t *testing.T) { t.Errorf("egress is optional: %v", err) } } + +func TestSourceVisibility(t *testing.T) { + // A static nook's files reach the browser anyway, so they are always readable. + m, err := Parse([]byte(`{"name":"page"}`)) + if err != nil || !m.SourceVisibleToViewers() { + t.Errorf("static source is always visible: %+v %v", m, err) + } + if m.Source != "" { + t.Errorf("static nooks carry no source field, got %q", m.Source) + } + open, err := Parse([]byte(`{"name":"board","type":"service","run":"x","port":8000,"source":"viewers"}`)) + if err != nil || !open.SourceVisibleToViewers() { + t.Errorf(`"viewers" opts in: %+v %v`, open, err) + } +}