@nanobpm/urban 0.76.0 → 0.77.1

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.
@@ -11,7 +11,8 @@
11
11
  // GET / (+ /app/runtime.js) → the schema-driven browser renderer
12
12
  // GET /app/pages/<id> → the page's page.json
13
13
  // GET /app/data/<source>/<table>[?where&order] → rows (filtered/ordered, whitelisted)
14
- // GET /{embed,standalone}.html /mount.js /cockpit.css → appView sidecars (from pagesDir, #420)
14
+ // GET /{embed,standalone}.html /mount.js /cockpit.css → flat appView sidecars (from pagesDir, #420)
15
+ // GET /<subdir>/<asset> → nested appView sidecars (pages/<subdir>/…, #442)
15
16
  // GET /dist/<path> → the app dist/ bundle the sidecars reference
16
17
  // POST /app/actions/start/<process> → engine.createInstance
17
18
  // POST /app/actions/cancel → engine.cancelInstance
@@ -45,7 +46,10 @@ function javascript(body, status = 200, extraHeaders) {
45
46
  // document then pulls `./cockpit.css`, `./mount.js`, and (via its import-map)
46
47
  // `../dist/…`. A DEPLOYED app served none of these, so the src 404'd and the cockpit
47
48
  // iframe loaded nothing (#416's user-facing "nothing shows in the Cockpit"). These are
48
- // the root-served sidecar files, mapped 1:1 onto `pagesDir/<name>`. The `dist/` tree the
49
+ // the root-served sidecar files, mapped 1:1 onto `pagesDir/<name>`. An app that ships
50
+ // MORE than one `appView` gives each its own SUBDIR sidecar (`./cockpit/embed.html`,
51
+ // `./board/embed.html`, …) — those are served by the nested prefix route below (#442);
52
+ // this flat set stays as the single-appView back-compat case (#420). The `dist/` tree the
49
53
  // import-map references is served separately under the `/dist/` prefix below.
50
54
  const SIDECAR_ASSETS = ["embed.html", "standalone.html", "mount.js", "cockpit.css"];
51
55
  // The app `dist/` directory (app-root sibling of the pages dir) the sidecar import-maps
@@ -485,6 +489,45 @@ export function createPagesRoutes(opts, deps) {
485
489
  }
486
490
  },
487
491
  });
492
+ // ── nested (multi-appView) sidecars (#442) ──────────────────────────────
493
+ // An app with more than one `appView` cannot share the four FLAT sidecar names above:
494
+ // each view ships its own SUBDIR sidecar — e.g. `pages/cockpit.page.json` →
495
+ // `./cockpit/embed.html`, `pages/board.page.json` → `./board/embed.html`. The renderer's
496
+ // `baseRelativeUrl(p.embed)` rebases those onto the mount root, so the browser fetches
497
+ // `/cockpit/embed.html`, `/board/mount.js`, … — every one of which #420's flat 4-name
498
+ // serve 404'd. Serve the `pagesDir` as a TREE, mapping `/<subpath>` onto
499
+ // `pagesDir/<subpath>` with the SAME traversal guard as the `/dist/` route (mirroring it).
500
+ // It is a `/`-prefix route, so to avoid a root catch-all silently swallowing `/healthz`,
501
+ // the app's own trigger routes, or any other route, the handler DECLINES (returns
502
+ // undefined → the router falls through to the next route, see `RouteHandler`) whenever it
503
+ // cannot serve a real nested asset:
504
+ // • a bare single-segment path (`/healthz`, the flat sidecars) — owned by its own exact
505
+ // route / the runtime's liveness route, never this tree;
506
+ // • an unsafe subpath (`..`/absolute/percent-escaped/backslash) — traversal-rejected
507
+ // before it reaches the filesystem;
508
+ // • a miss (no such file) — so a same-path route registered AFTER the pages surface (a
509
+ // trigger, the appended `/healthz`) still resolves instead of being masked by a 404.
510
+ // Registered LAST among the pages routes so every exact/prefix pages route above wins first.
511
+ routes.push({
512
+ method: "GET",
513
+ path: "/",
514
+ prefix: true,
515
+ source: "surface:pages",
516
+ handler: async (req) => {
517
+ const rest = req.path.slice(1); // drop the leading "/"
518
+ // Only a SUBDIR asset (has a `/`) belongs to this tree; a single-segment path is left
519
+ // to its own exact route / the appended `/healthz` liveness route.
520
+ if (!rest.includes("/"))
521
+ return undefined;
522
+ const safe = safeAssetSubpath(rest);
523
+ if (safe === null)
524
+ return undefined; // traversal/escape → decline (never touch the FS)
525
+ const res = await serveAsset(readAsset, `${pagesDir}/${safe}`);
526
+ // A miss declines rather than 404s, so a later same-path route (e.g. a trigger) can
527
+ // still claim it; the router's own default 404 covers a genuinely absent asset.
528
+ return res.status === 404 ? undefined : res;
529
+ },
530
+ });
488
531
  return routes;
489
532
  }
490
533
  /**