@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.
Files changed (48) hide show
  1. package/lib/.bsb/clients/service-betterportal-config-manager.d.ts +27 -8
  2. package/lib/.bsb/clients/service-betterportal-config-manager.d.ts.map +1 -1
  3. package/lib/.bsb/clients/service-betterportal-config-manager.js +19 -6
  4. package/lib/.bsb/clients/service-betterportal-config-manager.js.map +1 -1
  5. package/lib/bp-contracts/org.betterportal.config-manager.json +1 -1
  6. package/lib/plugins/service-betterportal-config-manager/.bp-generated/registry.d.ts +3 -3
  7. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts +3 -0
  8. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.d.ts.map +1 -1
  9. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js +85 -30
  10. package/lib/plugins/service-betterportal-config-manager/bootstrapEndpoint.js.map +1 -1
  11. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/GET.d.ts +1 -1
  12. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.d.ts.map +1 -1
  13. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.js +11 -5
  14. package/lib/plugins/service-betterportal-config-manager/bp-routes/services/_renderer.bootstrap5/GET.js.map +1 -1
  15. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts +3 -1
  16. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.d.ts.map +1 -1
  17. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.js +17 -9
  18. package/lib/plugins/service-betterportal-config-manager/cpBootstrap.js.map +1 -1
  19. package/lib/plugins/service-betterportal-config-manager/index.d.ts +25 -7
  20. package/lib/plugins/service-betterportal-config-manager/index.d.ts.map +1 -1
  21. package/lib/plugins/service-betterportal-config-manager/index.js +98 -25
  22. package/lib/plugins/service-betterportal-config-manager/index.js.map +1 -1
  23. package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts +3 -0
  24. package/lib/plugins/service-betterportal-config-manager/setupTokens.d.ts.map +1 -1
  25. package/lib/plugins/service-betterportal-config-manager/setupTokens.js +93 -16
  26. package/lib/plugins/service-betterportal-config-manager/setupTokens.js.map +1 -1
  27. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts +1 -0
  28. package/lib/plugins/service-betterportal-config-manager/storage/core.d.ts.map +1 -1
  29. package/lib/plugins/service-betterportal-config-manager/storage/core.js.map +1 -1
  30. package/lib/plugins/service-betterportal-config-manager/storage/index.d.ts +5 -0
  31. package/lib/plugins/service-betterportal-config-manager/storage/index.d.ts.map +1 -1
  32. package/lib/plugins/service-betterportal-config-manager/storage/index.js +5 -2
  33. package/lib/plugins/service-betterportal-config-manager/storage/index.js.map +1 -1
  34. package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts +95 -1
  35. package/lib/plugins/service-betterportal-config-manager/storage/postgres.d.ts.map +1 -1
  36. package/lib/plugins/service-betterportal-config-manager/storage/postgres.js +358 -25
  37. package/lib/plugins/service-betterportal-config-manager/storage/postgres.js.map +1 -1
  38. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts +2 -0
  39. package/lib/plugins/service-betterportal-config-manager/syncApi.d.ts.map +1 -1
  40. package/lib/plugins/service-betterportal-config-manager/syncApi.js +19 -0
  41. package/lib/plugins/service-betterportal-config-manager/syncApi.js.map +1 -1
  42. package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts +3 -1
  43. package/lib/plugins/service-betterportal-config-manager/webhooks.d.ts.map +1 -1
  44. package/lib/plugins/service-betterportal-config-manager/webhooks.js +41 -44
  45. package/lib/plugins/service-betterportal-config-manager/webhooks.js.map +1 -1
  46. package/lib/schemas/service-betterportal-config-manager.json +32 -15
  47. package/lib/schemas/service-betterportal-config-manager.plugin.json +9 -2
  48. 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;AAE5D,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,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,gBAAgB,CAsBnB"}
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 = loadOrGenerateKeyPair(keyPath);
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
- const pair = { privateKeyPem: privateKey, publicKeyPem: publicKey, kid };
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":"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"}
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
- sourceId: av.StringSchema;
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
- sourceId: av.StringSchema;
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 readonly changeSourceId;
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":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/service-betterportal-config-manager/index.ts"],"names":[],"mappings":"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"}
@@ -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(3300),
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
- sourceId: av.string().minLength(1),
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 saved by this config-manager instance.")
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
- changeSourceId = uuidv7();
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
- const cwd = this.cwd ?? ".";
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 ?? 3300
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.sourceId === this.changeSourceId)
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({ app: this.app, storage: this.storage, cpState: this.cpState });
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.start();
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 config = await this.storage.loadConfig();
348
- if (deleteExpiredPreviewDeployments(config).length > 0)
349
- await this.storage.saveConfig(config);
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
- if (options?.notify !== false) {
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
- sourceId: this.changeSourceId,
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));