@betterportal/config-manager 10.1.45 → 10.1.47

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 (24) 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/bp-contracts/org.betterportal.config-manager.json +1 -1
  4. package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts +12 -0
  5. package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -1
  6. package/lib/plugins/service-betterportal-config-manager/adminApi.js +93 -12
  7. package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -1
  8. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js +3 -3
  9. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js.map +1 -1
  10. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.d.ts.map +1 -1
  11. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.js +2 -2
  12. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.js.map +1 -1
  13. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.js +3 -3
  14. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/route.impl.js.map +1 -1
  15. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts +1 -0
  16. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -1
  17. package/lib/plugins/service-betterportal-config-manager/storage/core.js +30 -1
  18. package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -1
  19. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
  20. package/lib/plugins/service-betterportal-config-manager/syncApi.js +19 -17
  21. package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -1
  22. package/lib/schemas/service-betterportal-config-manager.json +1 -1
  23. package/lib/schemas/service-betterportal-config-manager.plugin.json +1 -1
  24. 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.1.45
4
+ * @version 10.1.47
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.1.45
4
+ * @version 10.1.47
5
5
  */
6
6
  import { ServiceClient, bsb, createBroadcastEvent, createEventSchemas } from "@bsb/base";
7
7
  const _platformConfigChangedSchema = bsb.object({
@@ -4,7 +4,7 @@
4
4
  "pluginId": "org.betterportal.config-manager",
5
5
  "title": "BetterPortal Config Manager",
6
6
  "description": "Admin-facing BetterPortal service for discovering and managing service config surfaces.",
7
- "version": "10.1.45",
7
+ "version": "10.1.47",
8
8
  "category": "service",
9
9
  "deploymentModes": [
10
10
  "self-hosted"
@@ -197,6 +197,18 @@ declare function automationServiceCatalog(config: BetterPortalConfig, appDef: Be
197
197
  })[];
198
198
  };
199
199
  export declare function renderAutomationLlmsApi(catalog: ReturnType<typeof automationServiceCatalog>): string;
200
+ export declare function purgeServiceReferences(config: BetterPortalConfig, tenantId: string, serviceId: string): {
201
+ routesRemoved: number;
202
+ menuItemsRemoved: number;
203
+ slotsRemoved: number;
204
+ fragmentsRemoved: number;
205
+ roleGrantsRemoved: number;
206
+ shellCleared: number;
207
+ authCleared: number;
208
+ m2mBindingsRemoved: number;
209
+ m2mGrantsRemoved: number;
210
+ webhooksRemoved: number;
211
+ };
200
212
  export declare function registerAdminApiRoutes(app: BetterPortalH3App, store: PlatformConfigStore, cpState: CpBootstrapState): void;
201
213
  export {};
202
214
  //# sourceMappingURL=adminApi.d.ts.map
@@ -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,EACnB,SAAS,EACV,MAAM,yBAAyB,CAAC;AAYjC,OAAO,KAAK,EAAuE,kBAAkB,EAAE,eAAe,EAA0C,MAAM,yBAAyB,CAAC;AAChM,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AA0LzD,iBAAS,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiEpF;AAGD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,GAAG,MAAM,CA2CpG;AAwkBD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,gBAAgB,GACxB,IAAI,CA+4CN"}
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,EACnB,SAAS,EACV,MAAM,yBAAyB,CAAC;AAYjC,OAAO,KAAK,EAAuE,kBAAkB,EAAE,eAAe,EAA0C,MAAM,yBAAyB,CAAC;AAChM,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AA0LzD,iBAAS,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiEpF;AAGD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,GAAG,MAAM,CA2CpG;AA8SD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG;IACvG,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;CACzB,CAmEA;AAiPD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,gBAAgB,GACxB,IAAI,CA48CN"}
@@ -525,9 +525,19 @@ function collectServiceDeleteBlockers(config, tenantId, serviceId) {
525
525
  }
526
526
  }
527
527
  }
528
+ for (const binding of config.m2m.bindings) {
529
+ if (binding.tenantId === tenantId && (binding.sourceServiceId === serviceId || binding.targetServiceId === serviceId)) {
530
+ blockers.push(`M2M binding ${binding.requestId} (${binding.id})`);
531
+ }
532
+ }
533
+ for (const target of config.webhooks.targets) {
534
+ if (target.tenantId === tenantId && target.serviceId === serviceId) {
535
+ blockers.push(`webhook target ${target.eventId} (${target.id})`);
536
+ }
537
+ }
528
538
  return blockers;
529
539
  }
530
- function purgeTenantServiceReferences(config, tenantId, serviceId) {
540
+ export function purgeServiceReferences(config, tenantId, serviceId) {
531
541
  const summary = {
532
542
  routesRemoved: 0,
533
543
  menuItemsRemoved: 0,
@@ -535,7 +545,10 @@ function purgeTenantServiceReferences(config, tenantId, serviceId) {
535
545
  fragmentsRemoved: 0,
536
546
  roleGrantsRemoved: 0,
537
547
  shellCleared: 0,
538
- authCleared: 0
548
+ authCleared: 0,
549
+ m2mBindingsRemoved: 0,
550
+ m2mGrantsRemoved: 0,
551
+ webhooksRemoved: 0
539
552
  };
540
553
  for (const app of config.apps.filter((candidate) => candidate.tenantId === tenantId)) {
541
554
  if (app.shell?.serviceId === serviceId) {
@@ -572,6 +585,18 @@ function purgeTenantServiceReferences(config, tenantId, serviceId) {
572
585
  delete app.fragments[location];
573
586
  }
574
587
  }
588
+ const bindingIds = new Set(config.m2m.bindings
589
+ .filter((binding) => binding.tenantId === tenantId && (binding.sourceServiceId === serviceId || binding.targetServiceId === serviceId))
590
+ .map((binding) => binding.id));
591
+ const bindingsBefore = config.m2m.bindings.length;
592
+ config.m2m.bindings = config.m2m.bindings.filter((binding) => !bindingIds.has(binding.id));
593
+ summary.m2mBindingsRemoved = bindingsBefore - config.m2m.bindings.length;
594
+ const grantsBefore = config.m2m.grants.length;
595
+ config.m2m.grants = config.m2m.grants.filter((grant) => !bindingIds.has(grant.bindingId));
596
+ summary.m2mGrantsRemoved = grantsBefore - config.m2m.grants.length;
597
+ const webhooksBefore = config.webhooks.targets.length;
598
+ config.webhooks.targets = config.webhooks.targets.filter((target) => target.tenantId !== tenantId || target.serviceId !== serviceId);
599
+ summary.webhooksRemoved = webhooksBefore - config.webhooks.targets.length;
575
600
  return summary;
576
601
  }
577
602
  function sharedServiceIdFor(service, requested) {
@@ -742,23 +767,23 @@ function migrateTenantServiceToShared(config, tenantId, serviceId, options = {})
742
767
  removedTenantService: shouldRemove
743
768
  };
744
769
  }
745
- function linkedServiceError(blockers) {
746
- return `Service is still linked and cannot be deleted. Remove these references first: ${blockers.slice(0, 8).join("; ")}${blockers.length > 8 ? `; and ${blockers.length - 8} more` : ""}`;
770
+ function linkedServiceError(blockers, action = "deleted") {
771
+ return `Service is still linked and cannot be ${action}. Remove these references first: ${blockers.slice(0, 8).join("; ")}${blockers.length > 8 ? `; and ${blockers.length - 8} more` : ""}`;
747
772
  }
748
- function linkedServiceHtmxError(tenantId, serviceId, blockers) {
773
+ function linkedServiceHtmxError(blockers, options) {
749
774
  const items = blockers.slice(0, 8).map((blocker) => `<li>${escapeHtml(blocker)}</li>`).join("");
750
775
  const more = blockers.length > 8 ? `<li>${blockers.length - 8} more references</li>` : "";
751
776
  return htmlResponse(`
752
777
  <div class="alert alert-warning">
753
- <div class="fw-semibold mb-2">Service is still linked and cannot be deleted.</div>
778
+ <div class="fw-semibold mb-2">${escapeHtml(options.heading)}</div>
754
779
  <ul class="mb-3">${items}${more}</ul>
755
780
  <button
756
781
  class="btn btn-sm btn-danger"
757
- hx-post="${API_BASE}/tenants/${encodeURIComponent(tenantId)}/services/${encodeURIComponent(serviceId)}/purge"
782
+ hx-post="${escapeHtml(options.actionUrl)}"
758
783
  hx-target="#bp-services-alerts"
759
784
  hx-swap="innerHTML"
760
- hx-confirm="Purge all routes, menu entries, fragments, slots, auth links and role grants for this service, then delete it?"
761
- >Purge references and delete service</button>
785
+ hx-confirm="${escapeHtml(options.confirm)}"
786
+ >${escapeHtml(options.buttonLabel)}</button>
762
787
  </div>
763
788
  `, 409, "text/html; mode=fragment");
764
789
  }
@@ -1101,12 +1126,63 @@ export function registerAdminApiRoutes(app, store, cpState) {
1101
1126
  if (!tenantId)
1102
1127
  return wantsHtmx(event) ? htmxError("tenantId required") : jsonResponse({ error: "tenantId required" }, 400);
1103
1128
  const config = await store.loadConfig();
1129
+ const activations = config.sharedServiceActivations.filter((activation) => activation.sharedServiceId === sharedServiceId
1130
+ && activation.tenantId === tenantId
1131
+ && activation.appId === appId);
1132
+ if (activations.length === 0) {
1133
+ return wantsHtmx(event) ? htmxError("Shared service activation not found", 404) : jsonResponse({ error: "Shared service activation not found" }, 404);
1134
+ }
1135
+ const blockers = activations.flatMap((activation) => collectServiceDeleteBlockers(config, tenantId, activation.id));
1136
+ if (blockers.length > 0) {
1137
+ const message = linkedServiceError(blockers, "deactivated");
1138
+ if (!wantsHtmx(event))
1139
+ return jsonResponse({ error: message, blockers }, 409);
1140
+ const query = new URLSearchParams({ tenantId });
1141
+ if (appId)
1142
+ query.set("appId", appId);
1143
+ return linkedServiceHtmxError(blockers, {
1144
+ heading: "Service is still linked and cannot be deactivated.",
1145
+ actionUrl: `${API_BASE}/shared-services/${encodeURIComponent(sharedServiceId)}/activations/purge?${query}`,
1146
+ confirm: "Purge all routes, menu entries, fragments, slots, auth links, role grants, M2M connections and webhooks for this activation, then deactivate it?",
1147
+ buttonLabel: "Purge references and deactivate service"
1148
+ });
1149
+ }
1104
1150
  config.sharedServiceActivations = config.sharedServiceActivations.filter((activation) => !(activation.sharedServiceId === sharedServiceId && activation.tenantId === tenantId && activation.appId === appId));
1105
1151
  await store.saveConfig(config);
1106
1152
  if (wantsHtmx(event))
1107
1153
  return htmxReload(`/services?tenantId=${encodeURIComponent(tenantId)}`);
1108
1154
  return jsonResponse({ ok: true });
1109
1155
  });
1156
+ app.post(`${API_BASE}/shared-services/:sharedServiceId/activations/purge`, async (event) => {
1157
+ const sharedServiceId = getParam(event, "sharedServiceId");
1158
+ if (!sharedServiceId)
1159
+ return jsonResponse({ error: "sharedServiceId required" }, 400);
1160
+ const url = new URL(event.req.url ?? "", RELATIVE_URL_PARSE_BASE);
1161
+ const tenantId = url.searchParams.get("tenantId") ?? "";
1162
+ const appId = url.searchParams.get("appId") ?? undefined;
1163
+ if (!tenantId)
1164
+ return wantsHtmx(event) ? htmxError("tenantId required") : jsonResponse({ error: "tenantId required" }, 400);
1165
+ const config = await store.loadConfig();
1166
+ const activations = config.sharedServiceActivations.filter((activation) => activation.sharedServiceId === sharedServiceId
1167
+ && activation.tenantId === tenantId
1168
+ && activation.appId === appId);
1169
+ if (activations.length === 0) {
1170
+ return wantsHtmx(event) ? htmxError("Shared service activation not found", 404) : jsonResponse({ error: "Shared service activation not found" }, 404);
1171
+ }
1172
+ const summaries = activations.map((activation) => purgeServiceReferences(config, tenantId, activation.id));
1173
+ config.sharedServiceActivations = config.sharedServiceActivations.filter((activation) => !activations.includes(activation));
1174
+ await store.saveConfig(config);
1175
+ const removed = (key) => summaries.reduce((total, summary) => total + summary[key], 0);
1176
+ if (wantsHtmx(event)) {
1177
+ return htmlResponse(`
1178
+ <div class="alert alert-success">
1179
+ Shared service deactivated. Purged ${removed("routesRemoved")} route(s), ${removed("slotsRemoved")} slot(s), ${removed("fragmentsRemoved")} fragment(s), ${removed("roleGrantsRemoved")} role grant(s), ${removed("shellCleared")} shell binding(s), ${removed("authCleared")} auth binding(s), ${removed("m2mBindingsRemoved")} M2M connection(s), and ${removed("webhooksRemoved")} webhook(s).
1180
+ </div>
1181
+ <div hx-get="/services?tenantId=${encodeURIComponent(tenantId)}" hx-trigger="load" hx-target="#bp-main" hx-swap="innerHTML"></div>
1182
+ `, 200, "text/html; mode=fragment");
1183
+ }
1184
+ return jsonResponse({ ok: true, summaries });
1185
+ });
1110
1186
  // Tenants
1111
1187
  app.get(`${API_BASE}/tenants/:tenantId/services`, async (event) => {
1112
1188
  const tenantId = getParam(event, "tenantId");
@@ -1166,7 +1242,12 @@ export function registerAdminApiRoutes(app, store, cpState) {
1166
1242
  const blockers = collectServiceDeleteBlockers(config, tenantId, serviceId);
1167
1243
  if (blockers.length > 0) {
1168
1244
  const message = linkedServiceError(blockers);
1169
- return wantsHtmx(event) ? linkedServiceHtmxError(tenantId, serviceId, blockers) : jsonResponse({ error: message, blockers }, 409);
1245
+ return wantsHtmx(event) ? linkedServiceHtmxError(blockers, {
1246
+ heading: "Service is still linked and cannot be deleted.",
1247
+ actionUrl: `${API_BASE}/tenants/${encodeURIComponent(tenantId)}/services/${encodeURIComponent(serviceId)}/purge`,
1248
+ confirm: "Purge all routes, menu entries, fragments, slots, auth links, role grants, M2M connections and webhooks for this service, then delete it?",
1249
+ buttonLabel: "Purge references and delete service"
1250
+ }) : jsonResponse({ error: message, blockers }, 409);
1170
1251
  }
1171
1252
  tenant.services = tenant.services.filter((s) => s.id !== serviceId);
1172
1253
  await store.saveConfig(config);
@@ -1190,13 +1271,13 @@ export function registerAdminApiRoutes(app, store, cpState) {
1190
1271
  const service = tenant.services.find((candidate) => candidate.id === serviceId);
1191
1272
  if (!service)
1192
1273
  return wantsHtmx(event) ? htmxError("Service not found", 404) : jsonResponse({ error: "Service not found" }, 404);
1193
- const summary = purgeTenantServiceReferences(config, tenantId, serviceId);
1274
+ const summary = purgeServiceReferences(config, tenantId, serviceId);
1194
1275
  tenant.services = tenant.services.filter((candidate) => candidate.id !== serviceId);
1195
1276
  await store.saveConfig(config);
1196
1277
  if (wantsHtmx(event)) {
1197
1278
  return htmlResponse(`
1198
1279
  <div class="alert alert-success">
1199
- Service deleted. Purged ${summary.routesRemoved} route(s), ${summary.slotsRemoved} slot(s), ${summary.fragmentsRemoved} fragment(s), ${summary.roleGrantsRemoved} role grant(s), ${summary.shellCleared} shell binding(s), and ${summary.authCleared} auth binding(s).
1280
+ Service deleted. Purged ${summary.routesRemoved} route(s), ${summary.slotsRemoved} slot(s), ${summary.fragmentsRemoved} fragment(s), ${summary.roleGrantsRemoved} role grant(s), ${summary.shellCleared} shell binding(s), ${summary.authCleared} auth binding(s), ${summary.m2mBindingsRemoved} M2M connection(s), and ${summary.webhooksRemoved} webhook(s).
1200
1281
  </div>
1201
1282
  <div hx-get="/services?tenantId=${encodeURIComponent(tenantId)}" hx-trigger="load" hx-target="#bp-main" hx-swap="innerHTML"></div>
1202
1283
  `, 200, "text/html; mode=fragment");