@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,86 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import { AuthService } from './auth.service'
3
- import type { ConfigService } from '../config/config.service'
4
- import type { TokenPayload } from '@camstack/types'
5
-
6
- function createMockConfig(overrides: Record<string, unknown> = {}): ConfigService {
7
- return {
8
- get: (path: string) => overrides[path],
9
- } as unknown as ConfigService
10
- }
11
-
12
- describe('AuthService', () => {
13
- let service: AuthService
14
-
15
- beforeEach(() => {
16
- service = new AuthService(createMockConfig({ 'auth.jwtSecret': 'test-secret' }))
17
- })
18
-
19
- describe('JWT sign/verify', () => {
20
- it('should sign and verify a token (roundtrip)', () => {
21
- const payload: Omit<TokenPayload, 'iat' | 'exp'> = {
22
- userId: 'user-1',
23
- username: 'admin',
24
- role: 'admin',
25
- allowedProviders: '*',
26
- allowedDevices: {},
27
- }
28
-
29
- const token = service.signToken(payload)
30
-
31
- const decoded = service.verifyToken(token)
32
-
33
- expect(decoded.userId).toBe('user-1')
34
-
35
- expect(decoded.username).toBe('admin')
36
-
37
- expect(decoded.role).toBe('admin')
38
-
39
- expect(decoded.allowedProviders).toBe('*')
40
-
41
- expect(decoded.iat).toBeDefined()
42
-
43
- expect(decoded.exp).toBeDefined()
44
- })
45
-
46
- it('should reject invalid tokens', () => {
47
- expect(() => service.verifyToken('invalid.token.here')).toThrow()
48
- })
49
- })
50
-
51
- describe('password hashing', () => {
52
- it('should hash and compare passwords correctly', async () => {
53
- const password = 'my-secure-password'
54
- const hash = await service.hashPassword(password)
55
-
56
- expect(hash).not.toBe(password)
57
- expect(await service.comparePassword(password, hash)).toBe(true)
58
- })
59
-
60
- it('should reject wrong passwords', async () => {
61
- const hash = await service.hashPassword('correct-password')
62
- expect(await service.comparePassword('wrong-password', hash)).toBe(false)
63
- })
64
- })
65
-
66
- describe('API key generation', () => {
67
- it('should generate an API key with token, hash, and prefix', () => {
68
- const key = service.generateApiKey()
69
-
70
- expect(key.token).toHaveLength(64) // 32 bytes hex
71
- expect(key.hash).toHaveLength(64) // SHA-256 hex
72
- expect(key.prefix).toHaveLength(8)
73
- expect(key.token.startsWith(key.prefix)).toBe(true)
74
- })
75
-
76
- it('should validate API key token against hash (correct)', () => {
77
- const key = service.generateApiKey()
78
- expect(service.validateApiKey(key.token, key.hash)).toBe(true)
79
- })
80
-
81
- it('should reject wrong API key token', () => {
82
- const key = service.generateApiKey()
83
- expect(service.validateApiKey('wrong-token', key.hash)).toBe(false)
84
- })
85
- })
86
- })
@@ -1,8 +0,0 @@
1
- import { AuthManager } from '@camstack/core'
2
- import { ConfigService } from '../config/config.service'
3
-
4
- export class AuthService extends AuthManager {
5
- constructor(config: ConfigService) {
6
- super(config)
7
- }
8
- }
@@ -1,66 +0,0 @@
1
- /**
2
- * CapabilityService — NestJS-injectable wrapper around the CapabilityRegistry.
3
- *
4
- * Server services inject this instead of accessing the registry directly.
5
- * The registry reference is set once during boot by AddonRegistryService.
6
- */
7
-
8
- import type { CapabilityRegistry } from '@camstack/kernel'
9
-
10
- export class CapabilityService {
11
- private registry: CapabilityRegistry | null = null
12
-
13
- /** Called once during boot by AddonRegistryService to wire the registry */
14
- setRegistry(registry: CapabilityRegistry): void {
15
- this.registry = registry
16
- }
17
-
18
- /** Get the underlying registry (may be null before boot completes) */
19
- getRegistry(): CapabilityRegistry | null {
20
- return this.registry
21
- }
22
-
23
- /** Get the active singleton provider for a capability */
24
- getSingleton<T>(capability: string): T | null {
25
- return this.registry?.getSingleton<T>(capability) ?? null
26
- }
27
-
28
- /**
29
- * Resolve the hub-local provider honoring the 'hub' per-node singleton override.
30
- * Delegates to `CapabilityRegistry.getSingletonForNode` so the in-process lookup
31
- * respects per-node overrides set by the operator.
32
- */
33
- getSingletonForNode<T>(capability: string, nodeId: string): T | null {
34
- return this.registry?.getSingletonForNode<T>(capability, nodeId) ?? null
35
- }
36
-
37
- /** Get the addon ID of the active singleton provider for a capability */
38
- getSingletonAddonId(capability: string): string | null {
39
- return this.registry?.getSingletonAddonId(capability) ?? null
40
- }
41
-
42
- /** Get all active collection providers for a capability */
43
- getCollection<T>(capability: string): readonly T[] {
44
- return this.registry?.getCollection<T>(capability) ?? []
45
- }
46
-
47
- /**
48
- * Like {@link getCollection} but returns `[addonId, provider]` tuples
49
- * so callers can attribute work back to the contributing addon —
50
- * required by the addon-widgets static file route to validate that
51
- * the requested `addonId` is a registered widget provider.
52
- */
53
- getCollectionEntries<T>(capability: string): readonly (readonly [string, T])[] {
54
- return this.registry?.getCollectionEntries<T>(capability) ?? []
55
- }
56
-
57
- /** Resolve a singleton provider for a specific device (with per-device override support) */
58
- resolveForDevice<T>(capability: string, deviceId: string): T | null {
59
- return this.registry?.resolveForDevice<T>(capability, deviceId) ?? null
60
- }
61
-
62
- /** Resolve collection providers for a specific device (with per-device filter support) */
63
- resolveCollectionForDevice<T>(capability: string, deviceId: string): readonly T[] {
64
- return this.registry?.resolveCollectionForDevice<T>(capability, deviceId) ?? []
65
- }
66
- }
@@ -1,3 +0,0 @@
1
- // Re-export from @camstack/kernel
2
- export { bootstrapSchema, RUNTIME_DEFAULTS } from '@camstack/kernel'
3
- export type { BootstrapConfig, AppConfig, ServerMode } from '@camstack/kernel'
@@ -1,175 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
- import * as fs from 'node:fs'
3
- import * as path from 'node:path'
4
- import * as os from 'node:os'
5
- import * as yaml from 'js-yaml'
6
- import { ConfigService } from './config.service'
7
-
8
- describe('ConfigService', () => {
9
- let tmpDir: string
10
- let originalEnv: Record<string, string | undefined>
11
-
12
- beforeEach(() => {
13
- // Phase 13 (settings redesign test fix): `ConfigManager` applies
14
- // `ENV_VAR_MAP` overrides on top of YAML values. When this suite
15
- // runs in parallel with others under `nx run-many`, environment
16
- // variables set by sibling tests (or the parent nx process) leak
17
- // in and override our carefully seeded YAML fixtures — making
18
- // `auth.adminPassword` come back as `'admin'` instead of
19
- // `'secret123'` and `server.dataPath` come back as the default
20
- // `'camstack-data'` instead of `'/custom/data'`.
21
- //
22
- // Snapshot and clear every env var that `ENV_VAR_MAP` consults.
23
- //
24
- // NOTE: `ADMIN_PASSWORD` is intentionally NOT cleared here.
25
- // `bootstrapSchema` in @camstack/kernel uses
26
- // `z.string().default(process.env.ADMIN_PASSWORD ?? 'changeme')`
27
- // which is evaluated once at module load time — clearing
28
- // `ADMIN_PASSWORD` here has no effect on the already-baked-in
29
- // schema default, and the "uses defaults when config file does
30
- // not exist" test still has to read that original value via
31
- // `process.env.ADMIN_PASSWORD ?? 'changeme'`. So we leave the
32
- // var alone.
33
- const envKeys = [
34
- 'CAMSTACK_PORT',
35
- 'CAMSTACK_HOST',
36
- 'CAMSTACK_DATA',
37
- 'CAMSTACK_JWT_SECRET',
38
- 'CAMSTACK_ADMIN_USER',
39
- 'CAMSTACK_ADMIN_PASS',
40
- ] as const
41
- originalEnv = {}
42
- for (const key of envKeys) {
43
- originalEnv[key] = process.env[key]
44
- delete process.env[key]
45
- }
46
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'camstack-config-'))
47
- })
48
-
49
- afterEach(() => {
50
- for (const [key, value] of Object.entries(originalEnv)) {
51
- if (value === undefined) delete process.env[key]
52
- else process.env[key] = value
53
- }
54
- fs.rmSync(tmpDir, { recursive: true, force: true })
55
- })
56
-
57
- const createConfigFile = (config: Record<string, unknown>): string => {
58
- const filePath = path.join(tmpDir, 'config.yaml')
59
- fs.writeFileSync(filePath, yaml.dump(config), 'utf-8')
60
- return filePath
61
- }
62
-
63
- const createService = async (configPath: string): Promise<ConfigService> => {
64
- return new ConfigService(configPath)
65
- }
66
-
67
- it('loads and validates a minimal config.yaml', async () => {
68
- const configPath = createConfigFile({
69
- server: { port: 5000, host: '127.0.0.1' },
70
- auth: { adminPassword: 'secret123' },
71
- })
72
-
73
- const service = await createService(configPath)
74
- const raw = service.raw
75
-
76
- expect(raw.server.port).toBe(5000)
77
- expect(raw.server.host).toBe('127.0.0.1')
78
- expect(raw.auth.adminPassword).toBe('secret123')
79
- expect(raw.auth.adminUsername).toBe('admin')
80
- })
81
-
82
- it('returns runtime defaults for optional sections via raw', async () => {
83
- const configPath = createConfigFile({
84
- server: { port: 4443 },
85
- auth: { adminPassword: 'secret123' },
86
- })
87
-
88
- const service = await createService(configPath)
89
- const raw = service.raw
90
-
91
- expect(raw.features.streaming).toBe(true)
92
- expect(raw.features.objectDetection).toBe(false)
93
- expect(raw.storage.provider).toBe('sqlite-storage')
94
- expect(raw.logging.level).toBe('info')
95
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access --
96
- expect((raw.logging as any).retentionDays).toBe(30)
97
- expect(raw.eventBus.ringBufferSize).toBe(10000)
98
- // addons.enabled removed — installed = active
99
- })
100
-
101
- it('throws on invalid config (port as string)', async () => {
102
- const configPath = createConfigFile({
103
- server: { port: 'not-a-number' },
104
- auth: { adminPassword: 'secret123' },
105
- })
106
-
107
- await expect(createService(configPath)).rejects.toThrow()
108
- })
109
-
110
- it('dot-path access works for bootstrap keys', async () => {
111
- const configPath = createConfigFile({
112
- server: { port: 9090 },
113
- auth: { adminPassword: 'secret123' },
114
- })
115
-
116
- const service = await createService(configPath)
117
-
118
- expect(service.get<number>('server.port')).toBe(9090)
119
- expect(service.get<string>('auth.adminPassword')).toBe('secret123')
120
- })
121
-
122
- it('dot-path access returns runtime defaults for non-bootstrap keys', async () => {
123
- const configPath = createConfigFile({
124
- server: { port: 4443 },
125
- auth: { adminPassword: 'secret123' },
126
- })
127
-
128
- const service = await createService(configPath)
129
-
130
- expect(service.get<string>('logging.level')).toBe('info')
131
- expect(service.get<number>('retention.detectionEventsDays')).toBe(30)
132
- expect(service.get<boolean>('features.objectDetection')).toBe(false)
133
- // addons.enabled removed — installed = active
134
- })
135
-
136
- it('uses defaults when config file does not exist', async () => {
137
- const configPath = path.join(tmpDir, 'nonexistent.yaml')
138
-
139
- const service = await createService(configPath)
140
- const raw = service.raw
141
-
142
- expect(raw.server.port).toBe(4443)
143
- expect(raw.server.host).toBe('0.0.0.0')
144
- expect(raw.auth.adminUsername).toBe('admin')
145
- expect(raw.auth.adminPassword).toBe(process.env.ADMIN_PASSWORD ?? 'changeme')
146
- })
147
-
148
- it('returns features via features getter with runtime defaults', async () => {
149
- const configPath = createConfigFile({
150
- server: { port: 4443 },
151
- auth: { adminPassword: 'secret123' },
152
- })
153
-
154
- const service = await createService(configPath)
155
- const features = service.features
156
-
157
- // Features come from RUNTIME_DEFAULTS (not YAML bootstrap config)
158
- expect(features.streaming).toBe(true)
159
- expect(features.objectDetection).toBe(false)
160
- expect(features.notifications).toBe(true)
161
- })
162
-
163
- it('getBootstrap returns bootstrap-only values', async () => {
164
- const configPath = createConfigFile({
165
- server: { port: 7777, dataPath: '/custom/data' },
166
- auth: { adminPassword: 'mypass', jwtSecret: 'mysecret' },
167
- })
168
-
169
- const service = await createService(configPath)
170
-
171
- expect(service.getBootstrap<number>('server.port')).toBe(7777)
172
- expect(service.getBootstrap<string>('server.dataPath')).toBe('/custom/data')
173
- expect(service.getBootstrap<string>('auth.jwtSecret')).toBe('mysecret')
174
- })
175
- })
@@ -1,7 +0,0 @@
1
- import { ConfigManager } from '@camstack/kernel'
2
-
3
- export class ConfigService extends ConfigManager {
4
- constructor(configPath: string) {
5
- super(configPath)
6
- }
7
- }
@@ -1,235 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
- import * as fs from 'node:fs'
3
- import * as path from 'node:path'
4
- import * as os from 'node:os'
5
- import * as yaml from 'js-yaml'
6
- import { ServiceBroker } from 'moleculer'
7
- import { EventBusService } from './event-bus.service'
8
- import { ConfigService } from '../config/config.service'
9
- import type { ISettingsStore } from '@camstack/kernel'
10
- import type { SystemEvent } from '@camstack/types'
11
-
12
- /** Flush pending queueMicrotask callbacks */
13
- const flush = () => new Promise<void>((resolve) => queueMicrotask(resolve))
14
-
15
- const makeEvent = (category: string, overrides: Partial<SystemEvent> = {}): SystemEvent => ({
16
- id: `evt-${Math.random().toString(36).slice(2, 8)}`,
17
- timestamp: new Date(),
18
- source: { type: 'device', id: 'cam-1' },
19
- category,
20
- data: {},
21
- ...overrides,
22
- })
23
-
24
- /**
25
- * In-memory settings store used by the test to drive `ConfigService.get`
26
- * for the `eventBus.ringBufferSize` key. The bootstrap YAML schema does not
27
- * declare `eventBus`, so writing it to the YAML file would be discarded by
28
- * `bootstrapSchema.parse`. Wiring an ISettingsStore is the clean path to
29
- * feed runtime values into `ConfigService` without casts.
30
- */
31
- class InMemorySettingsStore implements ISettingsStore {
32
- private readonly system: Record<string, unknown>
33
-
34
- constructor(seed: Record<string, unknown>) {
35
- this.system = { ...seed }
36
- }
37
-
38
- getSystem(key: string): unknown {
39
- return this.system[key]
40
- }
41
- setSystem(key: string, value: unknown): void {
42
- this.system[key] = value
43
- }
44
- getAllSystem(): Record<string, unknown> {
45
- return { ...this.system }
46
- }
47
-
48
- getAllAddon(_addonId: string): Record<string, unknown> {
49
- return {}
50
- }
51
- setAllAddon(_addonId: string, _config: Record<string, unknown>): void {
52
- /* no-op */
53
- }
54
- getAllProvider(_providerId: string): Record<string, unknown> {
55
- return {}
56
- }
57
- setProvider(_providerId: string, _key: string, _value: unknown): void {
58
- /* no-op */
59
- }
60
- getAllDevice(_deviceId: string): Record<string, unknown> {
61
- return {}
62
- }
63
- setDevice(_deviceId: string, _key: string, _value: unknown): void {
64
- /* no-op */
65
- }
66
- getAddonDevice(_addonId: string, _deviceId: string): Record<string, unknown> {
67
- return {}
68
- }
69
- setAddonDevice(_addonId: string, _deviceId: string, _values: Record<string, unknown>): void {
70
- /* no-op */
71
- }
72
- clearAddonDevice(_addonId: string, _deviceId: string): void {
73
- /* no-op */
74
- }
75
- }
76
-
77
- describe('EventBusService', () => {
78
- let tmpDir: string
79
- // Brokers spawned by `createService` — torn down in `afterEach` so
80
- // every test runs in isolation. The bus is a thin delegate over the
81
- // shared broker bus (`getBrokerEventBus(broker)`); attaching is the
82
- // entrypoint that wires `subscribe` / `emit` to a real implementation.
83
- const brokers: ServiceBroker[] = []
84
-
85
- beforeEach(() => {
86
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'camstack-eventbus-'))
87
- })
88
-
89
- afterEach(async () => {
90
- for (const b of brokers.splice(0)) {
91
- await b.stop()
92
- }
93
- fs.rmSync(tmpDir, { recursive: true, force: true })
94
- })
95
-
96
- const createService = async (bufferSize = 100): Promise<EventBusService> => {
97
- const configPath = path.join(tmpDir, 'config.yaml')
98
- fs.writeFileSync(
99
- configPath,
100
- yaml.dump({
101
- server: { port: 4443 },
102
- auth: { adminPassword: 'secret123' },
103
- }),
104
- 'utf-8',
105
- )
106
- const configService = new ConfigService(configPath)
107
- configService.setSettingsStore(
108
- new InMemorySettingsStore({
109
- 'eventBus.ringBufferSize': bufferSize,
110
- }),
111
- )
112
- const service = new EventBusService(configService)
113
- // EventBusService is a delegate that needs a broker to dispatch
114
- // through. Use a unique nodeID per call so each test gets its own
115
- // shared bus instance (`getBrokerEventBus(broker)` keys by broker).
116
- const broker = new ServiceBroker({
117
- nodeID: `eventbus-test-${brokers.length}`,
118
- transporter: 'Fake',
119
- logger: false,
120
- })
121
- await broker.start()
122
- brokers.push(broker)
123
- service.attachBroker(broker)
124
- return service
125
- }
126
-
127
- it('emits events to subscribers', async () => {
128
- const service = await createService()
129
- const received: SystemEvent[] = []
130
-
131
- service.subscribe({}, (event) => received.push(event))
132
-
133
- const event = makeEvent('device.online')
134
- service.emit(event)
135
- await flush()
136
-
137
- // The shared broker bus enriches every emit with `sourceNodeId`,
138
- // so the delivered object is a copy — assert key fields instead of
139
- // strict identity. See `addon-context-factory.ts::createSharedBus`.
140
- expect(received).toHaveLength(1)
141
- expect(received[0]).toMatchObject({ id: event.id, category: event.category })
142
- })
143
-
144
- it('filters by category wildcard (device.* matches device.online)', async () => {
145
- const service = await createService()
146
- const received: SystemEvent[] = []
147
-
148
- service.subscribe({ category: 'device.*' }, (event) => received.push(event))
149
-
150
- service.emit(makeEvent('device.online'))
151
- service.emit(makeEvent('device.offline'))
152
- service.emit(makeEvent('addon.started'))
153
- await flush()
154
-
155
- expect(received).toHaveLength(2)
156
-
157
- expect(received[0]!.category).toBe('device.online')
158
-
159
- expect(received[1]!.category).toBe('device.offline')
160
- })
161
-
162
- it('filters by exact category', async () => {
163
- const service = await createService()
164
- const received: SystemEvent[] = []
165
-
166
- service.subscribe({ category: 'device.online' }, (event) => received.push(event))
167
-
168
- service.emit(makeEvent('device.online'))
169
- service.emit(makeEvent('device.offline'))
170
- await flush()
171
-
172
- expect(received).toHaveLength(1)
173
-
174
- expect(received[0]!.category).toBe('device.online')
175
- })
176
-
177
- it('filters by source', async () => {
178
- const service = await createService()
179
- const received: SystemEvent[] = []
180
-
181
- service.subscribe({ source: { type: 'addon', id: 'frigate' } }, (event) => received.push(event))
182
-
183
- service.emit(makeEvent('addon.started', { source: { type: 'addon', id: 'frigate' } }))
184
- service.emit(makeEvent('addon.started', { source: { type: 'addon', id: 'scrypted' } }))
185
- await flush()
186
-
187
- expect(received).toHaveLength(1)
188
- })
189
-
190
- it('maintains a chronological recent-events buffer', async () => {
191
- // Buffer size is now hub-fixed at 10000 (see `createSharedBusState`),
192
- // so the legacy `bufferSize=3` argument no longer caps it. The
193
- // contract under test today is: every emit is appended in order and
194
- // surfaces via `getRecent()`, oldest first.
195
- const service = await createService()
196
-
197
- service.emit(makeEvent('a'))
198
- service.emit(makeEvent('b'))
199
- service.emit(makeEvent('c'))
200
- service.emit(makeEvent('d'))
201
-
202
- const recent = service.getRecent()
203
- expect(recent).toHaveLength(4)
204
- expect(recent[0]!.category).toBe('a')
205
- expect(recent[3]!.category).toBe('d')
206
- })
207
-
208
- it('unsubscribe stops delivery', async () => {
209
- const service = await createService()
210
- const received: SystemEvent[] = []
211
-
212
- const unsubscribe = service.subscribe({}, (event) => received.push(event))
213
-
214
- service.emit(makeEvent('first'))
215
- await flush()
216
- unsubscribe()
217
- service.emit(makeEvent('second'))
218
- await flush()
219
-
220
- expect(received).toHaveLength(1)
221
-
222
- expect(received[0]!.category).toBe('first')
223
- })
224
-
225
- it('getRecent supports filter', async () => {
226
- const service = await createService()
227
-
228
- service.emit(makeEvent('device.online'))
229
- service.emit(makeEvent('addon.started'))
230
- service.emit(makeEvent('device.offline'))
231
-
232
- const recent = service.getRecent({ category: 'device.*' })
233
- expect(recent).toHaveLength(2)
234
- })
235
- })
@@ -1,89 +0,0 @@
1
- import type { ServiceBroker } from 'moleculer'
2
- import { getBrokerEventBus } from '@camstack/kernel'
3
- import type { SystemEvent, IEventBus, EventFilter } from '@camstack/types'
4
- import { ConfigService } from '../config/config.service'
5
-
6
- /**
7
- * Hub-side event bus. Pre-broker boot the service buffers nothing —
8
- * subscribers / emits are no-ops until `attachBroker` lands the
9
- * underlying per-broker shared bus. Once attached every operation
10
- * delegates to the unified `getBrokerEventBus(broker)` instance,
11
- * which is the SAME bus that group-runner / agent subprocess addons
12
- * see when they call `ctx.eventBus.emit`. Single in-process delivery
13
- * map per broker, single `broker.broadcast` for cross-broker
14
- * delivery — no more dual hub-vs-subprocess implementations.
15
- *
16
- * **Persistence scope**: the in-memory ring buffer (10 000 events,
17
- * owned by `getBrokerEventBus`) survives admin-ui browser refreshes
18
- * but NOT server restarts — that's intentional. UI panels read
19
- * `getRecent` on mount and live-subscribe via `live.onEvent` /
20
- * `systemEvents.subscribe`, which is enough for the operator to see
21
- * continuity across page reloads.
22
- */
23
- export class EventBusService implements IEventBus {
24
- private broker: ServiceBroker | null = null
25
- private inner: IEventBus | null = null
26
- // Used only as a loose ring buffer for the very first events emitted
27
- // before `attachBroker` runs (NestJS-era boot legacy). Drained into
28
- // the real bus on attach.
29
- private pending: SystemEvent[] = []
30
- private deferredSubs: Array<{
31
- filter: EventFilter
32
- handler: (event: SystemEvent) => void
33
- unsub?: () => void
34
- }> = []
35
-
36
- constructor(_configService: ConfigService) {
37
- // The shared bus owns the ring-buffer size — hub-side config is
38
- // accepted for API compatibility but no longer drives the bus.
39
- void _configService
40
- }
41
-
42
- attachBroker(broker: ServiceBroker): void {
43
- if (this.broker === broker) return
44
- this.broker = broker
45
- const inner = getBrokerEventBus(broker)
46
- this.inner = inner
47
- // Replay deferred subscriptions onto the real bus.
48
- for (const sub of this.deferredSubs) {
49
- sub.unsub = inner.subscribe(sub.filter, sub.handler)
50
- }
51
- // Flush events emitted before the broker was ready.
52
- for (const evt of this.pending) {
53
- inner.emit(evt)
54
- }
55
- this.pending = []
56
- }
57
-
58
- detachBroker(): void {
59
- for (const sub of this.deferredSubs) {
60
- sub.unsub?.()
61
- sub.unsub = undefined
62
- }
63
- this.broker = null
64
- this.inner = null
65
- }
66
-
67
- emit(event: SystemEvent): void {
68
- if (!this.inner) {
69
- this.pending.push(event)
70
- return
71
- }
72
- this.inner.emit(event)
73
- }
74
-
75
- subscribe(filter: EventFilter, handler: (event: SystemEvent) => void): () => void {
76
- if (this.inner) return this.inner.subscribe(filter, handler)
77
- const sub = { filter, handler, unsub: undefined as (() => void) | undefined }
78
- this.deferredSubs.push(sub)
79
- return () => {
80
- if (sub.unsub) sub.unsub()
81
- const idx = this.deferredSubs.indexOf(sub)
82
- if (idx >= 0) this.deferredSubs.splice(idx, 1)
83
- }
84
- }
85
-
86
- getRecent(filter?: EventFilter, limit?: number): readonly SystemEvent[] {
87
- return this.inner?.getRecent(filter, limit) ?? []
88
- }
89
- }