diff --git a/website/MODULE_API.md b/website/MODULE_API.md index c731e9d..2bab8bd 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -1,6 +1,8 @@ # The Module API — the contract -**Status:** Phase 1 deliverable of [MODULE_SYSTEM.md](MODULE_SYSTEM.md). This document is the +**Status:** Phase 1 deliverable of [MODULE_SYSTEM.md](MODULE_SYSTEM.md), **validated by the atlas +spike** — Part 7 records what the spike proved, what it changed in this contract, and the three +artifacts in it that are not design. This document is the normative contract between the core website and an installed module. `MODULE_SYSTEM.md` decides *what* the module system is; this decides *exactly what a module may call, what it must provide, and what core promises not to break*. @@ -111,11 +113,13 @@ module-uo does not need is on the list. | Member | Signature | Backed by | First real caller | | --- | --- | --- | --- | +| `ctx.express` | the `express` namespace | core's `node_modules` | every module router (§7.2) | +| `ctx.validator` | the `express-validator` namespace | core's `node_modules` | `atlas.router.js` | | `ctx.db.query` | `(sql, params?) => Promise` | `utils/db` | every `*.db.js` | | `ctx.db.pool` | mariadb pool | `utils/db` | `shardAtlas.db.js` (streamed import) | | `ctx.log` | `(namespace) => { error, warn, info, debug }`, each `(msg, meta?)` | `utils/logger` | all nine UO utils | | `ctx.settings.get` | `(key) => Promise` | `model/settings` | `shardAtlas.model` | -| `ctx.settings.set` | `(key, value) => Promise` | `model/settings` | `shardAtlas.model` | +| `ctx.settings.set` | `(key, value, updatedBy?) => Promise` | `model/settings` | `shardAtlas.model:60` | | `ctx.settings.getInstanceName` | `() => Promise` | `model/settings` | `shardIngest.js:84` | | `ctx.auth.getUserFromRequest` | `(req) => { id, username, role } \| null` | `utils/auth` | `shardVisibility.js` | | `ctx.push.publish` | `(streamId, { ref?, ownerUserId? }) => Promise` | `utils/pushDispatch:92` | `shardIngest.js:22` | @@ -135,6 +139,12 @@ Three narrowings from `MODULE_SYSTEM.md` §2.1, all deliberate: registration/game-signup/app-links policy that is core's business. - **`ctx.posts` is four functions.** `create`/`update`/`remove` are the CMS, not a module's. +And one addition the spike forced: **`ctx.express` and `ctx.validator`**. A module lives at +`/modules//`, outside `server/`, so Node's resolver never reaches `server/node_modules` and +`require('express')` from a module simply fails — which is how this was found. Even where it +resolved, a second express in the process is a second `Router` prototype. Core owns one express, as +it owns one React (§7.2). + `ctx` is frozen (`Object.freeze`, one level deep) before it is handed over. That is a guard against accident, not against a hostile module — per `MODULE_SYSTEM.md` §2.2 the boundary is organisational, not a security boundary. @@ -289,12 +299,18 @@ window.__rg = { react, // the React namespace reactDom, // react-dom/client router, // react-router-dom namespace + jsxRuntime, // react/jsx-runtime — see below registry, // §3.3 ui, // §3.4 api, // §3.5 } ``` +`jsxRuntime` is not decoration. A module's bundler compiles every `.jsx` file to imports from +`react/jsx-runtime` under the modern automatic runtime, and those have to resolve to *core's* React +like every other import. Without it on the global a module would have to build with +`jsxRuntime: 'classic'`; with it, the module uses the default its tooling already assumes. + A module entry checks `window.__rg.version` against its own `coreApi` range and refuses to register on a mismatch, logging once — the client-side twin of §1.1, and the reason `version` is here at all. @@ -588,3 +604,96 @@ files. Two consequences: Neither changes a decision; both are corrected here rather than left to be tripped over when the extraction is counted against the plan. + +--- + +## Part 7 — What the spike proved + +The Phase 1 spike ran on `website` branch `spike/module-atlas`, cut from `edge` and **deliberately +never merged** — it is the evidence, not the implementation. Phase 2 rebuilds the loader properly. + +### 7.1 The exit criteria + +| Criterion | Result | +| --- | --- | +| No internal-file imports from the module into core | **pass** — the module's only non-builtin requires are `ctx.express` / `ctx.validator`; the built client chunk contains **zero** bare import specifiers | +| `npm run routes:manifest` produces a zero-line diff | **pass** — `routes.manifest.json` *and* `routes.guards.json` are byte-identical with the six atlas routes now served by the module | +| The chunk loads under the enforced CSP | **pass** — `/uo/atlas` and `/uo/atlas/:slug` render from `/modules/uo/entry.js` under `script-src 'self'`, with **zero** violation reports at `/api/csp-report` and a clean console | +| Everything still passes | **pass** — 729 core tests, 81 module tests | + +Also verified end to end against the real database: the schema fragment replayed after core's +(`schema ensured for module "uo"`), `onBoot` ran the atlas refresh, the module reached `started`, and +the six API URLs answered 200 unchanged at `/api/v1/public/atlas/*`. + +### 7.2 One express, one React — the same rule, twice + +The single biggest thing the spike changed. `MODULE_SYSTEM.md` §2.6 got the client half right — one +React, shared via a global — and said nothing about the server, where the identical problem exists +and bites harder: + +- A module lives at `/modules//`. Node's resolver walks *up* from there and never reaches + `server/node_modules`, so `require('express')` inside a module **fails outright**. This was the + first error the spike hit. +- Installing express into the module would fix resolution and break something worse: two `Router` + prototypes, two sets of `instanceof` checks — and it would mean the operator running `npm install` + in a module directory, which is the build the whole plan exists to avoid. + +Hence `ctx.express` and `ctx.validator`. The rule generalises: **anything shared between core and a +module is owned by core and handed over — never resolved by the module.** On the client that is +`react`, `react-dom/client`, `react-router-dom` and `react/jsx-runtime`; on the server it is +`express` and `express-validator`. + +Two mechanical traps inside that, both cheap once known and both silent otherwise: + +- **Vite's object-form `resolve.alias` does PREFIX matching.** A `react` key also rewrites + `react/jsx-runtime` into `src/shim/react.js/jsx-runtime`, a path that cannot exist. Use the array + form with anchored regexes (`/^react$/`). +- **`external` alone is not enough for an ESM library build.** Rollup then emits bare + `import 'react'`, which the browser cannot resolve without an import map — and CSP forbids the + inline `