@camstack/server 1.0.0 → 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
@@ -1,143 +0,0 @@
1
- import { LogManager } from '@camstack/core'
2
- import type { IScopedLogger, LogTags } from '@camstack/types'
3
- import { EventCategory } from '@camstack/types'
4
- import { ConfigService } from '../config/config.service'
5
- import type { EventBusService } from '../events/event-bus.service'
6
-
7
- /** Baseline tags every hub-local logger gets, so console output never
8
- * falls back to `?` for the agent slot. Hub-local addons already
9
- * re-tag with their addonId; this ensures non-addon scopes (core
10
- * services like `AddonRegistry`, `StreamProbeService`, `moleculer`)
11
- * render as `hub` in the agent column.
12
- *
13
- * `pid` is the hub renderer's own OS pid. Entries forwarded from
14
- * forked workers via `$hub.log` carry their own `tags.pid` which
15
- * wins over this baseline (right-biased merge in `writeFromWorker`). */
16
- const HUB_BASELINE_TAGS: LogTags = { agentId: 'hub', nodeId: 'hub', pid: process.pid }
17
-
18
- export class LoggingService extends LogManager {
19
- /**
20
- * Device-name cache consulted by the formatter when a log line
21
- * carries `tags.deviceId` but no explicit `deviceName`. Populated
22
- * by `setDeviceNames` whenever device-manager emits registered /
23
- * updated / removed events. Missing entries fall back to `#<id>`.
24
- */
25
- private readonly deviceNames = new Map<number, string>()
26
-
27
- constructor(configService: ConfigService) {
28
- // The log buffer is now partitioned per addonId (see PartitionedLogBuffer):
29
- // this value caps EACH addon's bucket, not the total. A chatty addon evicts
30
- // only its own lines, so quiet addons (e.g. a HomeAssistant `image` entity)
31
- // keep their sparse history. `eventBus.ringBufferSize` still sizes the
32
- // separate system-event ring; logs get their own per-addon cap.
33
- const perAddonCapacity = configService.get<number>('eventBus.perAddonLogBufferSize') ?? 5000
34
- super(perAddonCapacity)
35
- // Enriches every emitted LogEntry with `tags.deviceName` before
36
- // destinations / subscribers see it — works across bundled copies
37
- // of `@camstack/core` (addon packages) because the mutation
38
- // happens upstream of their formatters.
39
- this.setDeviceNameLookup((id) => this.deviceNames.get(id) ?? null)
40
- }
41
-
42
- /** Bulk-refresh from device-manager snapshot. Replaces every entry. */
43
- setDeviceNames(entries: ReadonlyArray<{ id: number; name: string }>): void {
44
- this.deviceNames.clear()
45
- for (const { id, name } of entries) {
46
- if (
47
- typeof id === 'number' &&
48
- Number.isFinite(id) &&
49
- typeof name === 'string' &&
50
- name.length > 0
51
- ) {
52
- this.deviceNames.set(id, name)
53
- }
54
- }
55
- }
56
-
57
- /** Incremental update — called from DeviceRegistered / DeviceUpdated. */
58
- upsertDeviceName(id: number, name: string | undefined): void {
59
- if (!Number.isFinite(id)) return
60
- if (typeof name === 'string' && name.length > 0) this.deviceNames.set(id, name)
61
- else this.deviceNames.delete(id)
62
- }
63
-
64
- /** Drop on DeviceRemoved. */
65
- removeDeviceName(id: number): void {
66
- this.deviceNames.delete(id)
67
- }
68
-
69
- /**
70
- * Subscribe to `device.*` events so the cache stays live: every
71
- * DeviceRegistered / updated emit carries `{deviceId, name}` — we
72
- * upsert, and DeviceUnregistered clears. Call once at boot.
73
- */
74
- attachDeviceNameStream(eventBus: EventBusService): void {
75
- const selfLogger = this.createLogger('logging').withTags({ addonId: 'logging' })
76
- eventBus.subscribe({ category: EventCategory.DeviceRegistered }, (event) => {
77
- const data = event.data as { deviceId?: number; name?: string } | undefined
78
- if (data && typeof data.deviceId === 'number') {
79
- this.upsertDeviceName(data.deviceId, data.name)
80
- selfLogger.info('device-name cache upserted', {
81
- meta: {
82
- deviceId: data.deviceId,
83
- name: data.name ?? null,
84
- cacheSize: this.deviceNames.size,
85
- },
86
- })
87
- }
88
- })
89
- eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (event) => {
90
- const data = event.data as { deviceId?: number } | undefined
91
- if (data && typeof data.deviceId === 'number') this.removeDeviceName(data.deviceId)
92
- })
93
- }
94
-
95
- /**
96
- * Override — every logger created hub-side comes pre-tagged with
97
- * `agentId: 'hub'`. Callers that need per-addon identity chain
98
- * `.withTags({ addonId })` on top (the merge is right-biased, so
99
- * the explicit tag wins).
100
- */
101
- override createLogger(scope?: string): IScopedLogger {
102
- return super.createLogger(scope).withTags(HUB_BASELINE_TAGS)
103
- }
104
-
105
- /**
106
- * Write a log entry received from a forked worker / remote agent.
107
- * Called by the `$hub.log` action and `log-receiver.ingest` action.
108
- *
109
- * Tags propagated by the worker (including `deviceId`, `deviceName`,
110
- * `integrationId`, `streamId`, etc.) are preserved via `withTags`;
111
- * baseline tags (`addonId`, `nodeId`, `agentId`) are always ensured
112
- * even if the worker didn't set them explicitly.
113
- */
114
- writeFromWorker(entry: {
115
- addonId: string
116
- nodeId?: string
117
- level: string
118
- message: string
119
- scope?: string
120
- tags?: LogTags
121
- meta?: Record<string, unknown>
122
- }): void {
123
- // Scope is a single optional sub-component label; addon/node identity
124
- // lives in tags, not in scope. Pass through whatever the worker set.
125
- let logger = this.createLogger(entry.scope)
126
- const nodeId = entry.nodeId
127
- const agentId = nodeId?.includes('/') ? nodeId.split('/')[0]! : nodeId
128
- const mergedTags: LogTags = {
129
- ...entry.tags,
130
- addonId: entry.addonId,
131
- ...(nodeId !== undefined ? { nodeId } : {}),
132
- ...(agentId !== undefined ? { agentId } : {}),
133
- }
134
- logger = logger.withTags(mergedTags)
135
- const level = entry.level as 'info' | 'warn' | 'error' | 'debug'
136
- const extras = entry.meta !== undefined ? { meta: entry.meta } : undefined
137
- if (typeof logger[level] === 'function') {
138
- logger[level](entry.message, extras)
139
- } else {
140
- logger.info(entry.message, extras)
141
- }
142
- }
143
- }
@@ -1,3 +0,0 @@
1
- // Re-export from @camstack/core
2
- export { ScopedLogger } from '@camstack/core'
3
- export type { IScopedLogger } from '@camstack/core'
@@ -1,173 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import type { CapRoute, CapCallInput } from '@camstack/kernel'
3
- import { buildCapCallFn, type CapCallFnLocalChild, type CapCallFnResolver } from './cap-call-fn.js'
4
-
5
- /**
6
- * buildCapCallFn — the per-(cap,node) dispatcher behind every CapabilityRegistry
7
- * provider proxy. These specs lock every routing branch so the UDS-migration
8
- * gap (an agent-hosted provider's cap call falling onto a `broker.call` to a
9
- * Moleculer node that does not exist → 30s `waitForServices` timeout) cannot
10
- * regress:
11
- * - hub-local child that provides → per-child UDS (collection-safe)
12
- * - hub-local child that does NOT provide → fail fast (NEVER Moleculer)
13
- * - agent-hosted / remote → delegate to the unified CapRouteResolver
14
- * - resolver not built yet → legacy broker fallback
15
- */
16
-
17
- const REMOTE_ROUTE: CapRoute = { kind: 'remote-moleculer', capName: 'cap-x', nodeId: 'dev-agent-0' }
18
-
19
- function recordingLocalChild(provides: boolean): {
20
- fake: CapCallFnLocalChild
21
- childProvidesCalls: Array<{ childId: string; capName: string; deviceId?: number }>
22
- callCapOnChildCalls: Array<{ childId: string; input: CapCallInput }>
23
- } {
24
- const childProvidesCalls: Array<{ childId: string; capName: string; deviceId?: number }> = []
25
- const callCapOnChildCalls: Array<{ childId: string; input: CapCallInput }> = []
26
- return {
27
- childProvidesCalls,
28
- callCapOnChildCalls,
29
- fake: {
30
- childProvides: (childId, capName, deviceId) => {
31
- childProvidesCalls.push({ childId, capName, deviceId })
32
- return provides
33
- },
34
- callCapOnChild: async (childId, input) => {
35
- callCapOnChildCalls.push({ childId, input })
36
- return { from: 'uds' }
37
- },
38
- },
39
- }
40
- }
41
-
42
- function recordingResolver(): {
43
- fake: CapCallFnResolver
44
- resolveCalls: Array<{ capName: string; nodeId?: string; deviceId?: number }>
45
- dispatchCalls: Array<{ route: CapRoute; method: string; args: unknown }>
46
- } {
47
- const resolveCalls: Array<{ capName: string; nodeId?: string; deviceId?: number }> = []
48
- const dispatchCalls: Array<{ route: CapRoute; method: string; args: unknown }> = []
49
- return {
50
- resolveCalls,
51
- dispatchCalls,
52
- fake: {
53
- resolveCapRoute: (capName, opts) => {
54
- resolveCalls.push({ capName, nodeId: opts.nodeId, deviceId: opts.deviceId })
55
- return REMOTE_ROUTE
56
- },
57
- dispatch: async (route, method, args) => {
58
- dispatchCalls.push({ route, method, args })
59
- return { from: 'resolver' }
60
- },
61
- },
62
- }
63
- }
64
-
65
- describe('buildCapCallFn', () => {
66
- it('hub-local child that provides the cap → routes per-child over UDS', async () => {
67
- const child = recordingLocalChild(true)
68
- const resolver = recordingResolver()
69
- const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>()
70
- const fn = buildCapCallFn({
71
- capName: 'cap-x',
72
- nodeId: 'hub/benchmark',
73
- udsChildId: 'benchmark',
74
- getLocalChildRegistry: () => child.fake,
75
- getResolver: () => resolver.fake,
76
- legacyBrokerCall: legacy,
77
- })
78
-
79
- const result = await fn('listPages', { deviceId: 7 })
80
-
81
- expect(result).toEqual({ from: 'uds' })
82
- expect(child.callCapOnChildCalls).toEqual([
83
- {
84
- childId: 'benchmark',
85
- input: { capName: 'cap-x', method: 'listPages', args: { deviceId: 7 }, deviceId: 7 },
86
- },
87
- ])
88
- expect(resolver.resolveCalls).toEqual([]) // resolver never consulted
89
- expect(legacy).not.toHaveBeenCalled()
90
- })
91
-
92
- it('hub-local child that does NOT provide → fails fast, never touches Moleculer', async () => {
93
- const child = recordingLocalChild(false)
94
- const resolver = recordingResolver()
95
- const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>()
96
- const fn = buildCapCallFn({
97
- capName: 'cap-x',
98
- nodeId: 'hub/benchmark',
99
- udsChildId: 'benchmark',
100
- getLocalChildRegistry: () => child.fake,
101
- getResolver: () => resolver.fake,
102
- legacyBrokerCall: legacy,
103
- })
104
-
105
- await expect(fn('listPages', undefined)).rejects.toThrow(/does not currently provide/)
106
- expect(child.callCapOnChildCalls).toEqual([])
107
- expect(resolver.resolveCalls).toEqual([])
108
- expect(resolver.dispatchCalls).toEqual([])
109
- expect(legacy).not.toHaveBeenCalled()
110
- })
111
-
112
- it('agent-hosted provider → delegates to the resolver (agent-child-forward)', async () => {
113
- const resolver = recordingResolver()
114
- const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>()
115
- const fn = buildCapCallFn({
116
- capName: 'cap-x',
117
- nodeId: 'dev-agent-0',
118
- udsChildId: null, // not hub-local
119
- getLocalChildRegistry: () => null,
120
- getResolver: () => resolver.fake,
121
- legacyBrokerCall: legacy,
122
- })
123
-
124
- const result = await fn('listPages', { deviceId: 3 })
125
-
126
- expect(result).toEqual({ from: 'resolver' })
127
- expect(resolver.resolveCalls).toEqual([
128
- { capName: 'cap-x', nodeId: 'dev-agent-0', deviceId: 3 },
129
- ])
130
- expect(resolver.dispatchCalls).toEqual([
131
- { route: REMOTE_ROUTE, method: 'listPages', args: { deviceId: 3 } },
132
- ])
133
- expect(legacy).not.toHaveBeenCalled()
134
- })
135
-
136
- it('explicit targetNodeId overrides the registered nodeId in resolution', async () => {
137
- const resolver = recordingResolver()
138
- const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>()
139
- const fn = buildCapCallFn({
140
- capName: 'cap-x',
141
- nodeId: 'dev-agent-0',
142
- udsChildId: null,
143
- getLocalChildRegistry: () => null,
144
- getResolver: () => resolver.fake,
145
- legacyBrokerCall: legacy,
146
- })
147
-
148
- await fn('listPages', undefined, 'dev-agent-1')
149
-
150
- expect(resolver.resolveCalls).toEqual([
151
- { capName: 'cap-x', nodeId: 'dev-agent-1', deviceId: undefined },
152
- ])
153
- })
154
-
155
- it('resolver not yet built → falls back to the legacy broker call', async () => {
156
- const legacy = vi.fn<(m: string, p: unknown, n: string) => Promise<unknown>>(async () => ({
157
- from: 'legacy',
158
- }))
159
- const fn = buildCapCallFn({
160
- capName: 'cap-x',
161
- nodeId: 'dev-agent-0',
162
- udsChildId: null,
163
- getLocalChildRegistry: () => null,
164
- getResolver: () => null, // pre-init window
165
- legacyBrokerCall: legacy,
166
- })
167
-
168
- const result = await fn('listPages', { deviceId: 1 })
169
-
170
- expect(result).toEqual({ from: 'legacy' })
171
- expect(legacy).toHaveBeenCalledWith('listPages', { deviceId: 1 }, 'dev-agent-0')
172
- })
173
- })
@@ -1,107 +0,0 @@
1
- /**
2
- * cap-call-fn — the per-(cap, node) dispatcher behind every CapabilityRegistry
3
- * provider proxy the hub builds in `applyNodeManifest`.
4
- *
5
- * Extracted from `MoleculerService` so its routing branches are unit-testable
6
- * in isolation (the closure that lived inline could only be exercised by
7
- * standing up a full broker). It closes a UDS-migration gap: the inline
8
- * version hand-rolled a `broker.call` for any non-hub-local provider, so an
9
- * AGENT-hosted addon cap (a UDS child of the agent, NOT a Moleculer service)
10
- * resolved to a Moleculer node that does not exist → `waitForServices` waited
11
- * its full 30s discovery timeout → "Services waiting is timed out". The fix
12
- * routes everything that isn't a hub-local child through the unified
13
- * `CapRouteResolver`, which classifies an agent node as `agent-child-forward`
14
- * (hub → agent over Moleculer → agent's UDS child) and a direct remote as
15
- * `remote-moleculer`.
16
- */
17
- import type { CallFn, CapRoute, CapRouteOpts, CapCallInput } from '@camstack/kernel'
18
-
19
- /** Minimal LocalChildRegistry surface this dispatcher needs (per-child UDS). */
20
- export interface CapCallFnLocalChild {
21
- childProvides(childId: string, capName: string, deviceId?: number): boolean
22
- callCapOnChild(childId: string, input: CapCallInput): Promise<unknown>
23
- }
24
-
25
- /** Minimal CapRouteResolver surface this dispatcher needs. */
26
- export interface CapCallFnResolver {
27
- resolveCapRoute(capName: string, opts: CapRouteOpts): CapRoute
28
- dispatch(route: CapRoute, method: string, args: unknown): Promise<unknown>
29
- }
30
-
31
- export interface CapCallFnDeps {
32
- /** The capability this dispatcher routes. */
33
- readonly capName: string
34
- /** The registering node's id (the agent node for agent-hosted providers). */
35
- readonly nodeId: string
36
- /**
37
- * Runner id of the hub-local UDS child that owns this provider, or `null`
38
- * for agent-hosted / remote providers. Only hub-local children are reachable
39
- * over UDS.
40
- */
41
- readonly udsChildId: string | null
42
- /** Live getter for the UDS child registry (`null` if the UDS server is down). */
43
- readonly getLocalChildRegistry: () => CapCallFnLocalChild | null
44
- /** Live getter for the resolver (`null` before `onModuleInit` builds it). */
45
- readonly getResolver: () => CapCallFnResolver | null
46
- /** Legacy Moleculer call — used ONLY in the pre-init window (no resolver yet). */
47
- readonly legacyBrokerCall: (
48
- method: string,
49
- params: unknown,
50
- targetNodeId: string,
51
- ) => Promise<unknown>
52
- /** Optional diagnostic hook fired the first time this cap routes over UDS. */
53
- readonly onUdsRoute?: (capName: string) => void
54
- }
55
-
56
- /** Extract a numeric `deviceId` routing hint out of arbitrary method args. */
57
- function extractDeviceId(params: unknown): number | undefined {
58
- if (params === null || typeof params !== 'object') return undefined
59
- const raw: unknown = Reflect.get(params, 'deviceId')
60
- return typeof raw === 'number' ? raw : undefined
61
- }
62
-
63
- /**
64
- * Build the `CallFn` (`(method, params, targetNodeId?) => Promise`) for one
65
- * registered provider. See module docs for the routing rationale.
66
- */
67
- export function buildCapCallFn(deps: CapCallFnDeps): CallFn {
68
- return async (method: string, params: unknown, targetNodeId?: string): Promise<unknown> => {
69
- const deviceId = extractDeviceId(params)
70
-
71
- // ── Hub-local child: route per-child over UDS (collection-safe — keyed by
72
- // the specific child, not by capName which would collapse a collection
73
- // cap onto the first child). NEVER fall back to Moleculer: a hub-local
74
- // child is not a Moleculer service, so a broker call would wait the full
75
- // discovery timeout for a node that never appears. If the child isn't
76
- // currently providing the cap, fail fast.
77
- if (deps.udsChildId !== null && targetNodeId === undefined) {
78
- const registry = deps.getLocalChildRegistry()
79
- if (registry !== null && registry.childProvides(deps.udsChildId, deps.capName, deviceId)) {
80
- deps.onUdsRoute?.(deps.capName)
81
- const input: CapCallInput = {
82
- capName: deps.capName,
83
- method,
84
- args: params,
85
- ...(deviceId !== undefined ? { deviceId } : {}),
86
- }
87
- return registry.callCapOnChild(deps.udsChildId, input)
88
- }
89
- throw new Error(
90
- `hub-local child "${deps.udsChildId}" does not currently provide cap "${deps.capName}"`,
91
- )
92
- }
93
-
94
- // ── Agent-hosted or explicit remote: delegate to the unified resolver.
95
- const resolver = deps.getResolver()
96
- if (resolver !== null) {
97
- const route = resolver.resolveCapRoute(deps.capName, {
98
- nodeId: targetNodeId ?? deps.nodeId,
99
- ...(deviceId !== undefined ? { deviceId } : {}),
100
- })
101
- return resolver.dispatch(route, method, params)
102
- }
103
-
104
- // ── Pre-init window (resolver not yet constructed): legacy Moleculer call.
105
- return deps.legacyBrokerCall(method, params, targetNodeId ?? deps.nodeId)
106
- }
107
- }
@@ -1,194 +0,0 @@
1
- /**
2
- * Adapter factories that bridge the server-side registry/service state into the
3
- * narrow interfaces that CapRouteResolver (in @camstack/kernel) requires.
4
- *
5
- * Layer note: this file is in server/backend and may import server-side types.
6
- * The resolver itself (in @camstack/kernel) must NOT import from here —
7
- * it depends only on the narrow interfaces (NodeCapAuthority, InProcessProviderLookup)
8
- * defined in the kernel. These factories are the wiring adapters.
9
- *
10
- * nodeIsAgent format rule (from agent-registry.service.ts:74-77):
11
- * hub → 'hub' → not an agent
12
- * hub child → 'hub/<runnerId>' → not an agent
13
- * agent → bare id with no '/' → agent
14
- */
15
-
16
- import type { NodeCapAuthority, InProcessProviderLookup } from '@camstack/kernel'
17
- import type { InProcessProviderRef } from '@camstack/kernel'
18
-
19
- // ---------------------------------------------------------------------------
20
- // Minimal interface for the HubNodeRegistry dependency
21
- // ---------------------------------------------------------------------------
22
-
23
- /**
24
- * Minimal view of HubNodeRegistry that this adapter needs.
25
- * Keeps the adapter decoupled from the concrete class — only structural match
26
- * is required. HubNodeRegistry satisfies this interface structurally.
27
- */
28
- export interface NodeRegistryLike {
29
- getNodeManifest(
30
- nodeId: string,
31
- ): readonly { readonly addonId: string; readonly capabilities: readonly string[] }[] | undefined
32
- listNodeIds(): readonly string[]
33
- /**
34
- * Optional: returns flat (nodeId, addonId, capName, deviceId) native-cap tuples.
35
- * When provided, `nodeKnowsCap` and `isNativeCap` also consult native caps so
36
- * device-scoped native caps (ptz, motion-zones, …) are visible to the resolver.
37
- */
38
- listNativeCapEntries?(): readonly {
39
- readonly nodeId: string
40
- readonly addonId: string
41
- readonly capName: string
42
- readonly deviceId: number
43
- }[]
44
- }
45
-
46
- // ---------------------------------------------------------------------------
47
- // Minimal interface for the CapabilityService dependency
48
- // ---------------------------------------------------------------------------
49
-
50
- /**
51
- * Minimal view of CapabilityService that the InProcessProviderLookup adapter needs.
52
- * The real CapabilityService satisfies this interface structurally.
53
- */
54
- export interface CapabilityServiceLike {
55
- getSingleton(capability: string): Record<string, unknown> | null
56
- /** Resolve the hub-local provider honoring the 'hub' per-node override. */
57
- getSingletonForNode?(capability: string, nodeId: string): Record<string, unknown> | null
58
- }
59
-
60
- // ---------------------------------------------------------------------------
61
- // createNodeCapAuthority
62
- // ---------------------------------------------------------------------------
63
-
64
- /**
65
- * Optional resolver injected into createNodeCapAuthority so that getAddonId
66
- * can honor per-node singleton overrides and the cluster-global default,
67
- * constrained to addons the node actually hosts in its manifest.
68
- */
69
- export interface SingletonNodeResolver {
70
- /** Bare addonId a node should use for a singleton cap (override→default→first), or null. */
71
- resolveSingleton(capName: string, nodeId: string): string | null
72
- }
73
-
74
- /**
75
- * Build a NodeCapAuthority backed by a HubNodeRegistry.
76
- *
77
- * nodeIsAgent rule: a node is an agent when its id is not 'hub' and does not
78
- * contain '/' (hub children are `hub/<runnerId>`; agents are bare ids).
79
- *
80
- * nodeOnline: uses registry membership — per CLAUDE.md "the registry is the
81
- * union of registerNode manifests minus disconnected nodes" because
82
- * `removeNode` is called on `$node.disconnected`. Registry membership is the
83
- * cleanest, cast-free liveness check.
84
- *
85
- * getAgentChildId: always returns null — the hub cannot resolve which forked
86
- * child under an agent provides a cap (the agent flattens its subtree into
87
- * one merged manifest). The agent resolves its own child locally (Task 6).
88
- */
89
- export function createNodeCapAuthority(
90
- nodeRegistry: NodeRegistryLike,
91
- resolver?: SingletonNodeResolver,
92
- ): NodeCapAuthority {
93
- return {
94
- nodeKnowsCap(nodeId: string, capName: string): boolean {
95
- // Check system (manifest) caps first
96
- const manifest = nodeRegistry.getNodeManifest(nodeId)
97
- if (
98
- manifest !== undefined &&
99
- manifest.some((addon) => addon.capabilities.includes(capName))
100
- ) {
101
- return true
102
- }
103
- // Also check device-scoped native caps — these are NOT in the addon manifest
104
- const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
105
- return nativeEntries.some((n) => n.nodeId === nodeId && n.capName === capName)
106
- },
107
-
108
- getAddonId(nodeId: string, capName: string): string | null {
109
- // Check system (manifest) caps first
110
- const manifest = nodeRegistry.getNodeManifest(nodeId)
111
- if (manifest !== undefined) {
112
- const manifestAddons = manifest
113
- .filter((addon) => addon.capabilities.includes(capName))
114
- .map((addon) => addon.addonId)
115
- if (manifestAddons.length > 0) {
116
- // Per-node override / global-default resolution, constrained to what
117
- // the node actually hosts. Falls back to the first manifest match.
118
- const resolved = resolver?.resolveSingleton(capName, nodeId) ?? null
119
- if (resolved !== null && manifestAddons.includes(resolved)) return resolved
120
- return manifestAddons[0] ?? null
121
- }
122
- }
123
- // Check device-scoped native caps (unchanged path)
124
- const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
125
- const nat = nativeEntries.find((n) => n.nodeId === nodeId && n.capName === capName)
126
- return nat?.addonId ?? null
127
- },
128
-
129
- nodeIsAgent(nodeId: string): boolean {
130
- return nodeId !== 'hub' && !nodeId.includes('/')
131
- },
132
-
133
- nodeOnline(nodeId: string): boolean {
134
- // O(1) Map lookup — registry membership is the authoritative liveness
135
- // check: HubNodeRegistry.removeNode is called on $node.disconnected,
136
- // so a defined manifest means the node is connected.
137
- return nodeRegistry.getNodeManifest(nodeId) !== undefined
138
- },
139
-
140
- listNodeIds(): readonly string[] {
141
- return nodeRegistry.listNodeIds()
142
- },
143
-
144
- getAgentChildId(_agentNodeId: string, _capName: string): string | null {
145
- // The hub cannot resolve the agent's child — the agent resolves locally (Task 6).
146
- return null
147
- },
148
-
149
- isNativeCap(nodeId: string, capName: string, deviceId?: number): boolean {
150
- const nativeEntries = nodeRegistry.listNativeCapEntries?.() ?? []
151
- if (deviceId !== undefined) {
152
- return nativeEntries.some(
153
- (n) => n.nodeId === nodeId && n.capName === capName && n.deviceId === deviceId,
154
- )
155
- }
156
- return nativeEntries.some((n) => n.nodeId === nodeId && n.capName === capName)
157
- },
158
- }
159
- }
160
-
161
- // ---------------------------------------------------------------------------
162
- // createInProcessProviderLookup
163
- // ---------------------------------------------------------------------------
164
-
165
- /**
166
- * Build an InProcessProviderLookup backed by a CapabilityService.
167
- *
168
- * Cast-free design: `getSingleton<Record<string, unknown>>` makes the provider
169
- * indexable without any `as` casts. `typeof fn === 'function'` narrows to
170
- * Function (implicit `any` return); calling via `fn.call(provider, args)` is
171
- * then safe and the result is captured as `unknown`. No `as` casts anywhere.
172
- */
173
- export function createInProcessProviderLookup(
174
- capabilityService: CapabilityServiceLike,
175
- ): InProcessProviderLookup {
176
- return (capName: string): InProcessProviderRef | null => {
177
- const provider =
178
- capabilityService.getSingletonForNode?.(capName, 'hub') ??
179
- capabilityService.getSingleton(capName)
180
- if (provider === null || provider === undefined) return null
181
-
182
- const ref: InProcessProviderRef = {
183
- invoke: (method: string, args: unknown): Promise<unknown> => {
184
- const fn = provider[method]
185
- if (typeof fn !== 'function') {
186
- return Promise.reject(new Error(`method "${method}" not found on cap "${capName}"`))
187
- }
188
- const result: unknown = fn.call(provider, args)
189
- return Promise.resolve(result)
190
- },
191
- }
192
- return ref
193
- }
194
- }