@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.
- package/README.md +3 -0
- package/bsb-plugin.json +23 -0
- package/bsb-tests.json +14 -0
- package/lib/.bsb/clients/service-betterportal-config-manager.d.ts +37 -0
- package/lib/.bsb/clients/service-betterportal-config-manager.d.ts.map +1 -0
- package/lib/.bsb/clients/service-betterportal-config-manager.js +40 -0
- package/lib/.bsb/clients/service-betterportal-config-manager.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.d.ts +3 -0
- package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.js +235 -0
- package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts +4 -0
- package/lib/plugins/service-betterportal-config-manager/adminApi.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/adminApi.js +2319 -0
- package/lib/plugins/service-betterportal-config-manager/adminApi.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts +21 -0
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js +269 -0
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bootstrapWizardHtml.d.ts +19 -0
- package/lib/plugins/service-betterportal-config-manager/bootstrapWizardHtml.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bootstrapWizardHtml.js +329 -0
- package/lib/plugins/service-betterportal-config-manager/bootstrapWizardHtml.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/index.d.ts +4 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/index.js +38 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.d.ts +96 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.js +78 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/auth/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.bootstrap1/index.d.ts +4 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.bootstrap1/index.js +62 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.embedded.d.ts +5 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.embedded.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.embedded.js +5 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/_theme.embedded.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.d.ts +43 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.js +68 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/config/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/_theme.bootstrap1/index.d.ts +5 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/_theme.bootstrap1/index.js +11 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.d.ts +32 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.js +32 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/fragments/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/_theme.bootstrap1/index.d.ts +4 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/_theme.bootstrap1/index.js +170 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.d.ts +60 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.js +48 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/menu/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/_theme.bootstrap1/index.d.ts +4 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/_theme.bootstrap1/index.js +28 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.d.ts +48 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.js +40 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/preview/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/index.d.ts +5 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/index.js +194 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.d.ts +80 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.js +59 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/routes/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/index.d.ts +5 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/index.js +167 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.d.ts +128 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.js +89 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/_theme.bootstrap1/index.d.ts +5 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/_theme.bootstrap1/index.js +8 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.d.ts +89 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.js +93 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/settings/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_theme.bootstrap1/index.d.ts +4 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_theme.bootstrap1/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_theme.bootstrap1/index.js +61 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/_theme.bootstrap1/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.d.ts +180 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.js +405 -0
- package/lib/plugins/service-betterportal-config-manager/bp-routes/tenants/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts +26 -0
- package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/cpBootstrap.js +58 -0
- package/lib/plugins/service-betterportal-config-manager/cpBootstrap.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/fragmentsEditor.d.ts +3 -0
- package/lib/plugins/service-betterportal-config-manager/fragmentsEditor.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/fragmentsEditor.js +365 -0
- package/lib/plugins/service-betterportal-config-manager/fragmentsEditor.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/index.d.ts +143 -0
- package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/index.js +696 -0
- package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts +3 -0
- package/lib/plugins/service-betterportal-config-manager/menuEditor.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/menuEditor.js +823 -0
- package/lib/plugins/service-betterportal-config-manager/menuEditor.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/routeContext.d.ts +10 -0
- package/lib/plugins/service-betterportal-config-manager/routeContext.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/routeContext.js +11 -0
- package/lib/plugins/service-betterportal-config-manager/routeContext.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts +18 -0
- package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/setupTokens.js +245 -0
- package/lib/plugins/service-betterportal-config-manager/setupTokens.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts +41 -0
- package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/core.js +396 -0
- package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/file.d.ts +10 -0
- package/lib/plugins/service-betterportal-config-manager/storage/file.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/file.js +30 -0
- package/lib/plugins/service-betterportal-config-manager/storage/file.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/index.d.ts +36 -0
- package/lib/plugins/service-betterportal-config-manager/storage/index.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/index.js +52 -0
- package/lib/plugins/service-betterportal-config-manager/storage/index.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts +15 -0
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.js +60 -0
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +44 -0
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/syncApi.js +280 -0
- package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts +6 -0
- package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts.map +1 -0
- package/lib/plugins/service-betterportal-config-manager/webhooks.js +372 -0
- package/lib/plugins/service-betterportal-config-manager/webhooks.js.map +1 -0
- package/lib/schemas/service-betterportal-config-manager.json +157 -0
- package/lib/schemas/service-betterportal-config-manager.plugin.json +135 -0
- 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
|