@camstack/server 1.2.593 → 1.2.595

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.
@@ -184,8 +184,11 @@ function writeConfigFile(configPath, data) {
184
184
  /** Live Moleculer registry snapshot — reused by agent-bootstrap's Option A. */
185
185
  function getRegistryNodes(broker) {
186
186
  try {
187
- const registry = broker.registry;
188
- return registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
187
+ return broker.registry.getNodeList({ onlyAvailable: true }).map((node) => ({
188
+ id: node.id,
189
+ ...(node.hostname != null ? { hostname: node.hostname } : {}),
190
+ ...(node.ipList != null ? { ipList: node.ipList } : {}),
191
+ }));
189
192
  }
190
193
  catch {
191
194
  return [];
@@ -202,8 +202,7 @@ function readDeployedAddonIds(addonDir) {
202
202
  }
203
203
  function isHubConnected(broker) {
204
204
  try {
205
- const registry = broker.registry;
206
- const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
205
+ const nodes = broker.registry.getNodeList({ onlyAvailable: true });
207
206
  return nodes.some((n) => n.id === 'hub');
208
207
  }
209
208
  catch {
@@ -367,8 +366,7 @@ function createAgentService(deps) {
367
366
  },
368
367
  deploy: {
369
368
  handler: async (ctx) => {
370
- const { params } = ctx;
371
- const { addonId, source, bundle } = params;
369
+ const { addonId, source, bundle } = ctx.params;
372
370
  // execFile (no shell) instead of execSync — addonId comes from RPC
373
371
  // params and was previously interpolated into a shell command, which
374
372
  // is a command-injection vector. argv form doesn't go through a shell
@@ -469,7 +467,7 @@ function createAgentService(deps) {
469
467
  */
470
468
  stageDevUploads: {
471
469
  handler: async (ctx) => {
472
- const { params } = ctx;
470
+ const params = ctx.params;
473
471
  const seam = {
474
472
  dataDir: deps.dataDir,
475
473
  fetchBundle: (s) => (0, fetch_bundle_from_hub_js_1.fetchBundleFromHub)(s),
@@ -487,7 +485,7 @@ function createAgentService(deps) {
487
485
  */
488
486
  distributeModel: {
489
487
  handler: async (ctx) => {
490
- const { params } = ctx;
488
+ const params = ctx.params;
491
489
  const { execFile } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
492
490
  const { promisify } = await Promise.resolve().then(() => __importStar(require('node:util')));
493
491
  const execFileAsync = promisify(execFile);
@@ -526,12 +524,13 @@ function createAgentService(deps) {
526
524
  undeploy: {
527
525
  handler: async (ctx) => {
528
526
  const { params, broker } = ctx;
527
+ const bkr = (0, system_1.asBrokerLike)(broker);
529
528
  return (0, agent_undeploy_js_1.undeployFromAgent)({
530
529
  addonsDir: deps.addonsDir,
531
530
  loadedAddons: deps.loadedAddons,
532
531
  bootstrapPackages: deps.bootstrapPackages ?? DEFAULT_AGENT_BOOTSTRAP_PACKAGES,
533
- listRunnersByAddon: (addonId) => broker.call('$process.listByAddon', { addonId }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS }),
534
- stopRunner: (name) => broker.call('$process.stop', { name }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS }),
532
+ listRunnersByAddon: (addonId) => bkr.call('$process.listByAddon', { addonId }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS }),
533
+ stopRunner: (name) => bkr.call('$process.stop', { name }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS }),
535
534
  destroyService: (addonId) => broker.destroyService(addonId),
536
535
  unregisterProvider: (capName, addonId) => deps.unregisterProvider?.(capName, addonId),
537
536
  respawnSurvivors: deps.reloadDeployedAddons,
@@ -566,6 +565,7 @@ function createAgentService(deps) {
566
565
  restart: {
567
566
  handler: async (ctx) => {
568
567
  const { params, broker } = ctx;
568
+ const bkr = (0, system_1.asBrokerLike)(broker);
569
569
  const { addonId } = params;
570
570
  // Delegate to THIS agent's own `$process` service — the SAME primitive
571
571
  // the hub uses for a local forked addon (AddonRegistryService.restartAddon
@@ -575,7 +575,7 @@ function createAgentService(deps) {
575
575
  // Moleculer node evicted, and the addon's caps re-registered — identical
576
576
  // semantics to a hub-local restart. Pin to the agent's own node so the
577
577
  // call always hits THIS agent's `$process`, never the hub's.
578
- const result = await broker.call('$process.restart', { name: addonId }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS });
578
+ const result = await bkr.call('$process.restart', { name: addonId }, { nodeID: broker.nodeID, timeout: AGENT_PROCESS_RESTART_TIMEOUT_MS });
579
579
  if (!result.success) {
580
580
  // Fail loudly so the hub's `nodes.restartAddon` surfaces the real
581
581
  // error instead of the old fire-and-forget phantom success.
@@ -279,8 +279,7 @@ async function startAgent(configPath) {
279
279
  // Fall through to live registry-derived state (connected/unconfigured/disconnected).
280
280
  // getRegistryNodes-equivalent inline: broker.registry.getNodeList
281
281
  try {
282
- const registry = broker.registry;
283
- const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
282
+ const nodes = broker.registry.getNodeList({ onlyAvailable: true });
284
283
  if (nodes.some((n) => n.id === 'hub'))
285
284
  return 'connected';
286
285
  }
@@ -840,12 +839,20 @@ async function startAgent(configPath) {
840
839
  // Wired after broker.start() so the broker event bus is operational.
841
840
  const agentReadinessRegistry = (0, system_1.getOrInitReadinessRegistry)(broker, agentBrokerEventBus, consoleLogger);
842
841
  agentUdsRegistry.onReadinessSnapshotRequest(() => agentReadinessRegistry.getSnapshotForTransport());
842
+ // D8 reverse leg: expose THIS agent's readiness snapshot so the hub can
843
+ // snapshot-reconcile agent-origin records on every (re)connect and
844
+ // periodically. Without this, the hub learns agent readiness ONLY from
845
+ // lossy cross-node `system.ready-state` broadcasts — a lost `ready`
846
+ // (2026-07-17 incident) left the hub's authoritative registry stuck on
847
+ // the synthesized `down` forever. Registered under
848
+ // `$agent-readiness` (NOT `$readiness`) so the agent's own unqualified
849
+ // `$readiness.getSnapshot` hub-hydrate call is never shadowed locally.
843
850
  broker.createService((0, system_1.createReadinessServiceForRegistry)(agentReadinessRegistry, system_1.AGENT_READINESS_SERVICE_NAME));
844
851
  }
845
852
  // ── HTTP server (Fastify) — status API + process management + UI ──
846
853
  // Started early (before addon boot) so the status page is reachable
847
854
  // even while addons are still loading.
848
- const getBrokerFn = (() => broker);
855
+ const getBrokerFn = () => broker;
849
856
  // ── Update controls for the agent UI ──────────────────────────────
850
857
  // Framework: the LOCAL `AgentUpdateService` — `npm view` + `npm install`
851
858
  // straight from the registry, no hub in the path, so an isolated node can
@@ -488,7 +488,7 @@ function createNodeRootPackageLookup(moleculer, serverUpdate) {
488
488
  };
489
489
  }
490
490
  function buildNodesProvider(agentRegistry, moleculer, addonRegistry, getNodeRootPackage, logger) {
491
- const broker = moleculer.broker;
491
+ const broker = (0, system_1.asBrokerLike)(moleculer.broker);
492
492
  return {
493
493
  topology: async () => computeTopology(agentRegistry, addonRegistry, getNodeRootPackage, (nodeId) => moleculer.isNodeRegistered(nodeId)),
494
494
  directory: async () => agentRegistry.listNodeDirectory(),
@@ -1462,7 +1462,7 @@ function buildUninstallSeam(ar, ps, broker, agentRegistry) {
1462
1462
  };
1463
1463
  }
1464
1464
  function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, agentRegistry) {
1465
- const broker = moleculer.broker;
1465
+ const broker = (0, system_1.asBrokerLike)(moleculer.broker);
1466
1466
  const uninstallSeam = buildUninstallSeam(ar, ps, broker, agentRegistry);
1467
1467
  const frameworkAllowSet = new Set([addon_package_service_js_1.SYSTEM_PACKAGE]);
1468
1468
  // Process-wide runner built once at boot (manual-boot wires its deps from the
@@ -10868,6 +10868,24 @@ function createCapRouter_storageAdmin(getProvider, createRemoteProxy) {
10868
10868
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
10869
10869
  return p.adoptRecords(methodInput);
10870
10870
  }),
10871
+ readLegacyCollection: trpc_middleware_js_1.adminProcedure
10872
+ .input(types_124.storageAdminCapability.methods.readLegacyCollection.input.loose())
10873
+ .output(types_124.storageAdminCapability.methods.readLegacyCollection.output)
10874
+ .query(async ({ input, ctx }) => {
10875
+ const { nodeId, ...methodInput } = input;
10876
+ const p = resolveProvider('storage-admin', nodeId, () => getProvider(ctx), createRemoteProxy);
10877
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
10878
+ return p.readLegacyCollection(methodInput);
10879
+ }),
10880
+ isAdoptedLegacyTable: trpc_middleware_js_1.adminProcedure
10881
+ .input(types_124.storageAdminCapability.methods.isAdoptedLegacyTable.input.loose())
10882
+ .output(types_124.storageAdminCapability.methods.isAdoptedLegacyTable.output)
10883
+ .query(async ({ input, ctx }) => {
10884
+ const { nodeId, ...methodInput } = input;
10885
+ const p = resolveProvider('storage-admin', nodeId, () => getProvider(ctx), createRemoteProxy);
10886
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
10887
+ return p.isAdoptedLegacyTable(methodInput);
10888
+ }),
10871
10889
  });
10872
10890
  }
10873
10891
  function createCapRouter_storageEvictable(getProvider, createRemoteProxy) {
@@ -61,8 +61,12 @@
61
61
  ],
62
62
  "lib/webrtc/src/media/rtpSender.js": ["RTP_HISTORY_SIZE = 4096"],
63
63
  "lib/rtp/src/rtp/rtp.js": ["camstack: D868 — SRTP output arena"],
64
- "lib/rtp/src/srtp/cipher/ctr.js": ["camstack: D868 — header, ciphertext and tag in ONE arena slice"],
65
- "lib/rtp/src/srtp/cipher/gcm.js": ["camstack: D868 — header, ciphertext and tag in ONE arena slice"]
64
+ "lib/rtp/src/srtp/cipher/ctr.js": [
65
+ "camstack: D868 — header, ciphertext and tag in ONE arena slice"
66
+ ],
67
+ "lib/rtp/src/srtp/cipher/gcm.js": [
68
+ "camstack: D868 — header, ciphertext and tag in ONE arena slice"
69
+ ]
66
70
  },
67
71
  "declaredBy": [
68
72
  "packages/addon-pipeline",
@@ -53,6 +53,7 @@ const addon_settings_provider_js_1 = require("./addon-settings-provider.js");
53
53
  const device_meta_mirror_js_1 = require("./device-meta-mirror.js");
54
54
  const forked_custom_actions_js_1 = require("./forked-custom-actions.js");
55
55
  const integration_visibility_js_1 = require("./integration-visibility.js");
56
+ const record_kinds_js_1 = require("../auth/record-kinds.js");
56
57
  const materialize_route_path_js_1 = require("./materialize-route-path.js");
57
58
  const package_dir_utils_1 = require("./package-dir-utils");
58
59
  const prune_misplaced_addons_js_1 = require("./prune-misplaced-addons.js");
@@ -96,6 +97,12 @@ function addonUpdatePayload(fromVersion, toVersion) {
96
97
  toVersion,
97
98
  };
98
99
  }
100
+ function isAddonDeclaration(value) {
101
+ if (typeof value !== 'object' || value === null)
102
+ return false;
103
+ const id = Reflect.get(value, 'id');
104
+ return typeof id === 'string' && id.length > 0;
105
+ }
99
106
  /**
100
107
  * Type predicate: true when an `ISettingsBackend` also satisfies the
101
108
  * sync `ISettingsStore` contract (system/addon/device key-value ops)
@@ -360,6 +367,9 @@ class AddonRegistryService {
360
367
  activeStorageProvider = null;
361
368
  activeSettingsBackend = null;
362
369
  integrationRegistry = null;
370
+ clusterNodeHistoryStore = null;
371
+ clusterAgentRecords = null;
372
+ shareTokenService = null;
363
373
  // Device architecture — in-memory registry of live IDevice instances.
364
374
  // Persistence is owned by the `device-manager` capability addon.
365
375
  deviceRegistry = new system_1.DeviceRegistry();
@@ -375,7 +385,7 @@ class AddonRegistryService {
375
385
  * `this.broker.call(...)` / `this.broker.nodeID` lint clean.
376
386
  */
377
387
  get broker() {
378
- return this.moleculer.broker;
388
+ return (0, system_1.asBrokerLike)(this.moleculer.broker);
379
389
  }
380
390
  constructor(loggingService, eventBusService, configService, storageService, capabilityService, moleculer, streamProbe) {
381
391
  this.loggingService = loggingService;
@@ -1334,6 +1344,43 @@ class AddonRegistryService {
1334
1344
  loadCollectionPreferences() {
1335
1345
  // TODO: implement CapabilityRegistry.loadDisabledProviders() to restore persisted preferences
1336
1346
  }
1347
+ /** Durable cluster-node history (A1) — bind records kind after orphan declare. */
1348
+ setClusterNodeHistoryStore(store) {
1349
+ this.clusterNodeHistoryStore = store;
1350
+ this.bindClusterNodeHistoryRecordsKind();
1351
+ }
1352
+ bindClusterNodeHistoryRecordsKind() {
1353
+ const clusterStore = this.clusterNodeHistoryStore;
1354
+ const records = this.clusterAgentRecords;
1355
+ if (clusterStore === null || records === null)
1356
+ return;
1357
+ const kind = records.kind(system_1.clusterNodeHistoryRecordKind);
1358
+ clusterStore.bindRecordsKind(() => kind);
1359
+ }
1360
+ setShareTokenService(service) {
1361
+ this.shareTokenService = service;
1362
+ }
1363
+ async wireHubMainOrphanRecords(store) {
1364
+ const recordsCap = this.capabilityRegistry.getSingleton('records');
1365
+ if (recordsCap === null || recordsCap === undefined)
1366
+ return;
1367
+ const integrationRecords = this.recordsForOwner('integration-registry', this.secretsForOwner('integration-registry'));
1368
+ await integrationRecords.declare(system_1.integrationRegistryRecordKinds);
1369
+ this.integrationRegistry = new system_1.IntegrationRegistry(store, system_1.IntegrationRegistry.recordStoresOf(integrationRecords));
1370
+ await this.integrationRegistry.initialize();
1371
+ this.logger.info('IntegrationRegistry initialized (records)', { meta: { phase: 'v2' } });
1372
+ const clusterRecords = this.recordsForOwner('cluster-agent', this.secretsForOwner('cluster-agent'));
1373
+ await clusterRecords.declare(system_1.clusterAgentRecordKinds);
1374
+ this.clusterAgentRecords = clusterRecords;
1375
+ this.bindClusterNodeHistoryRecordsKind();
1376
+ const shareTokens = this.shareTokenService;
1377
+ if (shareTokens !== null) {
1378
+ const coreRecords = this.recordsForOwner('core-auth', this.secretsForOwner('core-auth'));
1379
+ await coreRecords.declare(record_kinds_js_1.coreAuthRecordKinds);
1380
+ const shareKind = coreRecords.kind(record_kinds_js_1.coreAuthRecordKinds['share-view-tokens']);
1381
+ shareTokens.bindShareTokensKind(() => shareKind);
1382
+ }
1383
+ }
1337
1384
  /**
1338
1385
  * Returns the IntegrationRegistry filtered to exclude orphaned integrations
1339
1386
  * (where the addon is not currently installed). Orphaned data stays in DB
@@ -2210,11 +2257,12 @@ class AddonRegistryService {
2210
2257
  ...(bundleRaw.icon !== undefined ? { icon: bundleRaw.icon } : {}),
2211
2258
  }
2212
2259
  : undefined;
2213
- for (const decl of addons) {
2214
- const id = typeof decl['id'] === 'string' ? decl['id'] : '';
2215
- const name = typeof decl['name'] === 'string' ? decl['name'] : id;
2216
- if (!id)
2260
+ for (const rawDecl of addons) {
2261
+ if (!isAddonDeclaration(rawDecl))
2217
2262
  continue;
2263
+ const decl = rawDecl;
2264
+ const id = decl.id;
2265
+ const name = typeof decl.name === 'string' ? decl.name : id;
2218
2266
  const manifest = {
2219
2267
  ...decl,
2220
2268
  id,
@@ -2550,9 +2598,16 @@ class AddonRegistryService {
2550
2598
  registry: this.capabilityRegistry,
2551
2599
  logger: this.logger,
2552
2600
  }, store);
2553
- this.integrationRegistry = new system_1.IntegrationRegistry(store);
2554
- void this.integrationRegistry.initialize().then(() => {
2555
- this.logger.info('IntegrationRegistry initialized', { meta: { phase: 'v2' } });
2601
+ void this.wireHubMainOrphanRecords(store).catch((err) => {
2602
+ this.logger.warn('hub-main records wiring failed (best-effort)', {
2603
+ meta: { error: (0, types_1.errMsg)(err) },
2604
+ });
2605
+ this.integrationRegistry = new system_1.IntegrationRegistry(store);
2606
+ void this.integrationRegistry.initialize().then(() => {
2607
+ this.logger.info('IntegrationRegistry initialized (legacy)', {
2608
+ meta: { phase: 'v2' },
2609
+ });
2610
+ });
2556
2611
  });
2557
2612
  this.loadCollectionPreferences();
2558
2613
  // DeviceStore/ConfigStore are owned by the `device-persistence`
@@ -526,7 +526,7 @@ class AgentRegistryService {
526
526
  // connected when the listener registers. Classifies each as
527
527
  // agent or worker — same logic as the live handlers above.
528
528
  try {
529
- const registry = this.moleculer.broker.registry;
529
+ const registry = this.nodeCatalogRegistry();
530
530
  const existing = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
531
531
  for (const { id } of existing) {
532
532
  const kind = classifyNode(id);
@@ -571,7 +571,7 @@ class AgentRegistryService {
571
571
  * must not abort the pass or hub boot.
572
572
  */
573
573
  async reconcileConnectedAgents() {
574
- const registry = this.moleculer.broker.registry;
574
+ const registry = this.nodeCatalogRegistry();
575
575
  const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
576
576
  const agentIds = nodes
577
577
  .map((n) => n.id)
@@ -968,7 +968,7 @@ class AgentRegistryService {
968
968
  return (0, node_directory_1.toNodeDirectory)(this.listNodeLiveness(), history);
969
969
  }
970
970
  async listNodes() {
971
- const registry = this.moleculer.broker.registry;
971
+ const registry = this.nodeCatalogRegistry();
972
972
  // `onlyAvailable: false` intentionally includes nodes Moleculer still
973
973
  // remembers as disconnected (`available: false`) — the same registry
974
974
  // read `listNodeLiveness()` uses for `/health`. This surfaces recently
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClusterNodeHistoryStore = void 0;
4
+ const system_1 = require("@camstack/system");
4
5
  const zod_1 = require("zod");
5
6
  const types_1 = require("@camstack/types");
6
7
  const DescriptorSchema = zod_1.z.object({
@@ -83,9 +84,15 @@ class ClusterNodeHistoryStore {
83
84
  resolveSettingsStore;
84
85
  logger;
85
86
  declared = false;
86
- constructor(resolveSettingsStore, logger) {
87
+ resolveKind;
88
+ constructor(resolveSettingsStore, logger, resolveKind = () => null) {
87
89
  this.resolveSettingsStore = resolveSettingsStore;
88
90
  this.logger = logger;
91
+ this.resolveKind = resolveKind;
92
+ }
93
+ /** Hub wiring after `records.forOwner('cluster-agent').declare(...)`. */
94
+ bindRecordsKind(resolver) {
95
+ this.resolveKind = resolver;
89
96
  }
90
97
  /**
91
98
  * Idempotently declare the typed collection. Returns the resolved provider
@@ -93,6 +100,8 @@ class ClusterNodeHistoryStore {
93
100
  * unreachable) — callers treat null as "skip, best-effort".
94
101
  */
95
102
  async ensureDeclared() {
103
+ if (this.resolveKind() !== null)
104
+ return this.resolveSettingsStore();
96
105
  const store = this.resolveSettingsStore();
97
106
  if (!store)
98
107
  return null;
@@ -143,16 +152,12 @@ class ClusterNodeHistoryStore {
143
152
  // Carry the package roster forward. `listNodes()` does not resolve it
144
153
  // (only the reconcile does), so writing this descriptor blind would erase
145
154
  // the back-fill's intended set on the very next topology poll.
146
- const existing = await this.readRow(store, descriptor.id);
155
+ const existing = await this.readStored(descriptor.id);
147
156
  const packages = descriptor.packages ?? existing?.descriptor.packages;
148
157
  const next = packages === undefined ? descriptor : { ...descriptor, packages };
149
158
  if (existing !== null && !this.worthWriting(existing, next))
150
159
  return;
151
- await store.set({
152
- collection: COLLECTION,
153
- key: descriptor.id,
154
- value: { lastActive: Date.now(), descriptor: next },
155
- });
160
+ await this.writeStored(descriptor.id, Date.now(), next);
156
161
  }
157
162
  catch (err) {
158
163
  this.logger.warn('snapshot upsert failed (best-effort)', {
@@ -172,13 +177,48 @@ class ClusterNodeHistoryStore {
172
177
  return !descriptorsEqual(existing.descriptor, next);
173
178
  }
174
179
  /** Read one node's row, validated. `null` when absent or malformed. */
175
- async readRow(store, nodeId) {
180
+ async readStored(nodeId) {
181
+ const kind = this.resolveKind();
182
+ if (kind !== null) {
183
+ const row = await kind.get(nodeId);
184
+ if (row === null)
185
+ return null;
186
+ const parsed = RowDataSchema.safeParse({
187
+ lastActive: row.at ?? 0,
188
+ descriptor: row.data.descriptor,
189
+ });
190
+ return parsed.success ? parsed.data : null;
191
+ }
192
+ const store = this.resolveSettingsStore();
193
+ if (!store)
194
+ return null;
195
+ return this.readRowLegacy(store, nodeId);
196
+ }
197
+ async readRowLegacy(store, nodeId) {
176
198
  const raw = await store.get({ collection: COLLECTION, key: nodeId });
177
199
  if (raw === null || raw === undefined)
178
200
  return null;
179
201
  const parsed = RowDataSchema.safeParse(raw);
180
202
  return parsed.success ? parsed.data : null;
181
203
  }
204
+ async writeStored(nodeId, lastActive, descriptor) {
205
+ const kind = this.resolveKind();
206
+ if (kind !== null) {
207
+ const body = system_1.ClusterNodeHistoryBodySchema.safeParse({ descriptor });
208
+ if (!body.success)
209
+ return;
210
+ await kind.put({ id: nodeId, at: lastActive, data: body.data });
211
+ return;
212
+ }
213
+ const store = this.resolveSettingsStore();
214
+ if (!store)
215
+ return;
216
+ await store.set({
217
+ collection: COLLECTION,
218
+ key: nodeId,
219
+ value: { lastActive, descriptor },
220
+ });
221
+ }
182
222
  /** A placeholder descriptor for a node the topology pass has not seen yet. */
183
223
  static placeholderDescriptor(nodeId) {
184
224
  return {
@@ -206,7 +246,7 @@ class ClusterNodeHistoryStore {
206
246
  if (!store)
207
247
  return [];
208
248
  try {
209
- return (await this.readRow(store, nodeId))?.descriptor.packages ?? [];
249
+ return (await this.readStored(nodeId))?.descriptor.packages ?? [];
210
250
  }
211
251
  catch (err) {
212
252
  this.logger.warn('getPackages failed (best-effort)', {
@@ -231,7 +271,7 @@ class ClusterNodeHistoryStore {
231
271
  if (!store)
232
272
  return;
233
273
  try {
234
- const existing = await this.readRow(store, nodeId);
274
+ const existing = await this.readStored(nodeId);
235
275
  const merged = new Map((existing?.descriptor.packages ?? []).map((p) => [p.name, p]));
236
276
  // The node's current declaration wins on version — it is the authority on
237
277
  // what it is actually running.
@@ -241,11 +281,7 @@ class ClusterNodeHistoryStore {
241
281
  ...(existing?.descriptor ?? ClusterNodeHistoryStore.placeholderDescriptor(nodeId)),
242
282
  packages: [...merged.values()],
243
283
  };
244
- await store.set({
245
- collection: COLLECTION,
246
- key: nodeId,
247
- value: { lastActive: existing?.lastActive ?? Date.now(), descriptor },
248
- });
284
+ await this.writeStored(nodeId, existing?.lastActive ?? Date.now(), descriptor);
249
285
  }
250
286
  catch (err) {
251
287
  this.logger.warn('recordPackages failed (best-effort)', {
@@ -266,7 +302,7 @@ class ClusterNodeHistoryStore {
266
302
  if (!store)
267
303
  return;
268
304
  try {
269
- const existing = await this.readRow(store, nodeId);
305
+ const existing = await this.readStored(nodeId);
270
306
  if (existing === null)
271
307
  return;
272
308
  const current = existing.descriptor.packages;
@@ -276,13 +312,9 @@ class ClusterNodeHistoryStore {
276
312
  const remaining = current.filter((p) => !drop.has(p.name));
277
313
  if (remaining.length === current.length)
278
314
  return;
279
- await store.set({
280
- collection: COLLECTION,
281
- key: nodeId,
282
- value: {
283
- lastActive: existing.lastActive,
284
- descriptor: { ...existing.descriptor, packages: remaining },
285
- },
315
+ await this.writeStored(nodeId, existing.lastActive, {
316
+ ...existing.descriptor,
317
+ packages: remaining,
286
318
  });
287
319
  this.logger.info('pruned node package record', {
288
320
  tags: { nodeId },
@@ -306,6 +338,18 @@ class ClusterNodeHistoryStore {
306
338
  if (!store)
307
339
  return;
308
340
  try {
341
+ const existing = await this.readStored(nodeId);
342
+ if (existing === null)
343
+ return;
344
+ const kind = this.resolveKind();
345
+ if (kind !== null) {
346
+ await kind.put({
347
+ id: nodeId,
348
+ at: Date.now(),
349
+ data: { descriptor: existing.descriptor },
350
+ });
351
+ return;
352
+ }
309
353
  await store.update({
310
354
  collection: COLLECTION,
311
355
  id: nodeId,
@@ -335,8 +379,17 @@ class ClusterNodeHistoryStore {
335
379
  if (!store)
336
380
  return null;
337
381
  try {
338
- const rows = await store.query({ collection: COLLECTION });
382
+ const kind = this.resolveKind();
339
383
  const result = [];
384
+ const rows = kind !== null
385
+ ? (await (0, system_1.readAllRows)(kind)).map((row) => ({
386
+ id: row.id,
387
+ data: { lastActive: row.at ?? 0, descriptor: row.data.descriptor },
388
+ }))
389
+ : (await store.query({ collection: COLLECTION })).map((row) => ({
390
+ id: row.id,
391
+ data: row.data,
392
+ }));
340
393
  for (const row of rows) {
341
394
  const parsed = RowDataSchema.safeParse(row.data);
342
395
  if (!parsed.success) {
@@ -370,8 +423,14 @@ class ClusterNodeHistoryStore {
370
423
  // Read first: `delete` on this provider is idempotent and cannot tell a
371
424
  // removal from a no-op, and "there was nothing to remove" is the answer
372
425
  // that stops an operator repeating a working action forever.
373
- const existed = (await this.readRow(store, nodeId)) !== null;
374
- await store.delete({ collection: COLLECTION, key: nodeId });
426
+ const existed = (await this.readStored(nodeId)) !== null;
427
+ const kind = this.resolveKind();
428
+ if (kind !== null) {
429
+ await kind.delete(nodeId);
430
+ }
431
+ else {
432
+ await store.delete({ collection: COLLECTION, key: nodeId });
433
+ }
375
434
  if (!existed) {
376
435
  this.logger.info('cluster node had no history row to forget', { tags: { nodeId } });
377
436
  return 'not-found';
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.coreAuthRecordKinds = exports.SHARE_VIEW_TOKENS_LEGACY = void 0;
4
+ /**
5
+ * Record kinds owned by `core-auth` (hub-resident, phase 6 / §2.8).
6
+ */
7
+ const types_1 = require("@camstack/types");
8
+ const zod_1 = require("zod");
9
+ const ShareTokenScopeSchema = zod_1.z.object({
10
+ kind: zod_1.z.enum(types_1.SHARE_VIEW_KINDS),
11
+ deviceIds: zod_1.z.array(zod_1.z.number().int().nonnegative()).min(1).max(64),
12
+ talk: zod_1.z.boolean().optional(),
13
+ });
14
+ exports.SHARE_VIEW_TOKENS_LEGACY = 'share_view_tokens';
15
+ const ShareViewTokenBodySchema = zod_1.z.object({
16
+ userId: zod_1.z.string(),
17
+ tokenHash: zod_1.z.string(),
18
+ tokenPrefix: zod_1.z.string(),
19
+ scope: ShareTokenScopeSchema,
20
+ createdAt: zod_1.z.number(),
21
+ expiresAt: zod_1.z.number().nullable(),
22
+ lastUsedAt: zod_1.z.number().nullish(),
23
+ });
24
+ exports.coreAuthRecordKinds = (0, types_1.defineRecordKinds)({
25
+ 'share-view-tokens': {
26
+ schema: ShareViewTokenBodySchema,
27
+ class: 'config',
28
+ writeTrigger: 'operator mints a share link; validate touches lastUsedAt',
29
+ device: 'none',
30
+ at: 'none',
31
+ indexes: [
32
+ { name: 'hash', fields: ['tokenHash'], unique: true },
33
+ { name: 'user', fields: ['userId'] },
34
+ ],
35
+ retention: { none: 'revoke only; expiry enforced at validate' },
36
+ adoptsFrom: exports.SHARE_VIEW_TOKENS_LEGACY,
37
+ },
38
+ });
@@ -61,6 +61,7 @@ exports.ShareTokenService = exports.ShareTokenRecordSchema = exports.ShareTokenS
61
61
  const crypto = __importStar(require("node:crypto"));
62
62
  const zod_1 = require("zod");
63
63
  const types_1 = require("@camstack/types");
64
+ const record_kinds_js_1 = require("./record-kinds.js");
64
65
  /**
65
66
  * @durable class=config owner=core-auth
66
67
  * write="a user mints a share link; and a best-effort lastUsedAt touch on every successful validation"
@@ -153,6 +154,29 @@ function parseShareToken(data) {
153
154
  const parsed = exports.ShareTokenRecordSchema.safeParse(data);
154
155
  return parsed.success ? parsed.data : null;
155
156
  }
157
+ function recordToBody(record) {
158
+ const { id: _id, ...body } = record;
159
+ return body;
160
+ }
161
+ function rowToRecord(id, body) {
162
+ return exports.ShareTokenRecordSchema.safeParse({ id, ...body }).data ?? null;
163
+ }
164
+ async function readAllShareTokenRows(kind, where) {
165
+ const out = [];
166
+ let cursor;
167
+ for (let page = 0; page < 100; page += 1) {
168
+ const answer = await kind.query({
169
+ ...(where === undefined ? {} : { where }),
170
+ limit: 1000,
171
+ ...(cursor === undefined ? {} : { cursor }),
172
+ });
173
+ out.push(...answer.records);
174
+ if (answer.nextCursor === null)
175
+ return out;
176
+ cursor = answer.nextCursor;
177
+ }
178
+ throw new Error('share-view-tokens: row count exceeds pagination bound');
179
+ }
156
180
  class ShareTokenService {
157
181
  getStore;
158
182
  logger;
@@ -162,9 +186,59 @@ class ShareTokenService {
162
186
  * the sqlite builtin registers after this service is constructed).
163
187
  */
164
188
  collectionDeclared = false;
165
- constructor(getStore, logger = null) {
189
+ resolveKind;
190
+ constructor(getStore, logger = null, resolveKind = () => null) {
166
191
  this.getStore = getStore;
167
192
  this.logger = logger;
193
+ this.resolveKind = resolveKind;
194
+ }
195
+ /** Hub wiring after `records.forOwner('core-auth').declare(...)`. */
196
+ bindShareTokensKind(resolver) {
197
+ this.resolveKind = resolver;
198
+ this.collectionDeclared = true;
199
+ }
200
+ kind() {
201
+ return this.resolveKind();
202
+ }
203
+ async findById(id) {
204
+ const kind = this.kind();
205
+ if (kind !== null) {
206
+ const row = await kind.get(id);
207
+ return row === null ? null : rowToRecord(row.id, row.data);
208
+ }
209
+ const results = await (await this.store()).query({
210
+ collection: SHARE_TOKENS_COLLECTION,
211
+ filter: { where: { id } },
212
+ });
213
+ const first = results[0];
214
+ return first === undefined ? null : parseShareToken(first.data);
215
+ }
216
+ async persistRecord(record) {
217
+ const kind = this.kind();
218
+ if (kind !== null) {
219
+ const parsed = record_kinds_js_1.coreAuthRecordKinds['share-view-tokens'].schema.safeParse(recordToBody(record));
220
+ if (!parsed.success)
221
+ throw new Error(parsed.error.message);
222
+ await kind.put({
223
+ id: record.id,
224
+ expiresAt: record.expiresAt ?? undefined,
225
+ data: parsed.data,
226
+ });
227
+ return;
228
+ }
229
+ await (await this.store()).update({
230
+ collection: SHARE_TOKENS_COLLECTION,
231
+ id: record.id,
232
+ data: { ...record },
233
+ });
234
+ }
235
+ async deleteById(id) {
236
+ const kind = this.kind();
237
+ if (kind !== null) {
238
+ await kind.delete(id);
239
+ return;
240
+ }
241
+ await (await this.store()).delete({ collection: SHARE_TOKENS_COLLECTION, key: id });
168
242
  }
169
243
  /**
170
244
  * Resolve the backend AND ensure `share_view_tokens` is declared before
@@ -174,6 +248,13 @@ class ShareTokenService {
174
248
  * rows keep working with no migration.
175
249
  */
176
250
  async store() {
251
+ if (this.kind() !== null) {
252
+ const store = this.getStore();
253
+ if (!store) {
254
+ throw new Error('Share tokens unavailable — settings backend not ready');
255
+ }
256
+ return store;
257
+ }
177
258
  const store = this.getStore();
178
259
  if (!store) {
179
260
  throw new Error('Share tokens unavailable — settings backend not ready');
@@ -219,10 +300,23 @@ class ShareTokenService {
219
300
  createdAt: now,
220
301
  expiresAt: ttlSec === 'never' ? null : now + ttlSec * 1000,
221
302
  };
222
- await (await this.store()).insert({
223
- collection: SHARE_TOKENS_COLLECTION,
224
- record: { id: record.id, data: { ...record } },
225
- });
303
+ const kind = this.kind();
304
+ if (kind !== null) {
305
+ const parsed = record_kinds_js_1.coreAuthRecordKinds['share-view-tokens'].schema.safeParse(recordToBody(record));
306
+ if (!parsed.success)
307
+ throw new Error(parsed.error.message);
308
+ await kind.put({
309
+ id: record.id,
310
+ expiresAt: record.expiresAt ?? undefined,
311
+ data: parsed.data,
312
+ });
313
+ }
314
+ else {
315
+ await (await this.store()).insert({
316
+ collection: SHARE_TOKENS_COLLECTION,
317
+ record: { id: record.id, data: { ...record } },
318
+ });
319
+ }
226
320
  this.logger?.info('Share token minted', {
227
321
  meta: {
228
322
  id: record.id,
@@ -243,15 +337,22 @@ class ShareTokenService {
243
337
  if (!rawToken.startsWith(exports.SHARE_TOKEN_PREFIX))
244
338
  return null;
245
339
  const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
246
- const results = await (await this.store()).query({
247
- collection: SHARE_TOKENS_COLLECTION,
248
- filter: { where: { tokenHash } },
249
- });
250
- const first = results[0];
251
- if (!first)
252
- return null;
253
- const record = parseShareToken(first.data);
254
- if (!record)
340
+ const kind = this.kind();
341
+ let record = null;
342
+ if (kind !== null) {
343
+ const page = await kind.query({ where: { tokenHash }, limit: 1 });
344
+ const row = page.records[0];
345
+ record = row === undefined ? null : rowToRecord(row.id, row.data);
346
+ }
347
+ else {
348
+ const results = await (await this.store()).query({
349
+ collection: SHARE_TOKENS_COLLECTION,
350
+ filter: { where: { tokenHash } },
351
+ });
352
+ const first = results[0];
353
+ record = first === undefined ? null : parseShareToken(first.data);
354
+ }
355
+ if (record === null)
255
356
  return null;
256
357
  // `expiresAt: null` = never expires (explicit mint-time choice);
257
358
  // revocation remains the kill switch for those.
@@ -275,16 +376,8 @@ class ShareTokenService {
275
376
  throw new Error(`Share token TTL out of bounds — got ${input.ttlSec}s, allowed ` +
276
377
  `[${exports.SHARE_TOKEN_TTL_MIN_SEC}s, ${exports.SHARE_TOKEN_TTL_MAX_SEC}s]`);
277
378
  }
278
- const results = await (await this.store()).query({
279
- collection: SHARE_TOKENS_COLLECTION,
280
- filter: { where: { id: input.id } },
281
- });
282
- const first = results[0];
283
- if (!first) {
284
- throw (0, types_1.userError)('auth.share-token-not-found', { id: input.id }, `Share token not found: ${input.id}`);
285
- }
286
- const record = parseShareToken(first.data);
287
- if (!record) {
379
+ const record = await this.findById(input.id);
380
+ if (record === null) {
288
381
  throw (0, types_1.userError)('auth.share-token-not-found', { id: input.id }, `Share token not found: ${input.id}`);
289
382
  }
290
383
  if (!input.callerIsAdmin && record.userId !== input.callerUserId) {
@@ -301,11 +394,7 @@ class ShareTokenService {
301
394
  ...record,
302
395
  expiresAt: now + input.ttlSec * 1000,
303
396
  };
304
- await (await this.store()).update({
305
- collection: SHARE_TOKENS_COLLECTION,
306
- id: record.id,
307
- data: { ...updated },
308
- });
397
+ await this.persistRecord(updated);
309
398
  this.logger?.info('Share token extended', {
310
399
  meta: {
311
400
  id: updated.id,
@@ -321,23 +410,13 @@ class ShareTokenService {
321
410
  * permission mismatch so the router surfaces a FORBIDDEN.
322
411
  */
323
412
  async revoke(input) {
324
- const results = await (await this.store()).query({
325
- collection: SHARE_TOKENS_COLLECTION,
326
- filter: { where: { id: input.id } },
327
- });
328
- const first = results[0];
329
- if (!first)
413
+ const record = await this.findById(input.id);
414
+ if (record === null)
330
415
  return false;
331
- const record = parseShareToken(first.data);
332
- if (!record) {
333
- // Corrupt record — delete it regardless (it can never validate).
334
- await (await this.store()).delete({ collection: SHARE_TOKENS_COLLECTION, key: input.id });
335
- return true;
336
- }
337
416
  if (!input.callerIsAdmin && record.userId !== input.callerUserId) {
338
417
  throw (0, types_1.userError)('auth.share-owner-only-revoke', undefined, 'Only the token owner or an admin can revoke a share token');
339
418
  }
340
- await (await this.store()).delete({ collection: SHARE_TOKENS_COLLECTION, key: input.id });
419
+ await this.deleteById(input.id);
341
420
  this.logger?.info('Share token revoked', {
342
421
  meta: { id: record.id, byUserId: input.callerUserId },
343
422
  });
@@ -345,6 +424,13 @@ class ShareTokenService {
345
424
  }
346
425
  /** Every share token minted by `userId`, expired ones included. */
347
426
  async listForUser(userId) {
427
+ const kind = this.kind();
428
+ if (kind !== null) {
429
+ const rows = await readAllShareTokenRows(kind, { userId });
430
+ return rows
431
+ .map((row) => rowToRecord(row.id, row.data))
432
+ .filter((r) => r !== null);
433
+ }
348
434
  const results = await (await this.store()).query({
349
435
  collection: SHARE_TOKENS_COLLECTION,
350
436
  filter: { where: { userId } },
@@ -353,6 +439,13 @@ class ShareTokenService {
353
439
  }
354
440
  /** All share tokens (admin listing). */
355
441
  async listAll() {
442
+ const kind = this.kind();
443
+ if (kind !== null) {
444
+ const rows = await readAllShareTokenRows(kind);
445
+ return rows
446
+ .map((row) => rowToRecord(row.id, row.data))
447
+ .filter((r) => r !== null);
448
+ }
356
449
  const results = await (await this.store()).query({
357
450
  collection: SHARE_TOKENS_COLLECTION,
358
451
  filter: {},
@@ -360,11 +453,7 @@ class ShareTokenService {
360
453
  return results.map((r) => parseShareToken(r.data)).filter((r) => r !== null);
361
454
  }
362
455
  async touchLastUsed(record) {
363
- await (await this.store()).update({
364
- collection: SHARE_TOKENS_COLLECTION,
365
- id: record.id,
366
- data: { ...record, lastUsedAt: Date.now() },
367
- });
456
+ await this.persistRecord({ ...record, lastUsedAt: Date.now() });
368
457
  }
369
458
  }
370
459
  exports.ShareTokenService = ShareTokenService;
@@ -1,6 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildCapCallFn = buildCapCallFn;
4
+ /**
5
+ * cap-call-fn — the per-(cap, node) dispatcher behind every CapabilityRegistry
6
+ * provider proxy the hub builds in `applyNodeManifest`.
7
+ *
8
+ * Extracted from `MoleculerService` so its routing branches are unit-testable
9
+ * in isolation (the closure that lived inline could only be exercised by
10
+ * standing up a full broker). It closes a UDS-migration gap: the inline
11
+ * version hand-rolled a `broker.call` for any non-hub-local provider, so an
12
+ * AGENT-hosted addon cap (a UDS child of the agent, NOT a Moleculer service)
13
+ * resolved to a Moleculer node that does not exist → `waitForServices` waited
14
+ * its full 30s discovery timeout → "Services waiting is timed out". The fix
15
+ * routes everything that isn't a hub-local child through the unified
16
+ * `CapRouteResolver`, which classifies an agent node as `agent-child-forward`
17
+ * (hub → agent over Moleculer → agent's UDS child) and a direct remote as
18
+ * `remote-moleculer`.
19
+ */
20
+ const types_1 = require("@camstack/types");
4
21
  /** Extract a numeric `deviceId` routing hint out of arbitrary method args. */
5
22
  function extractDeviceId(params) {
6
23
  if (params === null || typeof params !== 'object')
@@ -8,6 +25,19 @@ function extractDeviceId(params) {
8
25
  const raw = Reflect.get(params, 'deviceId');
9
26
  return typeof raw === 'number' ? raw : undefined;
10
27
  }
28
+ /**
29
+ * Hub in-process builtins state `callerAddonId` in args (see
30
+ * `AddonRegistryService.inProcessRecordsTransport`). A registry proxy routes
31
+ * over UDS; without lifting this to {@link CapCallInput.callerAddonId} the
32
+ * child strips the claim (`stampCallerAddonId` with no wire stamp) and records
33
+ * refuses `no-caller`.
34
+ */
35
+ function extractStatedCallerAddonId(params) {
36
+ if (params === null || typeof params !== 'object' || Array.isArray(params))
37
+ return undefined;
38
+ const raw = Reflect.get(params, 'callerAddonId');
39
+ return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
40
+ }
11
41
  /**
12
42
  * Build the `CallFn` (`(method, params, targetNodeId?) => Promise`) for one
13
43
  * registered provider. See module docs for the routing rationale.
@@ -25,12 +55,16 @@ function buildCapCallFn(deps) {
25
55
  const registry = deps.getLocalChildRegistry();
26
56
  if (registry !== null && registry.childProvides(deps.udsChildId, deps.capName, deviceId)) {
27
57
  deps.onUdsRoute?.(deps.capName);
58
+ const statedCaller = types_1.CALLER_STAMPED_CAPABILITIES.includes(deps.capName)
59
+ ? extractStatedCallerAddonId(params)
60
+ : undefined;
28
61
  const input = {
29
62
  capName: deps.capName,
30
63
  method,
31
64
  args: params,
32
65
  ...(deviceId !== undefined ? { deviceId } : {}),
33
66
  ...(deps.addonId !== undefined ? { addonId: deps.addonId } : {}),
67
+ ...(statedCaller !== undefined ? { callerAddonId: statedCaller } : {}),
34
68
  };
35
69
  return registry.callCapOnChild(deps.udsChildId, input);
36
70
  }
@@ -45,11 +79,14 @@ function buildCapCallFn(deps) {
45
79
  // there is a harmless no-op.
46
80
  const resolver = deps.getResolver();
47
81
  if (resolver !== null) {
82
+ const statedCaller = types_1.CALLER_STAMPED_CAPABILITIES.includes(deps.capName)
83
+ ? extractStatedCallerAddonId(params)
84
+ : undefined;
48
85
  const route = resolver.resolveCapRoute(deps.capName, {
49
86
  nodeId: targetNodeId ?? deps.nodeId,
50
87
  ...(deviceId !== undefined ? { deviceId } : {}),
51
88
  });
52
- return resolver.dispatch(route, method, params, deps.addonId);
89
+ return resolver.dispatch(route, method, params, deps.addonId, statedCaller);
53
90
  }
54
91
  // ── Pre-init window (resolver not yet constructed): legacy Moleculer call.
55
92
  return deps.legacyBrokerCall(method, params, targetNodeId ?? deps.nodeId);
@@ -20,10 +20,6 @@ class MoleculerService {
20
20
  capabilityService;
21
21
  streamProbe;
22
22
  broker;
23
- /** Narrow-typed view of `this.broker` — see `BrokerLike` doc above. */
24
- get brokerSafe() {
25
- return this.broker;
26
- }
27
23
  logger;
28
24
  /**
29
25
  * D802, D857 — who may talk to the hub over the mesh: the broker middleware
@@ -172,7 +168,7 @@ class MoleculerService {
172
168
  /** This hub's Moleculer node id (e.g. `hub`). Hub-local forked children
173
169
  * register under `${nodeId}/${runnerId}`. */
174
170
  get nodeId() {
175
- return this.brokerSafe.nodeID;
171
+ return this.broker.nodeID;
176
172
  }
177
173
  constructor(eventBus, config, logging, capabilityService, streamProbe) {
178
174
  this.eventBus = eventBus;
@@ -208,7 +204,7 @@ class MoleculerService {
208
204
  this.broker = broker;
209
205
  this.readinessRegistry = new types_1.ReadinessRegistry({
210
206
  eventBus: this.eventBus,
211
- sourceNodeId: this.brokerSafe.nodeID,
207
+ sourceNodeId: this.broker.nodeID,
212
208
  logger: this.logging.createLogger('readiness'),
213
209
  });
214
210
  }
@@ -251,19 +247,20 @@ class MoleculerService {
251
247
  });
252
248
  },
253
249
  onSetLogLevel: (level) => {
254
- const factory = this.brokerSafe.logger;
255
- const appenders = factory['appenders'];
250
+ const factory = this.broker.logger;
251
+ const moleculerLogFactory = factory;
252
+ const appenders = moleculerLogFactory.appenders;
256
253
  if (appenders) {
257
254
  for (const appender of appenders) {
258
255
  appender.opts.level = level;
259
256
  }
260
257
  }
261
- const cache = factory['cache'];
258
+ const cache = moleculerLogFactory.cache;
262
259
  if (cache) {
263
260
  cache.clear();
264
261
  }
265
262
  logger.info('Moleculer log level changed', { meta: { level } });
266
- this.brokerSafe.call('$process.setLogLevel', { level }).catch(() => { });
263
+ this.broker.call('$process.setLogLevel', { level }).catch(() => { });
267
264
  },
268
265
  // D3: registration-handshake path. Nodes send $hub.registerNode with
269
266
  // their complete capability manifest; the hub applies it immediately.
@@ -277,7 +274,7 @@ class MoleculerService {
277
274
  meshIdentity: this.meshGuard.gate,
278
275
  };
279
276
  const hubService = (0, system_1.createHubService)(hubDeps);
280
- this.brokerSafe.createService(hubService);
277
+ this.broker.createService(hubService);
281
278
  // Forked addon-runner children must root their storage (addons-data,
282
279
  // the sqlite settings DB) under the SAME data path as the hub's main
283
280
  // process. The configured key is `server.dataPath` (populated from the
@@ -293,7 +290,7 @@ class MoleculerService {
293
290
  // broker-only — no parentUdsPath is propagated.
294
291
  let parentUdsPath;
295
292
  try {
296
- const nodeId = this.brokerSafe.nodeID;
293
+ const nodeId = this.broker.nodeID;
297
294
  // F0 (slice-5 outbound): when a forked child issues `ctx.api.<cap>` for a
298
295
  // cap NO local sibling owns, route it from the PARENT and return the
299
296
  // result over UDS — resolver-first, broker-fallback. Closes over `this`
@@ -419,7 +416,7 @@ class MoleculerService {
419
416
  // `broker.call('$hub-cap-fwd.forward', …)` discovers it here. Without it a
420
417
  // remote agent's raw `${cap}.*` discovery 30s-deadlines (hub-local addon
421
418
  // runners expose no Moleculer service). See hub-cap-forward.ts.
422
- this.brokerSafe.createService((0, system_1.createHubCapForwardService)(onUnownedCall, {
419
+ this.broker.createService((0, system_1.createHubCapForwardService)(onUnownedCall, {
423
420
  // The agent stamps WHO is calling (its child's runner id) into the
424
421
  // envelope (D787). Believe it only when that runner is on the agent
425
422
  // the request came from — a node's registered manifest lists the
@@ -516,7 +513,7 @@ class MoleculerService {
516
513
  // Phase F removes the child broker; `onRegisterNode`'s diff logic ensures
517
514
  // double-apply is idempotent (same nodeId + same caps → no-op on the second call).
518
515
  registry.onChildRegistered((child) => {
519
- const hubNodeId = this.brokerSafe.nodeID;
516
+ const hubNodeId = this.broker.nodeID;
520
517
  const childNodeId = `${hubNodeId}/${child.childId}`;
521
518
  // The catalogs first, and unconditionally: the manifest gate below
522
519
  // judges CAPS, and a frame whose caps are unchanged may still be the
@@ -568,7 +565,7 @@ class MoleculerService {
568
565
  // E1: cleanup on child disconnect — same effect as `$node.disconnected`
569
566
  // for hub-local children. The Moleculer path stays for AGENT nodes.
570
567
  registry.onChildGone((childId, incarnation) => {
571
- const hubNodeId = this.brokerSafe.nodeID;
568
+ const hubNodeId = this.broker.nodeID;
572
569
  const childNodeId = `${hubNodeId}/${childId}`;
573
570
  // The successor's first register must be a FIRST register: leaving the
574
571
  // record behind would let a same-incarnation reconnect (mixed-version
@@ -604,15 +601,14 @@ class MoleculerService {
604
601
  meta: { err: err instanceof Error ? err.message : String(err) },
605
602
  });
606
603
  }
607
- const processService = (0, system_1.createProcessService)(this.brokerSafe.nodeID, dataDir, undefined, undefined, parentUdsPath);
608
- this.brokerSafe.createService(processService);
604
+ const processService = (0, system_1.createProcessService)(this.broker.nodeID, dataDir, undefined, undefined, parentUdsPath);
605
+ this.broker.createService(processService);
609
606
  // $addonHost — REMOVED (Sprint 6). Three-level settings are now
610
607
  // served by the `addon-settings` singleton capability. Per-addon
611
608
  // Moleculer services expose `settings.*` actions for remote agents.
612
609
  // D3: mirror $node.disconnected onto the registry path so nodes that
613
610
  // sent a $hub.registerNode manifest get cleaned up on disconnect.
614
- const bridgeBus = this.broker;
615
- bridgeBus.localBus.on('$node.disconnected', ({ node }) => {
611
+ this.broker.localBus.on('$node.disconnected', ({ node }) => {
616
612
  // No owner token: this listener cannot observe WHICH incarnation died.
617
613
  // For a hub-local child the UDS `onChildGone` is the authority (it holds
618
614
  // the incarnation) and this call is refused if the id has been reclaimed;
@@ -630,16 +626,16 @@ class MoleculerService {
630
626
  // this registry stuck on the synthesized `down` forever (2026-07-17:
631
627
  // pipeline-executor@little-unraid, 3.5h dispatch outage). The
632
628
  // periodic sweep below repairs deltas lost while CONNECTED.
633
- bridgeBus.localBus.on('$node.connected', ({ node }) => {
629
+ this.broker.localBus.on('$node.connected', ({ node }) => {
634
630
  const nodeId = node.id;
635
- if (nodeId === this.brokerSafe.nodeID || nodeId.includes('/'))
631
+ if (nodeId === this.broker.nodeID || nodeId.includes('/'))
636
632
  return;
637
633
  void this.pullAgentReadinessSnapshot(nodeId);
638
634
  // The hub may have dropped this agent's registration on a disconnect
639
635
  // only the hub saw (gossip, a failed write): ask it for its full set
640
636
  // (`agent-registration-request.ts`, 2026-10-14).
641
637
  void (0, agent_registration_request_js_1.requestAgentRegistration)({
642
- call: (action, params, opts) => this.brokerSafe.call(action, params, opts),
638
+ call: (action, params, opts) => this.broker.call(action, params, opts),
643
639
  isRegistered: (id) => this.isNodeRegistered(id),
644
640
  warn: (message, id, meta) => this.logger.warn(message, { tags: { nodeId: id }, meta }),
645
641
  info: (message, id, meta) => this.logger.info(message, { tags: { nodeId: id }, meta }),
@@ -647,7 +643,7 @@ class MoleculerService {
647
643
  });
648
644
  this.agentReadinessPullTimer = setInterval(() => {
649
645
  for (const nodeId of this.nodeRegistry.listNodeIds()) {
650
- if (nodeId === this.brokerSafe.nodeID || nodeId.includes('/'))
646
+ if (nodeId === this.broker.nodeID || nodeId.includes('/'))
651
647
  continue;
652
648
  void this.pullAgentReadinessSnapshot(nodeId);
653
649
  }
@@ -665,14 +661,14 @@ class MoleculerService {
665
661
  // BEFORE broker.start() so workers / agents see it in their initial
666
662
  // INFO packet — post-start `createService` calls propagate via
667
663
  // heartbeat (several seconds) and would force workers to poll.
668
- this.brokerSafe.createService((0, system_1.createReadinessServiceForRegistry)(this.readinessRegistry));
664
+ this.broker.createService((0, system_1.createReadinessServiceForRegistry)(this.readinessRegistry));
669
665
  // Register the hub-authoritative `$stream-probe` service —
670
666
  // workers route RTSP probe + field-probe through this action when
671
667
  // the tRPC WSS link isn't available (default path for forked
672
668
  // workers). Keeps ffprobe + HTTP-reachability as a single
673
669
  // hub-side implementation; see `createStreamProbeBrokerService`
674
670
  // for the shape.
675
- this.brokerSafe.createService((0, system_1.createStreamProbeBrokerService)({
671
+ this.broker.createService((0, system_1.createStreamProbeBrokerService)({
676
672
  probe: (url, options) => this.streamProbe.probe(url, options),
677
673
  probeField: (key, value) => this.streamProbe.probeField(key, value),
678
674
  }));
@@ -693,7 +689,7 @@ class MoleculerService {
693
689
  peerLeaf: (nodeId) => (0, system_1.getBrokerTlsTransporter)(this.broker)?.peerLeaf(nodeId),
694
690
  dropPeer: (nodeId) => (0, system_1.getBrokerTlsTransporter)(this.broker)?.dropPeer(nodeId) ?? 0,
695
691
  });
696
- await this.brokerSafe.start();
692
+ await this.broker.start();
697
693
  logger.info('Moleculer broker started (TCP transport)');
698
694
  // Construct the CapRouteResolver now that both the broker and
699
695
  // localChildRegistry are ready. The resolver reads live registry state
@@ -702,8 +698,8 @@ class MoleculerService {
702
698
  // correctly. The localChildRegistry reference is captured and may be
703
699
  // null if UDS failed to start.
704
700
  this.resolver = new system_1.CapRouteResolver({
705
- hubNodeId: this.brokerSafe.nodeID,
706
- broker: this.brokerSafe,
701
+ hubNodeId: this.broker.nodeID,
702
+ broker: this.broker,
707
703
  hubLocalRegistry: this.localChildRegistry,
708
704
  nodeAuthority: (0, cap_route_authority_js_1.createNodeCapAuthority)(this.nodeRegistry, {
709
705
  resolveSingleton: (capName, nodeId) => this.capabilityService.getRegistry()?.resolveSingletonAddonIdForNode(capName, nodeId) ??
@@ -736,7 +732,7 @@ class MoleculerService {
736
732
  // is wired after attachBroker so the parentBus is backed by the real
737
733
  // shared broker bus and broker.broadcast is live.
738
734
  if (this.localChildRegistry !== null) {
739
- const hubNodeId = this.brokerSafe.nodeID;
735
+ const hubNodeId = this.broker.nodeID;
740
736
  this.udsEventBridgeDispose = (0, system_1.createUdsEventBridge)({
741
737
  registry: this.localChildRegistry,
742
738
  parentBus: this.eventBus,
@@ -763,7 +759,7 @@ class MoleculerService {
763
759
  * the updated service list to the network.
764
760
  */
765
761
  registerLogReceiver() {
766
- this.brokerSafe.createService({
762
+ this.broker.createService({
767
763
  name: 'log-receiver',
768
764
  actions: {
769
765
  ingest: {
@@ -795,7 +791,7 @@ class MoleculerService {
795
791
  * `registerLogReceiver` relies on the same post-init registration.
796
792
  */
797
793
  registerCoreCapService(service) {
798
- this.brokerSafe.createService(service);
794
+ this.broker.createService(service);
799
795
  }
800
796
  /**
801
797
  * The hub's claim authority (D663, S3): `CapabilityRegistry.resolveClaimedStatus`,
@@ -867,7 +863,7 @@ class MoleculerService {
867
863
  // Pre-init fallback (resolver not yet constructed — before onModuleInit).
868
864
  // This path is only reachable in tests that drive onRegisterNode without
869
865
  // calling onModuleInit first.
870
- if (nodeId === 'hub' || nodeId === this.brokerSafe.nodeID) {
866
+ if (nodeId === 'hub' || nodeId === this.broker.nodeID) {
871
867
  const registry = this.capabilityService.getRegistry();
872
868
  const provider = registry?.getSingleton(capabilityName) ?? null;
873
869
  if (provider !== null) {
@@ -946,7 +942,7 @@ class MoleculerService {
946
942
  */
947
943
  applyNodeManifest(params, previousManifest, owner, previousOwner) {
948
944
  const { nodeId, addons } = params;
949
- const hubNodeId = this.brokerSafe.nodeID;
945
+ const hubNodeId = this.broker.nodeID;
950
946
  const isLocalChild = nodeId.startsWith(hubNodeId + '/');
951
947
  const isHubInProcess = nodeId === hubNodeId;
952
948
  // Hub in-process addons register themselves during initialize() — skip.
@@ -1048,7 +1044,7 @@ class MoleculerService {
1048
1044
  udsChildId,
1049
1045
  getLocalChildRegistry: () => this.localChildRegistry,
1050
1046
  getResolver: () => this.resolver,
1051
- legacyBrokerCall: (method, methodParams, targetNode) => (0, system_1.callWithServiceDiscovery)(this.brokerSafe, addonId, (0, system_1.capActionName)(addonId, capName, method, false), (0, system_1.serializeTypedArrays)(methodParams), { nodeID: targetNode, timeout: 60_000 }),
1047
+ legacyBrokerCall: (method, methodParams, targetNode) => (0, system_1.callWithServiceDiscovery)(this.broker, addonId, (0, system_1.capActionName)(addonId, capName, method, false), (0, system_1.serializeTypedArrays)(methodParams), { nodeID: targetNode, timeout: 60_000 }),
1052
1048
  onUdsRoute: (cap) => {
1053
1049
  if (!this.udsRoutedCaps.has(cap)) {
1054
1050
  this.udsRoutedCaps.add(cap);
@@ -1121,7 +1117,7 @@ class MoleculerService {
1121
1117
  const manifest = this.nodeRegistry.getNodeManifest(nodeId);
1122
1118
  if (!manifest)
1123
1119
  return; // node never sent a handshake — nothing to do
1124
- const hubNodeId = this.brokerSafe.nodeID;
1120
+ const hubNodeId = this.broker.nodeID;
1125
1121
  const isLocalChild = nodeId.startsWith(hubNodeId + '/');
1126
1122
  const disconnectGen = `disconnect-${nodeId}-${(0, node_crypto_1.randomUUID)()}`;
1127
1123
  const agentNodeId = nodeId.includes('/') ? nodeId.split('/')[0] : nodeId;
@@ -1194,7 +1190,7 @@ class MoleculerService {
1194
1190
  isReachableViaLegacy(nodeId, capabilityName) {
1195
1191
  if (this.findCallFn(nodeId, capabilityName) !== undefined)
1196
1192
  return true;
1197
- return this.localChildRegistry !== null && nodeId.startsWith(`${this.brokerSafe.nodeID}/`);
1193
+ return this.localChildRegistry !== null && nodeId.startsWith(`${this.broker.nodeID}/`);
1198
1194
  }
1199
1195
  /**
1200
1196
  * Build a proxy object that forwards every method call on a capability
@@ -1329,7 +1325,7 @@ class MoleculerService {
1329
1325
  * to the Moleculer `$node-mgmt.setLogLevel` action.
1330
1326
  */
1331
1327
  setChildLogLevelByNodeId(nodeId, level) {
1332
- const hubNodeId = this.brokerSafe.nodeID;
1328
+ const hubNodeId = this.broker.nodeID;
1333
1329
  if (!nodeId.startsWith(`${hubNodeId}/`))
1334
1330
  return false;
1335
1331
  const childId = nodeId.slice(hubNodeId.length + 1);
@@ -1348,7 +1344,7 @@ class MoleculerService {
1348
1344
  */
1349
1345
  async pullAgentReadinessSnapshot(nodeId) {
1350
1346
  try {
1351
- const snapshot = await this.brokerSafe.call(`${system_1.AGENT_READINESS_SERVICE_NAME}.getSnapshot`, {}, { nodeID: nodeId });
1347
+ const snapshot = await this.broker.call(`${system_1.AGENT_READINESS_SERVICE_NAME}.getSnapshot`, {}, { nodeID: nodeId });
1352
1348
  if (!Array.isArray(snapshot) || snapshot.length === 0)
1353
1349
  return;
1354
1350
  const records = (0, agent_readiness_pull_js_1.filterAgentAuthoritativeRecords)(snapshot, nodeId);
@@ -1375,7 +1371,7 @@ class MoleculerService {
1375
1371
  }
1376
1372
  this.udsEventBridgeDispose?.();
1377
1373
  this.udsEventBridgeDispose = null;
1378
- await this.brokerSafe.stop();
1374
+ await this.broker.stop();
1379
1375
  await this.localChildRegistry?.close();
1380
1376
  }
1381
1377
  }
package/dist/main.js CHANGED
@@ -599,6 +599,7 @@ async function bootstrap() {
599
599
  // settings backend (lazy getter: the backend lands after the sqlite-storage
600
600
  // builtin registers; the service resolves it per call).
601
601
  const shareTokenService = new share_token_service_1.ShareTokenService(() => addonRegistry.getSettingsBackend(), loggingService.createLogger('share-tokens'));
602
+ addonRegistry.setShareTokenService(shareTokenService);
602
603
  // One-time native-app login handoff codes (in-memory, 60s TTL, single-use) —
603
604
  // minted by `auth.createHandoffCode`, redeemed by the viewer app via the
604
605
  // public `auth.redeemHandoffCode`.
@@ -166,8 +166,10 @@ async function bootManual(opts) {
166
166
  // A1: durable, routing-blind offline-node history — persisted through the
167
167
  // hub-resident `settings-store` cap (resolved lazily; null until it boots).
168
168
  // Wired post-construction so the service's 3-arg constructor stays intact.
169
- agentRegistryService.setClusterNodeHistoryStore(new cluster_node_history_store_1.ClusterNodeHistoryStore(() => capabilityService.getRegistry()?.getSingleton('settings-store') ??
170
- null, loggingService.createLogger('cluster-node-history')));
169
+ const clusterNodeHistoryStore = new cluster_node_history_store_1.ClusterNodeHistoryStore(() => capabilityService.getRegistry()?.getSingleton('settings-store') ??
170
+ null, loggingService.createLogger('cluster-node-history'));
171
+ agentRegistryService.setClusterNodeHistoryStore(clusterNodeHistoryStore);
172
+ addonRegistryService.setClusterNodeHistoryStore(clusterNodeHistoryStore);
171
173
  // ---- Layer 4: need AddonRegistry ---------------------------------------
172
174
  // AddonWidgetsService — needs AddonRegistryService for the bundled-addon
173
175
  // dist sub-folder lookup (see service docstring).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.593",
3
+ "version": "1.2.595",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -38,19 +38,19 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@camstack/addon-admin-ui": "1.2.435",
42
- "@camstack/addon-agent-ui": "1.2.242",
43
- "@camstack/addon-auth": "1.2.148",
41
+ "@camstack/addon-admin-ui": "1.2.436",
42
+ "@camstack/addon-agent-ui": "1.2.243",
43
+ "@camstack/addon-auth": "1.2.149",
44
44
  "@camstack/addon-decoder-nodeav": "1.2.123",
45
- "@camstack/addon-notifiers": "1.2.135",
46
- "@camstack/addon-pipeline": "1.2.375",
47
- "@camstack/addon-pipeline-orchestrator": "1.2.332",
48
- "@camstack/addon-post-analysis": "1.2.374",
49
- "@camstack/develop": "0.1.11",
50
- "@camstack/sdk": "1.2.132",
51
- "@camstack/system": "1.2.444",
52
- "@camstack/types": "1.2.380",
53
- "@camstack/ui-library": "1.2.298",
45
+ "@camstack/addon-notifiers": "1.2.601",
46
+ "@camstack/addon-pipeline": "1.2.601",
47
+ "@camstack/addon-pipeline-orchestrator": "1.2.601",
48
+ "@camstack/addon-post-analysis": "1.2.601",
49
+ "@camstack/develop": "0.1.12",
50
+ "@camstack/sdk": "1.2.133",
51
+ "@camstack/system": "1.2.445",
52
+ "@camstack/types": "1.2.381",
53
+ "@camstack/ui-library": "1.2.299",
54
54
  "@fastify/compress": "^9.0.0",
55
55
  "@fastify/cookie": "^11.0.2",
56
56
  "@fastify/cors": "^11.2.0",