@betterportal/config-manager 10.6.30 → 10.6.32

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 (55) 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 +3 -2
  5. package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -1
  6. package/lib/plugins/service-betterportal-config-manager/adminApi.js +58 -36
  7. package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -1
  8. package/lib/plugins/service-betterportal-config-manager/appQueries.d.ts +976 -0
  9. package/lib/plugins/service-betterportal-config-manager/appQueries.d.ts.map +1 -0
  10. package/lib/plugins/service-betterportal-config-manager/appQueries.js +330 -0
  11. package/lib/plugins/service-betterportal-config-manager/appQueries.js.map +1 -0
  12. package/lib/plugins/service-betterportal-config-manager/index.d.ts +6 -0
  13. package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -1
  14. package/lib/plugins/service-betterportal-config-manager/index.js +83 -23
  15. package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
  16. package/lib/plugins/service-betterportal-config-manager/m2mConnections.d.ts +6 -3
  17. package/lib/plugins/service-betterportal-config-manager/m2mConnections.d.ts.map +1 -1
  18. package/lib/plugins/service-betterportal-config-manager/m2mConnections.js.map +1 -1
  19. package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts +2 -1
  20. package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts.map +1 -1
  21. package/lib/plugins/service-betterportal-config-manager/menuEditor.js +131 -160
  22. package/lib/plugins/service-betterportal-config-manager/menuEditor.js.map +1 -1
  23. package/lib/plugins/service-betterportal-config-manager/previewEnvironments.d.ts +1 -1
  24. package/lib/plugins/service-betterportal-config-manager/previewEnvironments.d.ts.map +1 -1
  25. package/lib/plugins/service-betterportal-config-manager/previewEnvironments.js.map +1 -1
  26. package/lib/plugins/service-betterportal-config-manager/routeContext.d.ts +2 -0
  27. package/lib/plugins/service-betterportal-config-manager/routeContext.d.ts.map +1 -1
  28. package/lib/plugins/service-betterportal-config-manager/routeContext.js.map +1 -1
  29. package/lib/plugins/service-betterportal-config-manager/storage/appData.d.ts +1 -1
  30. package/lib/plugins/service-betterportal-config-manager/storage/appData.d.ts.map +1 -1
  31. package/lib/plugins/service-betterportal-config-manager/storage/appData.js +12 -5
  32. package/lib/plugins/service-betterportal-config-manager/storage/appData.js.map +1 -1
  33. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts +5 -3
  34. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -1
  35. package/lib/plugins/service-betterportal-config-manager/storage/core.js +1 -1
  36. package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -1
  37. package/lib/plugins/service-betterportal-config-manager/storage/entities.d.ts.map +1 -1
  38. package/lib/plugins/service-betterportal-config-manager/storage/entities.js +4 -2
  39. package/lib/plugins/service-betterportal-config-manager/storage/entities.js.map +1 -1
  40. package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts +19 -2
  41. package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts.map +1 -1
  42. package/lib/plugins/service-betterportal-config-manager/storage/postgres.js +74 -22
  43. package/lib/plugins/service-betterportal-config-manager/storage/postgres.js.map +1 -1
  44. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +4 -3
  45. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
  46. package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -1
  47. package/lib/plugins/service-betterportal-config-manager/tenantManagement.d.ts.map +1 -1
  48. package/lib/plugins/service-betterportal-config-manager/tenantManagement.js +3 -1
  49. package/lib/plugins/service-betterportal-config-manager/tenantManagement.js.map +1 -1
  50. package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts.map +1 -1
  51. package/lib/plugins/service-betterportal-config-manager/webhooks.js +147 -54
  52. package/lib/plugins/service-betterportal-config-manager/webhooks.js.map +1 -1
  53. package/lib/schemas/service-betterportal-config-manager.json +1 -1
  54. package/lib/schemas/service-betterportal-config-manager.plugin.json +1 -1
  55. 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.6.30
4
+ * @version 10.6.32
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.6.30
4
+ * @version 10.6.32
5
5
  */
6
6
  import { ServiceClient, importPortableSchema, createFireAndForgetEvent, createBroadcastEvent, createEventSchemas } from "@bsb/base";
7
7
  const _webhookDeliveryAvailableSchema = importPortableSchema(JSON.parse("{\"anyvaliVersion\":\"1.0\",\"schemaVersion\":\"1.1\",\"root\":{\"kind\":\"object\",\"properties\":{},\"required\":[],\"unknownKeys\":\"strip\"},\"definitions\":{},\"extensions\":{}}"));
@@ -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.6.30",
7
+ "version": "10.6.32",
8
8
  "category": "service",
9
9
  "deploymentModes": [
10
10
  "self-hosted"
@@ -1,8 +1,9 @@
1
+ import { type AppDatabase, type AppEditorData } from "./appQueries.js";
1
2
  import type { BetterPortalH3App, PlatformConfigStore, JsonValue } from "@betterportal/framework";
2
3
  import type { BetterPortalConfig, BetterPortalApp } from "@betterportal/framework";
3
4
  import type { CpBootstrapState } from "./cpBootstrap.js";
4
5
  export declare function validateFixedParamValue(name: string, value: string, paramsSchema?: Record<string, JsonValue>): string | undefined;
5
- declare function automationServiceCatalog(config: BetterPortalConfig, appDef: BetterPortalApp): {
6
+ declare function automationServiceCatalog(config: AppEditorData, appDef: BetterPortalApp): {
6
7
  protocol: string;
7
8
  tenantId: string;
8
9
  appId: string;
@@ -305,7 +306,7 @@ export declare function purgeServiceReferences(config: BetterPortalConfig, tenan
305
306
  m2mGrantsRemoved: number;
306
307
  webhooksRemoved: number;
307
308
  };
308
- export declare function registerAdminApiRoutes(app: BetterPortalH3App, store: PlatformConfigStore, cpState: CpBootstrapState): void;
309
+ export declare function registerAdminApiRoutes(app: BetterPortalH3App, store: PlatformConfigStore, cpState: CpBootstrapState, database?: AppDatabase): void;
309
310
  export declare function renderConfigClientShell(d: {
310
311
  hostname: string;
311
312
  tenantId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,iBAAiB,EAEjB,mBAAmB,EACnB,SAAS,EACV,MAAM,yBAAyB,CAAC;AAcjC,OAAO,KAAK,EAAuE,kBAAkB,EAAE,eAAe,EAAgE,MAAM,yBAAyB,CAAC;AACtN,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAgLzD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GACvC,MAAM,GAAG,SAAS,CAepB;AAwGD,iBAAS,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkEpF;AAGD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,GAAG,MAAM,CA4CpG;AA8RD,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAiDtH;AAED,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,CA2FA;AAkPD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,gBAAgB,GACxB,IAAI,CAwiDN;AAwPD,wBAAgB,uBAAuB,CAAC,CAAC,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD,GAAG,MAAM,CAwaT"}
1
+ {"version":3,"file":"adminApi.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/adminApi.ts"],"names":[],"mappings":"AACA,OAAO,EAA2C,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhH,OAAO,KAAK,EACV,iBAAiB,EAEjB,mBAAmB,EACnB,SAAS,EACV,MAAM,yBAAyB,CAAC;AAcjC,OAAO,KAAK,EAAuE,kBAAkB,EAAE,eAAe,EAAgE,MAAM,yBAAyB,CAAC;AACtN,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAkLzD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GACvC,MAAM,GAAG,SAAS,CAepB;AAwGD,iBAAS,wBAAwB,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkE/E;AAGD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,GAAG,MAAM,CA4CpG;AA8RD,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAiDtH;AAED,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,CA2FA;AAkPD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,gBAAgB,EACzB,QAAQ,CAAC,EAAE,WAAW,GACrB,IAAI,CAyjDN;AAwPD,wBAAgB,uBAAuB,CAAC,CAAC,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD,GAAG,MAAM,CAwaT"}
@@ -1,3 +1,5 @@
1
+ import { eventObservability } from "@betterportal/framework";
2
+ import { editAppRows, readApp, readRequestConfig } from "./appQueries.js";
1
3
  import { isMenuRouteExcluded } from "./routeMounts.js";
2
4
  import * as av from "anyvali";
3
5
  import { AppAuthRoleSchema, eventHeaders, htmlResponse, jsonResponse, resolveEmbeddedRequestContext, signServiceConfigTicket, uuidv7, BetterPortalMenuItemSchema, BetterPortalThemeConfigSchema } from "@betterportal/framework";
@@ -72,7 +74,7 @@ function validationError(event, message) {
72
74
  }
73
75
  // PostgreSQL edits read current rows under an app-scoped transaction.
74
76
  // File stores retain their existing single-process save semantics.
75
- async function editApp(store, appId, action) {
77
+ async function editApp(store, appId, action, database, event) {
76
78
  const apply = async (config) => {
77
79
  const response = await action(config);
78
80
  if (!response.ok)
@@ -80,6 +82,8 @@ async function editApp(store, appId, action) {
80
82
  return response;
81
83
  };
82
84
  try {
85
+ if (database)
86
+ return await editAppRows(database, appId, event ? eventObservability(event) : undefined, action);
83
87
  if (store.mutateApp)
84
88
  return await store.mutateApp(appId, apply);
85
89
  const config = await store.loadConfig();
@@ -984,12 +988,29 @@ function automationAppFromRequest(config, event) {
984
988
  : config.apps.find((entry) => tenantUrl && appMatchesTenantUrl(entry, tenantUrl));
985
989
  return resolved && !isPreviewApp(config, resolved.id) ? resolved : undefined;
986
990
  }
987
- export function registerAdminApiRoutes(app, store, cpState) {
991
+ export function registerAdminApiRoutes(app, store, cpState, database) {
992
+ const readCurrentApp = async (event) => {
993
+ if (!database) {
994
+ const config = await store.loadConfig();
995
+ return { config, appDef: currentAppFromRequest(config, event) };
996
+ }
997
+ const appId = new URL(event.req.url, RELATIVE_URL_PARSE_BASE).searchParams.get("appId") ?? event.req.headers.get("x-bp-app-id");
998
+ if (appId) {
999
+ const config = await readApp(database, appId);
1000
+ const appDef = config.apps[0];
1001
+ return { config, appDef: appDef && !isPreviewApp(config, appDef.id) ? appDef : undefined };
1002
+ }
1003
+ const config = await readRequestConfig(database, eventHeaders(event), eventObservability(event));
1004
+ return { config, appDef: currentAppFromRequest(config, event) };
1005
+ };
988
1006
  app.use(`${API_BASE}/**`, async (event) => {
989
- const config = await store.loadConfig();
1007
+ const deployments = database
1008
+ ? (await database.pool.query(`select value->>'tenantId' as "tenantId", value->>'appId' as "appId"
1009
+ from ${database.entities} where scope_id=$1 and kind='previewEnvironmentDeployments'`, [database.scope])).rows
1010
+ : (await store.loadConfig()).previewEnvironmentDeployments;
990
1011
  const url = new URL(event.req.url, RELATIVE_URL_PARSE_BASE);
991
1012
  const requestIds = `${url.pathname}\n${url.search}\n${event.req.headers.get("x-bp-tenant-id") ?? ""}\n${event.req.headers.get("x-bp-app-id") ?? ""}`;
992
- const targetsPreview = config.previewEnvironmentDeployments.some((deployment) => requestIds.includes(deployment.tenantId) || requestIds.includes(deployment.appId));
1013
+ const targetsPreview = deployments.some((deployment) => requestIds.includes(deployment.tenantId) || requestIds.includes(deployment.appId));
993
1014
  if (targetsPreview)
994
1015
  return jsonResponse({ error: "Preview resources are managed through Preview Environments" }, 404);
995
1016
  });
@@ -1014,8 +1035,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1014
1035
  });
1015
1036
  });
1016
1037
  app.get("/.well-known/bp/manage/current", async (event) => {
1017
- const config = await store.loadConfig();
1018
- const appDef = currentAppFromRequest(config, event);
1038
+ const { config, appDef } = await readCurrentApp(event);
1019
1039
  if (!appDef)
1020
1040
  return jsonResponse({ error: "Unable to resolve current BetterPortal app" }, 404);
1021
1041
  const tenant = config.tenants.find((entry) => entry.id === appDef.tenantId);
@@ -1039,8 +1059,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1039
1059
  });
1040
1060
  });
1041
1061
  app.get("/.well-known/bp/manage/services", async (event) => {
1042
- const config = await store.loadConfig();
1043
- const appDef = currentAppFromRequest(config, event);
1062
+ const { config, appDef } = await readCurrentApp(event);
1044
1063
  if (!appDef)
1045
1064
  return jsonResponse({ error: "Unable to resolve current BetterPortal app" }, 404);
1046
1065
  return jsonResponse(automationServiceCatalog(config, appDef));
@@ -1073,40 +1092,43 @@ export function registerAdminApiRoutes(app, store, cpState) {
1073
1092
  return jsonResponse({ ok: true, activation }, 201);
1074
1093
  });
1075
1094
  app.get("/.well-known/bp/manage/routes", async (event) => {
1076
- const config = await store.loadConfig();
1077
- const appDef = currentAppFromRequest(config, event);
1095
+ const { appDef } = await readCurrentApp(event);
1078
1096
  if (!appDef)
1079
1097
  return jsonResponse({ error: "Unable to resolve current BetterPortal app" }, 404);
1080
1098
  return jsonResponse({ appId: appDef.id, routes: appDef.routes });
1081
1099
  });
1082
1100
  app.post("/.well-known/bp/manage/routes", async (event) => {
1083
- const config = await store.loadConfig();
1084
- const appDef = currentAppFromRequest(config, event);
1101
+ const { appDef } = await readCurrentApp(event);
1085
1102
  if (!appDef)
1086
1103
  return jsonResponse({ error: "Unable to resolve current BetterPortal app" }, 404);
1087
1104
  const body = await readFormOrJsonBody(event);
1088
1105
  const parsed = parseRouteCreateBody(body);
1089
1106
  if (parsed.error || !parsed.route)
1090
1107
  return validationError(event, parsed.error ?? "Invalid route");
1091
- if (appDef.routes.some((candidate) => appRoutePatternKey(candidate.path) === appRoutePatternKey(parsed.route.path))) {
1092
- return validationError(event, `A route already exists at ${parsed.route.path}.`);
1093
- }
1094
- const route = { id: uuidv7(), ...parsed.route };
1095
- appDef.routes.push(route);
1096
- addRouteDependencies(appDef, route);
1097
- await store.saveConfig(config);
1098
- return jsonResponse({ ok: true }, 201);
1108
+ return editApp(store, appDef.id, async (data) => {
1109
+ const current = data.apps.find(app => app.id === appDef.id);
1110
+ if (!current || isPreviewApp(data, current.id))
1111
+ return jsonResponse({ error: "App not found" }, 404);
1112
+ const serviceError = validateRegisteredRouteService(data, current, parsed.route.serviceId);
1113
+ if (serviceError)
1114
+ return validationError(event, serviceError);
1115
+ if (current.routes.some(candidate => appRoutePatternKey(candidate.path) === appRoutePatternKey(parsed.route.path))) {
1116
+ return validationError(event, `A route already exists at ${parsed.route.path}.`);
1117
+ }
1118
+ const route = { id: uuidv7(), ...parsed.route };
1119
+ current.routes.push(route);
1120
+ addRouteDependencies(current, route);
1121
+ return jsonResponse({ ok: true }, 201);
1122
+ }, database, event);
1099
1123
  });
1100
1124
  app.get("/.well-known/bp/manage/fragments", async (event) => {
1101
- const config = await store.loadConfig();
1102
- const appDef = currentAppFromRequest(config, event);
1125
+ const { appDef } = await readCurrentApp(event);
1103
1126
  if (!appDef)
1104
1127
  return jsonResponse({ error: "Unable to resolve current BetterPortal app" }, 404);
1105
1128
  return jsonResponse({ appId: appDef.id, fragments: appDef.fragments });
1106
1129
  });
1107
1130
  app.get("/.well-known/bp/manage/theme", async (event) => {
1108
- const config = await store.loadConfig();
1109
- const appDef = currentAppFromRequest(config, event);
1131
+ const { appDef } = await readCurrentApp(event);
1110
1132
  if (!appDef)
1111
1133
  return jsonResponse({ error: "Unable to resolve current BetterPortal app" }, 404);
1112
1134
  return jsonResponse({ appId: appDef.id, themeConfig: appDef.themeConfig });
@@ -1745,7 +1767,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1745
1767
  const appId = getParam(event, "appId");
1746
1768
  if (!appId)
1747
1769
  return jsonResponse({ error: "appId required" }, 400);
1748
- const { appDef } = await getAppOr404(appId);
1770
+ const appDef = database ? (await readApp(database, appId)).apps[0] : (await getAppOr404(appId)).appDef;
1749
1771
  if (!appDef)
1750
1772
  return jsonResponse({ error: "App not found" }, 404);
1751
1773
  const auth = appDef.auth;
@@ -1777,7 +1799,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1777
1799
  return parsedRoles;
1778
1800
  authResult.auth.roles = parsedRoles;
1779
1801
  return jsonResponse({ ok: true, roles: parsedRoles.length });
1780
- });
1802
+ }, database, event);
1781
1803
  });
1782
1804
  app.put(`${API_BASE}/services/self-mutation`, async (event) => {
1783
1805
  const parsed = SelfMutationRequestSchema.safeParse(await readJsonBody(event));
@@ -1813,7 +1835,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1813
1835
  return parsedRoles;
1814
1836
  authResult.auth.roles = parsedRoles;
1815
1837
  return jsonResponse({ ok: true, type, roles: parsedRoles.length });
1816
- });
1838
+ }, database, event);
1817
1839
  });
1818
1840
  app.post(`${API_BASE}/apps/:appId/auth/roles`, async (event) => {
1819
1841
  const appId = getParam(event, "appId");
@@ -1849,7 +1871,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1849
1871
  if (wantsHtmx(event))
1850
1872
  return htmxReload(`/auth?appId=${encodeURIComponent(appId)}`);
1851
1873
  return jsonResponse(role, 201);
1852
- });
1874
+ }, database, event);
1853
1875
  });
1854
1876
  app.put(`${API_BASE}/apps/:appId/auth/roles/:roleId`, async (event) => {
1855
1877
  const appId = getParam(event, "appId");
@@ -1892,7 +1914,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1892
1914
  if (wantsHtmx(event))
1893
1915
  return htmxReload(`/auth?appId=${encodeURIComponent(appId)}`);
1894
1916
  return jsonResponse(role);
1895
- });
1917
+ }, database, event);
1896
1918
  });
1897
1919
  app.delete(`${API_BASE}/apps/:appId/auth/roles/:roleId`, async (event) => {
1898
1920
  const appId = getParam(event, "appId");
@@ -1914,7 +1936,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1914
1936
  if (wantsHtmx(event))
1915
1937
  return htmxReload(`/auth?appId=${encodeURIComponent(appId)}`);
1916
1938
  return jsonResponse({ ok: true });
1917
- });
1939
+ }, database, event);
1918
1940
  });
1919
1941
  // Service connections (per app)
1920
1942
  app.get(`${API_BASE}/apps/:appId/m2m/connections`, async (event) => {
@@ -1992,7 +2014,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
1992
2014
  const appId = getParam(event, "appId");
1993
2015
  if (!appId)
1994
2016
  return jsonResponse({ error: "appId required" }, 400);
1995
- const config = await store.loadConfig();
2017
+ const config = database ? await readApp(database, appId) : await store.loadConfig();
1996
2018
  const appDef = config.apps.find((a) => a.id === appId);
1997
2019
  if (!appDef)
1998
2020
  return jsonResponse({ error: "App not found" }, 404);
@@ -2026,7 +2048,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
2026
2048
  if (wantsHtmx(event))
2027
2049
  return htmxReload(routesReloadPath(appId));
2028
2050
  return jsonResponse({ ok: true, id: route.id }, 201);
2029
- });
2051
+ }, database, event);
2030
2052
  });
2031
2053
  app.put(`${API_BASE}/apps/:appId/routes/:routeId`, async (event) => {
2032
2054
  const appId = getParam(event, "appId");
@@ -2159,7 +2181,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
2159
2181
  if (wantsHtmx(event))
2160
2182
  return htmxReload(routesReloadPath(appId, route.kind === "api" ? route.serviceId : undefined));
2161
2183
  return jsonResponse({ ok: true });
2162
- });
2184
+ }, database, event);
2163
2185
  });
2164
2186
  app.delete(`${API_BASE}/apps/:appId/routes/:routeId`, async (event) => {
2165
2187
  const appId = getParam(event, "appId");
@@ -2193,14 +2215,14 @@ export function registerAdminApiRoutes(app, store, cpState) {
2193
2215
  if (wantsHtmx(event))
2194
2216
  return htmxReload(`/routes?appId=${encodeURIComponent(appId)}`);
2195
2217
  return jsonResponse({ ok: true });
2196
- });
2218
+ }, database, event);
2197
2219
  });
2198
2220
  // Menu (per app)
2199
2221
  app.get(`${API_BASE}/apps/:appId/menu`, async (event) => {
2200
2222
  const appId = getParam(event, "appId");
2201
2223
  if (!appId)
2202
2224
  return jsonResponse({ error: "appId required" }, 400);
2203
- const config = await store.loadConfig();
2225
+ const config = database ? await readApp(database, appId) : await store.loadConfig();
2204
2226
  const appDef = config.apps.find((a) => a.id === appId);
2205
2227
  if (!appDef)
2206
2228
  return jsonResponse({ error: "App not found" }, 404);
@@ -2225,7 +2247,7 @@ export function registerAdminApiRoutes(app, store, cpState) {
2225
2247
  return jsonResponse({ error: "Menu contains a missing or excluded route" }, 400);
2226
2248
  appDef.menu = menu;
2227
2249
  return jsonResponse({ ok: true });
2228
- });
2250
+ }, database, event);
2229
2251
  });
2230
2252
  // Full config (read-only)
2231
2253
  app.get(`${API_BASE}/config`, async () => {