@camstack/server 0.1.7 → 0.2.0

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 (135) hide show
  1. package/package.json +11 -9
  2. package/src/__tests__/addon-install-e2e.test.ts +0 -1
  3. package/src/__tests__/addon-pages-e2e.test.ts +40 -18
  4. package/src/__tests__/addon-settings-router.spec.ts +6 -1
  5. package/src/__tests__/addon-upload.spec.ts +91 -29
  6. package/src/__tests__/agent-registry.spec.ts +26 -9
  7. package/src/__tests__/agent-status-page.spec.ts +1 -3
  8. package/src/__tests__/auth-session-cookie.test.ts +28 -1
  9. package/src/__tests__/bulk-update-coordinator.spec.ts +48 -31
  10. package/src/__tests__/cap-ownership-authority.spec.ts +39 -8
  11. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +206 -0
  12. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +17 -3
  13. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +57 -11
  14. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +292 -0
  15. package/src/__tests__/cap-providers-bulk-update.spec.ts +27 -7
  16. package/src/__tests__/cap-route-adapter.spec.ts +28 -15
  17. package/src/__tests__/cap-routers/_meta.spec.ts +6 -7
  18. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +19 -10
  19. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +177 -0
  20. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +3 -1
  21. package/src/__tests__/cap-routers/capabilities-node.spec.ts +18 -5
  22. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +137 -0
  23. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +72 -20
  24. package/src/__tests__/cap-routers/harness.ts +11 -7
  25. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +17 -3
  26. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +5 -7
  27. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +35 -11
  28. package/src/__tests__/cap-routers/settings-store.router.spec.ts +59 -15
  29. package/src/__tests__/capability-e2e.test.ts +9 -11
  30. package/src/__tests__/cli-e2e.test.ts +80 -59
  31. package/src/__tests__/core-cap-bridge.spec.ts +3 -1
  32. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +12 -2
  33. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +61 -30
  34. package/src/__tests__/embedded-deps-e2e.test.ts +35 -19
  35. package/src/__tests__/event-bus-proxy-router.spec.ts +3 -0
  36. package/src/__tests__/framework-allowlist.spec.ts +5 -4
  37. package/src/__tests__/https-e2e.test.ts +12 -6
  38. package/src/__tests__/lifecycle-e2e.test.ts +60 -11
  39. package/src/__tests__/live-events-subscription.spec.ts +17 -18
  40. package/src/__tests__/moleculer/uds-readiness.spec.ts +11 -4
  41. package/src/__tests__/moleculer/uds-topology.spec.ts +39 -11
  42. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +265 -5
  43. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +16 -7
  44. package/src/__tests__/native-cap-route.spec.ts +42 -19
  45. package/src/__tests__/oauth2-account-linking.spec.ts +63 -17
  46. package/src/__tests__/singleton-contention.test.ts +23 -11
  47. package/src/__tests__/streaming-diagnostic.test.ts +156 -53
  48. package/src/__tests__/streaming-scale.test.ts +69 -35
  49. package/src/__tests__/uds-addon-call-wiring.spec.ts +6 -1
  50. package/src/agent-status-page.ts +4 -3
  51. package/src/api/__tests__/addons-custom.spec.ts +22 -8
  52. package/src/api/__tests__/capabilities.router.test.ts +18 -9
  53. package/src/api/addon-upload.ts +46 -15
  54. package/src/api/addons-custom.router.ts +7 -6
  55. package/src/api/auth-whoami.ts +3 -1
  56. package/src/api/bridge-addons.router.ts +3 -1
  57. package/src/api/capabilities.router.ts +117 -78
  58. package/src/api/core/__tests__/auth-router-totp.spec.ts +57 -16
  59. package/src/api/core/__tests__/integration-markers.spec.ts +10 -0
  60. package/src/api/core/addon-settings.router.ts +4 -1
  61. package/src/api/core/agents.router.ts +52 -53
  62. package/src/api/core/auth.router.ts +55 -36
  63. package/src/api/core/bulk-update-coordinator.ts +25 -22
  64. package/src/api/core/cap-providers.ts +459 -166
  65. package/src/api/core/capabilities.router.ts +30 -23
  66. package/src/api/core/hwaccel.router.ts +37 -10
  67. package/src/api/core/live-events.router.ts +16 -9
  68. package/src/api/core/logs.router.ts +58 -25
  69. package/src/api/core/notifications.router.ts +2 -1
  70. package/src/api/core/repl.router.ts +1 -3
  71. package/src/api/core/settings-backend.router.ts +68 -70
  72. package/src/api/core/system-events.router.ts +41 -32
  73. package/src/api/health/health.routes.ts +7 -13
  74. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +12 -2
  75. package/src/api/oauth2/consent-page.ts +4 -3
  76. package/src/api/oauth2/oauth2-routes.ts +41 -12
  77. package/src/api/trpc/__tests__/client-ip.spec.ts +27 -1
  78. package/src/api/trpc/__tests__/scope-access-device.spec.ts +68 -23
  79. package/src/api/trpc/__tests__/scope-access.spec.ts +8 -13
  80. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +136 -0
  81. package/src/api/trpc/cap-mount-helpers.ts +64 -44
  82. package/src/api/trpc/cap-route-error-formatter.ts +17 -9
  83. package/src/api/trpc/client-ip.ts +17 -0
  84. package/src/api/trpc/core-cap-bridge.ts +3 -1
  85. package/src/api/trpc/generated-cap-mounts.ts +801 -286
  86. package/src/api/trpc/generated-cap-routers.ts +5723 -719
  87. package/src/api/trpc/scope-access.ts +7 -7
  88. package/src/api/trpc/trpc.context.ts +7 -4
  89. package/src/api/trpc/trpc.middleware.ts +4 -2
  90. package/src/api/trpc/trpc.router.ts +117 -48
  91. package/src/auth/session-cookie.ts +10 -0
  92. package/src/boot/__tests__/integration-id-backfill.spec.ts +131 -0
  93. package/src/boot/boot-config.ts +103 -122
  94. package/src/boot/integration-id-backfill.ts +109 -0
  95. package/src/boot/post-boot.service.ts +5 -3
  96. package/src/core/addon/__tests__/addon-registry-capability.test.ts +12 -3
  97. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +62 -0
  98. package/src/core/addon/addon-call-gateway.ts +20 -6
  99. package/src/core/addon/addon-package.service.ts +183 -89
  100. package/src/core/addon/addon-registry.service.ts +1212 -1267
  101. package/src/core/addon/addon-row-manifest.ts +29 -0
  102. package/src/core/addon/addon-search.service.ts +2 -1
  103. package/src/core/addon/addon-settings-provider.ts +27 -7
  104. package/src/core/addon-bridge/addon-bridge.service.ts +11 -6
  105. package/src/core/addon-pages/addon-pages.service.ts +3 -1
  106. package/src/core/addon-widgets/addon-widgets.service.ts +5 -2
  107. package/src/core/agent/agent-registry.service.ts +60 -38
  108. package/src/core/auth/auth.service.spec.ts +6 -8
  109. package/src/core/config/config.service.spec.ts +1 -1
  110. package/src/core/events/event-bus.service.spec.ts +44 -21
  111. package/src/core/events/event-bus.service.ts +5 -1
  112. package/src/core/feature/feature.service.spec.ts +4 -1
  113. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +8 -10
  114. package/src/core/logging/logging.service.spec.ts +61 -21
  115. package/src/core/logging/logging.service.ts +19 -5
  116. package/src/core/moleculer/cap-call-fn.spec.ts +17 -10
  117. package/src/core/moleculer/cap-call-fn.ts +5 -1
  118. package/src/core/moleculer/cap-route-authority.ts +18 -6
  119. package/src/core/moleculer/moleculer.service.ts +145 -29
  120. package/src/core/network/network-quality.service.spec.ts +7 -1
  121. package/src/core/notification/notification-wrapper.service.ts +1 -3
  122. package/src/core/notification/toast-wrapper.service.ts +1 -5
  123. package/src/core/repl/repl-engine.service.spec.ts +66 -39
  124. package/src/core/repl/repl-engine.service.ts +11 -12
  125. package/src/core/storage/storage-location-manager.spec.ts +12 -3
  126. package/src/core/streaming/stream-probe.service.ts +22 -13
  127. package/src/core/topology/topology-emitter.service.ts +5 -1
  128. package/src/launcher.ts +14 -9
  129. package/src/main.ts +658 -495
  130. package/src/manual-boot.ts +133 -154
  131. package/tsconfig.json +20 -8
  132. package/src/core/storage/settings-store.spec.ts +0 -213
  133. package/src/core/storage/settings-store.ts +0 -2
  134. package/src/core/storage/sql-schema.spec.ts +0 -140
  135. package/src/core/storage/sql-schema.ts +0 -3
@@ -12,12 +12,17 @@ import type { IScopedLogger } from '@camstack/types'
12
12
  const FRIGATE = process.env.FRIGATE_HOST ?? ''
13
13
 
14
14
  const mockLogger: IScopedLogger = {
15
- debug: () => {}, info: () => {}, warn: console.warn, error: console.error,
15
+ debug: () => {},
16
+ info: () => {},
17
+ warn: console.warn,
18
+ error: console.error,
16
19
  child: () => mockLogger,
17
20
  }
18
21
  const mockLoggingService = { createLogger: () => mockLogger }
19
22
 
20
- function wait(ms: number) { return new Promise(r => setTimeout(r, ms)) }
23
+ function wait(ms: number) {
24
+ return new Promise((r) => setTimeout(r, ms))
25
+ }
21
26
 
22
27
  /**
23
28
  * Poll-based replacement for the removed `IStreamBroker.onDecodedFrame`
@@ -49,27 +54,29 @@ async function discoverSubStreams(): Promise<Array<{ name: string; url: string }
49
54
  return new Promise((resolve, reject) => {
50
55
  const req = http.get(`http://${FRIGATE}:5000/api/config`, (res) => {
51
56
  let data = ''
52
- res.on('data', (chunk: Buffer) => { data += chunk })
57
+ res.on('data', (chunk: Buffer) => {
58
+ data += chunk
59
+ })
53
60
  res.on('end', () => {
54
61
  try {
55
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment --
62
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment --
56
63
  const config = JSON.parse(data)
57
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
64
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
58
65
  const cameras = config.cameras || {}
59
66
  const streams: Array<{ name: string; url: string }> = []
60
- // eslint-disable-next-line @typescript-eslint/no-explicit-any --
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any --
61
68
  for (const [name, cam] of Object.entries(cameras) as [string, any][]) {
62
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access --
69
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access --
63
70
  if (!cam.enabled && cam.enabled !== undefined) continue
64
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
71
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access --
65
72
  const inputs = cam?.ffmpeg?.inputs || []
66
73
  for (const inp of inputs) {
67
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
74
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
68
75
  if (inp.roles?.includes('detect') && inp.path) {
69
76
  let streamName: string
70
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
77
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
71
78
  if (inp.path.includes('8554/')) {
72
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
79
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
73
80
  streamName = inp.path.split('8554/').pop()!
74
81
  } else {
75
82
  streamName = `${name}_sub`
@@ -79,11 +86,16 @@ async function discoverSubStreams(): Promise<Array<{ name: string; url: string }
79
86
  }
80
87
  }
81
88
  resolve(streams)
82
- } catch (e) { reject(e) }
89
+ } catch (e) {
90
+ reject(e)
91
+ }
83
92
  })
84
93
  })
85
94
  req.on('error', reject)
86
- req.setTimeout(5000, () => { req.destroy(); reject(new Error('Timeout')) })
95
+ req.setTimeout(5000, () => {
96
+ req.destroy()
97
+ reject(new Error('Timeout'))
98
+ })
87
99
  })
88
100
  }
89
101
 
@@ -92,7 +104,7 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
92
104
  const manager = new StreamBrokerManager([new FfmpegDecoderProvider()], mockLogger)
93
105
 
94
106
  afterAll(async () => {
95
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
107
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
96
108
  await manager.destroyAll()
97
109
  // Wait for ffmpeg processes to exit
98
110
  await wait(2000)
@@ -106,11 +118,13 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
106
118
  const workingStreams: typeof realStreams = []
107
119
  for (const s of realStreams.slice(0, 5)) {
108
120
  try {
109
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
121
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
110
122
  const broker = await manager.createBroker(`probe-${s.name}`, {
111
- type: 'rtsp', url: s.url, videoCodec: 'h264',
123
+ type: 'rtsp',
124
+ url: s.url,
125
+ videoCodec: 'h264',
112
126
  })
113
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
114
128
  await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: 'h264' })
115
129
  const ok = await new Promise<boolean>((resolve) => {
116
130
  const timeout = setTimeout(() => resolve(false), 5000)
@@ -119,17 +133,25 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
119
133
  clearTimeout(timeout)
120
134
  void unsub?.()
121
135
  resolve(true)
122
- }).then((fn) => { unsub = fn }).catch(() => resolve(false))
136
+ })
137
+ .then((fn) => {
138
+ unsub = fn
139
+ })
140
+ .catch(() => resolve(false))
123
141
  })
124
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
142
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
125
143
  await broker.stop()
126
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
144
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
127
145
  await manager.destroyBroker(`probe-${s.name}`)
128
146
  if (ok) workingStreams.push(s)
129
- } catch { /* skip */ }
147
+ } catch {
148
+ /* skip */
149
+ }
130
150
  }
131
151
 
132
- console.log(`\nDiscovered ${realStreams.length} sub-streams, ${workingStreams.length} responding`)
152
+ console.log(
153
+ `\nDiscovered ${realStreams.length} sub-streams, ${workingStreams.length} responding`,
154
+ )
133
155
  if (workingStreams.length === 0) {
134
156
  console.log(' No working sub-streams, skipping')
135
157
  return
@@ -148,7 +170,6 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
148
170
  const scaleLevels = [5, 10, 20, 30]
149
171
 
150
172
  for (const targetCount of scaleLevels) {
151
-
152
173
  console.log(`\n=== Scale: ${targetCount} sub-streams @ 2fps ===`)
153
174
 
154
175
  const memBefore = process.memoryUsage()
@@ -169,11 +190,13 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
169
190
  perCamera[s.name] = { frames: 0, totalBytes: 0 }
170
191
 
171
192
  try {
172
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
193
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
173
194
  const broker = await manager.createBroker(brokerId, {
174
- type: 'rtsp', url: s.url, videoCodec: 'h264',
195
+ type: 'rtsp',
196
+ url: s.url,
197
+ videoCodec: 'h264',
175
198
  })
176
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
177
200
  await (broker as any).start({ type: 'rtsp', url: s.url, videoCodec: 'h264' })
178
201
 
179
202
  // Wait for first frame with tight timeout
@@ -187,7 +210,11 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
187
210
  clearTimeout(timeout)
188
211
  void unsub?.()
189
212
  resolve()
190
- }).then((fn) => { unsub = fn }).catch(() => resolve())
213
+ })
214
+ .then((fn) => {
215
+ unsub = fn
216
+ })
217
+ .catch(() => resolve())
191
218
  })
192
219
 
193
220
  // Subscribe for motion analysis
@@ -204,7 +231,9 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
204
231
  await Promise.all(connectPromises)
205
232
 
206
233
  const connectTime = Date.now() - startTime
207
- console.log(` Connected ${targetCount - connectFailures}/${targetCount} in ${(connectTime / 1000).toFixed(1)}s`)
234
+ console.log(
235
+ ` Connected ${targetCount - connectFailures}/${targetCount} in ${(connectTime / 1000).toFixed(1)}s`,
236
+ )
208
237
 
209
238
  // Run for 5 seconds of actual streaming
210
239
  const measureStart = Date.now()
@@ -232,7 +261,10 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
232
261
  if (data.frames < 3) starvingCameras++ // less than 3 frames in 5s = starving
233
262
  }
234
263
 
235
- const cpuPct = ((cpuMeasure.user + cpuMeasure.system) / (measureDuration * 1_000_000) * 100).toFixed(1)
264
+ const cpuPct = (
265
+ ((cpuMeasure.user + cpuMeasure.system) / (measureDuration * 1_000_000)) *
266
+ 100
267
+ ).toFixed(1)
236
268
  const rssMB = (memAfter.rss / 1024 / 1024).toFixed(0)
237
269
  const heapMB = (memAfter.heapUsed / 1024 / 1024).toFixed(0)
238
270
  const combinedFps = (totalFrames / measureDuration).toFixed(1)
@@ -249,26 +281,28 @@ describe.skipIf(!FRIGATE)('Streaming Scale Test (requires Frigate)', () => {
249
281
 
250
282
  // Per-camera breakdown
251
283
  console.log(` ---`)
252
- const sorted = Object.entries(perCamera).sort((a, b) => b[1].frames - a[1].frames)
284
+ const sorted = Object.entries(perCamera).toSorted((a, b) => b[1].frames - a[1].frames)
253
285
  for (const [name, data] of sorted) {
254
286
  const fps = (data.frames / measureDuration).toFixed(1)
255
287
  const avgKB = data.frames > 0 ? (data.totalBytes / data.frames / 1024).toFixed(0) : '-'
256
288
  const status = data.frames >= 3 ? '✓' : data.frames > 0 ? '⚠' : '✗'
257
- console.log(` ${status} ${name.padEnd(35)} ${fps.padStart(4)} fps ${avgKB.padStart(5)} KB/f (${data.frames} frames)`)
289
+ console.log(
290
+ ` ${status} ${name.padEnd(35)} ${fps.padStart(4)} fps ${avgKB.padStart(5)} KB/f (${data.frames} frames)`,
291
+ )
258
292
  }
259
293
 
260
294
  // Cleanup for next scale level
261
295
  for (const id of brokerIds) {
262
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
296
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
263
297
  const b = manager.getBroker(id)
264
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
298
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
265
299
  if (b) await b.stop()
266
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
300
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access --
267
301
  await manager.destroyBroker(id)
268
302
  }
269
303
  brokerIds.length = 0
270
304
  unsubs.length = 0
271
- Object.keys(perCamera).forEach(k => delete perCamera[k])
305
+ Object.keys(perCamera).forEach((k) => delete perCamera[k])
272
306
 
273
307
  // Expect at least 80% of cameras producing
274
308
  expect(activeCameras).toBeGreaterThanOrEqual(Math.floor(targetCount * 0.8))
@@ -227,7 +227,12 @@ describe('F3 — forked-addon routes + custom actions over UDS (backend wiring)'
227
227
  target: 'routes',
228
228
  })
229
229
  expect(rawRoutes).toEqual([
230
- { method: 'GET', path: '/:providerId/start', access: 'public', description: 'Begin OIDC redirect login flow' },
230
+ {
231
+ method: 'GET',
232
+ path: '/:providerId/start',
233
+ access: 'public',
234
+ description: 'Begin OIDC redirect login flow',
235
+ },
231
236
  { method: 'GET', path: '/:providerId/callback', access: 'public' },
232
237
  ])
233
238
  // Coarse reachability gate the route-mount fallback uses: the child is
@@ -43,9 +43,10 @@ export function renderAgentStatusPage(data: AgentStatusData): string {
43
43
  const memUsedMB = data.memoryTotalMB - data.memoryFreeMB
44
44
  const memPercent = Math.round((memUsedMB / data.memoryTotalMB) * 100)
45
45
 
46
- const taskTypesList = data.taskTypes.length > 0
47
- ? data.taskTypes.map((t) => `<li>${escapeHtml(t)}</li>`).join('')
48
- : '<li class="muted">No task handlers registered</li>'
46
+ const taskTypesList =
47
+ data.taskTypes.length > 0
48
+ ? data.taskTypes.map((t) => `<li>${escapeHtml(t)}</li>`).join('')
49
+ : '<li class="muted">No task handlers registered</li>'
49
50
 
50
51
  return `<!DOCTYPE html>
51
52
  <html lang="en">
@@ -42,12 +42,18 @@ describe('api.addons.custom', () => {
42
42
  registry = new CustomActionRegistry()
43
43
  registry.registerAddon('benchmark', catalog, async (action, input) => {
44
44
  switch (action) {
45
- case 'ping': return 'pong'
46
- case 'echo': return { msg: (input as { msg: string }).msg }
47
- case 'adminOnly': return 'ok'
48
- case 'open': return 'open'
49
- case 'badOutput': return 'WRONG' // will fail output validation
50
- default: throw new Error(`unknown action ${action}`)
45
+ case 'ping':
46
+ return 'pong'
47
+ case 'echo':
48
+ return { msg: (input as { msg: string }).msg }
49
+ case 'adminOnly':
50
+ return 'ok'
51
+ case 'open':
52
+ return 'open'
53
+ case 'badOutput':
54
+ return 'WRONG' // will fail output validation
55
+ default:
56
+ throw new Error(`unknown action ${action}`)
51
57
  }
52
58
  })
53
59
  router = buildRouter(registry)
@@ -61,7 +67,11 @@ describe('api.addons.custom', () => {
61
67
 
62
68
  it('round-trips structured input through the action schema', async () => {
63
69
  const caller = router.createCaller(makeCtx('admin'))
64
- const result = await caller.custom({ addonId: 'benchmark', action: 'echo', input: { msg: 'hi' } })
70
+ const result = await caller.custom({
71
+ addonId: 'benchmark',
72
+ action: 'echo',
73
+ input: { msg: 'hi' },
74
+ })
65
75
  expect(result).toEqual({ msg: 'hi' })
66
76
  })
67
77
 
@@ -92,7 +102,11 @@ describe('api.addons.custom', () => {
92
102
 
93
103
  it('enforces per-action auth: admin-only action allows admin', async () => {
94
104
  const caller = router.createCaller(makeCtx('admin'))
95
- const result = await caller.custom({ addonId: 'benchmark', action: 'adminOnly', input: undefined })
105
+ const result = await caller.custom({
106
+ addonId: 'benchmark',
107
+ action: 'adminOnly',
108
+ input: undefined,
109
+ })
96
110
  expect(result).toBe('ok')
97
111
  })
98
112
 
@@ -1,4 +1,3 @@
1
-
2
1
  // server/backend/src/api/__tests__/capabilities.router.test.ts
3
2
  import { describe, it, expect, vi, beforeEach } from 'vitest'
4
3
  import { CapabilityRegistry } from '@camstack/kernel'
@@ -24,7 +23,12 @@ describe('capabilities tRPC router logic', () => {
24
23
 
25
24
  it('listCapabilities returns all declared capabilities', () => {
26
25
  registry.declareCapability({ name: 'storage', scope: 'system', mode: 'singleton', methods: {} })
27
- registry.declareCapability({ name: 'log-destination', scope: 'system', mode: 'collection', methods: {} })
26
+ registry.declareCapability({
27
+ name: 'log-destination',
28
+ scope: 'system',
29
+ mode: 'collection',
30
+ methods: {},
31
+ })
28
32
 
29
33
  const list = registry.listCapabilities()
30
34
  expect(list).toHaveLength(2)
@@ -33,15 +37,20 @@ describe('capabilities tRPC router logic', () => {
33
37
  })
34
38
 
35
39
  it('setActiveSingleton throws for unknown capability', async () => {
36
- await expect(
37
- registry.setActiveSingleton('nonexistent', 'some-addon', true),
38
- ).rejects.toThrow(/[Uu]nknown/)
40
+ await expect(registry.setActiveSingleton('nonexistent', 'some-addon', true)).rejects.toThrow(
41
+ /[Uu]nknown/,
42
+ )
39
43
  })
40
44
 
41
45
  it('setActiveSingleton throws for collection capability', async () => {
42
- registry.declareCapability({ name: 'log-destination', scope: 'system', mode: 'collection', methods: {} })
43
- await expect(
44
- registry.setActiveSingleton('log-destination', 'winston', true),
45
- ).rejects.toThrow(/singleton/)
46
+ registry.declareCapability({
47
+ name: 'log-destination',
48
+ scope: 'system',
49
+ mode: 'collection',
50
+ methods: {},
51
+ })
52
+ await expect(registry.setActiveSingleton('log-destination', 'winston', true)).rejects.toThrow(
53
+ /singleton/,
54
+ )
46
55
  })
47
56
  })
@@ -72,7 +72,7 @@ interface AgentDeployResponse {
72
72
  }
73
73
 
74
74
  function isTarball(filename: string): boolean {
75
- return TARBALL_EXTENSIONS.some(ext => filename.endsWith(ext))
75
+ return TARBALL_EXTENSIONS.some((ext) => filename.endsWith(ext))
76
76
  }
77
77
 
78
78
  /**
@@ -162,7 +162,9 @@ export async function registerAddonUploadRoute(
162
162
  }
163
163
  }
164
164
  if (!authOk) {
165
- return reply.status(403).send({ error: `Forbidden: ${authReason ?? 'admin or upload-scoped token required'}` })
165
+ return reply
166
+ .status(403)
167
+ .send({ error: `Forbidden: ${authReason ?? 'admin or upload-scoped token required'}` })
166
168
  }
167
169
 
168
170
  const data = await request.file()
@@ -178,12 +180,20 @@ export async function registerAddonUploadRoute(
178
180
  // because the multipart plugin types it as `unknown`.
179
181
  const nodeIdField = data.fields['nodeId']
180
182
  const addonIdField = data.fields['addonId']
181
- const nodeId = typeof nodeIdField === 'object' && nodeIdField !== null && 'value' in nodeIdField && typeof nodeIdField.value === 'string'
182
- ? nodeIdField.value
183
- : null
184
- const addonIdHint = typeof addonIdField === 'object' && addonIdField !== null && 'value' in addonIdField && typeof addonIdField.value === 'string'
185
- ? addonIdField.value
186
- : null
183
+ const nodeId =
184
+ typeof nodeIdField === 'object' &&
185
+ nodeIdField !== null &&
186
+ 'value' in nodeIdField &&
187
+ typeof nodeIdField.value === 'string'
188
+ ? nodeIdField.value
189
+ : null
190
+ const addonIdHint =
191
+ typeof addonIdField === 'object' &&
192
+ addonIdField !== null &&
193
+ 'value' in addonIdField &&
194
+ typeof addonIdField.value === 'string'
195
+ ? addonIdField.value
196
+ : null
187
197
 
188
198
  const buffer = await data.toBuffer()
189
199
 
@@ -192,7 +202,9 @@ export async function registerAddonUploadRoute(
192
202
  // agent path would otherwise fail mid-extraction with no clean rollback.
193
203
  const manifest = validateTarball(buffer, data.filename)
194
204
  if (!manifest) {
195
- return reply.status(400).send({ error: 'Tarball missing or malformed package/package.json (name + version required)' })
205
+ return reply.status(400).send({
206
+ error: 'Tarball missing or malformed package/package.json (name + version required)',
207
+ })
196
208
  }
197
209
 
198
210
  // Branch by deployment target. `nodeId === 'hub'` (or absent) installs on
@@ -200,7 +212,16 @@ export async function registerAddonUploadRoute(
200
212
  // the package's addons (i.e. anything not marked hub-only). Any other
201
213
  // explicit `nodeId` value routes only to that agent via `$agent.deploy`.
202
214
  if (!nodeId || nodeId === 'hub') {
203
- return installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, data.filename, buffer)
215
+ return installToHub(
216
+ reply,
217
+ addonBridge,
218
+ addonRegistry,
219
+ addonPackageService,
220
+ moleculer,
221
+ logger,
222
+ data.filename,
223
+ buffer,
224
+ )
204
225
  }
205
226
  const agentAddonId = addonIdHint ?? manifest.name
206
227
  return deployToAgent(reply, moleculer, nodeId, agentAddonId, buffer)
@@ -231,7 +252,8 @@ function packageHasAgentDeployable(addonsDir: string, packageName: string): bool
231
252
  const pkgPath = path.join(addonsDir, packageName, 'package.json')
232
253
  const parsed: unknown = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
233
254
  if (parsed === null || typeof parsed !== 'object') return false
234
- const camstack = (parsed as { camstack?: { addons?: readonly CamstackAddonDeclLike[] } }).camstack
255
+ const camstack = (parsed as { camstack?: { addons?: readonly CamstackAddonDeclLike[] } })
256
+ .camstack
235
257
  const addons = camstack?.addons ?? []
236
258
  return addons.some((a) => {
237
259
  const placement = a.execution?.placement ?? 'hub-only'
@@ -250,7 +272,11 @@ interface AgentDeployResult {
250
272
  }
251
273
 
252
274
  interface MoleculerBrokerLike {
253
- call(action: string, params: Record<string, unknown>, options: { nodeID: string; timeout: number }): Promise<unknown>
275
+ call(
276
+ action: string,
277
+ params: Record<string, unknown>,
278
+ options: { nodeID: string; timeout: number },
279
+ ): Promise<unknown>
254
280
  registry?: {
255
281
  getNodeList?: (opts: { onlyAvailable: boolean }) => readonly { id: string }[]
256
282
  }
@@ -306,7 +332,7 @@ async function propagateToAgents(
306
332
  reloadRaw !== null &&
307
333
  typeof reloadRaw === 'object' &&
308
334
  'loaded' in (reloadRaw as Record<string, unknown>)
309
- ? ((reloadRaw as { loaded: readonly string[] }).loaded)
335
+ ? (reloadRaw as { loaded: readonly string[] }).loaded
310
336
  : []
311
337
  results.push({ nodeId, success: true, loaded: reloaded })
312
338
  } catch (err: unknown) {
@@ -418,7 +444,10 @@ async function installToHub(
418
444
  for (const id of preInstallAddonIds) {
419
445
  void addonRegistry.restartAddon(id).then((r) => {
420
446
  if (!r.success) {
421
- logger.warn('background restart failed', { tags: { addonId: id }, meta: { error: r.error ?? 'unknown' } })
447
+ logger.warn('background restart failed', {
448
+ tags: { addonId: id },
449
+ meta: { error: r.error ?? 'unknown' },
450
+ })
422
451
  } else {
423
452
  logger.info('background restart OK', { tags: { addonId: id } })
424
453
  }
@@ -426,7 +455,9 @@ async function installToHub(
426
455
  }
427
456
  if (propagatable) {
428
457
  void propagateToAgents(moleculer, logger, result.name, buffer).then((agentResults) => {
429
- logger.info('propagation done', { meta: { packageName: result.name, agents: agentResults } })
458
+ logger.info('propagation done', {
459
+ meta: { packageName: result.name, agents: agentResults },
460
+ })
430
461
  })
431
462
  }
432
463
 
@@ -42,11 +42,13 @@ export interface AddonsCustomDeps {
42
42
  export function createAddonsCustomProcedures(deps: AddonsCustomDeps) {
43
43
  return {
44
44
  custom: protectedProcedure
45
- .input(z.object({
46
- addonId: z.string().min(1),
47
- action: z.string().min(1),
48
- input: z.unknown(),
49
- }))
45
+ .input(
46
+ z.object({
47
+ addonId: z.string().min(1),
48
+ action: z.string().min(1),
49
+ input: z.unknown(),
50
+ }),
51
+ )
50
52
  .output(z.unknown())
51
53
  .mutation(async ({ input, ctx }) => {
52
54
  const registry = deps.getCustomActionRegistry()
@@ -66,7 +68,6 @@ export function createAddonsCustomProcedures(deps: AddonsCustomDeps) {
66
68
  // Validate input against the action's declared Zod schema.
67
69
  const parsedInput = entry.spec.input.parse(input.input)
68
70
 
69
-
70
71
  // Dispatch through the addon handler.
71
72
  const result = await entry.handler(parsedInput)
72
73
 
@@ -78,7 +78,9 @@ export async function registerAuthWhoamiRoute(
78
78
  }
79
79
  const record = await userMgmt.validateScopedToken({ token })
80
80
  if (!record) {
81
- return reply.status(401).send({ ok: false, error: 'Token not recognised (revoked, expired, or never issued)' })
81
+ return reply
82
+ .status(401)
83
+ .send({ ok: false, error: 'Token not recognised (revoked, expired, or never issued)' })
82
84
  }
83
85
  const ok: WhoamiOk = {
84
86
  ok: true,
@@ -45,7 +45,9 @@ export function createBridgeAddonsRouter(
45
45
  }
46
46
  await installer.install(input.packageName, input.version)
47
47
  await bridge.reloadPackages()
48
- const result = addonRegistry ? await addonRegistry.loadNewAddons() : { loaded: [], failed: [] }
48
+ const result = addonRegistry
49
+ ? await addonRegistry.loadNewAddons()
50
+ : { loaded: [], failed: [] }
49
51
  toastService?.broadcast({
50
52
  title: 'Addon Installed',
51
53
  message: `${input.packageName} installed successfully${result.loaded.length ? ` (${result.loaded.join(', ')})` : ''}`,