Skip to content

fix(site): refuse a starter that does not exist instead of serving an empty root - #231

Merged
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/serve-template-validation
Aug 3, 2026
Merged

fix(site): refuse a starter that does not exist instead of serving an empty root#231
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/serve-template-validation

Conversation

@clawedassistant26

Copy link
Copy Markdown
Contributor

moshcode site <name> --template <starter> joins the value straight into a
path under examples/templates. When that directory turns out not to exist,
fs.stat fails, seed resolves to null, the seed step is never added, and
the install runs to the end and reports success — with nothing in the root.

The file's own comment says why that matters:

An empty root serves 404, which reads as a broken install at exactly the
moment someone is trying to tell those apart.

So the one flag whose job is to prevent that outcome produces it silently
whenever it is wrong, and the success message makes the empty page look like
a name that never resolved rather than a starter that was misspelled by one
letter.

Reproduced

Driven through serveCommand with the write/mkdir/copy deps stubbed, so
nothing touched this machine. Before:

--- typo in --template
    args: ["blue.eggs","--install","--template","caddy-statik"]
    exit: 0
    steps: write, mkdir, run          <- no seed
    copy():  (none)
    said anything about the template? NO

--- --template eats the next flag
    args: ["blue.eggs","--template","--install"]
    exit: 0
    steps: write, mkdir, run
    copy():  (none)
    said anything about the template? NO

The second one is the worse of the two: --template takes rest[at + 1]
with no check that a value was there, so --template --install reads
--install as the starter name — and still installs, because
rest.includes("--install") is a separate test.

The path, not just the name

The same value is joined into a path with no shape check. templates.mjs
already has that rule and says why:

Deliberately conservative about what counts as a bundled name: anything
with a slash, a colon, or a dot is treated as remote, so a name can never be
coaxed into reading a path outside the bundled directory.

site did not apply it, so a value with enough ../ in it names a copy
source anywhere on the box:

--- --template climbs out of examples/templates
    args: ["blue.eggs","--install","--template","../../../../tmp/repro-serve/outside"]
    exit: 0
    copy():  [["/tmp/repro-serve/outside/site","/srv/blue.eggs"]]

To be clear about the scope: the value is typed by whoever runs the command,
the source has to be a directory containing a site/ child, and the copy only
happens when the root is empty. It is not a way in from outside. It is a
sharp edge on a command that runs as root and copies into a directory a web
server is about to publish, and templates.mjs had already decided this
value should not be able to name a path.

The fix

One exported helper, chooseTemplate, checked before anything is written:

  • --empty still wins, and no --template still means the default starter.
  • --template with no value, or with a value that starts with -, is an error
    rather than a starter name.
  • the name is checked with classifySource from templates.mjs — the existing
    rule, not a new one — before it goes anywhere near a path.
  • the name is checked against listTemplates(), and the error names the ones
    that do exist, since the realistic case is off by one character:
moshcode site: there is no starter called "caddy-statik"
  bundled: bun-caddy-sqlite, caddy-static
  `moshcode template list` shows them all, or use --empty to seed nothing.

I ran moshcode template list and --empty before putting them in that
message; both do what it says.

Tests

Four added to test/serve.test.mjs, including a control that a valid
--template caddy-static still seeds from the right directory.

Fail-before was checked with the import of the new export removed, so the
behaviour test ran against the unfixed source rather than failing to link:
test 11 fails before and passes after, with all ten controls green both ways.

Full suite: 909 tests, 720 pass, 0 fail, 189 skipped (main is 905/716/0/189
on 053ec57).

Touches src/serve.mjs and test/serve.test.mjs only — no overlap with #223,
#227, #228, #229 or #230.

… empty root

`site <name> --template <starter>` joined the value straight into a path
under examples/templates. When that directory was not there — a typo, or
`--template --install` reading the next flag as the name — fs.stat failed,
the seed step was dropped, and the install carried on to the end reporting
success with nothing in the root.

That is the 404 the seeding exists to prevent, reached by the one route
that also hides it: the run says the config is installed, so the empty page
reads as a broken name rather than a mistyped flag.

The name is also now checked for shape before it is joined into a path.
templates.mjs already refuses to treat anything with a slash or a dot as a
bundled name, for exactly this reason; site did not apply that rule, so a
value with enough ../ in it named a copy source anywhere on the box, and
that copy lands in a root a web server is about to publish.
@ralyodio
ralyodio merged commit 0ffd785 into moshcoder:main Aug 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants