@nanobpm/urban 0.32.0 → 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.
@@ -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 rather than the origin root.
432
- return json({ ...d, servers: [{ url: base }] });
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, specRoutePath));
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(apiDocsPath)}" target="_blank" rel="noopener noreferrer">API docs \u2197</a>`
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">
@@ -686,12 +697,21 @@ function renderActionForm(node) {
686
697
  msg.textContent = "Field '" + p.action.correlationKeyField + "' is required";
687
698
  return;
688
699
  }
700
+ // Send the trimmed key back as the variable too, so the value a worker
701
+ // reads matches the key the message was correlated on (no leading/trailing
702
+ // whitespace divergence between correlationKey and variables[field]).
703
+ variables[p.action.correlationKeyField] = correlationKey;
689
704
  await getJSON("/app/actions/message", { method: "POST",
690
705
  headers: { "content-type": "application/json" },
691
706
  body: JSON.stringify({ name: p.action.message, correlationKey, variables }) });
692
707
  msg.className = "pc-msg ok";
693
708
  msg.textContent = "Message published";
694
709
  } else {
710
+ if (!p.action || !p.action.process) {
711
+ msg.className = "pc-msg err";
712
+ msg.textContent = "This form has no action configured";
713
+ return;
714
+ }
695
715
  const res = await getJSON("/app/actions/start/" + encodeURIComponent(p.action.process),
696
716
  { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ variables }) });
697
717
  msg.className = "pc-msg ok";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.32.0",
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 out" hits the real routes
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: "/app/api" }]);
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 route we serve.
474
- assert.match(body, /\/app\/api-docs\/openapi\.json/);
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 rather than the origin root.
543
- return json({ ...d, servers: [{ url: base }] });
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, specRoutePath));
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="\/app\/api-docs"/);
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
 
@@ -146,8 +169,14 @@ test("renderActionForm supports a publishMessage action (message-started process
146
169
  assert.match(js, /p\.action\.kind === "publishMessage"/);
147
170
  assert.match(js, /variables\[p\.action\.correlationKeyField\]/);
148
171
  assert.match(js, /"\/app\/actions\/message"/);
172
+ // The trimmed correlation key is written back as the variable, so the value a
173
+ // worker reads matches the key the message was correlated on.
174
+ assert.match(js, /variables\[p\.action\.correlationKeyField\] = correlationKey/);
149
175
  // startProcess remains the default/back-compat branch.
150
176
  assert.match(js, /"\/app\/actions\/start\/" \+ encodeURIComponent\(p\.action\.process\)/);
177
+ // A missing action is reported clearly instead of throwing on p.action.process.
178
+ assert.match(js, /if \(!p\.action \|\| !p\.action\.process\)/);
179
+ assert.match(js, /no action configured/);
151
180
  });
152
181
 
153
182
  test("the renderer bridges the console theme over postMessage", async () => {
@@ -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(apiDocsPath)}" target="_blank" rel="noopener noreferrer">API docs \u2197</a>`
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">
@@ -733,12 +745,21 @@ function renderActionForm(node) {
733
745
  msg.textContent = "Field '" + p.action.correlationKeyField + "' is required";
734
746
  return;
735
747
  }
748
+ // Send the trimmed key back as the variable too, so the value a worker
749
+ // reads matches the key the message was correlated on (no leading/trailing
750
+ // whitespace divergence between correlationKey and variables[field]).
751
+ variables[p.action.correlationKeyField] = correlationKey;
736
752
  await getJSON("/app/actions/message", { method: "POST",
737
753
  headers: { "content-type": "application/json" },
738
754
  body: JSON.stringify({ name: p.action.message, correlationKey, variables }) });
739
755
  msg.className = "pc-msg ok";
740
756
  msg.textContent = "Message published";
741
757
  } else {
758
+ if (!p.action || !p.action.process) {
759
+ msg.className = "pc-msg err";
760
+ msg.textContent = "This form has no action configured";
761
+ return;
762
+ }
742
763
  const res = await getJSON("/app/actions/start/" + encodeURIComponent(p.action.process),
743
764
  { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ variables }) });
744
765
  msg.className = "pc-msg ok";