@bongos/core 1.19.441 → 1.19.443

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/.bongos-core.json +496 -441
  2. package/clients/bongos-client/index.d.ts +1 -2
  3. package/config/branding.neutral.json +1 -1
  4. package/docs/adr/0218-the-apex-root-web-surface.md +55 -0
  5. package/docs/adr/README.md +1 -0
  6. package/docs/api/openapi.json +3 -20
  7. package/docs/api-reference.md +1 -1
  8. package/docs/architecture.md +2 -1
  9. package/docs/copy-inventory.md +363 -359
  10. package/docs/copy-registry.json +608 -561
  11. package/docs/design/mocks/hub/mine.html +1 -1
  12. package/docs/design/mocks/hub/picker.html +1 -1
  13. package/docs/design/mocks/hub/planets.html +1 -1
  14. package/docs/design/mocks/hub/v1.html +3 -3
  15. package/docs/design/mocks/hub/v2.html +2 -2
  16. package/docs/design/mocks/hub/v3.html +5 -5
  17. package/docs/design/mocks/hub/wizard.html +1 -1
  18. package/docs/design/mocks/landing/v1.html +1 -1
  19. package/docs/design/mocks/landing/v2.html +1 -1
  20. package/docs/design/mocks/landing/v3.html +1 -1
  21. package/docs/design/mocks/landing-page-mock.html +3 -3
  22. package/docs/design/mocks/projects-hub-mock.html +1 -1
  23. package/docs/design/mocks/projects-hub-variants.html +1 -1
  24. package/docs/design/mocks/projects-page-mock.html +1 -1
  25. package/docs/design/projects-hub-direction-v2.md +23 -0
  26. package/docs/file-map.md +3 -3
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/modules-contract.md +9 -2
  29. package/docs/recipes/ui-look-before-you-ship.md +4 -4
  30. package/modules/copy-desk/tests/copy_flags.mjs +4 -4
  31. package/modules/platform-identity/routes/projects.js +2 -1
  32. package/modules/provisioning/provisioning.js +11 -2
  33. package/modules/provisioning/routes/provisioning.js +10 -5
  34. package/modules/public-landing/CLAUDE.md +15 -0
  35. package/modules/public-landing/module.json +15 -0
  36. package/{public-landing → modules/public-landing/public}/assets/cosmos.css +8 -0
  37. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  38. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +14 -0
  39. package/modules/public-landing/public/assets/world/r4-card-deepfield.jpg +0 -0
  40. package/modules/public-landing/public/assets/world/r4-card-deepfield.jpg.json +14 -0
  41. package/modules/public-landing/public/assets/world/r4-card-dust.jpg +0 -0
  42. package/modules/public-landing/public/assets/world/r4-card-dust.jpg.json +14 -0
  43. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  44. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +14 -0
  45. package/{public-landing → modules/public-landing/public}/projects-floor.states.json +28 -1
  46. package/{public-landing → modules/public-landing/public}/projects.html +101 -1
  47. package/{public-landing → modules/public-landing/public}/projects.probes.json +6 -0
  48. package/{public-landing → modules/public-landing/public}/projects.states.json +1 -0
  49. package/modules/ui-design/kit/fixtures/featured-hub.json +1 -1
  50. package/modules/ui-design/kit/lib.js +2 -2
  51. package/modules/ui-design/kit/render.js +1 -1
  52. package/modules/ui-design/kit/serve.js +32 -7
  53. package/package-lock.json +2 -2
  54. package/package.json +1 -1
  55. package/scripts/gds/copy-inventory.js +3 -3
  56. package/scripts/gds/fitness-checks-identity.js +18 -1
  57. package/scripts/gds/publish-manifest.js +1 -1
  58. package/src/bongos/module-scope-map.js +9 -7
  59. package/src/bongos/serve-internal.js +13 -1
  60. package/src/module-api.js +1 -1
  61. package/src/module-loader/loader.js +34 -17
  62. package/src/module-loader/manifest-schema.js +16 -8
  63. package/src/platform-server.js +50 -18
  64. package/tests/copy_inventory.mjs +0 -0
  65. package/tests/cosmos_token_contrast.mjs +6 -6
  66. package/tests/fitness.mjs +20 -0
  67. package/tests/landing_legal_pages.mjs +2 -1
  68. package/tests/landing_page.mjs +7 -5
  69. package/tests/module-scope-map.mjs +2 -2
  70. package/tests/module_loader.mjs +4 -1
  71. package/tests/onboard.mjs +5 -5
  72. package/tests/projects_hub.mjs +105 -20
  73. package/tests/projects_hub_pre_uat.mjs +5 -3
  74. package/tests/provisioning_settings.mjs +49 -4
  75. package/tests/public_landing_projects.mjs +2 -2
  76. package/tests/terms_acceptance.mjs +1 -1
  77. package/tests/ui_design_kit.mjs +3 -3
  78. package/tests/world_tokens.mjs +3 -2
  79. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_00.webp +0 -0
  80. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_01.webp +0 -0
  81. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_02.webp +0 -0
  82. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_03.webp +0 -0
  83. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_04.webp +0 -0
  84. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_05.webp +0 -0
  85. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_06.webp +0 -0
  86. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_07.webp +0 -0
  87. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_08.webp +0 -0
  88. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_09.webp +0 -0
  89. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_10.webp +0 -0
  90. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_11.webp +0 -0
  91. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_12.webp +0 -0
  92. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_13.webp +0 -0
  93. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_14.webp +0 -0
  94. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_15.webp +0 -0
  95. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_16.webp +0 -0
  96. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_17.webp +0 -0
  97. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_18.webp +0 -0
  98. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_19.webp +0 -0
  99. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_20.webp +0 -0
  100. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_21.webp +0 -0
  101. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_22.webp +0 -0
  102. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_23.webp +0 -0
  103. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_24.webp +0 -0
  104. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_25.webp +0 -0
  105. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_26.webp +0 -0
  106. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_27.webp +0 -0
  107. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_28.webp +0 -0
  108. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_29.webp +0 -0
  109. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_30.webp +0 -0
  110. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_31.webp +0 -0
  111. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_32.webp +0 -0
  112. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_33.webp +0 -0
  113. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_34.webp +0 -0
  114. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_35.webp +0 -0
  115. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_36.webp +0 -0
  116. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_37.webp +0 -0
  117. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_38.webp +0 -0
  118. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_39.webp +0 -0
  119. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_40.webp +0 -0
  120. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_41.webp +0 -0
  121. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_42.webp +0 -0
  122. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_43.webp +0 -0
  123. /package/{public-landing → modules/public-landing/public}/assets/verro-medallion.png +0 -0
  124. /package/{public-landing → modules/public-landing/public}/assets/world/og-card.jpg +0 -0
  125. /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-a-cut.webp +0 -0
  126. /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-a-cut.webp.json +0 -0
  127. /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-b.jpg +0 -0
  128. /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-b.jpg.json +0 -0
  129. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  130. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one-cut.webp.json +0 -0
  131. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one.jpg +0 -0
  132. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one.jpg.json +0 -0
  133. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set-cut.webp +0 -0
  134. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set-cut.webp.json +0 -0
  135. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set.jpg +0 -0
  136. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set.jpg.json +0 -0
  137. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-bead-cut.webp +0 -0
  138. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-bead-cut.webp.json +0 -0
  139. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-blob-cut.webp +0 -0
  140. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-blob-cut.webp.json +0 -0
  141. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-pair-cut.webp +0 -0
  142. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-pair-cut.webp.json +0 -0
  143. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-bead-cut.webp +0 -0
  144. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-bead-cut.webp.json +0 -0
  145. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-blob-cut.webp +0 -0
  146. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-blob-cut.webp.json +0 -0
  147. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-pair-cut.webp +0 -0
  148. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-pair-cut.webp.json +0 -0
  149. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-bead-cut.webp +0 -0
  150. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-bead-cut.webp.json +0 -0
  151. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-blob-cut.webp +0 -0
  152. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-blob-cut.webp.json +0 -0
  153. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-pair-cut.webp +0 -0
  154. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-pair-cut.webp.json +0 -0
  155. /package/{public-landing → modules/public-landing/public}/assets/world/r3-space-nebula.jpg +0 -0
  156. /package/{public-landing → modules/public-landing/public}/assets/world/r3-space-nebula.jpg.json +0 -0
  157. /package/{public-landing → modules/public-landing/public}/assets/world.css +0 -0
  158. /package/{public-landing → modules/public-landing/public}/buddha.png +0 -0
  159. /package/{public-landing → modules/public-landing/public}/contact.html +0 -0
  160. /package/{public-landing → modules/public-landing/public}/index.html +0 -0
  161. /package/{public-landing → modules/public-landing/public}/index.probes.json +0 -0
  162. /package/{public-landing → modules/public-landing/public}/index.states.json +0 -0
  163. /package/{public-landing → modules/public-landing/public}/privacy.html +0 -0
  164. /package/{public-landing → modules/public-landing/public}/projects-empty.states.json +0 -0
  165. /package/{public-landing → modules/public-landing/public}/terms.html +0 -0
  166. /package/{public-landing → modules/public-landing/public}/world/assets/clouds/cloud-island-large.png +0 -0
  167. /package/{public-landing → modules/public-landing/public}/world/assets/clouds/cloud-puff.png +0 -0
  168. /package/{public-landing → modules/public-landing/public}/world/assets/creatures/creature-whale-cargo.png +0 -0
  169. /package/{public-landing → modules/public-landing/public}/world/assets/creatures/creature-whale-glide.png +0 -0
  170. /package/{public-landing → modules/public-landing/public}/world/assets/plates/plate-clouds-near.png +0 -0
  171. /package/{public-landing → modules/public-landing/public}/world/assets/plates/scene-establishing.png +0 -0
@@ -175,32 +175,49 @@ function hallWidgetScripts({ isEnabled } = {}, opts) {
175
175
  return out;
176
176
  }
177
177
 
178
- // Host-routed static web surfaces contributed by enabled modules (BV1.R84, ADR 0093
179
- // §1 / 02-module-map §6 — a web surface is first-class). A module that owns a web
180
- // surface (e.g. the status dashboard at status.<apex>) declares
181
- // contributes.webSurfaces:[{ host:"status.", dir:"public" }] in its manifest AND
182
- // ships that static dir at modules/<key>/<dir>/. This returns, for each discovered +
183
- // ENABLED module that declares webSurfaces and actually ships the directory, the
184
- // SERVING descriptor serve-internal.js iterates to route by Host header:
185
- // { key, host, dir } — host is the lowercase Host-header prefix; dir is the
186
- // ABSOLUTE on-disk path of the static directory.
187
- // The Host-routed twin of mountModuleRoutes / hallWidgetScripts: an enabled module's
188
- // surface answers on its host, a disabled one vanishes (it's never returned, so
189
- // serve-internal never routes its host). No-op (empty list) until a module ships a
190
- // web surface — same SAFE-BY-CONSTRUCTION posture as every other seam here. Pure
191
- // (reads disk only). Exported + module-agnostic so serve-internal hardcodes no key.
178
+ // Static web surfaces contributed by enabled modules (BV1.R84, ADR 0093 §1 /
179
+ // 02-module-map §6 — a web surface is first-class). A module that owns a web surface
180
+ // declares contributes.webSurfaces in its manifest AND ships that static dir at
181
+ // modules/<key>/<dir>/. A surface names WHERE it answers in one of two ways, never
182
+ // both and never neither (ADR 0218):
183
+ // { host: "status.", dir: "public" } → a Host-header PREFIX (status-UI, hall-UI)
184
+ // { apexRoot: true, dir: "public" } → the APEX ROOT (public-landing) — whatever
185
+ // host is left once the subdomains are taken
186
+ // This returns, for each discovered + ENABLED module that declares a surface and
187
+ // actually ships the directory, the SERVING descriptor its server iterates:
188
+ // { key, host, dir } — host is the lowercase Host-header prefix
189
+ // { key, apexRoot: true, dir } — NO host field at all
190
+ // dir is the ABSOLUTE on-disk path of the static directory in both shapes.
191
+ //
192
+ // `host: ""` is REJECTED, not treated as "the root" (ADR 0218 §3): the routers match
193
+ // with host.startsWith(s.host), and every string starts with "" — an empty prefix
194
+ // would answer on builders./status. too and make isModuleSurfaceHost() true for every
195
+ // request, which is the predicate the hall's own /builders guards use to step aside.
196
+ // A boolean cannot be mistaken for a prefix, so apex-root says so with a flag.
197
+ //
198
+ // The twin of mountModuleRoutes / hallWidgetScripts: an enabled module's surface
199
+ // answers, a disabled one vanishes (it's never returned, so nothing routes it — for
200
+ // the apex root that means the front door is simply gone and the 404 tail answers).
201
+ // No-op (empty list) until a module ships a web surface — same SAFE-BY-CONSTRUCTION
202
+ // posture as every other seam here. Pure (reads disk only). Exported + module-agnostic
203
+ // so both servers hardcode no key.
192
204
  function moduleWebSurfaces({ isEnabled } = {}, opts) {
193
205
  const out = [];
194
206
  for (const { key, dir, manifest } of discovered(opts).loaded) {
195
207
  if (typeof isEnabled === 'function' && !isEnabled(key)) continue;
196
208
  const surfaces = (manifest.contributes && manifest.contributes.webSurfaces) || [];
197
209
  for (const s of surfaces) {
198
- if (!s || typeof s.host !== 'string' || typeof s.dir !== 'string') continue;
210
+ if (!s || typeof s.dir !== 'string') continue;
211
+ const apexRoot = s.apexRoot === true;
212
+ const hasHost = typeof s.host === 'string' && s.host !== '';
213
+ // exactly one of the two, never both and never neither — and `host: ""` counts
214
+ // as NEITHER, which is what keeps an empty prefix from matching every host
215
+ if (apexRoot === hasHost) continue;
199
216
  const abs = path.join(dir, s.dir);
200
217
  let isDir = false;
201
218
  try { isDir = fs.statSync(abs).isDirectory(); } catch { isDir = false; }
202
- if (!isDir) continue; // declared but not shipped → skip (no host routed)
203
- out.push({ key, host: s.host.toLowerCase(), dir: abs });
219
+ if (!isDir) continue; // declared but not shipped → skip (nothing routed)
220
+ out.push(apexRoot ? { key, apexRoot: true, dir: abs } : { key, host: s.host.toLowerCase(), dir: abs });
204
221
  }
205
222
  }
206
223
  return out;
@@ -34,7 +34,9 @@
34
34
  // "disciplines": ["artist"], // disciplines this module adds
35
35
  // "skills": ["otb-…"], // slash-command skills it ships
36
36
  // "uiSections": ["boxes"], // hall/status UI sections it owns
37
- // "webSurfaces": [{ "host": "status.", "dir": "public" }], // host-routed static web surfaces (BV1.R84)
37
+ // "webSurfaces": [{ "host": "status.", "dir": "public" }], // static web surfaces. Each names a Host
38
+ // // PREFIX (BV1.R84) or, as { "apexRoot": true, "dir": "public" },
39
+ // // the APEX ROOT — never both (ADR 0218).
38
40
  // "migrations": true // true => modules/<key>/migrations/ exists (or an array of names)
39
41
  // },
40
42
  // "provides": ["reward"], // OPTIONAL. seam PORTS this module registers a provider for.
@@ -84,13 +86,19 @@ const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
84
86
  const isStr = (v) => typeof v === 'string' && v.length > 0;
85
87
  const isStrArray = (v) => Array.isArray(v) && v.every(isStr);
86
88
 
87
- // A webSurface entry: { host: "<subdomain-prefix>.", dir: "<static-dir-rel-to-module>" }.
88
- // `host` is the lowercase Host-header prefix the surface answers on (e.g. "status."
89
- // → status.<apex>); `dir` is the module-relative directory of static files to serve
90
- // (e.g. "public"). Both required, non-empty strings; `dir` must stay inside the
91
- // module (no absolute / "..").
89
+ // A webSurface entry names WHERE the surface answers, in exactly one of two ways
90
+ // (ADR 0218), plus the module-relative directory of static files to serve:
91
+ // { host: "status.", dir: "public" } — a lowercase Host-header PREFIX (status.<apex>)
92
+ // { apexRoot: true, dir: "public" } — the APEX ROOT: whatever host is left once
93
+ // the subdomains are taken (the front door)
94
+ // `dir` is required, non-empty, and must stay inside the module (no absolute / "..").
95
+ // A host surface's `host` must be a NON-EMPTY string: routers match with
96
+ // host.startsWith(host), and every string starts with "", so an empty prefix would
97
+ // answer on every host and silently swallow the other surfaces. That is exactly why
98
+ // the apex root is a boolean flag and not the empty prefix it superficially resembles.
99
+ const isWebSurfaceDir = (v) => isStr(v.dir) && !v.dir.startsWith('/') && !v.dir.split(/[\\/]/).includes('..');
92
100
  const isWebSurface = (v) =>
93
- isObj(v) && isStr(v.host) && isStr(v.dir) && !v.dir.startsWith('/') && !v.dir.split(/[\\/]/).includes('..');
101
+ isObj(v) && isWebSurfaceDir(v) && (v.apexRoot === true ? !('host' in v) : isStr(v.host));
94
102
  const isWebSurfaceArray = (v) => Array.isArray(v) && v.every(isWebSurface);
95
103
 
96
104
  function validateManifest(obj) {
@@ -172,7 +180,7 @@ function validateManifest(obj) {
172
180
  }
173
181
  }
174
182
  if ('webSurfaces' in obj.contributes && !isWebSurfaceArray(obj.contributes.webSurfaces)) {
175
- E('contributes.webSurfaces: must be an array of { host: "<prefix>.", dir: "<module-relative dir>" } objects (dir inside the module, no absolute path or "..")');
183
+ E('contributes.webSurfaces: must be an array of { host: "<prefix>.", dir: "<module-relative dir>" } or { apexRoot: true, dir: "<module-relative dir>" } objects — a surface names a Host prefix OR the apex root, never both and never neither (ADR 0218); dir stays inside the module (no absolute path or "..")');
176
184
  }
177
185
  if ('migrations' in obj.contributes) {
178
186
  const m = obj.contributes.migrations;
@@ -54,13 +54,40 @@ const { logger, requestIdMiddleware } = require('./bongos/logger');
54
54
  const PORT = Number(process.env.PORT) || 3000;
55
55
  const HOST = process.env.HOST || '127.0.0.1';
56
56
 
57
- // The apex landing page — a game-less instance's home (replaces the bare 404 the
58
- // apex host used to serve). A deliberately MINIMAL placeholder: the mascot + a
59
- // way into the Builders' Hall; the full landing is its own task. Served only on
60
- // the apex host (status./builders. are handled by mountInternalSurfaces). The
61
- // branding pack is injected once at boot (theme + window.__BRANDING__), exactly
62
- // as the hall pages are, so the look + identity come from config, not hardcode.
63
- const LANDING_DIR = path.join(__dirname, '..', 'public-landing');
57
+ // The apex front door — a game-less instance's home (it replaced the bare 404 the
58
+ // apex host used to serve). Served only on the apex host; status./builders. are
59
+ // handled by mountInternalSurfaces. The branding pack is injected once at boot
60
+ // (theme + window.__BRANDING__), exactly as the hall pages are, so the look and the
61
+ // identity come from config, never from hardcode.
62
+ //
63
+ // THE SURFACE IS A MODULE (task 1003322, ADR 0218). The front door used to be a
64
+ // hardcoded path here; it is now modules/public-landing/public/, resolved through the
65
+ // loader's web-surface seam by the APEX-ROOT declaration ({ apexRoot: true, dir }) —
66
+ // the one shape that seam grew for it, because the apex is not a Host PREFIX like
67
+ // "status."/"builders." but whatever is left once the subdomains are taken.
68
+ //
69
+ // Resolved by PREDICATE, never by key: this file names no module (ADR 0083 §Decision
70
+ // #4), so an instance shipping its own front door gets the apex by declaring apexRoot.
71
+ // Lazy + memoised so importing this module reads no disk and a test can build the app
72
+ // after setting up its own module state.
73
+ //
74
+ // null when NO enabled module claims the apex root — the module is disabled, or the
75
+ // instance ships no landing at all. Every consumer below already treats an absent page
76
+ // as "fall through", so the front door simply vanishes and the 404 tail answers.
77
+ let _landingDir;
78
+ function landingDir() {
79
+ if (_landingDir === undefined) {
80
+ // The apex root is singular by definition — two modules cannot both be "everything
81
+ // that is not a subdomain". Serve the first and SAY so, rather than silently
82
+ // shadowing one of them.
83
+ const claims = moduleLoader.moduleWebSurfaces({ isEnabled: isModuleEnabled }).filter((s) => s.apexRoot);
84
+ if (claims.length > 1) {
85
+ logger.warn({ keys: claims.map((s) => s.key) }, 'more than one enabled module declares an apex-root web surface — serving the first');
86
+ }
87
+ _landingDir = claims.length ? claims[0].dir : null;
88
+ }
89
+ return _landingDir;
90
+ }
64
91
  // ── CONTENT-STAMPED STATIC ASSETS (task 1002766) ──
65
92
  // The landing HTML is `cf-cache-status: DYNAMIC` and never cached, but the
66
93
  // assets it references come back `Cache-Control: public, max-age=14400` — four
@@ -80,7 +107,7 @@ const LANDING_DIR = path.join(__dirname, '..', 'public-landing');
80
107
  // shape serve-internal.js already stamps onto the hall's own scripts.
81
108
  //
82
109
  // Stamped by RESOLUTION, not by a path allow-list: a URL is an asset if it
83
- // actually names a file under public-landing/. Routes (`/projects`, `/create`)
110
+ // actually names a file under the landing surface. Routes (`/projects`, `/create`)
84
111
  // and API paths resolve to nothing and are left alone, so a new asset is
85
112
  // stamped the day it is added and nobody has to remember to list it.
86
113
  const _assetStamps = new Map();
@@ -88,9 +115,10 @@ function assetStamp(url) {
88
115
  if (!_assetStamps.has(url)) {
89
116
  let v = '';
90
117
  try {
91
- const abs = path.resolve(LANDING_DIR, '.' + url);
118
+ const root = landingDir();
119
+ const abs = path.resolve(root, '.' + url);
92
120
  // never let a template's own text walk out of the landing dir
93
- if (abs.startsWith(LANDING_DIR + path.sep)) {
121
+ if (abs.startsWith(root + path.sep)) {
94
122
  v = require('node:crypto').createHash('sha256')
95
123
  .update(fs.readFileSync(abs)).digest('hex').slice(0, 8);
96
124
  }
@@ -112,14 +140,14 @@ function landingPage(file) {
112
140
  try {
113
141
  // stamped LAST, so anything a later transform introduces is stamped too
114
142
  html = stampAssets(injectBrandingGlobal(stripApexOnly(
115
- fs.readFileSync(path.join(LANDING_DIR, file), 'utf8'))));
116
- } catch { /* absent → '' */ }
143
+ fs.readFileSync(path.join(landingDir(), file), 'utf8'))));
144
+ } catch { /* absent, or no module claims the apex root → '' */ }
117
145
  _landingPages.set(file, html);
118
146
  }
119
147
  return _landingPages.get(file);
120
148
  }
121
149
 
122
- // THE APEX CONDITIONAL (task 1002748). public-landing/ ships wholesale to every
150
+ // THE APEX CONDITIONAL (task 1002748). The landing surface ships wholesale to every
123
151
  // instance, so one file has to render two shapes: the platform's own front door,
124
152
  // and a child project's. Sections that are true only of the platform — the
125
153
  // catalog of OTHER instances, the "capture-resistant commons" claim, the door
@@ -231,11 +259,15 @@ function buildPlatformApp() {
231
259
  res.type('html').send(html);
232
260
  });
233
261
  }
234
- const landingAssets = express.static(LANDING_DIR, { index: false, fallthrough: true });
235
- app.use((req, res, next) => {
236
- if (!isApexHost(req)) return next();
237
- return landingAssets(req, res, next);
238
- });
262
+ // Assets fall through to the 404 tail when no module claims the apex root — the
263
+ // handler is not mounted at all rather than pointed at a missing directory.
264
+ const landingAssets = landingDir() && express.static(landingDir(), { index: false, fallthrough: true });
265
+ if (landingAssets) {
266
+ app.use((req, res, next) => {
267
+ if (!isApexHost(req)) return next();
268
+ return landingAssets(req, res, next);
269
+ });
270
+ }
239
271
 
240
272
  // Non-game tail. Anything that did not resolve to /api/gds, the status host,
241
273
  // or the builders' hall lands here. There is NO game to fall through to, so:
Binary file
@@ -1,6 +1,6 @@
1
1
  // Guards the WCAG AA contrast of the cosmos.css token layer (task 1002749).
2
2
  //
3
- // The tokens are the ONLY input here: this test PARSES public-landing/assets/cosmos.css
3
+ // The tokens are the ONLY input here: this test PARSES the landing surface’s assets/cosmos.css
4
4
  // and resolves var()/color-mix() itself, so it measures what the surfaces actually paint.
5
5
  // Duplicating the hex values would prove nothing.
6
6
  //
@@ -13,10 +13,10 @@ import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
 
16
- const CSS = path.join(
17
- path.dirname(fileURLToPath(import.meta.url)),
18
- '..', 'public-landing', 'assets', 'cosmos.css'
19
- );
16
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ // the front door is a module since task 1003322 (ADR 0218) — one constant, so the path moves once
18
+ const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
19
+ const CSS = path.join(LANDING, 'assets', 'cosmos.css');
20
20
  const src = fs.readFileSync(CSS, 'utf8');
21
21
 
22
22
  // The two sheets that actually ship, comment-stripped. Several static guards
@@ -76,7 +76,7 @@ const darkMedia = declarations(blockAfter(':root:not([data-mode="light"]){'));
76
76
  // The dark blocks are :root[data-mode="dark"] and :root:not([data-mode="light"]) —
77
77
  // both (0,2,0) — so they outrank the pack's (0,1,0) and win back.
78
78
  const packColors = JSON.parse(
79
- fs.readFileSync(path.join(path.dirname(CSS), '..', '..', 'config', 'branding.neutral.json'), 'utf8')
79
+ fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8')
80
80
  ).theme.ui.colors;
81
81
 
82
82
  // mirrors COLOR_VARS in src/bongos/serve-internal.js
package/tests/fitness.mjs CHANGED
@@ -660,6 +660,26 @@ test('identity gate skips comments + instance-injected tokens (no false positive
660
660
  assert.equal(scanPromptIdentity('x.html', '<title>{{worldName}} — the gate</title>').length, 0, 'a {{worldName}} token carries no literal');
661
661
  });
662
662
 
663
+ // task 1003322 / ADR 0218 — the front door became modules/public-landing/, which put
664
+ // the landing pages under this gate for the first time. Its apex-only regions name
665
+ // the projects HOSTED on the hub; the server deletes them on every non-hub instance
666
+ // (stripApexOnly), so they cannot produce the failure this gate names — "a non-OTB
667
+ // instance renders ITS OWN brand". Everything outside the sentinels still fails.
668
+ test('identity gate: an apex-only region is exempt, and only that region (ADR 0218)', () => {
669
+ const { scanPromptIdentity } = fitness;
670
+ const inside = '<ul>\n<!--apex-only:start-->\n<li>Off the Boats</li>\n<!--apex-only:end-->\n</ul>';
671
+ assert.equal(scanPromptIdentity('x.html', inside).length, 0, 'a literal the server strips on a child instance is exempt');
672
+
673
+ const outside = '<h1>Off the Boats</h1>\n<!--apex-only:start-->\n<li>ok</li>\n<!--apex-only:end-->';
674
+ const hits = scanPromptIdentity('x.html', outside);
675
+ assert.equal(hits.length, 1, 'a literal OUTSIDE the sentinels still fails — the exemption is the region, not the file');
676
+ assert.match(hits[0], /^x\.html:1 /, 'blanking preserves line numbers so the violation still points at the real line');
677
+
678
+ // an UNCLOSED start sentinel must not blank the rest of the file into silence
679
+ const unclosed = '<!--apex-only:start-->\n<li>ok</li>\n<h1>Off the Boats</h1>';
680
+ assert.equal(scanPromptIdentity('x.html', unclosed).length, 1, 'an unterminated region exempts nothing');
681
+ });
682
+
663
683
  // task 1002756 — the two holes that let migrate.sh's founder DB default survive:
664
684
  // (1) the stripper could be OPENED by a `/*` inside a `//` line comment (a real
665
685
  // `*/` hundreds of lines later then swallowed everything between — provision.js
@@ -22,7 +22,8 @@ import { createRequire } from 'node:module';
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
24
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
- const LANDING = path.join(ROOT, 'public-landing');
25
+ // the front door is a module since task 1003322 (ADR 0218) — one constant, so the path moves once
26
+ const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
26
27
  const read = (f) => fs.readFileSync(path.join(LANDING, f), 'utf8');
27
28
 
28
29
  const platformMod = require('../src/platform-server.js');
@@ -40,8 +40,10 @@ import { createRequire } from 'node:module';
40
40
 
41
41
  const require = createRequire(import.meta.url);
42
42
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
43
- const HTML = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
44
- const WORLD = fs.readFileSync(path.join(ROOT, 'public-landing', 'assets', 'world.css'), 'utf8');
43
+ // the front door is a module since task 1003322 (ADR 0218) — one constant, so the path moves once
44
+ const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
45
+ const HTML = fs.readFileSync(path.join(LANDING, 'index.html'), 'utf8');
46
+ const WORLD = fs.readFileSync(path.join(LANDING, 'assets', 'world.css'), 'utf8');
45
47
 
46
48
  const { stripApexOnly } = require('../src/platform-server.js');
47
49
  const { applyBrandTokens } = require('../src/bongos/serve-internal.js');
@@ -186,8 +188,8 @@ test('the social card is the world card, stamped onto THIS instance', () => {
186
188
  assert.equal(m[1], 'https://spike-harbor.example/assets/world/og-card.jpg');
187
189
  }
188
190
  assert.ok(
189
- fs.existsSync(path.join(ROOT, 'public-landing', 'assets', 'world', 'og-card.jpg')),
190
- 'the og card the head names must exist under public-landing/',
191
+ fs.existsSync(path.join(LANDING, 'assets', 'world', 'og-card.jpg')),
192
+ 'the og card the head names must exist under the landing surface',
191
193
  );
192
194
  });
193
195
 
@@ -210,7 +212,7 @@ test('a pack with no hall of its own falls back to its own apex, not the platfor
210
212
 
211
213
  test('a pack with NO domains drops the hall pill server-side rather than stamping href="" (task 1003242)', () => {
212
214
  // the hub has no client scrubber, so an empty token must never reach it as a dead link
213
- const HUB = fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.html'), 'utf8');
215
+ const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8');
214
216
  assert.match(HUB, /data-link-hall href="\{\{buildersOrigin\}\}"/, 'the hub pill is token-stamped');
215
217
  const none = applyBrandTokens(HUB, BRAND, '', '');
216
218
  assert.doesNotMatch(none, /href=""/, 'no empty href survives the stamp');
@@ -100,7 +100,7 @@ test('provisioning + public-landing joined the roster (task 1002690) — territo
100
100
  const prov = scope.globsForModules(['provisioning']);
101
101
  assert.ok(prov.includes('modules/provisioning/'), 'provisioning owns its module directory');
102
102
  assert.ok(prov.includes('scripts/gds/provision.js'), 'the control-plane runner travels with its domain');
103
- assert.ok(scope.globsForModules(['public-landing']).includes('public-landing/'), 'public-landing owns its surface directory');
103
+ assert.ok(scope.globsForModules(['public-landing']).includes('modules/public-landing/'), 'public-landing owns its surface directory');
104
104
  });
105
105
 
106
106
  test('platform-identity joined the roster (task 1002322) — the catalog territory goal 1000046 walls', () => {
@@ -127,7 +127,7 @@ test('platform-identity joined the roster (task 1002322) — the catalog territo
127
127
  'modules/platform-identity/routes/projects.js', // the feed + invite routes
128
128
  'modules/status-ui/public/explore.js', // orb render-type surface
129
129
  'modules/hall-ui/public/my-projects.js', // owner settings / applications queue
130
- 'public-landing/projects.html', // the creation step, ?view=new since task 1002763
130
+ 'modules/public-landing/public/projects.html', // the creation step, ?view=new since task 1002763
131
131
  'modules/provisioning/', // module-bundle selection at provision time
132
132
  ]) {
133
133
  assert.equal(covers(touch), true, `wall does not cover ${touch}`);
@@ -179,6 +179,9 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
179
179
  // And ui-design (task 1003324 / ADR 0197) — the design contract, its adapters
180
180
  // and the /design playbook as a platform capability: declaration-only (three
181
181
  // skills, no routes/port), default-on because every instance has a face.
182
+ // And public-landing (task 1003322 / ADR 0218) — the front door, the THIRD web
183
+ // surface after status-ui and hall-ui and the first to declare apexRoot instead
184
+ // of a Host prefix; default-on because every instance answers at its own apex.
182
185
  // All are discovered; none has validation
183
186
  // errors or coreVersion mismatches. Order is the readdir sort.
184
187
  //
@@ -203,7 +206,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
203
206
  // That is precisely how the agents regression reached an open PR (task 1002485).
204
207
  loader._reset();
205
208
  const snap = loader.loaderSnapshot();
206
- assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui', 'ui-design']);
209
+ assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'public-landing', 'security', 'sessions', 'status-ui', 'ui-design']);
207
210
  assert.deepEqual(snap.errors, []);
208
211
  assert.deepEqual(snap.skipped, []);
209
212
  loader._reset();
package/tests/onboard.mjs CHANGED
@@ -64,7 +64,7 @@ test('every form OFFERS the optional step and names what skipping costs — skil
64
64
  assert.match(cli, /off the public map/, 'and says what a missing description means');
65
65
 
66
66
  // the web form
67
- const hub = read('public-landing/projects.html');
67
+ const hub = read('modules/public-landing/public/projects.html');
68
68
  assert.match(hub, /id="panel-detail"/, 'the wizard ships the panel');
69
69
  assert.match(hub, /id="skipDetail"/, 'with a skip control of its own');
70
70
 
@@ -168,7 +168,7 @@ test('every render surface carries the terminal hall step (task 1002703)', () =>
168
168
  const cli = read('scripts/gds/onboard.js');
169
169
  assert.match(cli, /builders hall/i, 'the CLI walkthrough ends at the hall');
170
170
  assert.match(cli, /bongos dev/, 'the CLI offers the local-hall one-liner when no domain');
171
- const hubHtml = read('public-landing/projects.html');
171
+ const hubHtml = read('modules/public-landing/public/projects.html');
172
172
  const enterHall = hubHtml.match(/data-onboard-step="enter-hall"[^]*?<\/li>/g) || [];
173
173
  assert.ok(enterHall.length >= 1, 'the ?view=new wizard tags the enter-hall step');
174
174
  assert.match(enterHall.join(''), /\/builders/, 'the wizard points at the hall address');
@@ -180,7 +180,7 @@ test('the wizard done panel is plan-driven, manifest-driven, and owner-honest (t
180
180
  // The wizard lives inside the hub page since task 1002763.
181
181
  const { readFileSync } = require('node:fs');
182
182
  const path = require('node:path');
183
- const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/projects.html'), 'utf8');
183
+ const src = readFileSync(path.join(import.meta.dirname, '..', 'modules/public-landing/public/projects.html'), 'utf8');
184
184
  // gap 2 (idea 1000675): the checklist renders FROM the canonical plan endpoint
185
185
  assert.match(src, /provisioning\/onboard-plan/, 'the done panel fetches the canonical plan');
186
186
  // gap 3 (idea 1000676): the ADR 0133 one-click manifest flow has a UI caller again
@@ -203,7 +203,7 @@ test('the wizard gate trust link points at a live artifact, not the dead private
203
203
  // "open source" is the license itself.
204
204
  const { readFileSync } = require('node:fs');
205
205
  const path = require('node:path');
206
- const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/projects.html'), 'utf8');
206
+ const src = readFileSync(path.join(import.meta.dirname, '..', 'modules/public-landing/public/projects.html'), 'utf8');
207
207
  assert.doesNotMatch(src, /Claude-OTB/, 'the dead repo link is gone');
208
208
  assert.match(src, /https:\/\/www\.gnu\.org\/licenses\/agpl-3\.0/, 'the trust link resolves (the AGPL-3.0 license text)');
209
209
  });
@@ -266,7 +266,7 @@ test('the web form (?view=new) declares EXACTLY the canonical human steps — dr
266
266
  const path = require('node:path');
267
267
  const humanKeys = ob.onboardPlan().filter((s) => s.kind === 'human').map((s) => s.key).sort();
268
268
  const surfaces = {
269
- '/projects?view=new wizard': 'public-landing/projects.html',
269
+ '/projects?view=new wizard': 'modules/public-landing/public/projects.html',
270
270
  };
271
271
  for (const [name, rel] of Object.entries(surfaces)) {
272
272
  const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');