Skip to content
This repository was archived by the owner on Oct 13, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"`
Expand All @@ -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
}
Expand All @@ -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)
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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"
}
22 changes: 22 additions & 0 deletions manifest/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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])
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
Loading