@betterportal/config-manager 10.0.31 → 10.1.0

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 (47) hide show
  1. package/lib/.bsb/clients/service-betterportal-config-manager.d.ts +1 -1
  2. package/lib/.bsb/clients/service-betterportal-config-manager.js +1 -1
  3. package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -1
  4. package/lib/plugins/service-betterportal-config-manager/adminApi.js +97 -30
  5. package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -1
  6. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts.map +1 -1
  7. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js +13 -12
  8. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js.map +1 -1
  9. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/GET.d.ts.map +1 -1
  10. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/GET.js +121 -6
  11. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/GET.js.map +1 -1
  12. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/GET.d.ts.map +1 -1
  13. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/GET.js +36 -2
  14. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/GET.js.map +1 -1
  15. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.d.ts +6 -0
  16. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.d.ts.map +1 -1
  17. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.js +3 -0
  18. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/route.impl.js.map +1 -1
  19. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/GET.d.ts.map +1 -1
  20. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/GET.js +114 -31
  21. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/GET.js.map +1 -1
  22. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.d.ts.map +1 -1
  23. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.js +9 -4
  24. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.js.map +1 -1
  25. package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -1
  26. package/lib/plugins/service-betterportal-config-manager/index.js +28 -8
  27. package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
  28. package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts.map +1 -1
  29. package/lib/plugins/service-betterportal-config-manager/menuEditor.js +7 -1
  30. package/lib/plugins/service-betterportal-config-manager/menuEditor.js.map +1 -1
  31. package/lib/plugins/service-betterportal-config-manager/routeMounts.d.ts +8 -0
  32. package/lib/plugins/service-betterportal-config-manager/routeMounts.d.ts.map +1 -0
  33. package/lib/plugins/service-betterportal-config-manager/routeMounts.js +22 -0
  34. package/lib/plugins/service-betterportal-config-manager/routeMounts.js.map +1 -0
  35. package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts.map +1 -1
  36. package/lib/plugins/service-betterportal-config-manager/setupTokens.js +50 -0
  37. package/lib/plugins/service-betterportal-config-manager/setupTokens.js.map +1 -1
  38. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -1
  39. package/lib/plugins/service-betterportal-config-manager/storage/core.js +25 -0
  40. package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -1
  41. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +2 -1
  42. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
  43. package/lib/plugins/service-betterportal-config-manager/syncApi.js +56 -1
  44. package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -1
  45. package/lib/schemas/service-betterportal-config-manager.json +1 -1
  46. package/lib/schemas/service-betterportal-config-manager.plugin.json +1 -1
  47. package/package.json +3 -3
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Auto-generated BSB virtual client for service-betterportal-config-manager
3
3
  * DO NOT EDIT - Regenerated on every build
4
- * @version 10.0.31
4
+ * @version 10.1.0
5
5
  */
6
6
  import { ServiceClient, BSBService } from "@bsb/base";
7
7
  import type { Observable, BSBServiceClientDefinition, EventInputType } from "@bsb/base";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Auto-generated BSB virtual client for service-betterportal-config-manager
3
3
  * DO NOT EDIT - Regenerated on every build
4
- * @version 10.0.31
4
+ * @version 10.1.0
5
5
  */
6
6
  import { ServiceClient, bsb, createBroadcastEvent, createEventSchemas } from "@bsb/base";
7
7
  const _platformConfigChangedSchema = bsb.object({
@@ -1 +1 @@
1
- {"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAEjB,mBAAmB,EAEpB,MAAM,yBAAyB,CAAC;AAUjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AA4vBzD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,gBAAgB,GACxB,IAAI,CAwkCN"}
1
+ {"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAEjB,mBAAmB,EAEpB,MAAM,yBAAyB,CAAC;AAUjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAmyBzD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,gBAAgB,GACxB,IAAI,CAgnCN"}
@@ -1,6 +1,7 @@
1
1
  import { eventHeaders, htmlResponse, jsonResponse, resolveEmbeddedRequestContext, signServiceConfigTicket, uuidv7 } from "@betterportal/framework";
2
2
  import { generateApiKey, hashApiKey } from "./storage/index.js";
3
3
  import { getManifestCache } from "./syncApi.js";
4
+ import { apiRoutePath, isApiRoute } from "./routeMounts.js";
4
5
  const API_BASE = "/.well-known/bp/admin";
5
6
  const CONFIG_TICKET_TTL_SECONDS = 5 * 60;
6
7
  // Parse-only base for relative request URLs. Never emit this origin.
@@ -14,6 +15,24 @@ async function readFormBody(event) {
14
15
  out[k] = v; });
15
16
  return out;
16
17
  }
18
+ async function readFormBodyWithArrays(event) {
19
+ const fd = await event.req.formData().catch(() => null);
20
+ if (!fd)
21
+ return {};
22
+ const out = {};
23
+ fd.forEach((v, k) => {
24
+ if (typeof v !== "string")
25
+ return;
26
+ const existing = out[k];
27
+ if (Array.isArray(existing))
28
+ existing.push(v);
29
+ else if (typeof existing === "string")
30
+ out[k] = [existing, v];
31
+ else
32
+ out[k] = v;
33
+ });
34
+ return out;
35
+ }
17
36
  function escapeHtml(s) {
18
37
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
19
38
  }
@@ -26,7 +45,7 @@ async function readJsonBody(event) {
26
45
  async function readFormOrJsonBody(event) {
27
46
  const contentType = event.req.headers.get("content-type") ?? "";
28
47
  if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
29
- return readFormBody(event);
48
+ return readFormBodyWithArrays(event);
30
49
  }
31
50
  return readJsonBody(event);
32
51
  }
@@ -59,6 +78,12 @@ function routeMethodsFromManifest(methods) {
59
78
  const normalized = (methods ?? []).filter((method) => method === "GET" || method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE" || method === "OPTIONS");
60
79
  return normalized.length ? normalized : ["GET"];
61
80
  }
81
+ function hasPageRenderer(view) {
82
+ const renderers = view.html?.themeRenderers;
83
+ if (!renderers)
84
+ return view.renderable !== false;
85
+ return Object.values(renderers).some((renderer) => Array.isArray(renderer.renderModes) && renderer.renderModes.includes("page"));
86
+ }
62
87
  function appMatchesTenantUrl(app, tenantUrl) {
63
88
  let host = "";
64
89
  try {
@@ -187,7 +212,9 @@ function parseRouteCreateBody(body) {
187
212
  return { error: viewId.error };
188
213
  const manifestView = getManifestCache().get(serviceId.value)?.viewIndex[viewId.value];
189
214
  const renderable = manifestView?.renderable !== false;
190
- const path = renderable ? requiredRouteString(body, "path", "Mount path") : { value: manifestView?.path ?? `/${viewId.value}` };
215
+ const manifest = getManifestCache().get(serviceId.value);
216
+ const serviceRoutePath = manifestView?.path ?? `/${viewId.value}`;
217
+ const path = renderable ? requiredRouteString(body, "path", "Mount path") : { value: apiRoutePath(manifest?.serviceId ?? serviceId.value, serviceRoutePath) };
191
218
  if (path.error)
192
219
  return { error: path.error };
193
220
  if (!path.value.startsWith("/"))
@@ -196,6 +223,7 @@ function parseRouteCreateBody(body) {
196
223
  if (title.error)
197
224
  return { error: title.error };
198
225
  const route = {
226
+ kind: renderable ? "page" : "api",
199
227
  path: path.value,
200
228
  serviceId: serviceId.value,
201
229
  viewId: viewId.value,
@@ -281,7 +309,8 @@ function addRouteDependencies(appDef, route) {
281
309
  continue;
282
310
  appDef.routes.push({
283
311
  id: uuidv7(),
284
- path: dependency.path,
312
+ kind: "api",
313
+ path: apiRoutePath(manifest.serviceId, dependency.path),
285
314
  serviceId: route.serviceId,
286
315
  viewId: dependencyViewId,
287
316
  targetPath: dependency.path,
@@ -1424,15 +1453,20 @@ export function registerAdminApiRoutes(app, store, cpState) {
1424
1453
  return validationError(event, "View is required.");
1425
1454
  route.viewId = viewId;
1426
1455
  }
1427
- const manifestView = getManifestCache().get(route.serviceId)?.viewIndex[route.viewId];
1456
+ const manifest = getManifestCache().get(route.serviceId);
1457
+ const manifestView = manifest?.viewIndex[route.viewId];
1428
1458
  if (manifestView) {
1429
1459
  route.methods = routeMethodsFromManifest(manifestView.methods);
1430
1460
  route.targetPath = manifestView.path;
1431
1461
  if (manifestView.renderable === false) {
1432
- route.path = manifestView.path;
1462
+ route.kind = "api";
1463
+ route.path = apiRoutePath(manifest?.serviceId ?? route.serviceId, manifestView.path);
1433
1464
  route.title = manifestView.viewId;
1434
1465
  delete route.query;
1435
1466
  }
1467
+ else {
1468
+ route.kind = "page";
1469
+ }
1436
1470
  }
1437
1471
  if (body.targetPath !== undefined) {
1438
1472
  const targetPath = trimmedString(body, "targetPath");
@@ -1443,7 +1477,9 @@ export function registerAdminApiRoutes(app, store, cpState) {
1443
1477
  }
1444
1478
  if (body.query !== undefined) {
1445
1479
  const query = trimmedString(body, "query");
1446
- if (query)
1480
+ if (isApiRoute(route, manifestView?.renderable))
1481
+ delete route.query;
1482
+ else if (query)
1447
1483
  route.query = query.replace(/^\?+/, "");
1448
1484
  else
1449
1485
  delete route.query;
@@ -1452,7 +1488,8 @@ export function registerAdminApiRoutes(app, store, cpState) {
1452
1488
  const title = trimmedString(body, "title");
1453
1489
  if (!title)
1454
1490
  return validationError(event, "Display title is required.");
1455
- route.title = title;
1491
+ if (!isApiRoute(route, manifestView?.renderable))
1492
+ route.title = title;
1456
1493
  }
1457
1494
  if (body.enabled !== undefined)
1458
1495
  route.enabled = body.enabled === true || body.enabled === "true" || body.enabled === "on";
@@ -1530,7 +1567,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1530
1567
  // Browser-mediated: the admin UI fetches the manifest from the service
1531
1568
  // (CM cannot reach services) and POSTs the parsed payload here.
1532
1569
  app.post(`${API_BASE}/wizard/verify`, async (event) => {
1533
- const adminApiBase = adminApiBaseFromControlPlane(cpState);
1570
+ const adminApiBase = API_BASE;
1534
1571
  const form = await readFormBody(event);
1535
1572
  const tenantId = form.tenantId ?? "";
1536
1573
  const hostname = normalizeHostname(form.hostname);
@@ -1568,17 +1605,22 @@ export function registerAdminApiRoutes(app, store, cpState) {
1568
1605
  });
1569
1606
  // HTMX wizard: register service
1570
1607
  app.post(`${API_BASE}/wizard/register`, async (event) => {
1571
- const form = await readFormBody(event);
1572
- const tenantId = form.tenantId ?? "";
1573
- const hostname = (form.hostname ?? "").replace(/\/+$/, "");
1574
- const title = form.title || hostname;
1608
+ const body = await readFormOrJsonBody(event);
1609
+ const jsonMode = (event.req.headers.get("accept") ?? "").includes("application/json")
1610
+ || (event.req.headers.get("content-type") ?? "").includes("application/json");
1611
+ const registerError = (message, status = 400) => jsonMode
1612
+ ? jsonResponse({ error: message }, status)
1613
+ : htmlResponse(`<div class="alert alert-danger">${escapeHtml(message)}</div>`, 200, "text/html; mode=fragment");
1614
+ const tenantId = typeof body.tenantId === "string" ? body.tenantId : "";
1615
+ const hostname = (typeof body.hostname === "string" ? body.hostname : "").replace(/\/+$/, "");
1616
+ const title = (typeof body.title === "string" && body.title.trim().length > 0 ? body.title.trim() : hostname);
1575
1617
  if (!tenantId || !hostname) {
1576
- return htmlResponse(`<div class="alert alert-danger">Missing tenantId or hostname</div>`, 200, "text/html; mode=fragment");
1618
+ return registerError("Missing tenantId or hostname");
1577
1619
  }
1578
1620
  const config = await store.loadConfig();
1579
1621
  const tenant = config.tenants.find((t) => t.id === tenantId);
1580
1622
  if (!tenant) {
1581
- return htmlResponse(`<div class="alert alert-danger">Tenant not found</div>`, 200, "text/html; mode=fragment");
1623
+ return registerError("Tenant not found", 404);
1582
1624
  }
1583
1625
  // Schema is fetched by the browser from the service (CM cannot reach services)
1584
1626
  // and posted to us as a JSON string in form.schema.
@@ -1586,19 +1628,27 @@ export function registerAdminApiRoutes(app, store, cpState) {
1586
1628
  let capabilities = [];
1587
1629
  let serviceRoutes = [];
1588
1630
  let viewMeta = new Map();
1589
- if (form.schema) {
1631
+ const schemaText = typeof body.schema === "string" ? body.schema : "";
1632
+ if (schemaText) {
1590
1633
  try {
1591
- const schema = JSON.parse(form.schema);
1634
+ const schema = JSON.parse(schemaText);
1592
1635
  serviceId = schema.manifest?.pluginId;
1593
1636
  capabilities = Array.isArray(schema.manifest?.capabilities) ? schema.manifest.capabilities.filter((value) => typeof value === "string") : [];
1594
1637
  serviceRoutes = schema.routes ?? [];
1595
- viewMeta = new Map((schema.manifest?.views ?? []).map((v) => [v.viewId, v.title]));
1638
+ viewMeta = new Map((schema.manifest?.views ?? []).map((v) => [v.viewId, {
1639
+ title: v.title,
1640
+ renderable: v.renderable !== false,
1641
+ pageRenderable: hasPageRenderer(v),
1642
+ methods: Array.isArray(v.methods)
1643
+ ? v.methods.filter((value) => typeof value === "string")
1644
+ : []
1645
+ }]));
1596
1646
  }
1597
1647
  catch { /* malformed schema - register without routes */ }
1598
1648
  }
1599
1649
  const duplicate = duplicateTenantService(tenant, hostname, serviceId);
1600
1650
  if (duplicate) {
1601
- return htmlResponse(`<div class="alert alert-warning">${escapeHtml(duplicate)}</div>`, 200, "text/html; mode=fragment");
1651
+ return registerError(duplicate, 409);
1602
1652
  }
1603
1653
  const newServiceId = uuidv7();
1604
1654
  const service = {
@@ -1621,30 +1671,37 @@ export function registerAdminApiRoutes(app, store, cpState) {
1621
1671
  const groupChildren = [];
1622
1672
  const existingPaths = new Set(appDef.routes.map((r) => r.path));
1623
1673
  for (const r of serviceRoutes) {
1674
+ const meta = viewMeta.get(r.viewId);
1675
+ const renderable = meta?.renderable ?? r.renderable !== false;
1624
1676
  // Default mounting path is the service's own path; skip collisions
1625
- const mountPath = existingPaths.has(r.path) ? `/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}${r.path}` : r.path;
1677
+ const mountPath = renderable
1678
+ ? (existingPaths.has(r.path) ? `/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}${r.path}` : r.path)
1679
+ : apiRoutePath(serviceId ?? newServiceId, r.path);
1626
1680
  if (existingPaths.has(mountPath))
1627
1681
  continue;
1628
1682
  const routeId = uuidv7();
1629
- const routeTitle = viewMeta.get(r.viewId) ?? r.viewId;
1683
+ const routeTitle = meta?.title ?? r.viewId;
1630
1684
  appDef.routes.push({
1631
1685
  id: routeId,
1686
+ kind: renderable ? "page" : "api",
1632
1687
  path: mountPath,
1633
1688
  serviceId: newServiceId,
1634
1689
  viewId: r.viewId,
1635
1690
  targetPath: r.path,
1636
1691
  title: routeTitle,
1637
1692
  enabled: true,
1638
- methods: ["GET"]
1693
+ methods: routeMethodsFromManifest(r.methods)
1639
1694
  });
1640
1695
  existingPaths.add(mountPath);
1641
- groupChildren.push({
1642
- id: uuidv7(),
1643
- type: "link",
1644
- title: routeTitle,
1645
- routeId,
1646
- enabled: true
1647
- });
1696
+ if (meta?.pageRenderable === true && !isApiRoute({ kind: renderable ? "page" : "api", path: mountPath }, renderable)) {
1697
+ groupChildren.push({
1698
+ id: uuidv7(),
1699
+ type: "link",
1700
+ title: routeTitle,
1701
+ routeId,
1702
+ enabled: true
1703
+ });
1704
+ }
1648
1705
  }
1649
1706
  if (groupChildren.length > 0) {
1650
1707
  const menu = (appDef.menu ?? []);
@@ -1660,7 +1717,17 @@ export function registerAdminApiRoutes(app, store, cpState) {
1660
1717
  }
1661
1718
  }
1662
1719
  await store.saveConfig(config);
1663
- const adminApiBase = adminApiBaseFromControlPlane(cpState);
1720
+ if (jsonMode) {
1721
+ return jsonResponse({
1722
+ ok: true,
1723
+ tenantId,
1724
+ serviceInstanceId: newServiceId,
1725
+ serviceUrl: hostname,
1726
+ title,
1727
+ deploymentMode: service.deploymentMode
1728
+ });
1729
+ }
1730
+ const adminApiBase = API_BASE;
1664
1731
  return htmlResponse(renderWizardStep3({
1665
1732
  apiKey: "",
1666
1733
  deploymentMode: service.deploymentMode,
@@ -1691,7 +1758,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1691
1758
  const tenantId = url.searchParams.get("tenantId") ?? undefined;
1692
1759
  const hostname = url.searchParams.get("hostname") ?? undefined;
1693
1760
  const config = await store.loadConfig();
1694
- return htmlResponse(renderWizardStep1(config, undefined, { tenantId, hostname }, adminApiBaseFromControlPlane(cpState)), 200, "text/html; mode=fragment");
1761
+ return htmlResponse(renderWizardStep1(config, undefined, { tenantId, hostname }, API_BASE), 200, "text/html; mode=fragment");
1695
1762
  });
1696
1763
  // HTMX configure: load form
1697
1764
  app.get(`${API_BASE}/configure`, async (event) => {