@betterportal/config-manager 10.6.7 → 10.6.9
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/lib/.bsb/clients/service-betterportal-config-manager.d.ts +27 -8
- package/lib/.bsb/clients/service-betterportal-config-manager.d.ts.map +1 -1
- package/lib/.bsb/clients/service-betterportal-config-manager.js +19 -6
- package/lib/.bsb/clients/service-betterportal-config-manager.js.map +1 -1
- package/lib/bp-contracts/org.betterportal.config-manager.json +1 -1
- package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.d.ts +3 -3
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts +3 -0
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js +85 -30
- package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/GET.d.ts +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.js +11 -5
- package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts +3 -1
- package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/cpBootstrap.js +17 -9
- package/lib/plugins/service-betterportal-config-manager/cpBootstrap.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/index.d.ts +25 -7
- package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/index.js +98 -25
- package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts +3 -0
- package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/setupTokens.js +93 -16
- package/lib/plugins/service-betterportal-config-manager/setupTokens.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts +1 -0
- package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/index.d.ts +5 -0
- package/lib/plugins/service-betterportal-config-manager/storage/index.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/index.js +5 -2
- package/lib/plugins/service-betterportal-config-manager/storage/index.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts +95 -1
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.js +358 -25
- package/lib/plugins/service-betterportal-config-manager/storage/postgres.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +2 -0
- package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/syncApi.js +19 -0
- package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts +3 -1
- package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts.map +1 -1
- package/lib/plugins/service-betterportal-config-manager/webhooks.js +41 -44
- package/lib/plugins/service-betterportal-config-manager/webhooks.js.map +1 -1
- package/lib/schemas/service-betterportal-config-manager.json +32 -15
- package/lib/schemas/service-betterportal-config-manager.plugin.json +9 -2
- package/package.json +3 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RsaPublicJwk } from "@betterportal/framework";
|
|
2
|
+
import type { PostgresStorage } from "./storage/postgres.js";
|
|
2
3
|
export interface CpKeyPair {
|
|
3
4
|
privateKeyPem: string;
|
|
4
5
|
publicKeyPem: string;
|
|
@@ -19,9 +20,10 @@ export interface CpBootstrapState {
|
|
|
19
20
|
}
|
|
20
21
|
export declare function cpBootstrap(input: {
|
|
21
22
|
keyStorePath: string;
|
|
23
|
+
postgres?: PostgresStorage;
|
|
22
24
|
issuer?: string;
|
|
23
25
|
audience: string;
|
|
24
26
|
host: string;
|
|
25
27
|
port: number;
|
|
26
|
-
}): CpBootstrapState
|
|
28
|
+
}): Promise<CpBootstrapState>;
|
|
27
29
|
//# sourceMappingURL=cpBootstrap.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cpBootstrap.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/cpBootstrap.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"cpBootstrap.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/cpBootstrap.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,SAAS;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,SAAS,CAAC;IACnB,2CAA2C;IAC3C,GAAG,EAAE,YAAY,CAAC;IAClB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAyB5B"}
|
|
@@ -6,11 +6,14 @@ import { randomBytes, generateKeyPairSync, createPublicKey } from "node:crypto";
|
|
|
6
6
|
* Returns the same object on subsequent calls within a process.
|
|
7
7
|
*/
|
|
8
8
|
let cached = null;
|
|
9
|
-
export function cpBootstrap(input) {
|
|
9
|
+
export async function cpBootstrap(input) {
|
|
10
10
|
if (cached)
|
|
11
11
|
return cached;
|
|
12
12
|
const keyPath = resolve(input.keyStorePath);
|
|
13
|
-
const keyPair =
|
|
13
|
+
const keyPair = input.postgres
|
|
14
|
+
? await input.postgres.loadOrCreateIdentity(generateKeyPair)
|
|
15
|
+
: loadOrGenerateKeyPair(keyPath);
|
|
16
|
+
assertKeyPair(keyPair, input.postgres ? "PostgreSQL" : keyPath);
|
|
14
17
|
if (!input.issuer) {
|
|
15
18
|
throw new Error("Config manager cpIssuer is required and must be the public control-plane issuer URL.");
|
|
16
19
|
}
|
|
@@ -30,21 +33,26 @@ export function cpBootstrap(input) {
|
|
|
30
33
|
function loadOrGenerateKeyPair(filePath) {
|
|
31
34
|
if (existsSync(filePath)) {
|
|
32
35
|
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
33
|
-
if (typeof parsed.privateKeyPem !== "string" || typeof parsed.publicKeyPem !== "string" || typeof parsed.kid !== "string") {
|
|
34
|
-
throw new Error(`Malformed CP keypair file: ${filePath}`);
|
|
35
|
-
}
|
|
36
36
|
return parsed;
|
|
37
37
|
}
|
|
38
|
+
const pair = generateKeyPair();
|
|
39
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
40
|
+
writeFileSync(filePath, JSON.stringify(pair, null, 2), { mode: 0o600 });
|
|
41
|
+
return pair;
|
|
42
|
+
}
|
|
43
|
+
function assertKeyPair(value, source) {
|
|
44
|
+
if (typeof value?.privateKeyPem !== "string" || typeof value?.publicKeyPem !== "string" || typeof value?.kid !== "string") {
|
|
45
|
+
throw new Error(`Malformed CP keypair in ${source}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function generateKeyPair() {
|
|
38
49
|
const { privateKey, publicKey } = generateKeyPairSync("rsa", {
|
|
39
50
|
modulusLength: 2048,
|
|
40
51
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
41
52
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
42
53
|
});
|
|
43
54
|
const kid = randomBytes(16).toString("base64url");
|
|
44
|
-
|
|
45
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
46
|
-
writeFileSync(filePath, JSON.stringify(pair, null, 2), { mode: 0o600 });
|
|
47
|
-
return pair;
|
|
55
|
+
return { privateKeyPem: privateKey, publicKeyPem: publicKey, kid };
|
|
48
56
|
}
|
|
49
57
|
function publicKeyToJwk(publicKeyPem, kid) {
|
|
50
58
|
const keyObject = createPublicKey(publicKeyPem);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cpBootstrap.js","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/cpBootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"cpBootstrap.js","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/cpBootstrap.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,6 +25,7 @@ declare const Config: import("@bsb/base").BSBPluginConfigClass<av.ObjectSchema<{
|
|
|
25
25
|
connectionString: av.StringSchema;
|
|
26
26
|
tableName: av.OptionalSchema<av.StringSchema>;
|
|
27
27
|
rowId: av.OptionalSchema<av.StringSchema>;
|
|
28
|
+
legacyConfigPath: av.OptionalSchema<av.StringSchema>;
|
|
28
29
|
}>]>;
|
|
29
30
|
requestTimeoutMs: av.IntSchema;
|
|
30
31
|
/** Path to RSA keypair JSON used to sign envelope/setup tokens. Auto-generated on first boot. */
|
|
@@ -35,15 +36,20 @@ declare const Config: import("@bsb/base").BSBPluginConfigClass<av.ObjectSchema<{
|
|
|
35
36
|
cpAudience: av.StringSchema;
|
|
36
37
|
}>>;
|
|
37
38
|
declare const EventSchemas: {
|
|
38
|
-
readonly emitEvents: {
|
|
39
|
+
readonly emitEvents: {
|
|
40
|
+
readonly "webhook.delivery.available": {
|
|
41
|
+
input: av.ObjectSchema<{}>;
|
|
42
|
+
description?: string;
|
|
43
|
+
readonly __brand: "fire-and-forget";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
39
46
|
readonly onEvents: {};
|
|
40
47
|
readonly emitReturnableEvents: {};
|
|
41
48
|
readonly onReturnableEvents: {};
|
|
42
49
|
readonly emitBroadcast: {
|
|
43
50
|
readonly "platform-config.changed": {
|
|
44
51
|
input: av.ObjectSchema<{
|
|
45
|
-
|
|
46
|
-
backend: av.EnumSchema<readonly ["file", "postgres"]>;
|
|
52
|
+
revision: av.NumberSchema;
|
|
47
53
|
}>;
|
|
48
54
|
description?: string;
|
|
49
55
|
readonly __brand: "broadcast";
|
|
@@ -74,6 +80,7 @@ export declare class Plugin extends BPService<InstanceType<typeof Config>, typeo
|
|
|
74
80
|
connectionString: av.StringSchema;
|
|
75
81
|
tableName: av.OptionalSchema<av.StringSchema>;
|
|
76
82
|
rowId: av.OptionalSchema<av.StringSchema>;
|
|
83
|
+
legacyConfigPath: av.OptionalSchema<av.StringSchema>;
|
|
77
84
|
}>]>;
|
|
78
85
|
requestTimeoutMs: av.IntSchema;
|
|
79
86
|
/** Path to RSA keypair JSON used to sign envelope/setup tokens. Auto-generated on first boot. */
|
|
@@ -84,15 +91,20 @@ export declare class Plugin extends BPService<InstanceType<typeof Config>, typeo
|
|
|
84
91
|
cpAudience: av.StringSchema;
|
|
85
92
|
}>>;
|
|
86
93
|
static EventSchemas: {
|
|
87
|
-
readonly emitEvents: {
|
|
94
|
+
readonly emitEvents: {
|
|
95
|
+
readonly "webhook.delivery.available": {
|
|
96
|
+
input: av.ObjectSchema<{}>;
|
|
97
|
+
description?: string;
|
|
98
|
+
readonly __brand: "fire-and-forget";
|
|
99
|
+
};
|
|
100
|
+
};
|
|
88
101
|
readonly onEvents: {};
|
|
89
102
|
readonly emitReturnableEvents: {};
|
|
90
103
|
readonly onReturnableEvents: {};
|
|
91
104
|
readonly emitBroadcast: {
|
|
92
105
|
readonly "platform-config.changed": {
|
|
93
106
|
input: av.ObjectSchema<{
|
|
94
|
-
|
|
95
|
-
backend: av.EnumSchema<readonly ["file", "postgres"]>;
|
|
107
|
+
revision: av.NumberSchema;
|
|
96
108
|
}>;
|
|
97
109
|
description?: string;
|
|
98
110
|
readonly __brand: "broadcast";
|
|
@@ -104,7 +116,12 @@ export declare class Plugin extends BPService<InstanceType<typeof Config>, typeo
|
|
|
104
116
|
private storage;
|
|
105
117
|
private webhookRuntime?;
|
|
106
118
|
private previewExpiryTimer?;
|
|
107
|
-
private
|
|
119
|
+
private outboxTimer?;
|
|
120
|
+
private outboxDrain?;
|
|
121
|
+
private readonly workerId;
|
|
122
|
+
private lastConfigRevision;
|
|
123
|
+
private fileConfigRevision;
|
|
124
|
+
private postgresStorage?;
|
|
108
125
|
private readonly selfClient;
|
|
109
126
|
/** CP-side signing keypair + issuer/audience info. Built on first init. */
|
|
110
127
|
private cpState;
|
|
@@ -158,6 +175,7 @@ export declare class Plugin extends BPService<InstanceType<typeof Config>, typeo
|
|
|
158
175
|
private removeExpiredPreviews;
|
|
159
176
|
dispose(): Promise<void>;
|
|
160
177
|
private withChangeBroadcasts;
|
|
178
|
+
private startOutbox;
|
|
161
179
|
private populateConfigAdminContext;
|
|
162
180
|
private populateServicesContext;
|
|
163
181
|
private populateRoutesContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,qBAAqB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,qBAAqB,EAK1B,KAAK,UAAU,EAChB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,SAAS,EAA4B,MAAM,0BAA0B,CAAC;AAG/E,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAsB1E,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,kCAAkC,EACvC,KAAK,oBAAoB,EAG1B,MAAM,yBAAyB,CAAC;AA0EjC,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;IAdV,iGAAiG;;IAEjG,4GAA4G;;IAE5G,sDAAsD;;GAmBvD,CAAC;AAEF,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;CAiBhB,CAAC;AAEH,qBAAa,MAAO,SAAQ,SAAS,CAAC,YAAY,CAAC,OAAO,MAAM,CAAC,EAAE,OAAO,YAAY,CAAC;IACrF,MAAM,CAAC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;QA7Cb,iGAAiG;;QAEjG,4GAA4G;;QAE5G,sDAAsD;;QAyC/B;IACvB,MAAM,CAAC,YAAY;;;;;;;;;;;;;;;;;;;;;MAAgB;IACnC,SAAS,CAAC,QAAQ,CAAC,+BAA+B,SAAS;IAC3D,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,cAAc,CAAC,CAA0D;IACjF,OAAO,CAAC,kBAAkB,CAAC,CAAiB;IAC5C,OAAO,CAAC,WAAW,CAAC,CAAiB;IACrC,OAAO,CAAC,WAAW,CAAC,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAY;IACrC,OAAO,CAAC,kBAAkB,CAAM;IAChC,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,2EAA2E;IAC3E,OAAO,CAAC,OAAO,CAAoB;IACnC,wFAAwF;IACxF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqK;IACrM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAa;gBAEhC,GAAG,EAAE,qBAAqB,CAAC,YAAY,CAAC,OAAO,MAAM,CAAC,EAAE,OAAO,YAAY,CAAC;IAKxF,SAAS,CAAC,UAAU;;;;;;;;;;IAWpB;;;OAGG;IACH,SAAS,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAItF,SAAS,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAIlF,SAAS,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS;IAOnF,SAAS,CAAC,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;IAItH;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;;;;OAKG;YACW,aAAa;YAgCb,gBAAgB;cAgCL,qBAAqB,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kCAAkC,GAAG,IAAI,CAAC;cAe5G,0BAA0B,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;cASjI,YAAY,CAAC,QAAQ,EAAE,oBAAoB,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;YAsI/E,qBAAqB;IAUpB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAOvC,OAAO,CAAC,oBAAoB;IA0B5B,OAAO,CAAC,WAAW;YAiCL,0BAA0B;YA4D1B,uBAAuB;YAyGvB,qBAAqB;YAmHrB,mBAAmB;YA8BnB,wBAAwB;YAcxB,sBAAsB;YAuCtB,wBAAwB;YAoIxB,uBAAuB;CAwDtC;AAED,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createBroadcastEvent, createConfigSchema, createEventSchemas } from "@bsb/base";
|
|
1
|
+
import { createBroadcastEvent, createConfigSchema, createEventSchemas, createFireAndForgetEvent } from "@bsb/base";
|
|
2
2
|
import * as av from "anyvali";
|
|
3
3
|
import { BPService, BetterPortalConfigSchema } from "@betterportal/plugin-bsb";
|
|
4
4
|
import { resolve } from "node:path";
|
|
@@ -10,7 +10,7 @@ import { registerAdminApiRoutes } from "./adminApi.js";
|
|
|
10
10
|
import { registerMenuEditorRoutes } from "./menuEditor.js";
|
|
11
11
|
import { registerFragmentsEditorRoutes } from "./fragmentsEditor.js";
|
|
12
12
|
import { registerWebhookRoutes } from "./webhooks.js";
|
|
13
|
-
import { analyzeOperationDependencies, deriveRolePermissions, getCachedManifestForService, getManifestCache, reconcileServiceRegistry, registerSyncEndpoint } from "./syncApi.js";
|
|
13
|
+
import { analyzeOperationDependencies, deriveRolePermissions, getCachedManifestForService, getManifestCache, hydrateManifestCache, reconcileServiceRegistry, registerSyncEndpoint } from "./syncApi.js";
|
|
14
14
|
import { approveM2MConnections, buildM2MConnectionModel } from "./m2mConnections.js";
|
|
15
15
|
import { registerPreviewDeploymentApi } from "./previewApi.js";
|
|
16
16
|
import { deleteExpiredPreviewDeployments, reconcilePreviewService, visibleAdminConfig } from "./previewEnvironments.js";
|
|
@@ -18,7 +18,7 @@ import { setConfigManagerRouteContext } from "./routeContext.js";
|
|
|
18
18
|
import { isApiRoute } from "./routeMounts.js";
|
|
19
19
|
import { resolveRoleAuthority, resolveRoleSyncUrl } from "./roleAuthority.js";
|
|
20
20
|
import { describeEmbeddedContextResolution, eventHeaders, resolveEmbeddedRequestContext, uuidv7 } from "@betterportal/framework";
|
|
21
|
-
import { createStorageFromConfig, getAvailableServiceInstanceIdsForApp, getServicePluginId, PlatformConfigStorageSchema } from "./storage/index.js";
|
|
21
|
+
import { createStorageFromConfig, getAvailableServiceInstanceIdsForApp, getServicePluginId, PostgresStorage, PlatformConfigStorageSchema } from "./storage/index.js";
|
|
22
22
|
import BetterportalConfigManagerClient from "../../.bsb/clients/service-betterportal-config-manager.js";
|
|
23
23
|
// Parse-only base for relative request URLs. Never emit this origin.
|
|
24
24
|
const RELATIVE_URL_PARSE_BASE = "http://betterportal.invalid";
|
|
@@ -27,7 +27,7 @@ function applyWellKnownCors(event) {
|
|
|
27
27
|
event.res.headers.set("Access-Control-Allow-Origin", origin);
|
|
28
28
|
event.res.headers.set("Vary", "Origin");
|
|
29
29
|
event.res.headers.set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
|
|
30
|
-
event.res.headers.set("Access-Control-Allow-Headers", "Authorization,Content-Type,Accept,HX-Request,HX-Current-URL,HX-Target,HX-Trigger,HX-Trigger-Name,X-BP-Tenant-Id,X-BP-App-Id,traceparent,tracestate,baggage");
|
|
30
|
+
event.res.headers.set("Access-Control-Allow-Headers", "Authorization,Content-Type,Accept,Idempotency-Key,HX-Request,HX-Current-URL,HX-Target,HX-Trigger,HX-Trigger-Name,X-BP-Tenant-Id,X-BP-App-Id,traceparent,tracestate,baggage");
|
|
31
31
|
event.res.headers.set("Access-Control-Expose-Headers", "HX-Location,HX-Redirect,HX-Refresh,HX-Push-Url,HX-Replace-Url,BP-SetHeader,BP-RemoveHeader");
|
|
32
32
|
event.res.headers.set("Access-Control-Max-Age", "600");
|
|
33
33
|
if (event.req.method === "OPTIONS") {
|
|
@@ -52,7 +52,7 @@ function buildServiceIdAliases(config, tenantId) {
|
|
|
52
52
|
}
|
|
53
53
|
const PluginConfigSchema = av.object({
|
|
54
54
|
host: av.string().minLength(1).default("0.0.0.0"),
|
|
55
|
-
port: av.int().min(1).default(
|
|
55
|
+
port: av.int().min(1).default(80),
|
|
56
56
|
betterportal: BetterPortalConfigSchema,
|
|
57
57
|
storage: PlatformConfigStorageSchema,
|
|
58
58
|
requestTimeoutMs: av.int().min(1).default(2000),
|
|
@@ -64,9 +64,9 @@ const PluginConfigSchema = av.object({
|
|
|
64
64
|
cpAudience: av.string().minLength(1).default("betterportal-control-plane")
|
|
65
65
|
});
|
|
66
66
|
const PlatformConfigChangedEventSchema = av.object({
|
|
67
|
-
|
|
68
|
-
backend: av.enum_(["file", "postgres"])
|
|
67
|
+
revision: av.number().min(0)
|
|
69
68
|
});
|
|
69
|
+
const WebhookDeliveryAvailableEventSchema = av.object({});
|
|
70
70
|
const Config = createConfigSchema({
|
|
71
71
|
name: "service-betterportal-config-manager",
|
|
72
72
|
description: "BetterPortal config management admin service",
|
|
@@ -75,12 +75,14 @@ const Config = createConfigSchema({
|
|
|
75
75
|
image: "./betterportal-logo.png"
|
|
76
76
|
}, PluginConfigSchema);
|
|
77
77
|
const EventSchemas = createEventSchemas({
|
|
78
|
-
emitEvents: {
|
|
78
|
+
emitEvents: {
|
|
79
|
+
"webhook.delivery.available": createFireAndForgetEvent(WebhookDeliveryAvailableEventSchema, "Wakes one config-manager instance to process durable webhook deliveries.")
|
|
80
|
+
},
|
|
79
81
|
onEvents: {},
|
|
80
82
|
emitReturnableEvents: {},
|
|
81
83
|
onReturnableEvents: {},
|
|
82
84
|
emitBroadcast: {
|
|
83
|
-
"platform-config.changed": createBroadcastEvent(PlatformConfigChangedEventSchema, "Emitted after platform config is
|
|
85
|
+
"platform-config.changed": createBroadcastEvent(PlatformConfigChangedEventSchema, "Emitted after a platform config revision is committed.")
|
|
84
86
|
},
|
|
85
87
|
onBroadcast: {}
|
|
86
88
|
});
|
|
@@ -91,7 +93,12 @@ export class Plugin extends BPService {
|
|
|
91
93
|
storage;
|
|
92
94
|
webhookRuntime;
|
|
93
95
|
previewExpiryTimer;
|
|
94
|
-
|
|
96
|
+
outboxTimer;
|
|
97
|
+
outboxDrain;
|
|
98
|
+
workerId = uuidv7();
|
|
99
|
+
lastConfigRevision = -1;
|
|
100
|
+
fileConfigRevision = 0;
|
|
101
|
+
postgresStorage;
|
|
95
102
|
selfClient;
|
|
96
103
|
/** CP-side signing keypair + issuer/audience info. Built on first init. */
|
|
97
104
|
cpState;
|
|
@@ -243,17 +250,27 @@ export class Plugin extends BPService {
|
|
|
243
250
|
}
|
|
244
251
|
async onRegistered(registry, _obs) {
|
|
245
252
|
const resolvedStorage = createStorageFromConfig(this.config.storage, this.cwd);
|
|
253
|
+
const cwd = this.cwd ?? ".";
|
|
254
|
+
if (resolvedStorage.store instanceof PostgresStorage) {
|
|
255
|
+
this.postgresStorage = resolvedStorage.store;
|
|
256
|
+
await this.postgresStorage.initialize({
|
|
257
|
+
configPath: resolvedStorage.legacyConfigPath,
|
|
258
|
+
cpKeyPath: resolve(cwd, this.config.cpKeyStorePath ?? "./.bp-cp-state/keys.json"),
|
|
259
|
+
webhookDeliveryPath: resolve(cwd, "./.bp-webhook-deliveries.json")
|
|
260
|
+
});
|
|
261
|
+
}
|
|
246
262
|
this.storage = this.withChangeBroadcasts(resolvedStorage.store, _obs, {
|
|
247
263
|
backend: resolvedStorage.backend
|
|
248
264
|
});
|
|
265
|
+
hydrateManifestCache(await this.storage.loadConfig());
|
|
249
266
|
// Initialize CP keypair + JWKS (P7).
|
|
250
|
-
|
|
251
|
-
this.cpState = cpBootstrap({
|
|
267
|
+
this.cpState = await cpBootstrap({
|
|
252
268
|
keyStorePath: resolve(cwd, this.config.cpKeyStorePath ?? "./.bp-cp-state/keys.json"),
|
|
269
|
+
postgres: this.postgresStorage,
|
|
253
270
|
issuer: this.config.cpIssuer,
|
|
254
271
|
audience: this.config.cpAudience ?? "betterportal-control-plane",
|
|
255
272
|
host: this.config.host ?? "0.0.0.0",
|
|
256
|
-
port: this.config.port ??
|
|
273
|
+
port: this.config.port ?? 80
|
|
257
274
|
});
|
|
258
275
|
this.registerAsAuthProvider({
|
|
259
276
|
issuer: this.cpState.issuer,
|
|
@@ -267,12 +284,17 @@ export class Plugin extends BPService {
|
|
|
267
284
|
cpId: this.cpState.cpId
|
|
268
285
|
});
|
|
269
286
|
await this.selfClient.onPlatformConfigChanged(_obs, async (eventObs, event) => {
|
|
270
|
-
if (event.
|
|
287
|
+
if (event.revision <= this.lastConfigRevision)
|
|
271
288
|
return;
|
|
289
|
+
this.lastConfigRevision = event.revision;
|
|
272
290
|
this.storage.invalidate();
|
|
291
|
+
hydrateManifestCache(await this.storage.loadConfig());
|
|
273
292
|
// A config change may carry a freshly-pushed auth JWKS - rebuild verifiers.
|
|
274
293
|
await this.warmAuthCache(eventObs);
|
|
275
294
|
});
|
|
295
|
+
await this.selfClient.onWebhookDeliveryAvailable(_obs, async () => {
|
|
296
|
+
await this.webhookRuntime?.drain();
|
|
297
|
+
});
|
|
276
298
|
setConfigManagerRouteContext({
|
|
277
299
|
storage: this.storage,
|
|
278
300
|
cpState: this.cpState,
|
|
@@ -295,7 +317,7 @@ export class Plugin extends BPService {
|
|
|
295
317
|
registerAdminApiRoutes(this.app, this.storage, this.cpState);
|
|
296
318
|
registerMenuEditorRoutes(this.app, this.storage);
|
|
297
319
|
registerFragmentsEditorRoutes(this.app, this.storage);
|
|
298
|
-
this.webhookRuntime = registerWebhookRoutes(this.app, this.storage);
|
|
320
|
+
this.webhookRuntime = registerWebhookRoutes(this.app, this.storage, this.postgresStorage, this.workerId);
|
|
299
321
|
registerPreviewDeploymentApi({
|
|
300
322
|
app: this.app,
|
|
301
323
|
storage: this.storage,
|
|
@@ -323,18 +345,27 @@ export class Plugin extends BPService {
|
|
|
323
345
|
}
|
|
324
346
|
});
|
|
325
347
|
// Setup token mint + redeem endpoints (P4)
|
|
326
|
-
registerSetupEndpoints({
|
|
348
|
+
registerSetupEndpoints({
|
|
349
|
+
app: this.app,
|
|
350
|
+
storage: this.storage,
|
|
351
|
+
cpState: this.cpState,
|
|
352
|
+
postgres: this.postgresStorage,
|
|
353
|
+
owner: this.workerId
|
|
354
|
+
});
|
|
327
355
|
// Bootstrap detection + endpoint (P6) - opens vanilla HTML wizard on empty DB
|
|
328
356
|
await registerBootstrapEndpoint({
|
|
329
357
|
app: this.app,
|
|
330
358
|
storage: this.storage,
|
|
331
359
|
cpState: this.cpState,
|
|
332
|
-
logger: _obs
|
|
360
|
+
logger: _obs,
|
|
361
|
+
postgres: this.postgresStorage,
|
|
362
|
+
owner: this.workerId
|
|
333
363
|
});
|
|
334
364
|
// Warm auth verifiers from persisted JWKS so the first authenticated request
|
|
335
365
|
// after a restart never races an empty cache (#6).
|
|
336
366
|
await this.warmAuthCache(_obs);
|
|
337
|
-
this.webhookRuntime
|
|
367
|
+
this.webhookRuntime?.start();
|
|
368
|
+
this.startOutbox(_obs);
|
|
338
369
|
await this.removeExpiredPreviews();
|
|
339
370
|
this.previewExpiryTimer = setInterval(() => {
|
|
340
371
|
void this.removeExpiredPreviews().catch((error) => _obs.log.warn("Preview expiry cleanup failed: {msg}", {
|
|
@@ -344,13 +375,22 @@ export class Plugin extends BPService {
|
|
|
344
375
|
this.previewExpiryTimer.unref();
|
|
345
376
|
}
|
|
346
377
|
async removeExpiredPreviews() {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
378
|
+
const cleanup = async () => {
|
|
379
|
+
const config = await this.storage.loadConfig();
|
|
380
|
+
if (deleteExpiredPreviewDeployments(config).length > 0)
|
|
381
|
+
await this.storage.saveConfig(config);
|
|
382
|
+
await this.postgresStorage?.cleanupExpiredActions();
|
|
383
|
+
};
|
|
384
|
+
if (this.postgresStorage)
|
|
385
|
+
await this.postgresStorage.tryRunExclusive("maintenance", cleanup);
|
|
386
|
+
else
|
|
387
|
+
await cleanup();
|
|
350
388
|
}
|
|
351
389
|
async dispose() {
|
|
352
390
|
if (this.previewExpiryTimer)
|
|
353
391
|
clearInterval(this.previewExpiryTimer);
|
|
392
|
+
if (this.outboxTimer)
|
|
393
|
+
clearInterval(this.outboxTimer);
|
|
354
394
|
this.webhookRuntime?.stop();
|
|
355
395
|
await super.dispose();
|
|
356
396
|
}
|
|
@@ -358,11 +398,12 @@ export class Plugin extends BPService {
|
|
|
358
398
|
return {
|
|
359
399
|
loadConfig: () => store.loadConfig(),
|
|
360
400
|
saveConfig: async (config, options) => {
|
|
361
|
-
await store.saveConfig(config, options
|
|
362
|
-
|
|
401
|
+
await store.saveConfig(config, metadata.backend === "file" && options?.notify !== false
|
|
402
|
+
? { ...options, notify: false }
|
|
403
|
+
: options);
|
|
404
|
+
if (options?.notify !== false && metadata.backend === "file") {
|
|
363
405
|
await this.events.emitBroadcast("platform-config.changed", obs, {
|
|
364
|
-
|
|
365
|
-
backend: metadata.backend
|
|
406
|
+
revision: ++this.fileConfigRevision
|
|
366
407
|
});
|
|
367
408
|
}
|
|
368
409
|
},
|
|
@@ -373,6 +414,38 @@ export class Plugin extends BPService {
|
|
|
373
414
|
onChange: (listener) => store.onChange(listener)
|
|
374
415
|
};
|
|
375
416
|
}
|
|
417
|
+
startOutbox(obs) {
|
|
418
|
+
if (!this.postgresStorage)
|
|
419
|
+
return;
|
|
420
|
+
const run = async () => {
|
|
421
|
+
const records = await this.postgresStorage.claimOutbox(this.workerId);
|
|
422
|
+
for (const record of records) {
|
|
423
|
+
try {
|
|
424
|
+
if (record.eventName === "platform-config.changed") {
|
|
425
|
+
await this.events.emitBroadcast("platform-config.changed", obs, {
|
|
426
|
+
revision: Number(record.payload.revision)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
else if (record.eventName === "webhook.delivery.available") {
|
|
430
|
+
await this.events.emitEvent("webhook.delivery.available", obs, {});
|
|
431
|
+
}
|
|
432
|
+
await this.postgresStorage.completeOutbox(record.id, this.workerId);
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
await this.postgresStorage.releaseOutbox(record.id, this.workerId);
|
|
436
|
+
obs.log.warn("Config-manager outbox delivery failed: {message}", {
|
|
437
|
+
message: error instanceof Error ? error.message : String(error)
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
const drain = () => this.outboxDrain ??= run().finally(() => { this.outboxDrain = undefined; });
|
|
443
|
+
void drain().catch((error) => obs.log.warn("Config-manager outbox drain failed: {message}", {
|
|
444
|
+
message: error instanceof Error ? error.message : String(error)
|
|
445
|
+
}));
|
|
446
|
+
this.outboxTimer = setInterval(() => void drain().catch((error) => obs.log.warn("Config-manager outbox drain failed: {message}", { message: error instanceof Error ? error.message : String(error) })), 1_000);
|
|
447
|
+
this.outboxTimer.unref();
|
|
448
|
+
}
|
|
376
449
|
async populateConfigAdminContext(event) {
|
|
377
450
|
const portalConfig = visibleAdminConfig(await this.storage.loadConfig());
|
|
378
451
|
const requestContext = resolveEmbeddedRequestContext(portalConfig, eventHeaders(event));
|