@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
@@ -1,155 +0,0 @@
1
- import { ReplEngine } from '@camstack/core'
2
- import type { IReplContextProvider } from '@camstack/core'
3
- import { SystemMirror, type SystemMirrorApi, type DeviceProxy } from '@camstack/types'
4
- import { AddonRegistryService } from '../addon/addon-registry.service'
5
- import { EventBusService } from '../events/event-bus.service'
6
- import { LoggingService } from '../logging/logging.service'
7
-
8
- export class ReplEngineService extends ReplEngine {
9
- /**
10
- * Lazily-instantiated `SystemMirror` shared across REPL sessions.
11
- * Holds a single warm-boot mirror that every `sm.getDeviceById(id)`
12
- * lookup serves from. Init runs at first access (Promise cached so
13
- * concurrent sessions don't double-fetch).
14
- */
15
- private static systemMirror: SystemMirror | null = null
16
- private static systemMirrorInit: Promise<SystemMirror> | null = null
17
-
18
- /**
19
- * Build a `SystemMirrorApi` for in-process server use. The cap-method
20
- * surface (deviceManager / deviceState queries) goes through the
21
- * standard broker tRPC client — those resolve locally via
22
- * `localProviderLink`. The `live.onEvent` channel is NOT a cap, so
23
- * the broker can't route it; we synthesize the same shape over the
24
- * local `EventBusService` so SystemMirror subscriptions work without
25
- * crossing the network boundary or polling the broker for a
26
- * non-existent `live` service.
27
- */
28
- private static buildInProcessApi(
29
- addonRegistry: AddonRegistryService,
30
- eventBus: EventBusService,
31
- ): SystemMirrorApi {
32
- const baseApi = addonRegistry.getBrokerApi() as unknown as SystemMirrorApi
33
- return {
34
- ...baseApi,
35
- live: {
36
- onEvent: {
37
- subscribe: (input, opts) => {
38
- const off = eventBus.subscribe({ category: input.category }, (evt) => {
39
- try {
40
- opts.onData({ data: evt.data })
41
- } catch (err) {
42
- opts.onError?.(err)
43
- }
44
- })
45
- return { unsubscribe: off }
46
- },
47
- },
48
- },
49
- }
50
- }
51
-
52
- private static getOrInitSystemMirror(
53
- addonRegistry: AddonRegistryService,
54
- eventBus: EventBusService,
55
- ): Promise<SystemMirror> {
56
- if (this.systemMirror) return Promise.resolve(this.systemMirror)
57
- if (!this.systemMirrorInit) {
58
- const api = this.buildInProcessApi(addonRegistry, eventBus)
59
- const sm = new SystemMirror(api)
60
- this.systemMirrorInit = sm.init().then(() => {
61
- this.systemMirror = sm
62
- return sm
63
- })
64
- }
65
- return this.systemMirrorInit
66
- }
67
-
68
- constructor(
69
- addonRegistry: AddonRegistryService,
70
- eventBus: EventBusService,
71
- _loggingService: LoggingService,
72
- ) {
73
- const contextProvider: IReplContextProvider = {
74
- async getSystemSandbox() {
75
- const integrationRegistry = addonRegistry.getIntegrationRegistry()
76
- const deviceRegistry = addonRegistry.getDeviceRegistry()
77
- // Warm-boot the SystemMirror so `sm.getDeviceById(id)` is sync
78
- // for the first user expression.
79
- const sm = await ReplEngineService.getOrInitSystemMirror(addonRegistry, eventBus)
80
- return {
81
- // ── New canonical API ───────────────────────────────────────
82
- /**
83
- * SystemMirror — the cap-driven, reactive view of every
84
- * device. Sync `getDeviceById(id)`, typed `state.<cap>.value`
85
- * reads, full method dispatch, query helpers.
86
- */
87
- sm,
88
- // ── Legacy (kept for backward-compat) ──────────────────────
89
- addonRegistry,
90
- eventBus,
91
- integrationRegistry,
92
- devices: () => deviceRegistry.getAll(),
93
- integrations: async () => (await integrationRegistry?.listIntegrations()) ?? [],
94
- addons: () => addonRegistry.listAddons(),
95
- getDevice: (id: number) => deviceRegistry.getById(id),
96
- getIntegration: async (id: string) =>
97
- (await integrationRegistry?.getIntegration(id)) ?? null,
98
- getSystemMirror: () => Promise.resolve(sm),
99
- }
100
- },
101
- async getDeviceSandbox(deviceId: number) {
102
- const deviceRegistry = addonRegistry.getDeviceRegistry()
103
- const rawDevice = deviceRegistry.getById(deviceId)
104
- const sm = await ReplEngineService.getOrInitSystemMirror(addonRegistry, eventBus)
105
- // `device` is the typed DeviceProxy backed by the SystemMirror
106
- // mirror — same shape as `sm.getDeviceById(deviceId)`. Sync
107
- // state reads + cap-method dispatch via the wrapper chain.
108
- const device: DeviceProxy | null = sm.getDeviceById(deviceId)
109
- return {
110
- /** SystemMirror — full cluster view. */
111
- sm,
112
- /**
113
- * The current device as a DeviceProxy. Sync state reads
114
- * (`device.state.battery.value`) + async cap methods
115
- * (`await device.snapshot.getSnapshot({})`).
116
- */
117
- device,
118
- /** Numeric device id (same as URL). */
119
- deviceId,
120
- /** Device metadata (name, addonId, type, online, …). */
121
- info: sm.getDeviceInfo(deviceId),
122
- /** Raw IDevice instance — escape hatch for legacy access.
123
- * Prefer `device` (DeviceProxy) for new code. */
124
- rawDevice,
125
- }
126
- },
127
- getProviderSandbox(addonId: string) {
128
- const integrationRegistry = addonRegistry.getIntegrationRegistry()
129
- const deviceRegistry = addonRegistry.getDeviceRegistry()
130
- const devices = deviceRegistry.getAllForAddon(addonId)
131
- return {
132
- getIntegration: () =>
133
- integrationRegistry?.getIntegration(addonId) ?? Promise.resolve(null),
134
- devices,
135
- }
136
- },
137
- getAddonSandbox(addonId: string) {
138
- // REPL exposes addon metadata only — never the live in-process
139
- // instance. Direct addon access only works for hub-local addons
140
- // and breaks for remote-agent addons. To invoke addon behaviour
141
- // from the REPL, use the cap router via tRPC instead.
142
- const entry = addonRegistry.listAddons().find((e) => e.manifest.id === addonId)
143
- return {
144
- manifest: entry?.manifest,
145
- declaration: entry?.declaration,
146
- source: entry?.source,
147
- installSource: entry?.installSource,
148
- process: entry?.process,
149
- }
150
- },
151
- }
152
-
153
- super(contextProvider)
154
- }
155
- }
@@ -1,70 +0,0 @@
1
- // server/backend/src/core/storage/fs-storage-backend.spec.ts
2
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
3
- import { mkdirSync, rmSync } from 'node:fs'
4
- import { join } from 'node:path'
5
- import { tmpdir } from 'node:os'
6
- import { FsStorageBackend } from './fs-storage-backend'
7
-
8
- describe('FsStorageBackend', () => {
9
- let tempDir: string
10
- let backend: FsStorageBackend
11
-
12
- beforeEach(() => {
13
- tempDir = join(tmpdir(), `fs-backend-test-${Date.now()}`)
14
- mkdirSync(tempDir, { recursive: true })
15
- backend = new FsStorageBackend(tempDir)
16
- })
17
-
18
- afterEach(() => {
19
- rmSync(tempDir, { recursive: true, force: true })
20
- })
21
-
22
- it('type is local', () => {
23
- expect(backend.type).toBe('local')
24
- })
25
-
26
- it('basePath is resolved to absolute', () => {
27
- const relative = new FsStorageBackend('./relative/path')
28
- expect(relative.basePath).toContain('/')
29
- expect(relative.basePath).not.toContain('./')
30
- })
31
-
32
- it('resolve joins subpath', () => {
33
- expect(backend.resolve('models/yolov8n.onnx')).toBe(join(tempDir, 'models/yolov8n.onnx'))
34
- })
35
-
36
- it('resolve returns absolute paths as-is', () => {
37
- expect(backend.resolve('/absolute/path')).toBe('/absolute/path')
38
- })
39
-
40
- it('isAvailable returns true for existing writable dir', () => {
41
- expect(backend.isAvailable()).toBe(true)
42
- })
43
-
44
- it('isAvailable returns false when no existing ancestor is writable', () => {
45
- // Nothing under /nonexistent exists, so no writable ancestor →
46
- // the backend cannot be created when needed.
47
- const missing = new FsStorageBackend('/nonexistent/path/xyz')
48
- expect(missing.isAvailable()).toBe(false)
49
- })
50
-
51
- it('isAvailable returns true for a not-yet-existing dir whose parent is writable', () => {
52
- // Lazy creation: the backend reports itself as available when the
53
- // nearest existing ancestor (tempDir) is writable, because we can
54
- // `mkdir -p` the missing segments on first write.
55
- const notYet = new FsStorageBackend(join(tempDir, 'sub', 'dir'))
56
- expect(notYet.isAvailable()).toBe(true)
57
- })
58
-
59
- it('initialize is a no-op — directory is created lazily on first write', async () => {
60
- const { existsSync } = await import('node:fs')
61
- const newPath = join(tempDir, 'sub', 'dir')
62
- const newBackend = new FsStorageBackend(newPath)
63
- // The base dir doesn't exist yet, but the ancestor chain is
64
- // writable — so the backend is available and initialize is a
65
- // no-op (doesn't create the dir eagerly).
66
- expect(newBackend.isAvailable()).toBe(true)
67
- await newBackend.initialize()
68
- expect(existsSync(newPath)).toBe(false)
69
- })
70
- })
@@ -1,3 +0,0 @@
1
- // Re-export from @camstack/core
2
- export { FsStorageBackend } from '@camstack/core'
3
- export type { IStorageBackend } from '@camstack/core'
@@ -1,130 +0,0 @@
1
- // server/backend/src/core/storage/storage-location-manager.spec.ts
2
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
3
- import { existsSync, mkdirSync, rmSync } from 'node:fs'
4
- import { join } from 'node:path'
5
- import { tmpdir } from 'node:os'
6
- import { StorageLocationManager, type StorageLocationName } from './storage-location-manager'
7
-
8
- describe('StorageLocationManager', () => {
9
- let tempDir: string
10
- let manager: StorageLocationManager
11
-
12
- beforeEach(() => {
13
- tempDir = join(tmpdir(), `slm-test-${Date.now()}`)
14
- mkdirSync(tempDir, { recursive: true })
15
- manager = new StorageLocationManager(tempDir)
16
- })
17
-
18
- afterEach(() => {
19
- rmSync(tempDir, { recursive: true, force: true })
20
- rmSync('/tmp/camstack-cache', { recursive: true, force: true })
21
- })
22
-
23
- it('initializeDefaults registers all 6 location backends', async () => {
24
- await manager.initializeDefaults()
25
- const names = manager.getLocationNames()
26
- expect(names).toHaveLength(6)
27
- expect(names).toContain('data')
28
- expect(names).toContain('media')
29
- expect(names).toContain('recordings')
30
- expect(names).toContain('models')
31
- expect(names).toContain('cache')
32
- expect(names).toContain('logs')
33
- })
34
-
35
- it('initializeDefaults does NOT create the location directories on disk (lazy)', async () => {
36
- await manager.initializeDefaults()
37
- // None of the 6 location paths should exist — directories are
38
- // created on first write by the filesystem storage provider, not
39
- // eagerly at boot. This is the anti-bloat invariant: a fresh
40
- // installation should not materialise empty `recordings/`,
41
- // `media/`, `logs/` folders just because StorageLocationManager
42
- // was initialized.
43
- expect(existsSync(join(tempDir, 'db'))).toBe(false)
44
- expect(existsSync(join(tempDir, 'media'))).toBe(false)
45
- expect(existsSync(join(tempDir, 'recordings'))).toBe(false)
46
- expect(existsSync(join(tempDir, 'models'))).toBe(false)
47
- expect(existsSync(join(tempDir, 'logs'))).toBe(false)
48
- })
49
-
50
- it('initializeDefaults reports all locations as available (parent dir writable)', async () => {
51
- await manager.initializeDefaults()
52
- const status = manager.getStatus()
53
- // A backend is "available" as long as it can be created when
54
- // needed (nearest existing ancestor is writable). Since `tempDir`
55
- // exists and is writable, every location underneath it is
56
- // available even though the location dir itself does not yet
57
- // exist.
58
- for (const entry of status) {
59
- expect(entry.available).toBe(true)
60
- }
61
- })
62
-
63
- it('getLocationNames returns all 6 location names', async () => {
64
- await manager.initializeDefaults()
65
- const names = manager.getLocationNames()
66
- const expected: StorageLocationName[] = [
67
- 'data',
68
- 'media',
69
- 'recordings',
70
- 'models',
71
- 'cache',
72
- 'logs',
73
- ]
74
- expect(names.toSorted()).toEqual(expected.toSorted())
75
- })
76
-
77
- it('resolve joins subpath correctly within a location', async () => {
78
- await manager.initializeDefaults()
79
- const resolved = manager.resolve('data', 'camstack.db')
80
- expect(resolved).toBe(join(tempDir, 'db', 'camstack.db'))
81
- })
82
-
83
- it('resolve for cache uses /tmp/camstack-cache base', async () => {
84
- await manager.initializeDefaults()
85
- const resolved = manager.resolve('cache', 'thumbnails')
86
- expect(resolved).toBe('/tmp/camstack-cache/thumbnails')
87
- })
88
-
89
- it('setLocationPath overrides a location with absolute path', async () => {
90
- await manager.initializeDefaults()
91
- const newPath = join(tempDir, 'custom-media')
92
- await manager.setLocationPath('media', newPath)
93
- const backend = manager.getBackend('media')
94
- expect(backend.basePath).toBe(newPath)
95
- // Still lazy — overriding the path doesn't eagerly create it.
96
- expect(backend.isAvailable()).toBe(true)
97
- expect(existsSync(newPath)).toBe(false)
98
- })
99
-
100
- it('setLocationPath overrides a location with relative path (resolved against dataPath)', async () => {
101
- await manager.initializeDefaults()
102
- await manager.setLocationPath('logs', 'custom-logs')
103
- const backend = manager.getBackend('logs')
104
- expect(backend.basePath).toBe(join(tempDir, 'custom-logs'))
105
- expect(backend.isAvailable()).toBe(true)
106
- })
107
-
108
- it('getStatus reports all locations with name, available, path', async () => {
109
- await manager.initializeDefaults()
110
- const status = manager.getStatus()
111
- expect(status).toHaveLength(6)
112
- for (const entry of status) {
113
- expect(entry).toHaveProperty('name')
114
- expect(entry).toHaveProperty('available')
115
- expect(entry).toHaveProperty('path')
116
- expect(typeof entry.available).toBe('boolean')
117
- }
118
- })
119
-
120
- it('missing location throws error', () => {
121
- // not initialized
122
- expect(() => manager.getBackend('data')).toThrow('Storage location "data" not initialized')
123
- })
124
-
125
- it('resolve on uninitialized location throws error', () => {
126
- expect(() => manager.resolve('media', 'file.mp4')).toThrow(
127
- 'Storage location "media" not initialized',
128
- )
129
- })
130
- })
@@ -1,3 +0,0 @@
1
- // Re-export from @camstack/core
2
- export { StorageLocationManager } from '@camstack/core'
3
- export type { StorageLocationName } from '@camstack/core'
@@ -1,73 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import { StorageService } from './storage.service'
3
- import type { ICoreStorageProvider as IStorageProvider, IStorageLocation } from '@camstack/core'
4
-
5
- const createMockProvider = (): IStorageProvider => {
6
- const mockLocation: IStorageLocation = { structured: undefined, files: undefined }
7
- return {
8
- initialize: vi.fn(),
9
- shutdown: vi.fn(),
10
- getLocation: vi.fn().mockReturnValue(mockLocation),
11
- export: vi.fn(),
12
- import: vi.fn(),
13
- }
14
- }
15
-
16
- describe('StorageService', () => {
17
- const createService = (): StorageService => {
18
- return new StorageService()
19
- }
20
-
21
- it('delegates getLocation to active provider', () => {
22
- const service = createService()
23
- const provider = createMockProvider()
24
- // eslint-disable-next-line @typescript-eslint/no-deprecated
25
- service.setProvider(provider)
26
-
27
- const result = service.getLocation('data')
28
-
29
- expect(provider.getLocation).toHaveBeenCalledWith('data')
30
- expect(result).toBeDefined()
31
- })
32
-
33
- it('maps legacy location names to new names', () => {
34
- const service = createService()
35
- const provider = createMockProvider()
36
- // eslint-disable-next-line @typescript-eslint/no-deprecated
37
- service.setProvider(provider)
38
-
39
- // Legacy 'config' and 'events' both map to 'data'.
40
- // StorageManager.getLocation accepts `StorageLocationName | string`,
41
- // so string literals flow through without casts.
42
- service.getLocation('config')
43
- expect(provider.getLocation).toHaveBeenCalledWith('data')
44
-
45
- service.getLocation('events')
46
- expect(provider.getLocation).toHaveBeenNthCalledWith(2, 'data')
47
- })
48
-
49
- it('throws if no provider set', () => {
50
- const service = createService()
51
-
52
- expect(() => service.getLocation('data')).toThrow('No storage provider configured')
53
- expect(() => service.getProvider()).toThrow('No storage provider configured')
54
- })
55
-
56
- it('setProvider changes the active provider', () => {
57
- const service = createService()
58
- const provider1 = createMockProvider()
59
- const provider2 = createMockProvider()
60
-
61
- // eslint-disable-next-line @typescript-eslint/no-deprecated
62
- service.setProvider(provider1)
63
- expect(service.getProvider()).toBe(provider1)
64
-
65
- // eslint-disable-next-line @typescript-eslint/no-deprecated
66
- service.setProvider(provider2)
67
- expect(service.getProvider()).toBe(provider2)
68
-
69
- service.getLocation('data')
70
- expect(provider2.getLocation).toHaveBeenCalledWith('data')
71
- expect(provider1.getLocation).not.toHaveBeenCalled()
72
- })
73
- })
@@ -1,3 +0,0 @@
1
- import { StorageManager } from '@camstack/core'
2
-
3
- export class StorageService extends StorageManager {}
@@ -1,221 +0,0 @@
1
- import { execFile } from 'child_process'
2
- import { promisify } from 'util'
3
-
4
- import { LoggingService } from '../logging/logging.service'
5
- import type { StreamMetadata, StreamAudioMetadata } from '@camstack/types'
6
- import type { IScopedLogger } from '@camstack/types'
7
- import { errMsg } from '@camstack/types'
8
-
9
- const execFileAsync = promisify(execFile)
10
-
11
- const CACHE_TTL_MS = 3_600_000 // 1 hour
12
- const PROBE_TIMEOUT_MS = 5_000
13
-
14
- /** Codec aliases normalised to canonical names. */
15
- const CODEC_ALIASES: Readonly<Record<string, string>> = {
16
- hevc: 'h265',
17
- }
18
-
19
- interface CacheEntry {
20
- readonly metadata: StreamMetadata
21
- readonly timestamp: number
22
- }
23
-
24
- export class StreamProbeService {
25
- private readonly logger: IScopedLogger
26
- private readonly cache = new Map<string, CacheEntry>()
27
-
28
- constructor(loggingService: LoggingService) {
29
- this.logger = loggingService.createLogger('StreamProbeService')
30
- }
31
-
32
- // ---------------------------------------------------------------------------
33
- // Public API
34
- // ---------------------------------------------------------------------------
35
-
36
- /** Probe a stream URL and return its metadata (cached for 1 hour). */
37
- async probe(url: string, options?: { force?: boolean }): Promise<StreamMetadata> {
38
- const force = options?.force ?? false
39
-
40
- if (!force) {
41
- const cached = this.cache.get(url)
42
- if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
43
- return cached.metadata
44
- }
45
- }
46
-
47
- const metadata = await this.runProbe(url)
48
- this.cache.set(url, { metadata, timestamp: Date.now() })
49
- return metadata
50
- }
51
-
52
- /**
53
- * Generic field probe: given a field key and value, decides how to probe.
54
- * Stream fields (stream_*) → ffprobe, other URLs → HTTP HEAD check.
55
- */
56
- async probeField(
57
- key: string,
58
- value: unknown,
59
- ): Promise<{ status: 'ok' | 'error'; labels?: string[]; error?: string }> {
60
- const url = String(value ?? '').trim()
61
- if (!url) {
62
- return { status: 'error', error: 'No URL provided' }
63
- }
64
-
65
- // Stream fields: use ffprobe
66
- if (key.startsWith('stream')) {
67
- const meta = await this.probe(url, { force: true })
68
- const labels: string[] = []
69
- if (meta.width && meta.height) labels.push(`${meta.width}\u00d7${meta.height}`)
70
- if (meta.codec) {
71
- const codecDisplay: Record<string, string> = { h265: 'H.265', h264: 'H.264', hevc: 'H.265' }
72
- labels.push(codecDisplay[meta.codec] ?? meta.codec.toUpperCase())
73
- }
74
- if (meta.fps) labels.push(`${Math.round(meta.fps)}fps`)
75
- if (meta.audio?.codec) {
76
- const audioCodec = meta.audio.codec.toUpperCase()
77
- const audioBits: string[] = [audioCodec]
78
- if (meta.audio.sampleRate) audioBits.push(`${Math.round(meta.audio.sampleRate / 1000)}kHz`)
79
- if (meta.audio.channels === 1) audioBits.push('mono')
80
- else if (meta.audio.channels === 2) audioBits.push('stereo')
81
- else if (meta.audio.channels) audioBits.push(`${meta.audio.channels}ch`)
82
- labels.push(`audio: ${audioBits.join(' ')}`)
83
- }
84
- if (labels.length === 0) {
85
- return { status: 'error', error: 'No video streams found at URL' }
86
- }
87
- return { status: 'ok', labels }
88
- }
89
-
90
- // Other URL fields (snapshot, etc.): GET with early abort to check reachability + content-type
91
- try {
92
- const controller = new AbortController()
93
- const timeout = AbortSignal.timeout(5000)
94
- timeout.addEventListener('abort', () => controller.abort(timeout.reason))
95
- const response = await fetch(url, { method: 'GET', signal: controller.signal })
96
- if (!response.ok) {
97
- controller.abort()
98
- return { status: 'error', error: `HTTP ${response.status} ${response.statusText}` }
99
- }
100
- const contentType = response.headers.get('content-type') ?? ''
101
- controller.abort() // Don't download the full body
102
- const labels: string[] = ['Reachable']
103
- if (contentType.startsWith('image/')) {
104
- labels.push(contentType.replace('image/', '').toUpperCase())
105
- }
106
- return { status: 'ok', labels }
107
- } catch (err) {
108
- if (err instanceof Error && err.name === 'AbortError') {
109
- // We aborted intentionally after reading headers — that's success
110
- return { status: 'ok', labels: ['Reachable'] }
111
- }
112
- const msg = errMsg(err)
113
- return { status: 'error', error: `Unreachable: ${msg}` }
114
- }
115
- }
116
-
117
- /** Clear cached metadata for a single URL or all URLs. */
118
- clearCache(url?: string): void {
119
- if (url) {
120
- this.cache.delete(url)
121
- } else {
122
- this.cache.clear()
123
- }
124
- }
125
-
126
- // ---------------------------------------------------------------------------
127
- // Internal
128
- // ---------------------------------------------------------------------------
129
-
130
- private async runProbe(url: string): Promise<StreamMetadata> {
131
- try {
132
- // Query first video + first audio stream in one ffprobe pass.
133
- // `-show_streams` returns every stream in the container; we filter
134
- // by codec_type when parsing. Fields cover both kinds.
135
- const { stdout } = await execFileAsync(
136
- 'ffprobe',
137
- [
138
- '-v',
139
- 'error',
140
- '-rtsp_transport',
141
- 'tcp',
142
- '-timeout',
143
- '5000000',
144
- '-show_entries',
145
- 'stream=codec_type,codec_name,profile,width,height,r_frame_rate,bit_rate,sample_rate,channels',
146
- '-of',
147
- 'json',
148
- url,
149
- ],
150
- { timeout: PROBE_TIMEOUT_MS },
151
- )
152
-
153
- return this.parseOutput(stdout)
154
- } catch (err) {
155
- this.logger.error('ffprobe failed', { meta: { url, error: String(err) } })
156
- return {}
157
- }
158
- }
159
-
160
- private parseOutput(stdout: string): StreamMetadata {
161
- const parsed: unknown = JSON.parse(stdout)
162
- const streams = (parsed as { streams?: unknown[] }).streams
163
- if (!Array.isArray(streams) || streams.length === 0) {
164
- return {}
165
- }
166
-
167
- const meta: StreamMetadata = {}
168
-
169
- for (const raw of streams) {
170
- const stream = raw as Record<string, unknown>
171
- const codecType = typeof stream.codec_type === 'string' ? stream.codec_type : undefined
172
-
173
- if (codecType === 'video' && meta.codec === undefined) {
174
- const rawCodec = typeof stream.codec_name === 'string' ? stream.codec_name : undefined
175
- meta.codec = rawCodec ? (CODEC_ALIASES[rawCodec] ?? rawCodec) : undefined
176
- if (typeof stream.width === 'number') meta.width = stream.width
177
- if (typeof stream.height === 'number') meta.height = stream.height
178
- const fps = this.parseFps(stream.r_frame_rate)
179
- if (fps !== undefined) meta.fps = fps
180
- const bitrateKbps = this.parseBitrateKbps(stream.bit_rate)
181
- if (bitrateKbps !== undefined) meta.bitrateKbps = bitrateKbps
182
- } else if (codecType === 'audio' && meta.audio === undefined) {
183
- const audio: StreamAudioMetadata = {}
184
- if (typeof stream.codec_name === 'string') audio.codec = stream.codec_name
185
- if (typeof stream.profile === 'string') audio.profile = stream.profile
186
- const sampleRate = this.parseIntField(stream.sample_rate)
187
- if (sampleRate !== undefined) audio.sampleRate = sampleRate
188
- if (typeof stream.channels === 'number') audio.channels = stream.channels
189
- const audioBitrateKbps = this.parseBitrateKbps(stream.bit_rate)
190
- if (audioBitrateKbps !== undefined) audio.bitrateKbps = audioBitrateKbps
191
- meta.audio = audio
192
- }
193
- }
194
-
195
- return meta
196
- }
197
-
198
- private parseIntField(value: unknown): number | undefined {
199
- const n = typeof value === 'string' ? Number(value) : typeof value === 'number' ? value : NaN
200
- if (!Number.isFinite(n) || n <= 0) return undefined
201
- return Math.round(n)
202
- }
203
-
204
- /** Parse fractional frame rate string like "25/1" → 25. */
205
- private parseFps(value: unknown): number | undefined {
206
- if (typeof value !== 'string') return undefined
207
- const parts = value.split('/')
208
- if (parts.length !== 2) return undefined
209
- const num = Number(parts[0])
210
- const den = Number(parts[1])
211
- if (!Number.isFinite(num) || !Number.isFinite(den) || den === 0) return undefined
212
- return Math.round((num / den) * 100) / 100
213
- }
214
-
215
- /** Parse bit_rate string (bps) → kbps. */
216
- private parseBitrateKbps(value: unknown): number | undefined {
217
- const n = typeof value === 'string' ? Number(value) : typeof value === 'number' ? value : NaN
218
- if (!Number.isFinite(n) || n <= 0) return undefined
219
- return Math.round(n / 1000)
220
- }
221
- }