@camstack/server 1.1.26 → 1.1.28

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.
@@ -12,6 +12,7 @@ const hwaccel_router_js_1 = require("../core/hwaccel.router.js");
12
12
  const live_events_router_js_1 = require("../core/live-events.router.js");
13
13
  const logs_router_js_1 = require("../core/logs.router.js");
14
14
  const notifications_router_js_1 = require("../core/notifications.router.js");
15
+ const cluster_nodes_router_js_1 = require("../core/cluster-nodes.router.js");
15
16
  const repl_router_js_1 = require("../core/repl.router.js");
16
17
  const settings_backend_router_js_1 = require("../core/settings-backend.router.js");
17
18
  const stream_probe_router_js_1 = require("../core/stream-probe.router.js");
@@ -143,10 +144,14 @@ function buildCapabilityRouters(services) {
143
144
  live: (0, live_events_router_js_1.createLiveEventsRouter)(services.eventBus, services.addonRegistry),
144
145
  // stream-probe — fixed core API (ffprobe wrapper), not a cap.
145
146
  streamProbe: (0, stream_probe_router_js_1.createStreamProbeRouter)(services.streamProbe),
146
- // hwaccel — fixed core API, wraps the per-node `$hwaccel` Moleculer
147
- // service. UI pipeline / NodeDetail pages query per-node to show
148
- // which hardware backend each agent will use.
149
- hwaccel: (0, hwaccel_router_js_1.createHwAccelRouter)(services.moleculer?.broker ?? null),
147
+ // hwaccel — fixed core API, repointed onto the `platform-probe` cap
148
+ // (the sole cross-node-pinnable hwaccel surface). UI pipeline /
149
+ // NodeDetail pages query per-node to show which hardware backend each
150
+ // agent will use.
151
+ hwaccel: (0, hwaccel_router_js_1.createHwAccelRouter)(services.capabilityRegistry, services.moleculer),
152
+ // clusterNodes — fixed core API. Write-side purge for the durable
153
+ // offline-node history (Track A "Forget node"); read side is push-only.
154
+ clusterNodes: (0, cluster_nodes_router_js_1.createClusterNodesRouter)(services.agentRegistry),
150
155
  auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry),
151
156
  // ── Cap overrides: `mount: { kind: 'custom' }` ──────────────────
152
157
  // `snapshot-provider.supportsDevice` is an OR across providers;
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AddonRegistryService = void 0;
37
+ exports.shouldEmitProviderRegisteredReady = shouldEmitProviderRegisteredReady;
37
38
  /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access -- pre-existing lint debt across this 2200-line orchestration class. The flagged sites (StorageService.setLocationManager / setSettingsBackend, LoggingService.addDestination, RouteRegistry, etc.) are typed as `unknown` by their owning services to break circular construction-order dependencies; runtime contracts are validated structurally. Tracked separately; do not amend in unrelated edits. */
38
39
  const os = __importStar(require("node:os"));
39
40
  const addon_row_manifest_1 = require("./addon-row-manifest");
@@ -159,6 +160,19 @@ function parseDataPlaneEndpoints(raw) {
159
160
  // shim that consumed it were deleted. All settings flow through the
160
161
  // new `getAddonSettings / getGlobalSettings / getDeviceSettings`
161
162
  // endpoints on the `addon-settings` singleton capability.
163
+ /**
164
+ * Whether the `capability:provider-registered` hook may emit `ready` for a
165
+ * capability. `platform-probe` OWNS its readiness protocol: its provider
166
+ * registers synchronously from `onInitialize` while the REAL probe
167
+ * (hardware + embedded-Python EP scoring) is still running async — the
168
+ * registration-time emit would flip it `ready` prematurely and probe-gated
169
+ * consumers (detection-pipeline engine auto-pick) would read
170
+ * accelerator-blind results. The addon emits `starting`/`ready`/`down`
171
+ * itself (`autoEmitReadiness=false`, ready on probe-done).
172
+ */
173
+ function shouldEmitProviderRegisteredReady(capability) {
174
+ return capability !== 'platform-probe';
175
+ }
162
176
  class AddonRegistryService {
163
177
  loggingService;
164
178
  eventBusService;
@@ -2048,13 +2062,18 @@ class AddonRegistryService {
2048
2062
  // loops). The node-scoped emit is sufficient: the hydrate
2049
2063
  // path on subprocess brokers replays whichever records the
2050
2064
  // hub's `$readiness.getSnapshot` returns, scope and all.
2051
- try {
2052
- this.moleculer.readinessRegistry.emitReady(capability, { type: 'node', nodeId: 'hub' });
2053
- }
2054
- catch (err) {
2055
- this.logger.warn('emitReady failed', {
2056
- meta: { capability, addonId, error: (0, types_1.errMsg)(err) },
2057
- });
2065
+ // Caps that OWN their readiness (platform-probe: registration
2066
+ // happens while its async probe still runs) are excluded —
2067
+ // this emit would flip them `ready` prematurely.
2068
+ if (shouldEmitProviderRegisteredReady(capability)) {
2069
+ try {
2070
+ this.moleculer.readinessRegistry.emitReady(capability, { type: 'node', nodeId: 'hub' });
2071
+ }
2072
+ catch (err) {
2073
+ this.logger.warn('emitReady failed', {
2074
+ meta: { capability, addonId, error: (0, types_1.errMsg)(err) },
2075
+ });
2076
+ }
2058
2077
  }
2059
2078
  switch (capability) {
2060
2079
  case 'storage': {
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AgentRegistryService = void 0;
37
37
  exports.toNodeLiveness = toNodeLiveness;
38
+ exports.toDescriptor = toDescriptor;
38
39
  const node_crypto_1 = require("node:crypto");
39
40
  const os = __importStar(require("node:os"));
40
41
  const system_1 = require("@camstack/system");
@@ -81,6 +82,28 @@ function toNodeLiveness(nodes) {
81
82
  .filter((node) => !node.id.includes('/'))
82
83
  .map((node) => ({ id: node.id, isHub: node.id === 'hub', isOnline: node.available }));
83
84
  }
85
+ /**
86
+ * Project a live `AgentListItem` into the durable history descriptor. Field
87
+ * derivations mirror `computeTopology` exactly (engines from `pythonRuntimes`,
88
+ * cpuModel `undefined`→`null`, addonIds from the reported `agentAddons`) so an
89
+ * offline row rendered from history is indistinguishable from the live shape.
90
+ */
91
+ function toDescriptor(entry) {
92
+ return {
93
+ id: entry.info.id,
94
+ name: entry.info.name,
95
+ hostname: entry.info.hostname ?? entry.info.id,
96
+ platform: entry.info.platform ?? 'unknown',
97
+ arch: entry.info.arch ?? 'unknown',
98
+ cpuModel: entry.info.cpuModel ?? null,
99
+ cpuCores: entry.info.cpuCores ?? 0,
100
+ memoryMB: entry.info.memoryMB ?? 0,
101
+ engines: [...(entry.info.pythonRuntimes ?? [])],
102
+ isHub: entry.isHub,
103
+ localIps: [...(entry.localIps ?? [])],
104
+ addonIds: [...(entry.agentAddons ?? [])],
105
+ };
106
+ }
84
107
  class AgentRegistryService {
85
108
  eventBus;
86
109
  moleculer;
@@ -96,6 +119,13 @@ class AgentRegistryService {
96
119
  * order (see `manual-boot.ts`).
97
120
  */
98
121
  addonRegistry = null;
122
+ /**
123
+ * Durable, routing-blind offline-node history (A1). Separate from
124
+ * `HubNodeRegistry` on purpose — see `cluster-node-history-store.ts`. Wired
125
+ * post-construction from manual-boot so the 3-arg constructor (and its unit
126
+ * fakes) stay untouched; a null store means "history disabled" (best-effort).
127
+ */
128
+ historyStore = null;
99
129
  constructor(eventBus, moleculer, capabilityService) {
100
130
  this.eventBus = eventBus;
101
131
  this.moleculer = moleculer;
@@ -105,6 +135,22 @@ class AgentRegistryService {
105
135
  setAddonRegistry(addonRegistry) {
106
136
  this.addonRegistry = addonRegistry;
107
137
  }
138
+ /** Wire the durable offline-node history store (A1). */
139
+ setClusterNodeHistoryStore(store) {
140
+ this.historyStore = store;
141
+ }
142
+ /**
143
+ * Read the durable offline-node history — consumed by `computeTopology`
144
+ * to render OFFLINE rows for nodes no longer in the live Moleculer window.
145
+ * Returns `[]` when no history store is wired (unit fakes).
146
+ */
147
+ async getClusterNodeHistory() {
148
+ return this.historyStore ? this.historyStore.getAll() : [];
149
+ }
150
+ /** Permanently forget a node's persisted history (inline "Forget node"). */
151
+ async forgetClusterNode(nodeId) {
152
+ await this.historyStore?.forget(nodeId);
153
+ }
108
154
  /** Typed view of the Moleculer broker — single documented cast. */
109
155
  get broker() {
110
156
  return this.moleculer.broker;
@@ -154,6 +200,10 @@ class AgentRegistryService {
154
200
  return;
155
201
  if (kind === 'agent') {
156
202
  console.log(`[agent-registry] Agent disconnected: ${node.id}`);
203
+ // A1: stamp the disconnect time as the node's `lastActive` so its
204
+ // offline row reports an honest "last seen". Best-effort, no-op if the
205
+ // node was never snapshotted while online.
206
+ void this.historyStore?.touch(node.id);
157
207
  this.eventBus.emit({
158
208
  id: (0, node_crypto_1.randomUUID)(),
159
209
  timestamp: new Date(),
@@ -407,12 +457,27 @@ class AgentRegistryService {
407
457
  const hubEntry = await this.buildHubEntry(hubProcesses);
408
458
  const remoteEntries = [];
409
459
  const registry = this.moleculer.broker.registry;
410
- const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
460
+ // `onlyAvailable: false` intentionally includes nodes Moleculer still
461
+ // remembers as disconnected (`available: false`) — the same registry
462
+ // read `listNodeLiveness()` uses for `/health`. This surfaces recently
463
+ // offline agents inline instead of silently omitting them; it is NOT a
464
+ // new shadow map (no persistence, no timestamp field added anywhere) —
465
+ // once Moleculer's own `cleanOfflineNodesTimeout` purges the entry, the
466
+ // row disappears again. A hub-restart-surviving offline history is a
467
+ // separate, deliberately deferred piece of work.
468
+ const nodes = registry?.getNodeList?.({ onlyAvailable: false }) ?? [];
411
469
  for (const node of nodes) {
412
470
  const nodeId = node.id;
413
471
  // Skip hub (already included) and child processes (contain '/')
414
472
  if (nodeId === 'hub' || nodeId.includes('/'))
415
473
  continue;
474
+ if (!node.available) {
475
+ // Offline: skip the `$agent.status`/`$process.list` RPC fan-out
476
+ // entirely (the node is unreachable — those calls would just time
477
+ // out) and surface a minimal, honestly-degraded row instead.
478
+ remoteEntries.push(this.buildOfflineEntry(node));
479
+ continue;
480
+ }
416
481
  try {
417
482
  const status = (await this.broker.call('$agent.status', {}, {
418
483
  nodeID: nodeId,
@@ -486,12 +551,64 @@ class AgentRegistryService {
486
551
  // Skip nodes without $agent service
487
552
  }
488
553
  }
489
- // TODO(D3 follow-up): offline-agent history dropped with knownAgents.
490
- // Previously, agents that disconnected were kept in a shadow map and
491
- // surfaced here as offline rows. listNodes now reflects only live
492
- // broker.registry nodes.
554
+ // A1 (R2 capture point): snapshot every ONLINE node's static descriptor +
555
+ // last-known addon roster into the durable, routing-blind history store on
556
+ // each refresh. This is the ONLY place cpuModel/cores/memory/localIps are
557
+ // resolved from live `$agent.status`, so it is where an offline row's
558
+ // descriptor must be captured while the node is still reachable. Offline
559
+ // entries (the `!node.available` branch above) are NOT re-snapshotted —
560
+ // their `lastActive` is stamped on `$node.disconnected` instead. This is a
561
+ // separate durable store, never a `knownAgents`-style shadow of
562
+ // `HubNodeRegistry` (which stays the ephemeral live cap authority).
563
+ await this.snapshotOnlineNodes([hubEntry, ...remoteEntries]);
493
564
  return [hubEntry, ...remoteEntries];
494
565
  }
566
+ /**
567
+ * Best-effort upsert of every online entry's descriptor into the history
568
+ * store. Never throws — the store swallows its own errors and this awaits
569
+ * `allSettled` so a storage hiccup can never fail `listNodes()`.
570
+ */
571
+ async snapshotOnlineNodes(entries) {
572
+ if (!this.historyStore)
573
+ return;
574
+ const online = entries.filter((e) => e.isOnline !== false);
575
+ await Promise.allSettled(online.map((e) => this.historyStore.snapshot(toDescriptor(e))));
576
+ }
577
+ /**
578
+ * Minimal, honestly-degraded entry for a node Moleculer's registry still
579
+ * remembers but marks `available: false`. No RPC round-trip is attempted
580
+ * (the node is unreachable by definition) so every live-only field is
581
+ * zeroed/defaulted rather than guessed. `connectedSince: Date.now()`
582
+ * keeps the downstream `uptime = Date.now() - connectedSince` topology
583
+ * projection at ~0 instead of asserting a stale/undefined value.
584
+ */
585
+ buildOfflineEntry(node) {
586
+ return {
587
+ info: {
588
+ id: node.id,
589
+ name: node.id,
590
+ hostname: node.hostname ?? node.id,
591
+ capabilities: [],
592
+ platform: 'unknown',
593
+ arch: 'unknown',
594
+ cpuCores: 0,
595
+ memoryMB: 0,
596
+ },
597
+ localIps: [],
598
+ status: {
599
+ activeCameras: 0,
600
+ cpuPercent: 0,
601
+ memoryPercent: 0,
602
+ fps: {},
603
+ errors: [],
604
+ },
605
+ connectedSince: Date.now(),
606
+ isHub: false,
607
+ isOnline: false,
608
+ subProcesses: [],
609
+ agentAddons: [],
610
+ };
611
+ }
495
612
  async buildHubEntry(subProcesses = []) {
496
613
  const cpus = os.cpus();
497
614
  // Get live metrics from the metrics-provider capability (NativeMetricsProvider).
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClusterNodeHistoryStore = void 0;
4
+ const zod_1 = require("zod");
5
+ const types_1 = require("@camstack/types");
6
+ const DescriptorSchema = zod_1.z.object({
7
+ id: zod_1.z.string(),
8
+ name: zod_1.z.string(),
9
+ hostname: zod_1.z.string(),
10
+ platform: zod_1.z.string(),
11
+ arch: zod_1.z.string(),
12
+ cpuModel: zod_1.z.string().nullable(),
13
+ cpuCores: zod_1.z.number(),
14
+ memoryMB: zod_1.z.number(),
15
+ engines: zod_1.z.array(zod_1.z.string()).readonly(),
16
+ isHub: zod_1.z.boolean(),
17
+ localIps: zod_1.z.array(zod_1.z.string()).readonly(),
18
+ addonIds: zod_1.z.array(zod_1.z.string()).readonly(),
19
+ });
20
+ /** Shape of a `settings-store.query` record's `data` for this collection. */
21
+ const RowDataSchema = zod_1.z.object({
22
+ lastActive: zod_1.z.number(),
23
+ descriptor: DescriptorSchema,
24
+ });
25
+ const COLLECTION = 'cluster-node-history';
26
+ class ClusterNodeHistoryStore {
27
+ resolveSettingsStore;
28
+ logger;
29
+ declared = false;
30
+ constructor(resolveSettingsStore, logger) {
31
+ this.resolveSettingsStore = resolveSettingsStore;
32
+ this.logger = logger;
33
+ }
34
+ /**
35
+ * Idempotently declare the typed collection. Returns the resolved provider
36
+ * or null if the settings-store isn't available yet (early boot / hub
37
+ * unreachable) — callers treat null as "skip, best-effort".
38
+ */
39
+ async ensureDeclared() {
40
+ const store = this.resolveSettingsStore();
41
+ if (!store)
42
+ return null;
43
+ if (this.declared)
44
+ return store;
45
+ try {
46
+ await store.declareCollection({
47
+ collection: COLLECTION,
48
+ columns: [
49
+ { name: 'id', type: 'TEXT', primaryKey: true, notNull: true },
50
+ { name: 'lastActive', type: 'INTEGER', notNull: true },
51
+ { name: 'descriptor', type: 'JSON', notNull: true },
52
+ ],
53
+ });
54
+ this.declared = true;
55
+ return store;
56
+ }
57
+ catch (err) {
58
+ this.logger.warn('declareCollection failed (best-effort)', {
59
+ meta: { error: (0, types_1.errMsg)(err) },
60
+ });
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Upsert a node's descriptor + addon roster while it is ONLINE, stamping
66
+ * `lastActive = Date.now()`. Called on each `listNodes()` refresh for every
67
+ * live node (R2 capture point).
68
+ */
69
+ async snapshot(descriptor) {
70
+ const store = await this.ensureDeclared();
71
+ if (!store)
72
+ return;
73
+ try {
74
+ await store.set({
75
+ collection: COLLECTION,
76
+ key: descriptor.id,
77
+ value: { lastActive: Date.now(), descriptor },
78
+ });
79
+ }
80
+ catch (err) {
81
+ this.logger.warn('snapshot upsert failed (best-effort)', {
82
+ tags: { nodeId: descriptor.id },
83
+ meta: { error: (0, types_1.errMsg)(err) },
84
+ });
85
+ }
86
+ }
87
+ /**
88
+ * Touch `lastActive` for a node without rewriting its descriptor — used on
89
+ * `$node.disconnected` so an offline node's "last seen" is the disconnect
90
+ * time. A no-op if the node was never snapshotted (nothing to update).
91
+ */
92
+ async touch(nodeId) {
93
+ const store = await this.ensureDeclared();
94
+ if (!store)
95
+ return;
96
+ try {
97
+ await store.update({
98
+ collection: COLLECTION,
99
+ id: nodeId,
100
+ data: { lastActive: Date.now() },
101
+ });
102
+ }
103
+ catch (err) {
104
+ this.logger.warn('touch failed (best-effort)', {
105
+ tags: { nodeId },
106
+ meta: { error: (0, types_1.errMsg)(err) },
107
+ });
108
+ }
109
+ }
110
+ /** Read the full persisted history, validated. Malformed rows are dropped. */
111
+ async getAll() {
112
+ const store = await this.ensureDeclared();
113
+ if (!store)
114
+ return [];
115
+ try {
116
+ const rows = await store.query({ collection: COLLECTION });
117
+ const result = [];
118
+ for (const row of rows) {
119
+ const parsed = RowDataSchema.safeParse(row.data);
120
+ if (!parsed.success) {
121
+ this.logger.warn('dropping malformed cluster-node-history row', {
122
+ tags: { nodeId: row.id },
123
+ });
124
+ continue;
125
+ }
126
+ result.push({ ...parsed.data.descriptor, lastActive: parsed.data.lastActive });
127
+ }
128
+ return result;
129
+ }
130
+ catch (err) {
131
+ this.logger.warn('getAll failed (best-effort)', { meta: { error: (0, types_1.errMsg)(err) } });
132
+ return [];
133
+ }
134
+ }
135
+ /** Permanently forget a node's history row (the inline "Forget node" action). */
136
+ async forget(nodeId) {
137
+ const store = await this.ensureDeclared();
138
+ if (!store)
139
+ return;
140
+ try {
141
+ await store.delete({ collection: COLLECTION, key: nodeId });
142
+ this.logger.info('forgot cluster node', { tags: { nodeId } });
143
+ }
144
+ catch (err) {
145
+ this.logger.warn('forget failed (best-effort)', {
146
+ tags: { nodeId },
147
+ meta: { error: (0, types_1.errMsg)(err) },
148
+ });
149
+ }
150
+ }
151
+ }
152
+ exports.ClusterNodeHistoryStore = ClusterNodeHistoryStore;
@@ -251,6 +251,14 @@ class MoleculerService {
251
251
  // handler retries the hub-local route then throws a precise error rather
252
252
  // than the unroutable broker fallback (`switch.switch.getStatus`).
253
253
  isDeviceNativeCap: (capName) => this.capabilityService.getRegistry()?.getDefinition(capName)?.deviceNative === true,
254
+ // `nodeIdMode: 'data'` signal: for these caps (`addon-settings`,
255
+ // `addons`, `nodes`, `pipeline-orchestrator`) an inline `args.nodeId`
256
+ // is provider DATA — the hub singleton dispatches internally — never a
257
+ // routing pin. Without this a forked child's call carrying `nodeId`
258
+ // gets pinned to a node with no provider (dead broker fallback →
259
+ // ServiceNotFoundError). The kernel layer has no cap registry, so feed
260
+ // it the flag from the cap definition.
261
+ isDataNodeIdCap: (capName) => this.capabilityService.getRegistry()?.getDefinition(capName)?.nodeIdMode === 'data',
254
262
  // Empty-collection fast-path: a `collection` cap's array-output method
255
263
  // with ZERO providers registered cluster-wide IS the empty aggregate —
256
264
  // return [] instead of routing (resolver miss → broker fallback → ~30s
@@ -388,12 +396,6 @@ class MoleculerService {
388
396
  probe: (url, options) => this.streamProbe.probe(url, options),
389
397
  probeField: (key, value) => this.streamProbe.probeField(key, value),
390
398
  }));
391
- // Register `$hwaccel` on hub — every node in the cluster does the
392
- // same so `broker.call('$hwaccel.resolve', params, { nodeID })`
393
- // returns the backend list for whichever host the caller targets.
394
- // Admin UI uses this to show per-agent hwaccel info on the
395
- // pipeline / NodeDetail pages.
396
- this.brokerSafe.createService((0, system_1.createHwAccelService)((0, system_1.createKernelHwAccel)()));
397
399
  await this.brokerSafe.start();
398
400
  logger.info('Moleculer broker started (TCP transport)');
399
401
  // Construct the CapRouteResolver now that both the broker and
@@ -67,6 +67,7 @@ const addon_widgets_service_1 = require("./core/addon-widgets/addon-widgets.serv
67
67
  const addon_bridge_service_1 = require("./core/addon-bridge/addon-bridge.service");
68
68
  const moleculer_service_1 = require("./core/moleculer/moleculer.service");
69
69
  const agent_registry_service_1 = require("./core/agent/agent-registry.service");
70
+ const cluster_node_history_store_1 = require("./core/agent/cluster-node-history-store");
70
71
  const addon_registry_service_1 = require("./core/addon/addon-registry.service");
71
72
  const addon_search_service_1 = require("./core/addon/addon-search.service");
72
73
  const addon_package_service_1 = require("./core/addon/addon-package.service");
@@ -128,6 +129,11 @@ async function bootManual(opts) {
128
129
  // reconciliation, but is constructed first (it has no other dependency
129
130
  // on AddonRegistryService) — wire it post-construction.
130
131
  agentRegistryService.setAddonRegistry(addonRegistryService);
132
+ // A1: durable, routing-blind offline-node history — persisted through the
133
+ // hub-resident `settings-store` cap (resolved lazily; null until it boots).
134
+ // Wired post-construction so the service's 3-arg constructor stays intact.
135
+ agentRegistryService.setClusterNodeHistoryStore(new cluster_node_history_store_1.ClusterNodeHistoryStore(() => capabilityService.getRegistry()?.getSingleton('settings-store') ??
136
+ null, loggingService.createLogger('cluster-node-history')));
131
137
  // ---- Layer 4: need AddonRegistry ---------------------------------------
132
138
  // AddonWidgetsService — needs AddonRegistryService for the bundled-addon
133
139
  // 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.1.26",
3
+ "version": "1.1.28",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",