@betterportal/config-manager 0.0.1

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 (154) hide show
  1. package/README.md +3 -0
  2. package/bsb-plugin.json +23 -0
  3. package/bsb-tests.json +14 -0
  4. package/lib/.bsb/clients/service-betterportal-config-manager.d.ts +37 -0
  5. package/lib/.bsb/clients/service-betterportal-config-manager.d.ts.map +1 -0
  6. package/lib/.bsb/clients/service-betterportal-config-manager.js +40 -0
  7. package/lib/.bsb/clients/service-betterportal-config-manager.js.map +1 -0
  8. package/lib/index.d.ts +2 -0
  9. package/lib/index.d.ts.map +1 -0
  10. package/lib/index.js +2 -0
  11. package/lib/index.js.map +1 -0
  12. package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.d.ts +3 -0
  13. package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.d.ts.map +1 -0
  14. package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.js +235 -0
  15. package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.js.map +1 -0
  16. package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts +4 -0
  17. package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -0
  18. package/lib/plugins/service-betterportal-config-manager/adminApi.js +2319 -0
  19. package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -0
  20. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts +21 -0
  21. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts.map +1 -0
  22. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js +269 -0
  23. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js.map +1 -0
  24. package/lib/plugins/service-betterportal-config-manager/bootstrapWizardHtml.d.ts +19 -0
  25. package/lib/plugins/service-betterportal-config-manager/bootstrapWizardHtml.d.ts.map +1 -0
  26. package/lib/plugins/service-betterportal-config-manager/bootstrapWizardHtml.js +329 -0
  27. package/lib/plugins/service-betterportal-config-manager/bootstrapWizardHtml.js.map +1 -0
  28. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/index.d.ts +4 -0
  29. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/index.d.ts.map +1 -0
  30. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/index.js +38 -0
  31. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/index.js.map +1 -0
  32. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.d.ts +96 -0
  33. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.d.ts.map +1 -0
  34. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.js +78 -0
  35. package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.js.map +1 -0
  36. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.bootstrap1/index.d.ts +4 -0
  37. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.bootstrap1/index.d.ts.map +1 -0
  38. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.bootstrap1/index.js +62 -0
  39. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.bootstrap1/index.js.map +1 -0
  40. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.embedded.d.ts +5 -0
  41. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.embedded.d.ts.map +1 -0
  42. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.embedded.js +5 -0
  43. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.embedded.js.map +1 -0
  44. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.d.ts +43 -0
  45. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.d.ts.map +1 -0
  46. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.js +68 -0
  47. package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.js.map +1 -0
  48. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/_theme.bootstrap1/index.d.ts +5 -0
  49. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/_theme.bootstrap1/index.d.ts.map +1 -0
  50. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/_theme.bootstrap1/index.js +11 -0
  51. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/_theme.bootstrap1/index.js.map +1 -0
  52. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.d.ts +32 -0
  53. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.d.ts.map +1 -0
  54. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.js +32 -0
  55. package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.js.map +1 -0
  56. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/_theme.bootstrap1/index.d.ts +4 -0
  57. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/_theme.bootstrap1/index.d.ts.map +1 -0
  58. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/_theme.bootstrap1/index.js +170 -0
  59. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/_theme.bootstrap1/index.js.map +1 -0
  60. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.d.ts +60 -0
  61. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.d.ts.map +1 -0
  62. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.js +48 -0
  63. package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.js.map +1 -0
  64. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/_theme.bootstrap1/index.d.ts +4 -0
  65. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/_theme.bootstrap1/index.d.ts.map +1 -0
  66. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/_theme.bootstrap1/index.js +28 -0
  67. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/_theme.bootstrap1/index.js.map +1 -0
  68. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.d.ts +48 -0
  69. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.d.ts.map +1 -0
  70. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.js +40 -0
  71. package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.js.map +1 -0
  72. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/index.d.ts +5 -0
  73. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/index.d.ts.map +1 -0
  74. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/index.js +194 -0
  75. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/index.js.map +1 -0
  76. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.d.ts +80 -0
  77. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.d.ts.map +1 -0
  78. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.js +59 -0
  79. package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.js.map +1 -0
  80. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/index.d.ts +5 -0
  81. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/index.d.ts.map +1 -0
  82. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/index.js +167 -0
  83. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/index.js.map +1 -0
  84. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.d.ts +128 -0
  85. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.d.ts.map +1 -0
  86. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.js +89 -0
  87. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.js.map +1 -0
  88. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/_theme.bootstrap1/index.d.ts +5 -0
  89. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/_theme.bootstrap1/index.d.ts.map +1 -0
  90. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/_theme.bootstrap1/index.js +8 -0
  91. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/_theme.bootstrap1/index.js.map +1 -0
  92. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.d.ts +89 -0
  93. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.d.ts.map +1 -0
  94. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.js +93 -0
  95. package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.js.map +1 -0
  96. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_theme.bootstrap1/index.d.ts +4 -0
  97. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_theme.bootstrap1/index.d.ts.map +1 -0
  98. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_theme.bootstrap1/index.js +61 -0
  99. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_theme.bootstrap1/index.js.map +1 -0
  100. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.d.ts +180 -0
  101. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.d.ts.map +1 -0
  102. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.js +405 -0
  103. package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.js.map +1 -0
  104. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts +26 -0
  105. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts.map +1 -0
  106. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.js +58 -0
  107. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.js.map +1 -0
  108. package/lib/plugins/service-betterportal-config-manager/fragmentsEditor.d.ts +3 -0
  109. package/lib/plugins/service-betterportal-config-manager/fragmentsEditor.d.ts.map +1 -0
  110. package/lib/plugins/service-betterportal-config-manager/fragmentsEditor.js +365 -0
  111. package/lib/plugins/service-betterportal-config-manager/fragmentsEditor.js.map +1 -0
  112. package/lib/plugins/service-betterportal-config-manager/index.d.ts +143 -0
  113. package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -0
  114. package/lib/plugins/service-betterportal-config-manager/index.js +696 -0
  115. package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -0
  116. package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts +3 -0
  117. package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts.map +1 -0
  118. package/lib/plugins/service-betterportal-config-manager/menuEditor.js +823 -0
  119. package/lib/plugins/service-betterportal-config-manager/menuEditor.js.map +1 -0
  120. package/lib/plugins/service-betterportal-config-manager/routeContext.d.ts +10 -0
  121. package/lib/plugins/service-betterportal-config-manager/routeContext.d.ts.map +1 -0
  122. package/lib/plugins/service-betterportal-config-manager/routeContext.js +11 -0
  123. package/lib/plugins/service-betterportal-config-manager/routeContext.js.map +1 -0
  124. package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts +18 -0
  125. package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts.map +1 -0
  126. package/lib/plugins/service-betterportal-config-manager/setupTokens.js +245 -0
  127. package/lib/plugins/service-betterportal-config-manager/setupTokens.js.map +1 -0
  128. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts +41 -0
  129. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -0
  130. package/lib/plugins/service-betterportal-config-manager/storage/core.js +396 -0
  131. package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -0
  132. package/lib/plugins/service-betterportal-config-manager/storage/file.d.ts +10 -0
  133. package/lib/plugins/service-betterportal-config-manager/storage/file.d.ts.map +1 -0
  134. package/lib/plugins/service-betterportal-config-manager/storage/file.js +30 -0
  135. package/lib/plugins/service-betterportal-config-manager/storage/file.js.map +1 -0
  136. package/lib/plugins/service-betterportal-config-manager/storage/index.d.ts +36 -0
  137. package/lib/plugins/service-betterportal-config-manager/storage/index.d.ts.map +1 -0
  138. package/lib/plugins/service-betterportal-config-manager/storage/index.js +52 -0
  139. package/lib/plugins/service-betterportal-config-manager/storage/index.js.map +1 -0
  140. package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts +15 -0
  141. package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts.map +1 -0
  142. package/lib/plugins/service-betterportal-config-manager/storage/postgres.js +60 -0
  143. package/lib/plugins/service-betterportal-config-manager/storage/postgres.js.map +1 -0
  144. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +44 -0
  145. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -0
  146. package/lib/plugins/service-betterportal-config-manager/syncApi.js +280 -0
  147. package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -0
  148. package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts +6 -0
  149. package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts.map +1 -0
  150. package/lib/plugins/service-betterportal-config-manager/webhooks.js +372 -0
  151. package/lib/plugins/service-betterportal-config-manager/webhooks.js.map +1 -0
  152. package/lib/schemas/service-betterportal-config-manager.json +157 -0
  153. package/lib/schemas/service-betterportal-config-manager.plugin.json +135 -0
  154. package/package.json +69 -0
@@ -0,0 +1,696 @@
1
+ import { createBroadcastEvent, createConfigSchema, createEventSchemas } from "@bsb/base";
2
+ import * as av from "anyvali";
3
+ import { BPService } from "@betterportal/plugin-bsb";
4
+ import { resolve } from "node:path";
5
+ import { createStaticJwksVerifier } from "@betterportal/framework";
6
+ import { registry } from "./.bp-generated/registry.js";
7
+ import { cpBootstrap } from "./cpBootstrap.js";
8
+ import { registerSetupEndpoints } from "./setupTokens.js";
9
+ import { registerBootstrapEndpoint } from "./bootstrapEndpoint.js";
10
+ import { registerAdminApiRoutes } from "./adminApi.js";
11
+ import { registerMenuEditorRoutes } from "./menuEditor.js";
12
+ import { registerFragmentsEditorRoutes } from "./fragmentsEditor.js";
13
+ import { registerWebhookRoutes } from "./webhooks.js";
14
+ import { getManifestCache, registerSyncEndpoint } from "./syncApi.js";
15
+ import { setConfigManagerRouteContext } from "./routeContext.js";
16
+ import { describeEmbeddedContextResolution, eventHeaders, resolveEmbeddedRequestContext, uuidv7 } from "@betterportal/framework";
17
+ import { createStorageFromConfig, PlatformConfigStorageSchema } from "./storage/index.js";
18
+ import BetterportalConfigManagerClient from "../../.bsb/clients/service-betterportal-config-manager.js";
19
+ /** Tenant service-instance id (UUIDv7) -> pluginId, for the auth permission check. */
20
+ function buildServiceIdAliases(config, tenantId) {
21
+ const aliases = {};
22
+ const tenant = config.tenants.find((t) => t.id === tenantId);
23
+ for (const svc of tenant?.services ?? []) {
24
+ if (svc.serviceId)
25
+ aliases[svc.id] = svc.serviceId;
26
+ }
27
+ for (const activation of config.sharedServiceActivations ?? []) {
28
+ if (activation.enabled !== false && activation.tenantId === tenantId) {
29
+ aliases[activation.id] = activation.sharedServiceId;
30
+ }
31
+ }
32
+ return aliases;
33
+ }
34
+ const PluginConfigSchema = av.object({
35
+ host: av.string().minLength(1).default("0.0.0.0"),
36
+ port: av.int().min(1).default(3300),
37
+ storage: PlatformConfigStorageSchema,
38
+ requestTimeoutMs: av.int().min(1).default(2000),
39
+ /** Path to RSA keypair JSON used to sign envelope/setup tokens. Auto-generated on first boot. */
40
+ cpKeyStorePath: av.string().minLength(1).default("./.bp-cp-state/keys.json"),
41
+ /** Public issuer URL (matches token `iss` claim). Default: derived from host:port. */
42
+ cpIssuer: av.optional(av.string().minLength(1)),
43
+ /** Required audience for tokens issued by this CP. */
44
+ cpAudience: av.string().minLength(1).default("betterportal-control-plane")
45
+ }, { unknownKeys: "strip" });
46
+ const PlatformConfigChangedEventSchema = av.object({
47
+ sourceId: av.string().minLength(1),
48
+ backend: av.enum_(["file", "postgres"])
49
+ }, { unknownKeys: "strip" });
50
+ const Config = createConfigSchema({
51
+ name: "service-betterportal-config-manager",
52
+ description: "BetterPortal config management admin service",
53
+ tags: ["betterportal", "service", "admin", "config"],
54
+ documentation: ["./README.md"]
55
+ }, PluginConfigSchema);
56
+ const EventSchemas = createEventSchemas({
57
+ emitEvents: {},
58
+ onEvents: {},
59
+ emitReturnableEvents: {},
60
+ onReturnableEvents: {},
61
+ emitBroadcast: {
62
+ "platform-config.changed": createBroadcastEvent(PlatformConfigChangedEventSchema, "Emitted after platform config is saved by this config-manager instance.")
63
+ },
64
+ onBroadcast: {}
65
+ });
66
+ export class Plugin extends BPService {
67
+ static Config = Config;
68
+ static EventSchemas = EventSchemas;
69
+ requireBetterPortalConfigSource = false;
70
+ storage;
71
+ webhookRuntime;
72
+ changeSourceId = uuidv7();
73
+ selfClient;
74
+ /** CP-side signing keypair + issuer/audience info. Built on first init. */
75
+ cpState;
76
+ /** Cache of (tenantId, appId) -> app.auth config + JWT verifier from synced storage. */
77
+ authConfigCache = new Map();
78
+ authCacheTtlMs = 60 * 1000;
79
+ constructor(cfg) {
80
+ super({ ...cfg, eventSchemas: EventSchemas });
81
+ this.selfClient = new BetterportalConfigManagerClient(this);
82
+ }
83
+ definition() {
84
+ return {
85
+ manifest: {
86
+ pluginId: "service.betterportal.config-manager",
87
+ title: "BetterPortal Config Manager",
88
+ description: "Admin-facing BetterPortal service for discovering and managing service config surfaces.",
89
+ cacheHints: { metadataTtlSeconds: 300 }
90
+ },
91
+ registry
92
+ };
93
+ }
94
+ /**
95
+ * CM uses its own storage as the source of app.auth config (it IS the CP).
96
+ * Loads sync - async load via cached promise to keep getJwtVerifier signature simple.
97
+ */
98
+ getAppAuthConfig(tenantId, appId) {
99
+ return this.readAuthCacheEntry(tenantId, appId)?.auth;
100
+ }
101
+ getJwtVerifier(tenantId, appId) {
102
+ return this.readAuthCacheEntry(tenantId, appId)?.verifier;
103
+ }
104
+ getServiceIdAliases(tenantId) {
105
+ for (const [key, entry] of this.authConfigCache) {
106
+ if (key.startsWith(`${tenantId}::`))
107
+ return entry.aliases;
108
+ }
109
+ return undefined;
110
+ }
111
+ /**
112
+ * Stale-while-revalidate read of the auth cache. Returns any cached entry
113
+ * (even past its TTL) and kicks off a background refresh when stale, so an
114
+ * expired entry never causes a transient "Auth context unavailable" 401. The
115
+ * cache is warmed at startup and on every config change (see warmAuthCache),
116
+ * so a missing entry means the app genuinely has no auth configured.
117
+ */
118
+ readAuthCacheEntry(tenantId, appId) {
119
+ const key = `${tenantId}::${appId}`;
120
+ const cached = this.authConfigCache.get(key);
121
+ if (!cached)
122
+ return undefined;
123
+ if ((Date.now() - cached.cachedAt) >= this.authCacheTtlMs) {
124
+ void this.refreshAuthCache(tenantId, appId);
125
+ }
126
+ return cached;
127
+ }
128
+ /**
129
+ * Eagerly build verifiers for every app that already has pushed JWKS in
130
+ * persisted config. Called at startup (publicKeys survive restarts) and after
131
+ * each config change (e.g. an auth service installing and pushing its JWKS),
132
+ * so the first authenticated request never races an empty cache.
133
+ */
134
+ async warmAuthCache(obs) {
135
+ try {
136
+ const config = await this.storage.loadConfig();
137
+ let warmed = 0;
138
+ for (const app of config.apps) {
139
+ const auth = app.auth;
140
+ if (!auth?.publicKeys || !Array.isArray(auth.publicKeys.keys) || auth.publicKeys.keys.length === 0) {
141
+ continue;
142
+ }
143
+ this.authConfigCache.set(`${app.tenantId}::${app.id}`, {
144
+ auth,
145
+ verifier: createStaticJwksVerifier({
146
+ jwks: auth.publicKeys,
147
+ expectedIssuer: auth.expectedIssuer,
148
+ expectedAudience: auth.expectedAudience,
149
+ expectedTokenType: "access"
150
+ }),
151
+ aliases: buildServiceIdAliases(config, app.tenantId),
152
+ cachedAt: Date.now()
153
+ });
154
+ warmed += 1;
155
+ }
156
+ if (warmed > 0)
157
+ obs?.log.debug("Warmed auth verifier cache for {count} app(s)", { count: warmed });
158
+ }
159
+ catch {
160
+ // Non-fatal - getJwtVerifier falls back to lazy refresh on demand.
161
+ }
162
+ }
163
+ async refreshAuthCache(tenantId, appId) {
164
+ const key = `${tenantId}::${appId}`;
165
+ try {
166
+ const config = await this.storage.loadConfig();
167
+ const app = config.apps.find((a) => a.id === appId && a.tenantId === tenantId);
168
+ const auth = app?.auth;
169
+ if (!auth)
170
+ return;
171
+ // CM cannot reach services - must use the JWKS the auth service pushed at /install.
172
+ if (!auth.publicKeys || !Array.isArray(auth.publicKeys.keys) || auth.publicKeys.keys.length === 0) {
173
+ return;
174
+ }
175
+ const verifier = createStaticJwksVerifier({
176
+ jwks: auth.publicKeys,
177
+ expectedIssuer: auth.expectedIssuer,
178
+ expectedAudience: auth.expectedAudience,
179
+ expectedTokenType: "access"
180
+ });
181
+ this.authConfigCache.set(key, { auth, verifier, aliases: buildServiceIdAliases(config, tenantId), cachedAt: Date.now() });
182
+ }
183
+ catch {
184
+ // silent - next request retries
185
+ }
186
+ }
187
+ async resolveCorsContext(event) {
188
+ const config = await this.storage.loadConfig();
189
+ const context = resolveEmbeddedRequestContext(config, eventHeaders(event));
190
+ if (!context) {
191
+ return null;
192
+ }
193
+ const adminTenantId = config.configManagement.adminTenantId ?? "betterportal";
194
+ if (context.tenant.id !== adminTenantId) {
195
+ return null;
196
+ }
197
+ return context;
198
+ }
199
+ async describeCorsContextFailure(event) {
200
+ const config = await this.storage.loadConfig();
201
+ const details = describeEmbeddedContextResolution(config, eventHeaders(event));
202
+ return {
203
+ candidateHosts: details.candidates.join(","),
204
+ configuredAppHosts: details.appHosts.map((app) => `${app.appId}:[${app.hosts.join(",")}]`).join(";")
205
+ };
206
+ }
207
+ async onRegistered(_registry, _obs) {
208
+ const resolvedStorage = createStorageFromConfig(this.config.storage, this.cwd);
209
+ this.storage = this.withChangeBroadcasts(resolvedStorage.store, _obs, {
210
+ backend: resolvedStorage.backend
211
+ });
212
+ // Initialize CP keypair + JWKS (P7).
213
+ const cwd = this.cwd ?? ".";
214
+ this.cpState = cpBootstrap({
215
+ keyStorePath: resolve(cwd, this.config.cpKeyStorePath ?? "./.bp-cp-state/keys.json"),
216
+ issuer: this.config.cpIssuer,
217
+ audience: this.config.cpAudience ?? "betterportal-control-plane",
218
+ host: this.config.host ?? "0.0.0.0",
219
+ port: this.config.port ?? 3300
220
+ });
221
+ this.registerAsAuthProvider({
222
+ jwks: { keys: [this.cpState.jwk] }
223
+ });
224
+ _obs.log.info("CP issuer={issuer} kid={kid} cpId={cpId}; JWKS exposed at /.well-known/jwks.json", {
225
+ issuer: this.cpState.issuer,
226
+ kid: this.cpState.keyPair.kid,
227
+ cpId: this.cpState.cpId
228
+ });
229
+ await this.selfClient.onPlatformConfigChanged(_obs, async (eventObs, event) => {
230
+ if (event.sourceId === this.changeSourceId)
231
+ return;
232
+ this.storage.invalidate();
233
+ // A config change may carry a freshly-pushed auth JWKS - rebuild verifiers.
234
+ await this.warmAuthCache(eventObs);
235
+ });
236
+ setConfigManagerRouteContext({
237
+ storage: this.storage,
238
+ cpState: this.cpState,
239
+ serviceBaseUrl: `http://${this.config.host === "0.0.0.0" ? "localhost" : this.config.host}:${this.config.port}`
240
+ });
241
+ this.app.use("/config", (event) => this.populateConfigAdminContext(event));
242
+ this.app.use("/services", (event) => this.populateServicesContext(event));
243
+ this.app.use("/routes", (event) => this.populateRoutesContext(event));
244
+ this.app.use("/menu", (event) => this.populateMenuContext(event));
245
+ this.app.use("/fragments", (event) => this.populateFragmentsContext(event));
246
+ this.app.use("/preview", (event) => this.populatePreviewContext(event));
247
+ this.app.use("/auth", (event) => this.populateAdminAuthContext(event));
248
+ this.app.use("/settings", (event) => this.populateSettingsContext(event));
249
+ registerAdminApiRoutes(this.app, this.storage, this.cpState);
250
+ registerMenuEditorRoutes(this.app, this.storage);
251
+ registerFragmentsEditorRoutes(this.app, this.storage);
252
+ this.webhookRuntime = registerWebhookRoutes(this.app, this.storage);
253
+ registerSyncEndpoint(this.app, this.storage);
254
+ // Setup token mint + redeem endpoints (P4)
255
+ registerSetupEndpoints({ app: this.app, storage: this.storage, cpState: this.cpState });
256
+ // Bootstrap detection + endpoint (P6) - opens vanilla HTML wizard on empty DB
257
+ await registerBootstrapEndpoint({
258
+ app: this.app,
259
+ storage: this.storage,
260
+ cpState: this.cpState,
261
+ logger: _obs
262
+ });
263
+ // Warm auth verifiers from persisted JWKS so the first authenticated request
264
+ // after a restart never races an empty cache (#6).
265
+ await this.warmAuthCache(_obs);
266
+ this.webhookRuntime.start();
267
+ }
268
+ withChangeBroadcasts(store, obs, metadata) {
269
+ return {
270
+ loadConfig: () => store.loadConfig(),
271
+ saveConfig: async (config) => {
272
+ await store.saveConfig(config);
273
+ await this.events.emitBroadcast("platform-config.changed", obs, {
274
+ sourceId: this.changeSourceId,
275
+ backend: metadata.backend
276
+ });
277
+ },
278
+ validateApiKey: (apiKey) => store.validateApiKey(apiKey),
279
+ getScopedConfig: (serviceId, scope, tenantId) => store.getScopedConfig(serviceId, scope, tenantId),
280
+ invalidate: () => store.invalidate(),
281
+ onChange: (listener) => store.onChange(listener)
282
+ };
283
+ }
284
+ async populateConfigAdminContext(event) {
285
+ const portalConfig = await this.storage.loadConfig();
286
+ const requestContext = resolveEmbeddedRequestContext(portalConfig, eventHeaders(event));
287
+ if (requestContext) {
288
+ const tenant = requestContext.tenant;
289
+ const allServices = [
290
+ ...tenant.services.filter((s) => s.enabled).map((s) => ({
291
+ serviceId: s.serviceId ?? s.id,
292
+ hostname: s.hostname,
293
+ deploymentMode: s.deploymentMode,
294
+ title: s.title,
295
+ scope: "tenant"
296
+ })),
297
+ ...tenant.activatedPlatformServices
298
+ .map((psId) => portalConfig.platformServices.find((ps) => ps.id === psId && ps.enabled))
299
+ .filter((ps) => ps !== null && ps !== undefined)
300
+ .map((ps) => ({
301
+ serviceId: ps.serviceId ?? ps.id,
302
+ hostname: ps.hostname,
303
+ deploymentMode: "bp-hosted",
304
+ title: ps.title,
305
+ scope: "platform"
306
+ })),
307
+ ...portalConfig.sharedServiceActivations
308
+ .filter((activation) => activation.enabled && activation.tenantId === tenant.id && (!activation.appId || activation.appId === requestContext.app.id))
309
+ .map((activation) => {
310
+ const shared = portalConfig.sharedServiceCatalog.find((service) => service.enabled && service.id === activation.sharedServiceId);
311
+ if (!shared)
312
+ return undefined;
313
+ return {
314
+ serviceId: shared.serviceId ?? shared.id,
315
+ hostname: shared.baseUrl,
316
+ deploymentMode: "bp-hosted",
317
+ title: shared.title,
318
+ scope: "shared"
319
+ };
320
+ })
321
+ .filter((service) => !!service)
322
+ ];
323
+ const responseModel = {
324
+ title: "Config Manager",
325
+ tenantId: tenant.id,
326
+ appId: requestContext.app.id,
327
+ requestTimeoutMs: this.config.requestTimeoutMs,
328
+ services: allServices.map((svc) => ({
329
+ serviceId: svc.serviceId,
330
+ bindingId: svc.serviceId,
331
+ endpointBaseUrl: svc.hostname,
332
+ deploymentMode: svc.deploymentMode,
333
+ healthUrl: `${svc.hostname.replace(/\/+$/, "")}/.well-known/bp/health`,
334
+ schemaUrl: `${svc.hostname.replace(/\/+$/, "")}/.well-known/bp/config/schema`,
335
+ manifestUrl: `${svc.hostname.replace(/\/+$/, "")}/.well-known/bp/manifest`
336
+ }))
337
+ };
338
+ event.__bpResponseModel = responseModel;
339
+ }
340
+ }
341
+ async populateServicesContext(event) {
342
+ const config = await this.storage.loadConfig();
343
+ const url = new URL(event.req.url ?? "", `http://${event.req.headers.get("host") ?? "localhost"}`);
344
+ const requestedTenantId = url.searchParams.get("tenantId") ?? undefined;
345
+ const selectedTenantId = config.tenants.some((tenant) => tenant.id === requestedTenantId)
346
+ ? requestedTenantId
347
+ : config.tenants[0]?.id;
348
+ const tenantApps = {};
349
+ for (const t of config.tenants) {
350
+ tenantApps[t.id] = config.apps
351
+ .filter((a) => a.tenantId === t.id)
352
+ .map((a) => ({ id: a.id, title: a.title, shellServiceId: a.shell?.serviceId }));
353
+ }
354
+ const manifestCache = getManifestCache();
355
+ const configMetadata = (serviceInstanceId, pluginId) => {
356
+ const cached = manifestCache.get(serviceInstanceId) ?? (pluginId ? manifestCache.get(pluginId) : undefined);
357
+ const configManifestKnown = Boolean(cached) || pluginId === this.manifest.pluginId;
358
+ const hasConfigSchemas = (cached?.configSchemas?.length ?? 0) > 0;
359
+ return {
360
+ supportsCustomUi: false,
361
+ customUiPath: undefined,
362
+ configManifestKnown,
363
+ hasConfigurableOptions: hasConfigSchemas
364
+ };
365
+ };
366
+ const tenantSvcsRaw = config.tenants.flatMap((t) => t.services.map((s) => {
367
+ const cached = manifestCache.get(s.id);
368
+ const capabilities = cached?.capabilities?.length ? cached.capabilities : (s.capabilities ?? []);
369
+ const isTheme = capabilities.includes("theme");
370
+ return {
371
+ id: s.id, hostname: s.hostname, serviceId: s.serviceId, capabilities,
372
+ title: cached?.title ?? s.title, description: s.description,
373
+ createdAt: s.createdAt, lastSeenAt: s.lastSeenAt,
374
+ enabled: s.enabled,
375
+ scope: isTheme ? "theme" : "tenant",
376
+ themeId: undefined,
377
+ tenantId: t.id,
378
+ pushBase: `/settings/service/${s.id}`,
379
+ ...configMetadata(s.id, s.serviceId)
380
+ };
381
+ }));
382
+ const platformSvcsRaw = config.platformServices.map((ps) => ({
383
+ id: ps.id, hostname: ps.hostname, serviceId: ps.serviceId, capabilities: ps.capabilities ?? [],
384
+ title: ps.title, description: ps.description,
385
+ createdAt: ps.createdAt, lastSeenAt: undefined,
386
+ enabled: ps.enabled, scope: "platform", tenantId: undefined,
387
+ pushBase: `/settings/platform/${ps.id}`,
388
+ ...configMetadata(ps.id, ps.serviceId)
389
+ }));
390
+ const sharedSvcsRaw = config.sharedServiceActivations
391
+ .filter((activation) => activation.enabled)
392
+ .map((activation) => {
393
+ const shared = config.sharedServiceCatalog.find((service) => service.id === activation.sharedServiceId && service.enabled);
394
+ if (!shared)
395
+ return undefined;
396
+ const capabilities = shared.tags ?? [];
397
+ return {
398
+ id: activation.id,
399
+ hostname: shared.baseUrl,
400
+ serviceId: shared.serviceId ?? shared.id,
401
+ capabilities,
402
+ title: shared.title,
403
+ description: shared.description,
404
+ createdAt: activation.activatedAt,
405
+ lastSeenAt: undefined,
406
+ enabled: activation.enabled,
407
+ scope: "shared",
408
+ themeId: undefined,
409
+ tenantId: activation.tenantId,
410
+ pushBase: `/settings/shared/${activation.id}`,
411
+ ...configMetadata(activation.id, shared.id)
412
+ };
413
+ })
414
+ .filter((service) => !!service);
415
+ const allServices = [...tenantSvcsRaw, ...sharedSvcsRaw, ...platformSvcsRaw];
416
+ event.__bpResponseModel = {
417
+ title: "Service Registry",
418
+ services: allServices,
419
+ tenants: config.tenants.map((t) => ({ id: t.id, title: t.title })),
420
+ selectedTenantId,
421
+ sharedServiceCatalog: config.sharedServiceCatalog.map((service) => ({
422
+ ...service,
423
+ installed: typeof service.apiKeyHash === "string" && service.apiKeyHash.length > 0
424
+ })),
425
+ sharedServiceActivations: config.sharedServiceActivations,
426
+ apps: config.apps.map((a) => ({ id: a.id, tenantId: a.tenantId, title: a.title })),
427
+ tenantApps,
428
+ adminApiBase: "/.well-known/bp/admin",
429
+ serviceBaseUrl: `http://${this.config.host === "0.0.0.0" ? "localhost" : this.config.host}:${this.config.port}`
430
+ };
431
+ }
432
+ async populateRoutesContext(event) {
433
+ const config = await this.storage.loadConfig();
434
+ const url = new URL(event.req.url ?? "", `http://${event.req.headers.get("host") ?? "localhost"}`);
435
+ const selectedAppId = url.searchParams.get("appId") ?? undefined;
436
+ const selectedApp = selectedAppId ? config.apps.find((a) => a.id === selectedAppId) : undefined;
437
+ const selectedTenant = selectedApp ? config.tenants.find((t) => t.id === selectedApp.tenantId) : undefined;
438
+ const cache = getManifestCache();
439
+ const viewsForService = (serviceInstanceId) => {
440
+ if (!serviceInstanceId)
441
+ return [];
442
+ const manifest = cache.get(serviceInstanceId);
443
+ if (!manifest)
444
+ return [];
445
+ return Object.values(manifest.viewIndex)
446
+ .map((v) => ({ viewId: v.viewId, title: v.viewId, path: v.path, methods: v.methods, renderable: v.renderable, dependencies: v.dependencies }));
447
+ };
448
+ const availableServices = selectedTenant
449
+ ? [
450
+ ...selectedTenant.services.filter((s) => s.enabled).map((s) => ({
451
+ id: s.id,
452
+ title: s.title ?? s.serviceId ?? s.hostname,
453
+ hostname: s.hostname,
454
+ serviceId: s.serviceId ?? s.id,
455
+ views: viewsForService(s.id)
456
+ })),
457
+ ...selectedTenant.activatedPlatformServices
458
+ .map((psId) => config.platformServices.find((ps) => ps.id === psId && ps.enabled))
459
+ .filter((ps) => !!ps)
460
+ .map((ps) => ({
461
+ id: ps.id,
462
+ title: `${ps.title} (platform)`,
463
+ hostname: ps.hostname,
464
+ serviceId: ps.serviceId ?? ps.id,
465
+ views: viewsForService(ps.id)
466
+ })),
467
+ ...config.sharedServiceActivations
468
+ .filter((activation) => activation.enabled
469
+ && activation.tenantId === selectedTenant.id
470
+ && (!activation.appId || activation.appId === selectedApp?.id))
471
+ .map((activation) => {
472
+ const shared = config.sharedServiceCatalog.find((service) => service.id === activation.sharedServiceId && service.enabled);
473
+ if (!shared)
474
+ return undefined;
475
+ return {
476
+ id: activation.id,
477
+ title: `${shared.title} (shared)`,
478
+ hostname: shared.baseUrl,
479
+ serviceId: shared.serviceId ?? shared.id,
480
+ views: viewsForService(shared.id)
481
+ };
482
+ })
483
+ .filter((service) => !!service)
484
+ ]
485
+ : [];
486
+ event.__bpResponseModel = {
487
+ title: "Route Designer",
488
+ apps: config.apps.map((a) => ({ id: a.id, title: a.title, tenantId: a.tenantId })),
489
+ selectedAppId,
490
+ routes: (selectedApp?.routes ?? []).map((r) => ({
491
+ id: r.id, path: r.path, serviceId: r.serviceId, viewId: r.viewId,
492
+ query: r.query,
493
+ title: r.title,
494
+ renderable: cache.get(r.serviceId)?.viewIndex[r.viewId]?.renderable ?? true,
495
+ enabled: r.enabled
496
+ })),
497
+ availableServices,
498
+ adminApiBase: "/.well-known/bp/admin",
499
+ serviceBaseUrl: `http://${this.config.host === "0.0.0.0" ? "localhost" : this.config.host}:${this.config.port}`
500
+ };
501
+ }
502
+ async populateMenuContext(event) {
503
+ const config = await this.storage.loadConfig();
504
+ const url = new URL(event.req.url ?? "", `http://${event.req.headers.get("host") ?? "localhost"}`);
505
+ const selectedAppId = url.searchParams.get("appId") ?? undefined;
506
+ const selectedApp = selectedAppId ? config.apps.find((a) => a.id === selectedAppId) : undefined;
507
+ event.__bpResponseModel = {
508
+ title: "Menu Designer",
509
+ apps: config.apps.map((a) => ({ id: a.id, title: a.title, tenantId: a.tenantId })),
510
+ selectedAppId,
511
+ menu: (selectedApp?.menu ?? []).map((m) => ({
512
+ id: m.id, type: m.type, title: m.title,
513
+ routeId: m.routeId, href: m.href, enabled: m.enabled !== false
514
+ })),
515
+ routes: (selectedApp?.routes ?? []).filter((r) => r.enabled).map((r) => ({
516
+ id: r.id, path: r.path, title: r.title ?? r.path
517
+ })),
518
+ adminApiBase: "/.well-known/bp/admin",
519
+ serviceBaseUrl: `http://${this.config.host === "0.0.0.0" ? "localhost" : this.config.host}:${this.config.port}`
520
+ };
521
+ }
522
+ async populateFragmentsContext(event) {
523
+ const config = await this.storage.loadConfig();
524
+ const url = new URL(event.req.url ?? "", `http://${event.req.headers.get("host") ?? "localhost"}`);
525
+ const selectedAppId = url.searchParams.get("appId") ?? undefined;
526
+ event.__bpResponseModel = {
527
+ title: "Fragments",
528
+ apps: config.apps.map((a) => ({ id: a.id, title: a.title, tenantId: a.tenantId })),
529
+ selectedAppId,
530
+ adminApiBase: "/.well-known/bp/admin",
531
+ serviceBaseUrl: `http://${this.config.host === "0.0.0.0" ? "localhost" : this.config.host}:${this.config.port}`
532
+ };
533
+ }
534
+ async populatePreviewContext(event) {
535
+ const config = await this.storage.loadConfig();
536
+ const services = [];
537
+ const allServiceHostnames = [
538
+ ...config.tenants.flatMap((t) => t.services.filter((s) => s.enabled).map((s) => ({
539
+ serviceId: s.serviceId ?? s.id, hostname: s.hostname
540
+ }))),
541
+ ...config.platformServices.filter((ps) => ps.enabled).map((ps) => ({
542
+ serviceId: ps.serviceId ?? ps.id, hostname: ps.hostname
543
+ }))
544
+ ];
545
+ const seen = new Set();
546
+ for (const svc of allServiceHostnames) {
547
+ if (seen.has(svc.hostname))
548
+ continue;
549
+ seen.add(svc.hostname);
550
+ services.push({
551
+ serviceId: svc.serviceId,
552
+ endpointBaseUrl: svc.hostname,
553
+ views: []
554
+ });
555
+ }
556
+ event.__bpResponseModel = {
557
+ title: "Component Preview",
558
+ services
559
+ };
560
+ }
561
+ async populateAdminAuthContext(event) {
562
+ const config = await this.storage.loadConfig();
563
+ const url = new URL(event.req.url ?? "", `http://${event.req.headers.get("host") ?? "localhost"}`);
564
+ const selectedAppId = url.searchParams.get("appId") ?? undefined;
565
+ const selectedApp = selectedAppId
566
+ ? config.apps.find((a) => a.id === selectedAppId)
567
+ : config.apps[0];
568
+ const selectedTenantId = selectedApp?.tenantId;
569
+ // All services known to CP (tenant-registered + platform) with their manifest permissions.
570
+ const allServices = [
571
+ ...config.tenants.flatMap((t) => t.services.filter((s) => s.enabled).map((s) => ({
572
+ id: s.id,
573
+ serviceId: s.serviceId ?? s.id,
574
+ hostname: s.hostname,
575
+ title: s.title ?? s.serviceId ?? s.id
576
+ }))),
577
+ ...config.platformServices.filter((ps) => ps.enabled).map((ps) => ({
578
+ id: ps.id,
579
+ serviceId: ps.serviceId ?? ps.id,
580
+ hostname: ps.hostname,
581
+ title: ps.title
582
+ })),
583
+ ...config.sharedServiceActivations
584
+ .filter((activation) => activation.enabled && (!selectedTenantId || activation.tenantId === selectedTenantId))
585
+ .map((activation) => {
586
+ const shared = config.sharedServiceCatalog.find((service) => service.id === activation.sharedServiceId && service.enabled);
587
+ if (!shared)
588
+ return undefined;
589
+ return {
590
+ id: activation.id,
591
+ serviceId: shared.serviceId ?? shared.id,
592
+ hostname: shared.baseUrl,
593
+ title: shared.title
594
+ };
595
+ })
596
+ .filter((service) => !!service)
597
+ ];
598
+ // De-dupe by service instance id.
599
+ const servicesById = new Map();
600
+ for (const svc of allServices) {
601
+ if (!servicesById.has(svc.id))
602
+ servicesById.set(svc.id, svc);
603
+ }
604
+ // Pull per-view permissions from the manifest cache (populated when services poll).
605
+ const cache = getManifestCache();
606
+ const servicePermissions = Array.from(servicesById.values()).map((svc) => {
607
+ const cachedManifest = cache.get(svc.id);
608
+ const views = cachedManifest
609
+ ? Object.values(cachedManifest.viewIndex).map((v) => ({
610
+ viewId: v.viewId,
611
+ path: v.path,
612
+ methods: v.methods,
613
+ ...(v.role ? { role: v.role } : {}),
614
+ requiredPermissions: v.permissions
615
+ }))
616
+ : [];
617
+ return {
618
+ serviceId: svc.id,
619
+ title: svc.title,
620
+ hostname: svc.hostname,
621
+ manifestVersion: cachedManifest?.manifestVersion,
622
+ views
623
+ };
624
+ });
625
+ const appWithAuth = selectedApp;
626
+ const authConfigured = Boolean(appWithAuth?.auth?.serviceId
627
+ && appWithAuth.auth.expectedIssuer
628
+ && appWithAuth.auth.expectedAudience
629
+ && appWithAuth.auth.jwksUri);
630
+ const currentRoles = appWithAuth?.auth?.roles ?? [];
631
+ event.__bpResponseModel = {
632
+ title: "Permission Manager",
633
+ apps: config.apps.map((a) => ({ id: a.id, tenantId: a.tenantId, title: a.title })),
634
+ selectedAppId: selectedApp?.id,
635
+ selectedTenantId,
636
+ authConfigured,
637
+ servicePermissions,
638
+ currentRoles,
639
+ adminApiBase: "/.well-known/bp/admin",
640
+ serviceBaseUrl: `http://${this.config.host === "0.0.0.0" ? "localhost" : this.config.host}:${this.config.port}`
641
+ };
642
+ }
643
+ async populateSettingsContext(event) {
644
+ const config = await this.storage.loadConfig();
645
+ const url = new URL(event.req.url ?? "", `http://${event.req.headers.get("host") ?? "localhost"}`);
646
+ const requestContext = resolveEmbeddedRequestContext(config, eventHeaders(event));
647
+ const requestedAppId = url.searchParams.get("appId") ?? undefined;
648
+ const app = requestContext?.app
649
+ ?? (requestedAppId ? config.apps.find((candidate) => candidate.id === requestedAppId) : undefined)
650
+ ?? config.apps[0];
651
+ if (!app)
652
+ return;
653
+ const tenant = requestContext?.tenant
654
+ ?? config.tenants.find((candidate) => candidate.id === app.tenantId);
655
+ if (!tenant)
656
+ return;
657
+ const activeSharedServiceIds = new Set(config.sharedServiceActivations
658
+ .filter((activation) => activation.enabled
659
+ && activation.tenantId === tenant.id
660
+ && (!activation.appId || activation.appId === app.id))
661
+ .map((activation) => activation.sharedServiceId));
662
+ event.__bpResponseModel = {
663
+ title: "App Settings",
664
+ tenant: { id: tenant.id, title: tenant.title },
665
+ app: { id: app.id, tenantId: app.tenantId, title: app.title, hostnames: app.hostnames },
666
+ idsVisible: true,
667
+ managementDiscoveryUrl: "/.well-known/bp/management",
668
+ automationCatalogUrl: `/.well-known/bp/automation/catalog?appId=${encodeURIComponent(app.id)}`,
669
+ endpoints: {
670
+ current: "/.well-known/bp/manage/current",
671
+ services: "/.well-known/bp/manage/services",
672
+ activateService: "/.well-known/bp/manage/services/activate",
673
+ routes: "/.well-known/bp/manage/routes",
674
+ fragments: "/.well-known/bp/manage/fragments",
675
+ theme: "/.well-known/bp/manage/theme",
676
+ webhooks: "/.well-known/bp/manage/webhooks/targets",
677
+ webhookEvents: "/.well-known/bp/manage/webhooks/events"
678
+ },
679
+ sharedServices: config.sharedServiceCatalog.map((service) => ({
680
+ id: service.id,
681
+ serviceId: service.serviceId,
682
+ title: service.title,
683
+ description: service.description,
684
+ baseUrl: service.baseUrl,
685
+ category: service.category,
686
+ tags: service.tags,
687
+ enabled: service.enabled,
688
+ active: activeSharedServiceIds.has(service.id)
689
+ })),
690
+ routeCount: app.routes.length,
691
+ fragmentCount: app.fragments.length
692
+ };
693
+ }
694
+ }
695
+ export { Config, EventSchemas };
696
+ //# sourceMappingURL=index.js.map