@camstack/server 0.2.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
  2. package/dist/api/addon-upload.js +441 -0
  3. package/dist/api/addons-custom.router.js +91 -0
  4. package/dist/api/auth-whoami.js +55 -0
  5. package/dist/api/bridge-addons.router.js +109 -0
  6. package/dist/api/capabilities.router.js +229 -0
  7. package/dist/api/core/addon-settings.router.js +117 -0
  8. package/dist/api/core/agents.router.js +73 -0
  9. package/dist/api/core/auth.router.js +286 -0
  10. package/dist/api/core/bulk-update-coordinator.js +229 -0
  11. package/dist/api/core/cap-providers.js +1124 -0
  12. package/dist/api/core/capabilities.router.js +138 -0
  13. package/dist/api/core/collection-preference.js +17 -0
  14. package/dist/api/core/event-bus-proxy.router.js +45 -0
  15. package/dist/api/core/hwaccel.router.js +91 -0
  16. package/dist/api/core/live-events.router.js +61 -0
  17. package/dist/api/core/logs.router.js +172 -0
  18. package/dist/api/core/notifications.router.js +67 -0
  19. package/dist/api/core/repl.router.js +35 -0
  20. package/dist/api/core/settings-backend.router.js +121 -0
  21. package/dist/api/core/stream-probe.router.js +58 -0
  22. package/dist/api/core/system-events.router.js +100 -0
  23. package/dist/api/health/health.routes.js +68 -0
  24. package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
  25. package/dist/api/oauth2/oauth2-routes.js +219 -0
  26. package/dist/api/trpc/cap-mount-helpers.js +194 -0
  27. package/dist/api/trpc/cap-route-error-formatter.js +133 -0
  28. package/dist/api/trpc/client-ip.js +147 -0
  29. package/dist/api/trpc/core-cap-bridge.js +115 -0
  30. package/dist/api/trpc/generated-cap-mounts.js +388 -0
  31. package/dist/api/trpc/generated-cap-routers.js +7635 -0
  32. package/dist/api/trpc/scope-access.js +93 -0
  33. package/dist/api/trpc/trpc.context.js +184 -0
  34. package/dist/api/trpc/trpc.middleware.js +139 -0
  35. package/dist/api/trpc/trpc.router.js +188 -0
  36. package/dist/auth/session-cookie.js +47 -0
  37. package/dist/boot/boot-config.js +241 -0
  38. package/dist/boot/integration-id-backfill.js +76 -0
  39. package/dist/boot/post-boot.service.js +85 -0
  40. package/dist/core/addon/addon-call-gateway.js +99 -0
  41. package/dist/core/addon/addon-package.service.js +1560 -0
  42. package/dist/core/addon/addon-registry.service.js +2739 -0
  43. package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
  44. package/dist/core/addon/addon-search.service.js +62 -0
  45. package/dist/core/addon/addon-settings-provider.js +102 -0
  46. package/dist/core/addon/addon.tokens.js +5 -0
  47. package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
  48. package/dist/core/addon-pages/addon-pages.service.js +107 -0
  49. package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
  50. package/dist/core/agent/agent-registry.service.js +477 -0
  51. package/dist/core/auth/auth.service.js +10 -0
  52. package/dist/core/capability/capability.service.js +58 -0
  53. package/dist/core/config/config.schema.js +7 -0
  54. package/dist/core/config/config.service.js +10 -0
  55. package/dist/core/events/event-bus.service.js +83 -0
  56. package/dist/core/feature/feature.service.js +10 -0
  57. package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
  58. package/dist/core/logging/log-ring-buffer.js +6 -0
  59. package/dist/core/logging/logging.service.js +130 -0
  60. package/dist/core/logging/scoped-logger.js +6 -0
  61. package/dist/core/moleculer/cap-call-fn.js +50 -0
  62. package/dist/core/moleculer/cap-route-authority.js +122 -0
  63. package/dist/core/moleculer/moleculer.service.js +898 -0
  64. package/dist/core/network/network-quality.service.js +7 -0
  65. package/dist/core/notification/notification-wrapper.service.js +33 -0
  66. package/dist/core/notification/toast-wrapper.service.js +25 -0
  67. package/dist/core/provider/provider.tokens.js +4 -0
  68. package/dist/core/repl/repl-engine.service.js +140 -0
  69. package/dist/core/storage/fs-storage-backend.js +6 -0
  70. package/dist/core/storage/storage-location-manager.js +6 -0
  71. package/dist/core/storage/storage.service.js +7 -0
  72. package/dist/core/streaming/stream-probe.service.js +209 -0
  73. package/dist/core/topology/topology-emitter.service.js +106 -0
  74. package/dist/launcher.js +325 -0
  75. package/dist/main.js +1098 -0
  76. package/dist/manual-boot.js +227 -0
  77. package/package.json +5 -1
  78. package/src/__tests__/addon-install-e2e.test.ts +0 -74
  79. package/src/__tests__/addon-pages-e2e.test.ts +0 -200
  80. package/src/__tests__/addon-route-session.test.ts +0 -17
  81. package/src/__tests__/addon-settings-router.spec.ts +0 -67
  82. package/src/__tests__/addon-upload.spec.ts +0 -475
  83. package/src/__tests__/agent-registry.spec.ts +0 -179
  84. package/src/__tests__/agent-status-page.spec.ts +0 -82
  85. package/src/__tests__/auth-session-cookie.test.ts +0 -48
  86. package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
  87. package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
  88. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
  89. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
  90. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
  91. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
  92. package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
  93. package/src/__tests__/cap-route-adapter.spec.ts +0 -302
  94. package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
  95. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
  96. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
  97. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
  98. package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
  99. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
  100. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
  101. package/src/__tests__/cap-routers/harness.ts +0 -163
  102. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
  103. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
  104. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
  105. package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
  106. package/src/__tests__/capability-e2e.test.ts +0 -384
  107. package/src/__tests__/cli-e2e.test.ts +0 -150
  108. package/src/__tests__/core-cap-bridge.spec.ts +0 -91
  109. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
  110. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
  111. package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
  112. package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
  113. package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
  114. package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
  115. package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
  116. package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
  117. package/src/__tests__/framework-allowlist.spec.ts +0 -96
  118. package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
  119. package/src/__tests__/https-e2e.test.ts +0 -124
  120. package/src/__tests__/lifecycle-e2e.test.ts +0 -189
  121. package/src/__tests__/live-events-subscription.spec.ts +0 -149
  122. package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
  123. package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
  124. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
  125. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
  126. package/src/__tests__/native-cap-route.spec.ts +0 -427
  127. package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
  128. package/src/__tests__/post-boot-restart.spec.ts +0 -161
  129. package/src/__tests__/singleton-contention.test.ts +0 -499
  130. package/src/__tests__/streaming-diagnostic.test.ts +0 -615
  131. package/src/__tests__/streaming-scale.test.ts +0 -314
  132. package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
  133. package/src/__tests__/uds-log-ingest.spec.ts +0 -183
  134. package/src/api/__tests__/addons-custom.spec.ts +0 -148
  135. package/src/api/__tests__/capabilities.router.test.ts +0 -56
  136. package/src/api/addon-upload.ts +0 -529
  137. package/src/api/addons-custom.router.ts +0 -101
  138. package/src/api/auth-whoami.ts +0 -101
  139. package/src/api/bridge-addons.router.ts +0 -122
  140. package/src/api/capabilities.router.ts +0 -265
  141. package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
  142. package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
  143. package/src/api/core/addon-settings.router.ts +0 -127
  144. package/src/api/core/agents.router.ts +0 -86
  145. package/src/api/core/auth.router.ts +0 -322
  146. package/src/api/core/bulk-update-coordinator.ts +0 -305
  147. package/src/api/core/cap-providers.ts +0 -1339
  148. package/src/api/core/capabilities.router.ts +0 -149
  149. package/src/api/core/collection-preference.ts +0 -40
  150. package/src/api/core/event-bus-proxy.router.ts +0 -45
  151. package/src/api/core/hwaccel.router.ts +0 -108
  152. package/src/api/core/live-events.router.ts +0 -67
  153. package/src/api/core/logs.router.ts +0 -195
  154. package/src/api/core/notifications.router.ts +0 -66
  155. package/src/api/core/repl.router.ts +0 -39
  156. package/src/api/core/settings-backend.router.ts +0 -140
  157. package/src/api/core/stream-probe.router.ts +0 -57
  158. package/src/api/core/system-events.router.ts +0 -125
  159. package/src/api/health/health.routes.ts +0 -117
  160. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
  161. package/src/api/oauth2/oauth2-routes.ts +0 -281
  162. package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
  163. package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
  164. package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
  165. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
  166. package/src/api/trpc/cap-mount-helpers.ts +0 -245
  167. package/src/api/trpc/cap-route-error-formatter.ts +0 -171
  168. package/src/api/trpc/client-ip.ts +0 -147
  169. package/src/api/trpc/core-cap-bridge.ts +0 -154
  170. package/src/api/trpc/generated-cap-mounts.ts +0 -1240
  171. package/src/api/trpc/generated-cap-routers.ts +0 -11523
  172. package/src/api/trpc/scope-access.ts +0 -110
  173. package/src/api/trpc/trpc.context.ts +0 -258
  174. package/src/api/trpc/trpc.middleware.ts +0 -146
  175. package/src/api/trpc/trpc.router.ts +0 -389
  176. package/src/auth/session-cookie.ts +0 -54
  177. package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
  178. package/src/boot/boot-config.ts +0 -259
  179. package/src/boot/integration-id-backfill.ts +0 -109
  180. package/src/boot/post-boot.service.ts +0 -105
  181. package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
  182. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
  183. package/src/core/addon/addon-call-gateway.ts +0 -171
  184. package/src/core/addon/addon-package.service.ts +0 -1787
  185. package/src/core/addon/addon-registry.service.ts +0 -3130
  186. package/src/core/addon/addon-search.service.ts +0 -91
  187. package/src/core/addon/addon-settings-provider.ts +0 -220
  188. package/src/core/addon/addon.tokens.ts +0 -2
  189. package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
  190. package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
  191. package/src/core/addon-pages/addon-pages.service.ts +0 -82
  192. package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
  193. package/src/core/agent/agent-registry.service.ts +0 -529
  194. package/src/core/auth/auth.service.spec.ts +0 -86
  195. package/src/core/auth/auth.service.ts +0 -8
  196. package/src/core/capability/capability.service.ts +0 -66
  197. package/src/core/config/config.schema.ts +0 -3
  198. package/src/core/config/config.service.spec.ts +0 -175
  199. package/src/core/config/config.service.ts +0 -7
  200. package/src/core/events/event-bus.service.spec.ts +0 -235
  201. package/src/core/events/event-bus.service.ts +0 -89
  202. package/src/core/feature/feature.service.spec.ts +0 -99
  203. package/src/core/feature/feature.service.ts +0 -8
  204. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
  205. package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
  206. package/src/core/logging/log-ring-buffer.ts +0 -3
  207. package/src/core/logging/logging.service.spec.ts +0 -287
  208. package/src/core/logging/logging.service.ts +0 -143
  209. package/src/core/logging/scoped-logger.ts +0 -3
  210. package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
  211. package/src/core/moleculer/cap-call-fn.ts +0 -107
  212. package/src/core/moleculer/cap-route-authority.ts +0 -194
  213. package/src/core/moleculer/moleculer.service.ts +0 -1072
  214. package/src/core/network/network-quality.service.spec.ts +0 -53
  215. package/src/core/network/network-quality.service.ts +0 -5
  216. package/src/core/notification/notification-wrapper.service.ts +0 -34
  217. package/src/core/notification/toast-wrapper.service.ts +0 -27
  218. package/src/core/provider/provider.tokens.ts +0 -1
  219. package/src/core/repl/repl-engine.service.spec.ts +0 -444
  220. package/src/core/repl/repl-engine.service.ts +0 -155
  221. package/src/core/storage/fs-storage-backend.spec.ts +0 -70
  222. package/src/core/storage/fs-storage-backend.ts +0 -3
  223. package/src/core/storage/storage-location-manager.spec.ts +0 -130
  224. package/src/core/storage/storage-location-manager.ts +0 -3
  225. package/src/core/storage/storage.service.spec.ts +0 -73
  226. package/src/core/storage/storage.service.ts +0 -3
  227. package/src/core/streaming/stream-probe.service.ts +0 -221
  228. package/src/core/topology/topology-emitter.service.ts +0 -105
  229. package/src/launcher.ts +0 -314
  230. package/src/main.ts +0 -1245
  231. package/src/manual-boot.ts +0 -301
  232. package/tsconfig.build.json +0 -8
  233. package/tsconfig.json +0 -33
  234. package/vitest.config.ts +0 -26
@@ -0,0 +1,477 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AgentRegistryService = void 0;
37
+ const node_crypto_1 = require("node:crypto");
38
+ const os = __importStar(require("node:os"));
39
+ const types_1 = require("@camstack/types");
40
+ /** Per-call timeout for `$agent.*` RPC during reconciliation. */
41
+ const AGENT_RECONCILE_RPC_TIMEOUT_MS = 8_000;
42
+ class AgentRegistryService {
43
+ eventBus;
44
+ moleculer;
45
+ capabilityService;
46
+ // Role assignments: key = `${cameraId}:${role}`
47
+ assignments = new Map();
48
+ bootTimestamp = Date.now();
49
+ /**
50
+ * Hub addon registry — the source of truth for which addons are
51
+ * installed and their `execution.placement`. Injected via
52
+ * `setAddonRegistry()` rather than the constructor because
53
+ * `AddonRegistryService` is built AFTER this service in the boot
54
+ * order (see `manual-boot.ts`).
55
+ */
56
+ addonRegistry = null;
57
+ constructor(eventBus, moleculer, capabilityService) {
58
+ this.eventBus = eventBus;
59
+ this.moleculer = moleculer;
60
+ this.capabilityService = capabilityService;
61
+ }
62
+ /** Wire the hub addon registry once it has been constructed. */
63
+ setAddonRegistry(addonRegistry) {
64
+ this.addonRegistry = addonRegistry;
65
+ }
66
+ /** Typed view of the Moleculer broker — single documented cast. */
67
+ get broker() {
68
+ return this.moleculer.broker;
69
+ }
70
+ onModuleInit() {
71
+ const classifyNode = (id) => {
72
+ if (id === 'hub')
73
+ return null;
74
+ if (id.includes('/'))
75
+ return 'worker';
76
+ return 'agent';
77
+ };
78
+ // D3: reconcile placement when an agent completes the $hub.registerNode
79
+ // handshake — the manifest is authoritative and complete at that point,
80
+ // no grace delay needed. MoleculerService fires this callback from its
81
+ // onRegisterNode dep for every bare-ID agent node.
82
+ this.moleculer.setOnAgentRegistered((agentId) => {
83
+ void this.reconcileAgentAddons(agentId);
84
+ });
85
+ this.broker.localBus.on('$node.connected', ({ node }) => {
86
+ const kind = classifyNode(node.id);
87
+ if (!kind)
88
+ return;
89
+ if (kind === 'agent') {
90
+ console.log(`[agent-registry] Agent connected: ${node.id}`);
91
+ this.eventBus.emit({
92
+ id: (0, node_crypto_1.randomUUID)(),
93
+ timestamp: new Date(),
94
+ source: { type: 'core', id: 'agent-registry' },
95
+ category: types_1.EventCategory.AgentOnline,
96
+ data: { agentId: node.id },
97
+ });
98
+ }
99
+ else {
100
+ this.eventBus.emit({
101
+ id: (0, node_crypto_1.randomUUID)(),
102
+ timestamp: new Date(),
103
+ source: { type: 'core', id: 'agent-registry' },
104
+ category: types_1.EventCategory.WorkerOnline,
105
+ data: { workerId: node.id },
106
+ });
107
+ }
108
+ });
109
+ this.broker.localBus.on('$node.disconnected', ({ node }) => {
110
+ const kind = classifyNode(node.id);
111
+ if (!kind)
112
+ return;
113
+ if (kind === 'agent') {
114
+ console.log(`[agent-registry] Agent disconnected: ${node.id}`);
115
+ this.eventBus.emit({
116
+ id: (0, node_crypto_1.randomUUID)(),
117
+ timestamp: new Date(),
118
+ source: { type: 'core', id: 'agent-registry' },
119
+ category: types_1.EventCategory.AgentOffline,
120
+ data: { agentId: node.id },
121
+ });
122
+ }
123
+ else {
124
+ this.eventBus.emit({
125
+ id: (0, node_crypto_1.randomUUID)(),
126
+ timestamp: new Date(),
127
+ source: { type: 'core', id: 'agent-registry' },
128
+ category: types_1.EventCategory.WorkerOffline,
129
+ data: { workerId: node.id },
130
+ });
131
+ }
132
+ });
133
+ // Retroactive scan: emit lifecycle events for nodes already
134
+ // connected when the listener registers. Classifies each as
135
+ // agent or worker — same logic as the live handlers above.
136
+ try {
137
+ const registry = this.moleculer.broker.registry;
138
+ const existing = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
139
+ for (const { id } of existing) {
140
+ const kind = classifyNode(id);
141
+ if (!kind)
142
+ continue;
143
+ if (kind === 'agent') {
144
+ this.eventBus.emit({
145
+ id: (0, node_crypto_1.randomUUID)(),
146
+ timestamp: new Date(),
147
+ source: { type: 'core', id: 'agent-registry' },
148
+ category: types_1.EventCategory.AgentOnline,
149
+ data: { agentId: id },
150
+ });
151
+ }
152
+ else {
153
+ this.eventBus.emit({
154
+ id: (0, node_crypto_1.randomUUID)(),
155
+ timestamp: new Date(),
156
+ source: { type: 'core', id: 'agent-registry' },
157
+ category: types_1.EventCategory.WorkerOnline,
158
+ data: { workerId: id },
159
+ });
160
+ }
161
+ }
162
+ }
163
+ catch {
164
+ // Registry shape varies across Moleculer versions — never fatal.
165
+ }
166
+ }
167
+ // ---- Placement reconciliation ----
168
+ /**
169
+ * Boot-time reconciliation pass. Agents already connected when the hub
170
+ * starts never fire a fresh `$node.connected` event, so the per-connect
171
+ * trigger would miss them — this method walks the current node list once
172
+ * and reconciles every connected agent. Must be invoked AFTER
173
+ * `AddonRegistryService.onModuleInit()` so the hub's installed-addon set
174
+ * is populated. Per-agent failures are isolated — one unreachable agent
175
+ * must not abort the pass or hub boot.
176
+ */
177
+ async reconcileConnectedAgents() {
178
+ const registry = this.moleculer.broker.registry;
179
+ const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
180
+ const agentIds = nodes.map((n) => n.id).filter((id) => id !== 'hub' && !id.includes('/'));
181
+ if (agentIds.length === 0)
182
+ return;
183
+ console.log(`[agent-registry] Boot reconcile: ${agentIds.length} connected agent(s)`);
184
+ for (const agentId of agentIds) {
185
+ await this.reconcileAgentAddons(agentId);
186
+ }
187
+ }
188
+ /**
189
+ * Reconcile a single agent's deployed addons against the hub's installed
190
+ * set + placements. An addon running on the agent is STALE — and must be
191
+ * undeployed — when it is either:
192
+ * - not installed on the hub at all, or
193
+ * - installed but declared `execution.placement: 'hub-only'`.
194
+ *
195
+ * `agent-only` / `any-node` addons that ARE installed on the hub are
196
+ * legitimate agent residents and left untouched.
197
+ *
198
+ * Matching is by addon DECLARATION id: `$agent.status` reports
199
+ * `addons[].id` (the decl id), and the hub's `listAddons()` rows expose
200
+ * the same decl id at `manifest.id`. Package names are NOT used for the
201
+ * match — a single package can ship multiple addons with distinct ids
202
+ * and placements.
203
+ *
204
+ * All errors are caught and logged so a single bad agent never breaks
205
+ * the caller (connect handler or boot pass).
206
+ */
207
+ async reconcileAgentAddons(agentId) {
208
+ if (!this.addonRegistry) {
209
+ console.warn(`[agent-registry] Reconcile skipped for ${agentId}: addon registry not wired`);
210
+ return;
211
+ }
212
+ try {
213
+ const broker = this.broker;
214
+ const statusRaw = await broker.call('$agent.status', {}, {
215
+ nodeID: agentId,
216
+ timeout: AGENT_RECONCILE_RPC_TIMEOUT_MS,
217
+ });
218
+ const agentAddons = this.extractAgentAddons(statusRaw);
219
+ if (agentAddons.length === 0)
220
+ return;
221
+ // Build the hub's placement map: decl id → placement. Absence from
222
+ // this map means "not installed on the hub".
223
+ const hubPlacements = new Map();
224
+ for (const row of this.addonRegistry.listAddons()) {
225
+ const declId = row.manifest.id;
226
+ if (typeof declId !== 'string')
227
+ continue;
228
+ const decl = row.declaration ?? row.manifest;
229
+ hubPlacements.set(declId, (0, types_1.resolveAddonPlacement)(decl));
230
+ }
231
+ const stale = agentAddons.filter((addon) => {
232
+ const placement = hubPlacements.get(addon.id);
233
+ // Not installed on the hub → stale.
234
+ if (placement === undefined)
235
+ return true;
236
+ // Installed but pinned to the hub → must not run on an agent.
237
+ return placement === 'hub-only';
238
+ });
239
+ if (stale.length === 0) {
240
+ console.log(`[agent-registry] Reconcile ${agentId}: no stale addons (${agentAddons.length} checked)`);
241
+ return;
242
+ }
243
+ for (const addon of stale) {
244
+ const reason = hubPlacements.has(addon.id)
245
+ ? 'placement is hub-only'
246
+ : 'not installed on hub';
247
+ try {
248
+ await broker.call('$agent.undeploy', { addonId: addon.id }, {
249
+ nodeID: agentId,
250
+ timeout: AGENT_RECONCILE_RPC_TIMEOUT_MS,
251
+ });
252
+ console.log(`[agent-registry] Reconcile ${agentId}: undeployed stale addon "${addon.id}" (${reason})`);
253
+ this.eventBus.emit({
254
+ id: (0, node_crypto_1.randomUUID)(),
255
+ timestamp: new Date(),
256
+ source: { type: 'core', id: 'agent-registry' },
257
+ category: types_1.EventCategory.AddonUninstalled,
258
+ data: { addonId: addon.id, agentId, reason },
259
+ });
260
+ }
261
+ catch (err) {
262
+ console.error(`[agent-registry] Reconcile ${agentId}: failed to undeploy "${addon.id}":`, err instanceof Error ? err.message : String(err));
263
+ }
264
+ }
265
+ }
266
+ catch (err) {
267
+ console.error(`[agent-registry] Reconcile failed for agent ${agentId}:`, err instanceof Error ? err.message : String(err));
268
+ }
269
+ }
270
+ /** Narrow the `$agent.status` response down to its addon list. */
271
+ extractAgentAddons(statusRaw) {
272
+ if (statusRaw === null || typeof statusRaw !== 'object')
273
+ return [];
274
+ const addons = statusRaw.addons;
275
+ if (!Array.isArray(addons))
276
+ return [];
277
+ const result = [];
278
+ for (const entry of addons) {
279
+ if (entry === null || typeof entry !== 'object')
280
+ continue;
281
+ const id = entry.id;
282
+ if (typeof id !== 'string' || id.length === 0)
283
+ continue;
284
+ result.push({ id });
285
+ }
286
+ return result;
287
+ }
288
+ /** Log an agent rename (name changes are persisted via $agent.rename RPC). */
289
+ updateAgentName(nodeId, name) {
290
+ console.log(`[agent-registry] Agent renamed: "${nodeId}" → "${name}"`);
291
+ }
292
+ async listNodes() {
293
+ // Get child processes for hub via $process.list
294
+ let hubProcesses = [];
295
+ try {
296
+ const processes = (await this.broker.call('$process.list'));
297
+ hubProcesses = processes.map((p) => ({
298
+ pid: p.pid ?? 0,
299
+ name: p.name ?? '',
300
+ command: 'moleculer-service',
301
+ state: p.state ?? 'running',
302
+ cpuPercent: p.cpuPercent ?? 0,
303
+ memoryRss: p.memoryRss ?? 0,
304
+ uptimeSeconds: p.uptimeSeconds ?? 0,
305
+ addonIds: p.addonIds ?? [],
306
+ groupId: p.groupId ?? null,
307
+ }));
308
+ }
309
+ catch {
310
+ // $process service may not be ready yet
311
+ }
312
+ const hubEntry = await this.buildHubEntry(hubProcesses);
313
+ const remoteEntries = [];
314
+ const registry = this.moleculer.broker.registry;
315
+ const nodes = registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
316
+ for (const node of nodes) {
317
+ const nodeId = node.id;
318
+ // Skip hub (already included) and child processes (contain '/')
319
+ if (nodeId === 'hub' || nodeId.includes('/'))
320
+ continue;
321
+ try {
322
+ const status = (await this.broker.call('$agent.status', {}, {
323
+ nodeID: nodeId,
324
+ timeout: 5000,
325
+ }));
326
+ // Get real sub-process stats from the agent's $process.list
327
+ let subProcesses = [];
328
+ try {
329
+ const processes = (await this.broker.call('$process.list', {}, {
330
+ nodeID: nodeId,
331
+ timeout: 5000,
332
+ }));
333
+ subProcesses = processes.map((p) => ({
334
+ pid: p.pid ?? 0,
335
+ name: p.name ?? '',
336
+ command: 'moleculer-service',
337
+ state: p.state ?? 'running',
338
+ cpuPercent: p.cpuPercent ?? 0,
339
+ memoryRss: p.memoryRss ?? 0,
340
+ uptimeSeconds: p.uptimeSeconds ?? 0,
341
+ addonIds: p.addonIds ?? [],
342
+ groupId: p.groupId ?? null,
343
+ }));
344
+ }
345
+ catch {
346
+ // Fall back to addon list from $agent.status (no stats)
347
+ subProcesses =
348
+ status.addons?.map((a) => ({
349
+ pid: 0,
350
+ name: a.id ?? '',
351
+ command: 'moleculer-service',
352
+ state: (a.status ?? 'running'),
353
+ cpuPercent: 0,
354
+ memoryRss: 0,
355
+ uptimeSeconds: 0,
356
+ })) ?? [];
357
+ }
358
+ // Extract addon IDs from $agent.status
359
+ const agentAddons = status.addons?.map((a) => a.id) ?? [];
360
+ const hostname = typeof status.hostname === 'string' ? status.hostname : null;
361
+ const agentName = typeof status.name === 'string' ? status.name : nodeId;
362
+ remoteEntries.push({
363
+ info: {
364
+ id: nodeId,
365
+ name: agentName,
366
+ hostname: hostname ?? nodeId,
367
+ capabilities: [],
368
+ platform: status.platform ?? 'unknown',
369
+ arch: status.arch ?? 'unknown',
370
+ cpuCores: status.cpuCores ?? 0,
371
+ memoryMB: status.totalMemoryMB ?? 0,
372
+ cpuModel: status.cpuModel,
373
+ },
374
+ localIps: Array.isArray(status.localIps) ? status.localIps : [],
375
+ status: {
376
+ activeCameras: 0,
377
+ cpuPercent: status.cpuPercent ?? 0,
378
+ memoryPercent: status.memoryPercent ?? 0,
379
+ fps: {},
380
+ errors: [],
381
+ },
382
+ connectedSince: typeof status.uptime === 'number'
383
+ ? Date.now() - status.uptime * 1000
384
+ : Date.now(),
385
+ isHub: false,
386
+ subProcesses,
387
+ agentAddons,
388
+ });
389
+ }
390
+ catch {
391
+ // Skip nodes without $agent service
392
+ }
393
+ }
394
+ // TODO(D3 follow-up): offline-agent history dropped with knownAgents.
395
+ // Previously, agents that disconnected were kept in a shadow map and
396
+ // surfaced here as offline rows. listNodes now reflects only live
397
+ // broker.registry nodes.
398
+ return [hubEntry, ...remoteEntries];
399
+ }
400
+ async buildHubEntry(subProcesses = []) {
401
+ const cpus = os.cpus();
402
+ // Get live metrics from the metrics-provider capability (NativeMetricsProvider).
403
+ // The cap contract only exposes async snapshots; the cached read is cheap
404
+ // because the addon's background sampler keeps the snapshot warm.
405
+ let cpuPercent = 0;
406
+ let memoryPercent = 0;
407
+ const registry = this.capabilityService.getRegistry();
408
+ if (registry) {
409
+ const metricsProvider = registry.getSingleton('metrics-provider');
410
+ if (metricsProvider) {
411
+ const snapshot = await metricsProvider.getCached();
412
+ if (snapshot) {
413
+ cpuPercent = snapshot.cpu.total;
414
+ memoryPercent = snapshot.memory.percent;
415
+ }
416
+ }
417
+ }
418
+ return {
419
+ info: {
420
+ id: 'hub',
421
+ name: 'Hub',
422
+ hostname: os.hostname(),
423
+ capabilities: [],
424
+ platform: os.platform(),
425
+ arch: os.arch(),
426
+ cpuCores: cpus.length,
427
+ memoryMB: Math.round(os.totalmem() / 1024 / 1024),
428
+ cpuModel: cpus[0]?.model,
429
+ },
430
+ status: {
431
+ activeCameras: 0,
432
+ cpuPercent,
433
+ memoryPercent,
434
+ fps: {},
435
+ errors: [],
436
+ },
437
+ connectedSince: this.bootTimestamp,
438
+ isHub: true,
439
+ subProcesses,
440
+ };
441
+ }
442
+ // ---- Role assignments ----
443
+ getAssignments(cameraId) {
444
+ const all = [...this.assignments.values()];
445
+ if (cameraId !== undefined)
446
+ return all.filter((a) => a.cameraId === cameraId);
447
+ return all;
448
+ }
449
+ setAssignment(assignment) {
450
+ const key = `${assignment.cameraId}:${assignment.role}`;
451
+ this.assignments.set(key, { ...assignment });
452
+ }
453
+ removeAssignment(cameraId, role) {
454
+ const key = `${cameraId}:${role}`;
455
+ this.assignments.delete(key);
456
+ }
457
+ activateBackup(cameraId, role) {
458
+ const primaryKey = `${cameraId}:${role}`;
459
+ const primary = this.assignments.get(primaryKey);
460
+ const backup = [...this.assignments.values()].find((a) => a.cameraId === cameraId && a.role === role && a.priority === 'backup');
461
+ if (!backup)
462
+ return;
463
+ if (primary) {
464
+ this.assignments.delete(primaryKey);
465
+ }
466
+ const promoted = { ...backup, priority: 'primary' };
467
+ this.assignments.set(primaryKey, promoted);
468
+ this.eventBus.emit({
469
+ id: (0, node_crypto_1.randomUUID)(),
470
+ timestamp: new Date(),
471
+ source: { type: 'core', id: 'agent-registry' },
472
+ category: types_1.EventCategory.AgentBackupActivated,
473
+ data: { cameraId, role, agentId: promoted.agentId },
474
+ });
475
+ }
476
+ }
477
+ exports.AgentRegistryService = AgentRegistryService;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthService = void 0;
4
+ const core_1 = require("@camstack/core");
5
+ class AuthService extends core_1.AuthManager {
6
+ constructor(config) {
7
+ super(config);
8
+ }
9
+ }
10
+ exports.AuthService = AuthService;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * CapabilityService — NestJS-injectable wrapper around the CapabilityRegistry.
4
+ *
5
+ * Server services inject this instead of accessing the registry directly.
6
+ * The registry reference is set once during boot by AddonRegistryService.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.CapabilityService = void 0;
10
+ class CapabilityService {
11
+ registry = null;
12
+ /** Called once during boot by AddonRegistryService to wire the registry */
13
+ setRegistry(registry) {
14
+ this.registry = registry;
15
+ }
16
+ /** Get the underlying registry (may be null before boot completes) */
17
+ getRegistry() {
18
+ return this.registry;
19
+ }
20
+ /** Get the active singleton provider for a capability */
21
+ getSingleton(capability) {
22
+ return this.registry?.getSingleton(capability) ?? null;
23
+ }
24
+ /**
25
+ * Resolve the hub-local provider honoring the 'hub' per-node singleton override.
26
+ * Delegates to `CapabilityRegistry.getSingletonForNode` so the in-process lookup
27
+ * respects per-node overrides set by the operator.
28
+ */
29
+ getSingletonForNode(capability, nodeId) {
30
+ return this.registry?.getSingletonForNode(capability, nodeId) ?? null;
31
+ }
32
+ /** Get the addon ID of the active singleton provider for a capability */
33
+ getSingletonAddonId(capability) {
34
+ return this.registry?.getSingletonAddonId(capability) ?? null;
35
+ }
36
+ /** Get all active collection providers for a capability */
37
+ getCollection(capability) {
38
+ return this.registry?.getCollection(capability) ?? [];
39
+ }
40
+ /**
41
+ * Like {@link getCollection} but returns `[addonId, provider]` tuples
42
+ * so callers can attribute work back to the contributing addon —
43
+ * required by the addon-widgets static file route to validate that
44
+ * the requested `addonId` is a registered widget provider.
45
+ */
46
+ getCollectionEntries(capability) {
47
+ return this.registry?.getCollectionEntries(capability) ?? [];
48
+ }
49
+ /** Resolve a singleton provider for a specific device (with per-device override support) */
50
+ resolveForDevice(capability, deviceId) {
51
+ return this.registry?.resolveForDevice(capability, deviceId) ?? null;
52
+ }
53
+ /** Resolve collection providers for a specific device (with per-device filter support) */
54
+ resolveCollectionForDevice(capability, deviceId) {
55
+ return this.registry?.resolveCollectionForDevice(capability, deviceId) ?? [];
56
+ }
57
+ }
58
+ exports.CapabilityService = CapabilityService;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RUNTIME_DEFAULTS = exports.bootstrapSchema = void 0;
4
+ // Re-export from @camstack/kernel
5
+ var kernel_1 = require("@camstack/kernel");
6
+ Object.defineProperty(exports, "bootstrapSchema", { enumerable: true, get: function () { return kernel_1.bootstrapSchema; } });
7
+ Object.defineProperty(exports, "RUNTIME_DEFAULTS", { enumerable: true, get: function () { return kernel_1.RUNTIME_DEFAULTS; } });
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConfigService = void 0;
4
+ const kernel_1 = require("@camstack/kernel");
5
+ class ConfigService extends kernel_1.ConfigManager {
6
+ constructor(configPath) {
7
+ super(configPath);
8
+ }
9
+ }
10
+ exports.ConfigService = ConfigService;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventBusService = void 0;
4
+ const kernel_1 = require("@camstack/kernel");
5
+ /**
6
+ * Hub-side event bus. Pre-broker boot the service buffers nothing —
7
+ * subscribers / emits are no-ops until `attachBroker` lands the
8
+ * underlying per-broker shared bus. Once attached every operation
9
+ * delegates to the unified `getBrokerEventBus(broker)` instance,
10
+ * which is the SAME bus that group-runner / agent subprocess addons
11
+ * see when they call `ctx.eventBus.emit`. Single in-process delivery
12
+ * map per broker, single `broker.broadcast` for cross-broker
13
+ * delivery — no more dual hub-vs-subprocess implementations.
14
+ *
15
+ * **Persistence scope**: the in-memory ring buffer (10 000 events,
16
+ * owned by `getBrokerEventBus`) survives admin-ui browser refreshes
17
+ * but NOT server restarts — that's intentional. UI panels read
18
+ * `getRecent` on mount and live-subscribe via `live.onEvent` /
19
+ * `systemEvents.subscribe`, which is enough for the operator to see
20
+ * continuity across page reloads.
21
+ */
22
+ class EventBusService {
23
+ broker = null;
24
+ inner = null;
25
+ // Used only as a loose ring buffer for the very first events emitted
26
+ // before `attachBroker` runs (NestJS-era boot legacy). Drained into
27
+ // the real bus on attach.
28
+ pending = [];
29
+ deferredSubs = [];
30
+ constructor(_configService) {
31
+ // The shared bus owns the ring-buffer size — hub-side config is
32
+ // accepted for API compatibility but no longer drives the bus.
33
+ void _configService;
34
+ }
35
+ attachBroker(broker) {
36
+ if (this.broker === broker)
37
+ return;
38
+ this.broker = broker;
39
+ const inner = (0, kernel_1.getBrokerEventBus)(broker);
40
+ this.inner = inner;
41
+ // Replay deferred subscriptions onto the real bus.
42
+ for (const sub of this.deferredSubs) {
43
+ sub.unsub = inner.subscribe(sub.filter, sub.handler);
44
+ }
45
+ // Flush events emitted before the broker was ready.
46
+ for (const evt of this.pending) {
47
+ inner.emit(evt);
48
+ }
49
+ this.pending = [];
50
+ }
51
+ detachBroker() {
52
+ for (const sub of this.deferredSubs) {
53
+ sub.unsub?.();
54
+ sub.unsub = undefined;
55
+ }
56
+ this.broker = null;
57
+ this.inner = null;
58
+ }
59
+ emit(event) {
60
+ if (!this.inner) {
61
+ this.pending.push(event);
62
+ return;
63
+ }
64
+ this.inner.emit(event);
65
+ }
66
+ subscribe(filter, handler) {
67
+ if (this.inner)
68
+ return this.inner.subscribe(filter, handler);
69
+ const sub = { filter, handler, unsub: undefined };
70
+ this.deferredSubs.push(sub);
71
+ return () => {
72
+ if (sub.unsub)
73
+ sub.unsub();
74
+ const idx = this.deferredSubs.indexOf(sub);
75
+ if (idx >= 0)
76
+ this.deferredSubs.splice(idx, 1);
77
+ };
78
+ }
79
+ getRecent(filter, limit) {
80
+ return this.inner?.getRecent(filter, limit) ?? [];
81
+ }
82
+ }
83
+ exports.EventBusService = EventBusService;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeatureService = void 0;
4
+ const core_1 = require("@camstack/core");
5
+ class FeatureService extends core_1.FeatureManager {
6
+ constructor(configService) {
7
+ super(configService);
8
+ }
9
+ }
10
+ exports.FeatureService = FeatureService;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LifecycleStateMachine = void 0;
4
+ // Re-export from @camstack/core — LifecycleStateMachine is already a plain class
5
+ var core_1 = require("@camstack/core");
6
+ Object.defineProperty(exports, "LifecycleStateMachine", { enumerable: true, get: function () { return core_1.LifecycleStateMachine; } });
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LogRingBuffer = void 0;
4
+ // Re-export from @camstack/core
5
+ var core_1 = require("@camstack/core");
6
+ Object.defineProperty(exports, "LogRingBuffer", { enumerable: true, get: function () { return core_1.LogRingBuffer; } });