@nanobpm/urban 0.32.1 → 0.32.2
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.
- package/dist/runtime/core/modules/api.js +24 -4
- package/dist/runtime/core/modules/pages.js +12 -1
- package/package.json +1 -1
- package/src/runtime/core/modules/api.test.ts +7 -4
- package/src/runtime/core/modules/api.ts +24 -4
- package/src/runtime/core/modules/pages.test.ts +24 -1
- package/src/runtime/core/modules/pages.ts +13 -1
|
@@ -156,6 +156,25 @@ export function mountApi(ctx, app) {
|
|
|
156
156
|
const dir = binding.dir ?? "operations";
|
|
157
157
|
const surfaceEject = binding.eject === true;
|
|
158
158
|
const specRoutePath = `${docsBase}/openapi.json`;
|
|
159
|
+
// The Swagger UI page is served at `docsBase` (no trailing slash — the slash variant 308-
|
|
160
|
+
// redirects onto it), so its browser-relative base is the PARENT directory. Hand Swagger UI a
|
|
161
|
+
// DOCUMENT-relative spec URL and `servers` entry so both rebase onto the app's mount root:
|
|
162
|
+
// correct at the origin root (CLI :3000) AND under the Nano console reverse proxy
|
|
163
|
+
// (/console/app-view/<name>/), where a root-absolute "/app/…" escapes the prefix and hits the
|
|
164
|
+
// console origin instead of the app (issue #151).
|
|
165
|
+
const lastSegment = (p) => p.split("/").filter(Boolean).pop() ?? "";
|
|
166
|
+
const parentPath = (p) => {
|
|
167
|
+
const parts = p.split("/").filter(Boolean);
|
|
168
|
+
parts.pop();
|
|
169
|
+
return `/${parts.join("/")}`;
|
|
170
|
+
};
|
|
171
|
+
// `${docsBase}/openapi.json` reached relatively from the docs page = `<docs-segment>/openapi.json`.
|
|
172
|
+
const specDocUrl = `${lastSegment(docsBase)}/openapi.json`;
|
|
173
|
+
// `servers` can only be relativized when the operations `base` shares the docs page's parent
|
|
174
|
+
// directory (the default `${base}-docs` sibling). For a fully custom `docs` path under a
|
|
175
|
+
// different parent, keep the root-absolute base (works direct; try-it-out through the proxy is
|
|
176
|
+
// the documented limitation in #151).
|
|
177
|
+
const serversUrl = parentPath(base) === parentPath(docsBase) ? lastSegment(base) : base;
|
|
159
178
|
// Resolve + parse the spec lazily on first request and cache it (mirroring the lazy module load
|
|
160
179
|
// pattern). A malformed/unreadable spec surfaces as a 500 on request — with the reason — rather
|
|
161
180
|
// than failing app boot.
|
|
@@ -427,12 +446,13 @@ export function mountApi(ctx, app) {
|
|
|
427
446
|
return json({ error: errorMessage(e) }, 500);
|
|
428
447
|
}
|
|
429
448
|
// Point Swagger UI "Try it out" at the mounted namespace: operations live under `base`
|
|
430
|
-
// (e.g. /app/api/invoices), not the spec's bare paths (/invoices). Overriding `servers`
|
|
431
|
-
// makes the interactive console call the real Urban routes
|
|
432
|
-
|
|
449
|
+
// (e.g. /app/api/invoices), not the spec's bare paths (/invoices). Overriding `servers` with a
|
|
450
|
+
// document-relative URL (issue #151) makes the interactive console call the real Urban routes
|
|
451
|
+
// both at the origin root and under the Nano console reverse proxy.
|
|
452
|
+
return json({ ...d, servers: [{ url: serversUrl }] });
|
|
433
453
|
};
|
|
434
454
|
const docsTitle = `${ctx.manifest.name ?? "App"} — API docs`;
|
|
435
|
-
const serveDocs = () => html(swaggerUiPage(docsTitle,
|
|
455
|
+
const serveDocs = () => html(swaggerUiPage(docsTitle, specDocUrl));
|
|
436
456
|
// A permanent redirect for the trailing-slash variant (`${docsBase}/`). The router only does
|
|
437
457
|
// exact matches for non-prefix routes, so without this a reverse proxy or browser that appends
|
|
438
458
|
// a slash would 404 on the Swagger UI. 308 preserves the method and points at the canonical
|
|
@@ -369,8 +369,19 @@ function rendererShell(homePage, apiDocsPath) {
|
|
|
369
369
|
// A persistent "API docs" badge (spec-first apps get their Swagger UI linked from their own
|
|
370
370
|
// UI for free). `target="_blank"` + hardened `rel` so the docs open without giving the docs
|
|
371
371
|
// tab a handle back to this window (reverse-tabnabbing).
|
|
372
|
+
//
|
|
373
|
+
// The href must be DOCUMENT-relative, exactly like the `./app/runtime.js` script tag below:
|
|
374
|
+
// the shell is served at the app's mount root (always a trailing "/"), which is the origin
|
|
375
|
+
// root for a direct run (CLI on :3000) but a path prefix under the Nano console's reverse
|
|
376
|
+
// proxy (/console/app-view/<name>/). A root-absolute "/app/api-docs" would escape that prefix
|
|
377
|
+
// and open the CONSOLE origin (e.g. :8080/app/api-docs) instead of the app's docs. Strip the
|
|
378
|
+
// single leading slash so it rebases onto the mount root; leave a protocol-relative "//host"
|
|
379
|
+
// (which apiDocsPath never emits) untouched.
|
|
380
|
+
const badgeHref = apiDocsPath && apiDocsPath.startsWith("/") && !apiDocsPath.startsWith("//")
|
|
381
|
+
? `./${apiDocsPath.slice(1)}`
|
|
382
|
+
: apiDocsPath;
|
|
372
383
|
const apiDocsBadge = apiDocsPath
|
|
373
|
-
? `\n <a class="pc-apidocs" href="${escapeAttr(
|
|
384
|
+
? `\n <a class="pc-apidocs" href="${escapeAttr(badgeHref)}" target="_blank" rel="noopener noreferrer">API docs \u2197</a>`
|
|
374
385
|
: "";
|
|
375
386
|
return `<!doctype html>
|
|
376
387
|
<html lang="en">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.2",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -458,9 +458,10 @@ test("serves the OpenAPI spec at {base}-docs/openapi.json, rebased to the mounte
|
|
|
458
458
|
const doc = JSON.parse(res.body ?? "{}");
|
|
459
459
|
// The doc is served intact...
|
|
460
460
|
assert.ok(doc.paths["/invoices"], "keeps the operation paths");
|
|
461
|
-
// ...but `servers` is rebased to `base` so Swagger UI "Try it
|
|
461
|
+
// ...but `servers` is rebased to `base` (document-relative, issue #151) so Swagger UI "Try it
|
|
462
|
+
// out" hits the real routes both at the origin root and under the Nano console reverse proxy
|
|
462
463
|
// (operations mount under /app/api, not the spec's bare /invoices).
|
|
463
|
-
assert.deepEqual(doc.servers, [{ url: "
|
|
464
|
+
assert.deepEqual(doc.servers, [{ url: "api" }]);
|
|
464
465
|
});
|
|
465
466
|
|
|
466
467
|
test("serves the Swagger UI page at {base}-docs by default", async () => {
|
|
@@ -470,8 +471,10 @@ test("serves the Swagger UI page at {base}-docs by default", async () => {
|
|
|
470
471
|
assert.match(res.headers?.["content-type"] ?? "", /text\/html/);
|
|
471
472
|
const body = res.body ?? "";
|
|
472
473
|
assert.match(body, /swagger-ui/);
|
|
473
|
-
// The UI reads the spec
|
|
474
|
-
|
|
474
|
+
// The UI reads the spec via a DOCUMENT-relative URL (issue #151) so it resolves through the
|
|
475
|
+
// Nano console reverse proxy, not a root-absolute path that escapes to the console origin.
|
|
476
|
+
assert.match(body, /url: "api-docs\/openapi\.json"/);
|
|
477
|
+
assert.doesNotMatch(body, /url: "\/app\/api-docs\/openapi\.json"/);
|
|
475
478
|
});
|
|
476
479
|
|
|
477
480
|
test("the trailing-slash docs variant 308-redirects to the canonical path", async () => {
|
|
@@ -252,6 +252,25 @@ export function mountApi(ctx: RuntimeContext, app: AppApi): ApiHandle {
|
|
|
252
252
|
const dir = binding.dir ?? "operations";
|
|
253
253
|
const surfaceEject = binding.eject === true;
|
|
254
254
|
const specRoutePath = `${docsBase}/openapi.json`;
|
|
255
|
+
// The Swagger UI page is served at `docsBase` (no trailing slash — the slash variant 308-
|
|
256
|
+
// redirects onto it), so its browser-relative base is the PARENT directory. Hand Swagger UI a
|
|
257
|
+
// DOCUMENT-relative spec URL and `servers` entry so both rebase onto the app's mount root:
|
|
258
|
+
// correct at the origin root (CLI :3000) AND under the Nano console reverse proxy
|
|
259
|
+
// (/console/app-view/<name>/), where a root-absolute "/app/…" escapes the prefix and hits the
|
|
260
|
+
// console origin instead of the app (issue #151).
|
|
261
|
+
const lastSegment = (p: string): string => p.split("/").filter(Boolean).pop() ?? "";
|
|
262
|
+
const parentPath = (p: string): string => {
|
|
263
|
+
const parts = p.split("/").filter(Boolean);
|
|
264
|
+
parts.pop();
|
|
265
|
+
return `/${parts.join("/")}`;
|
|
266
|
+
};
|
|
267
|
+
// `${docsBase}/openapi.json` reached relatively from the docs page = `<docs-segment>/openapi.json`.
|
|
268
|
+
const specDocUrl = `${lastSegment(docsBase)}/openapi.json`;
|
|
269
|
+
// `servers` can only be relativized when the operations `base` shares the docs page's parent
|
|
270
|
+
// directory (the default `${base}-docs` sibling). For a fully custom `docs` path under a
|
|
271
|
+
// different parent, keep the root-absolute base (works direct; try-it-out through the proxy is
|
|
272
|
+
// the documented limitation in #151).
|
|
273
|
+
const serversUrl = parentPath(base) === parentPath(docsBase) ? lastSegment(base) : base;
|
|
255
274
|
|
|
256
275
|
// Resolve + parse the spec lazily on first request and cache it (mirroring the lazy module load
|
|
257
276
|
// pattern). A malformed/unreadable spec surfaces as a 500 on request — with the reason — rather
|
|
@@ -538,12 +557,13 @@ export function mountApi(ctx: RuntimeContext, app: AppApi): ApiHandle {
|
|
|
538
557
|
return json({ error: errorMessage(e) }, 500);
|
|
539
558
|
}
|
|
540
559
|
// Point Swagger UI "Try it out" at the mounted namespace: operations live under `base`
|
|
541
|
-
// (e.g. /app/api/invoices), not the spec's bare paths (/invoices). Overriding `servers`
|
|
542
|
-
// makes the interactive console call the real Urban routes
|
|
543
|
-
|
|
560
|
+
// (e.g. /app/api/invoices), not the spec's bare paths (/invoices). Overriding `servers` with a
|
|
561
|
+
// document-relative URL (issue #151) makes the interactive console call the real Urban routes
|
|
562
|
+
// both at the origin root and under the Nano console reverse proxy.
|
|
563
|
+
return json({ ...d, servers: [{ url: serversUrl }] });
|
|
544
564
|
};
|
|
545
565
|
const docsTitle = `${ctx.manifest.name ?? "App"} — API docs`;
|
|
546
|
-
const serveDocs = (): HttpResponse => html(swaggerUiPage(docsTitle,
|
|
566
|
+
const serveDocs = (): HttpResponse => html(swaggerUiPage(docsTitle, specDocUrl));
|
|
547
567
|
// A permanent redirect for the trailing-slash variant (`${docsBase}/`). The router only does
|
|
548
568
|
// exact matches for non-prefix routes, so without this a reverse proxy or browser that appends
|
|
549
569
|
// a slash would 404 on the Swagger UI. 308 preserves the method and points at the canonical
|
|
@@ -98,6 +98,29 @@ test("GET / serves the renderer shell with the home marker", async () => {
|
|
|
98
98
|
assert.doesNotMatch(res.body ?? "", /src="\/app\/runtime\.js"/);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
test("the API docs badge links via a document-relative href (proxy-safe)", async () => {
|
|
102
|
+
// With an `api` binding, the shell renders a persistent "API docs" badge. Its
|
|
103
|
+
// href must be DOCUMENT-relative (./app/api-docs), mirroring the runtime.js
|
|
104
|
+
// script tag, so it resolves against the shell's mount root. A root-absolute
|
|
105
|
+
// "/app/api-docs" would escape the Nano console's /console/app-view/<name>/
|
|
106
|
+
// reverse proxy and open the console origin (e.g. :8080/app/api-docs) instead
|
|
107
|
+
// of the app's own docs — the port-mismatch bug this guards against.
|
|
108
|
+
const { engine } = fakeEngine();
|
|
109
|
+
const routes = createPagesRoutes(
|
|
110
|
+
{ pagesDir: "pages", homePage: "home", sourceName: "app", apiDocsPath: "/app/api-docs" },
|
|
111
|
+
{
|
|
112
|
+
db: fakeDb(),
|
|
113
|
+
engine,
|
|
114
|
+
readPage: async () => JSON.stringify({ title: "Home", nodes: [] }),
|
|
115
|
+
listPages: async () => ["home"],
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
const res = await makeRouter(routes)(req("GET", "/"));
|
|
119
|
+
assert.match(res.body ?? "", /class="pc-apidocs"/);
|
|
120
|
+
assert.match(res.body ?? "", /href="\.\/app\/api-docs"/);
|
|
121
|
+
assert.doesNotMatch(res.body ?? "", /href="\/app\/api-docs"/);
|
|
122
|
+
});
|
|
123
|
+
|
|
101
124
|
test("the shell styles the app through the shared --nano-* token contract", async () => {
|
|
102
125
|
const res = await dispatch("GET", "/");
|
|
103
126
|
const html = res.body ?? "";
|
|
@@ -129,7 +152,7 @@ test("the shell renders an API-docs badge only when an apiDocsPath is provided",
|
|
|
129
152
|
);
|
|
130
153
|
const withBadge = (await makeRouter(routes)(req("GET", "/"))).body ?? "";
|
|
131
154
|
assert.match(withBadge, /class="pc-apidocs"/);
|
|
132
|
-
assert.match(withBadge, /href="
|
|
155
|
+
assert.match(withBadge, /href="\.\/app\/api-docs"/);
|
|
133
156
|
// Opens in a new tab with a hardened rel (no reverse-tabnabbing handle).
|
|
134
157
|
assert.match(withBadge, /rel="noopener noreferrer"/);
|
|
135
158
|
|
|
@@ -414,8 +414,20 @@ function rendererShell(homePage: string, apiDocsPath?: string): string {
|
|
|
414
414
|
// A persistent "API docs" badge (spec-first apps get their Swagger UI linked from their own
|
|
415
415
|
// UI for free). `target="_blank"` + hardened `rel` so the docs open without giving the docs
|
|
416
416
|
// tab a handle back to this window (reverse-tabnabbing).
|
|
417
|
+
//
|
|
418
|
+
// The href must be DOCUMENT-relative, exactly like the `./app/runtime.js` script tag below:
|
|
419
|
+
// the shell is served at the app's mount root (always a trailing "/"), which is the origin
|
|
420
|
+
// root for a direct run (CLI on :3000) but a path prefix under the Nano console's reverse
|
|
421
|
+
// proxy (/console/app-view/<name>/). A root-absolute "/app/api-docs" would escape that prefix
|
|
422
|
+
// and open the CONSOLE origin (e.g. :8080/app/api-docs) instead of the app's docs. Strip the
|
|
423
|
+
// single leading slash so it rebases onto the mount root; leave a protocol-relative "//host"
|
|
424
|
+
// (which apiDocsPath never emits) untouched.
|
|
425
|
+
const badgeHref =
|
|
426
|
+
apiDocsPath && apiDocsPath.startsWith("/") && !apiDocsPath.startsWith("//")
|
|
427
|
+
? `./${apiDocsPath.slice(1)}`
|
|
428
|
+
: apiDocsPath;
|
|
417
429
|
const apiDocsBadge = apiDocsPath
|
|
418
|
-
? `\n <a class="pc-apidocs" href="${escapeAttr(
|
|
430
|
+
? `\n <a class="pc-apidocs" href="${escapeAttr(badgeHref!)}" target="_blank" rel="noopener noreferrer">API docs \u2197</a>`
|
|
419
431
|
: "";
|
|
420
432
|
return `<!doctype html>
|
|
421
433
|
<html lang="en">
|