docs(website): record slice 1, and correct two rules it disproved

The install service and admin API as built (website#142), plus the two
things §2.7.2 got wrong before there was any code to check them against.

The top-level directory rule was checking against nothing real. module-uo's
release workflow packs module-uo-<version>/, not uo/, so "reject a top-level
directory whose name is not the manifest's id" would have refused every
bundle that exists. That level is stripped instead - its name belongs to
whoever published the bundle, the directory it lands in has to be the id the
loader scans for - and what replaces the check is stronger: the UNPACKED
module.json must agree with the install manifest about id and version.

And tar has to be pinned forward rather than merely depended on. Installing
it gets 6.x, which npm audit reports as critical, and the advisory list reads
as this feature's own threat model: hardlink traversal via drive-relative
linkpath, symlink poisoning, hardlink escape through a symlink chain, PAX
size override on GNU long-name headers, decompression DoS. Refusing symlink
and hardlink entry types outright is what takes the extractor off most of
that list rather than depending on the library to contain them.

Also records the measurement behind the two-pass unpack, which was assumed in
the plan and is now known: node-tar DOES reject an escaping member, but late.
An archive whose fourth member escapes throws and leaves the first three on
disk - and the loader only asks whether module.json is present, so a
half-unpacked bundle is a module as far as the next boot is concerned.

Refreshes api-route-inventory.json (158 -> 166 public) and the route count in
BACKEND_DESIGN, and documents module_source_hosts beside the other seeded
settings keys - bootstrapped from the environment, owned by the database.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-12 03:12:25 -05:00
parent 6bca9adf49
commit 88b6257333
3 changed files with 126 additions and 7 deletions

View File

@@ -275,7 +275,15 @@ Seeded with the 8 spec categories: `new-player-guide, maps-atlas, systems, items
Seeded keys: `site_mode` (default `maintenance`), `site_mode_changed_at`,
`site_mode_changed_by`, `maintenance_message`, `status_message`, `homepage_teaser`,
`contact_email` (=UOMysticmoon@gmail.com), `site_title`, `player_registration`
(default `disabled`), `mobile_app_links_enabled`.
(default `disabled`), `mobile_app_links_enabled`, `module_source_hosts`.
`module_source_hosts` is the allowlist of hostnames a module may be installed from
(MODULE_SYSTEM.md §2.7.2 decision 6), edited in Admin → Modules and audited as
`module.sources`. It is **bootstrapped** from `MODULE_SOURCE_HOSTS` and not owned by
it: `seedDefault` is an `INSERT IGNORE`, so the environment supplies a default on a
fresh install and changing the variable later cannot reach back in and overwrite what
an operator chose. Installs are `https`-only, every redirect hop is re-checked against
this list, and an empty value forbids every install rather than allowing every host.
**Keys a MODULE seeds into this table.** `settings` is core's, but a module's
schema fragment may `INSERT IGNORE` its own rows into it, and module-uo seeds two:
@@ -731,7 +739,7 @@ are authoritative, and they answer different questions:
| Artifact | Source of truth for | Generated by |
|---|---|---|
| `server/routes.manifest.json` — mirrored as [api-route-inventory.json](./api-route-inventory.json) | **What URLs CORE serves.** 158 public routes + 2 on the internal listener, sorted, method + path only. | `npm run routes:manifest`, by walking the live Express stack |
| `server/routes.manifest.json` — mirrored as [api-route-inventory.json](./api-route-inventory.json) | **What URLs CORE serves.** 166 public routes + 2 on the internal listener, sorted, method + path only. | `npm run routes:manifest`, by walking the live Express stack |
| `server/swagger/swagger-output.json` — merged into `/api/docs` | **What each core route means.** Parameters, bodies, response codes, security. | `npm run swagger`, from `#swagger.*` annotations |
Both are **core's**. An installed module's routes are in neither: they are in that module's own