@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,259 +0,0 @@
1
- /**
2
- * Phase 1: Bootstrap config loading and infrastructure setup.
3
- *
4
- * Extracted from main.ts — pure extraction, no behavior change.
5
- */
6
-
7
- import * as fs from 'node:fs'
8
- import * as path from 'node:path'
9
- import * as yaml from 'js-yaml'
10
- import { randomBytes } from 'node:crypto'
11
- import { asJsonObject } from '@camstack/types'
12
- import { bootstrapSchema } from '../core/config/config.schema'
13
- import type { BootstrapConfig } from '../core/config/config.schema'
14
- import { StorageLocationManager } from '../core/storage/storage-location-manager'
15
-
16
- // ---------------------------------------------------------------------------
17
- // Types
18
- // ---------------------------------------------------------------------------
19
-
20
- export type { BootstrapConfig }
21
-
22
- export interface InfraContext {
23
- readonly bootstrapConfig: BootstrapConfig
24
- readonly dataPath: string
25
- readonly locationManager: StorageLocationManager
26
- readonly tlsOptions: { key: Buffer; cert: Buffer } | undefined
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // Constants
31
- // ---------------------------------------------------------------------------
32
-
33
- const CONFIG_DEFAULTS: Record<string, unknown> = {
34
- server: { port: 4443, host: '0.0.0.0', dataPath: 'camstack-data' },
35
- auth: {
36
- jwtSecret: null,
37
- adminUsername: 'admin',
38
- adminPassword: 'changeme',
39
- },
40
- }
41
-
42
- const ENV_VAR_MAP: Record<string, string> = {
43
- CAMSTACK_PORT: 'server.port',
44
- CAMSTACK_HOST: 'server.host',
45
- CAMSTACK_DATA: 'server.dataPath',
46
- CAMSTACK_JWT_SECRET: 'auth.jwtSecret',
47
- CAMSTACK_ADMIN_USER: 'auth.adminUsername',
48
- CAMSTACK_ADMIN_PASS: 'auth.adminPassword',
49
- CAMSTACK_HUB_URL: 'hub.url',
50
- CAMSTACK_HUB_TOKEN: 'hub.token',
51
- CAMSTACK_AGENT_NAME: 'agent.name',
52
- CAMSTACK_TLS_ENABLED: 'tls.enabled',
53
- CAMSTACK_TLS_CERT: 'tls.certPath',
54
- CAMSTACK_TLS_KEY: 'tls.keyPath',
55
- }
56
-
57
- // ---------------------------------------------------------------------------
58
- // Helpers
59
- // ---------------------------------------------------------------------------
60
-
61
- function setNested(
62
- obj: Record<string, unknown>,
63
- p: string,
64
- value: unknown,
65
- ): Record<string, unknown> {
66
- const [head, ...rest] = p.split('.')
67
- if (!head) return obj
68
- if (rest.length === 0) return { ...obj, [head]: value }
69
- const child = asJsonObject(obj[head]) ?? {}
70
- return { ...obj, [head]: setNested(child, rest.join('.'), value) }
71
- }
72
-
73
- // ---------------------------------------------------------------------------
74
- // loadBootstrapConfig
75
- // ---------------------------------------------------------------------------
76
-
77
- /**
78
- * Load bootstrap config from a YAML file, apply env-var overrides, and
79
- * validate via the bootstrap Zod schema.
80
- */
81
- export function loadBootstrapConfig(configPath: string): BootstrapConfig {
82
- // Only bootstrap sections live in config.yaml.
83
- // All runtime settings are stored in the SQL system_settings table.
84
-
85
- let raw: Record<string, unknown>
86
-
87
- if (fs.existsSync(configPath)) {
88
- const content = fs.readFileSync(configPath, 'utf-8')
89
- raw = asJsonObject(yaml.load(content)) ?? {}
90
- // Merge in any missing bootstrap sections (server, auth only)
91
- let updated = false
92
- for (const [key, defaults] of Object.entries(CONFIG_DEFAULTS)) {
93
- if (!(key in raw)) {
94
- raw[key] = defaults
95
- updated = true
96
- }
97
- }
98
- if (updated) {
99
- try {
100
- const tmpPath = `${configPath}.tmp`
101
- fs.writeFileSync(
102
- tmpPath,
103
- yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }),
104
- 'utf-8',
105
- )
106
- fs.renameSync(tmpPath, configPath)
107
- console.log(`[Phase1] Updated config.yaml with missing bootstrap defaults`)
108
- } catch (err) {
109
- console.warn(`[Phase1] Could not update config.yaml:`, err)
110
- }
111
- }
112
- console.log(`[Phase1] Loaded bootstrap config from: ${configPath}`)
113
- } else {
114
- console.log(`[Phase1] Config file not found at: ${configPath} — writing defaults`)
115
- const defaults = { ...CONFIG_DEFAULTS }
116
- try {
117
- fs.mkdirSync(path.dirname(configPath), { recursive: true })
118
- const tmpPath = `${configPath}.tmp`
119
- fs.writeFileSync(
120
- tmpPath,
121
- yaml.dump(defaults, { lineWidth: 120, indent: 2, quotingType: '"' }),
122
- 'utf-8',
123
- )
124
- fs.renameSync(tmpPath, configPath)
125
- console.log(`[Phase1] Default config.yaml written to: ${configPath}`)
126
- } catch (err) {
127
- console.warn(`[Phase1] Could not write default config.yaml:`, err)
128
- }
129
- raw = defaults
130
- }
131
-
132
- // Apply env var overrides for bootstrap keys
133
- for (const [envKey, configPath_] of Object.entries(ENV_VAR_MAP)) {
134
- const envValue = process.env[envKey]
135
- if (envValue === undefined || envValue === '') continue
136
- const coerced: unknown =
137
- configPath_ === 'server.port'
138
- ? Number(envValue)
139
- : configPath_ === 'tls.enabled'
140
- ? envValue === 'true'
141
- : envValue
142
- raw = setNested(raw, configPath_, coerced)
143
- console.log(`[Phase1] Env override: ${envKey} → ${configPath_}`)
144
- }
145
-
146
- return bootstrapSchema.parse(raw)
147
- }
148
-
149
- // ---------------------------------------------------------------------------
150
- // autoGenerateJwtSecret
151
- // ---------------------------------------------------------------------------
152
-
153
- /**
154
- * If `jwtSecret` is null in the parsed config, generate a random secret,
155
- * persist it to the YAML file, and return an updated config.
156
- */
157
- export function autoGenerateJwtSecret(
158
- configPath: string,
159
- bootstrapConfig: BootstrapConfig,
160
- ): BootstrapConfig {
161
- if (bootstrapConfig.auth.jwtSecret !== null) {
162
- return bootstrapConfig
163
- }
164
-
165
- const secret = randomBytes(32).toString('hex')
166
- console.log('[Phase1] jwtSecret is null — auto-generating and writing to config.yaml')
167
-
168
- let raw: Record<string, unknown> = {}
169
- if (fs.existsSync(configPath)) {
170
- raw = asJsonObject(yaml.load(fs.readFileSync(configPath, 'utf-8'))) ?? {}
171
- }
172
-
173
- const authSection = asJsonObject(raw.auth) ?? {}
174
- raw.auth = { ...authSection, jwtSecret: secret }
175
-
176
- const tmpPath = `${configPath}.tmp`
177
- try {
178
- fs.mkdirSync(path.dirname(configPath), { recursive: true })
179
- fs.writeFileSync(
180
- tmpPath,
181
- yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }),
182
- 'utf-8',
183
- )
184
- fs.renameSync(tmpPath, configPath)
185
- } catch (err) {
186
- console.warn('[Phase1] Could not write auto-generated jwtSecret to config.yaml:', err)
187
- }
188
-
189
- return {
190
- ...bootstrapConfig,
191
- auth: { ...bootstrapConfig.auth, jwtSecret: secret },
192
- }
193
- }
194
-
195
- // ---------------------------------------------------------------------------
196
- // setupInfra
197
- // ---------------------------------------------------------------------------
198
-
199
- /**
200
- * Phase 2: Create StorageLocationManager, ensure directories, configure TLS.
201
- * Returns the full InfraContext needed by subsequent boot phases.
202
- */
203
- export async function setupInfra(
204
- configPath: string,
205
- bootstrapConfig: BootstrapConfig,
206
- ): Promise<InfraContext> {
207
- // Auto-generate jwtSecret if not set
208
- const config = autoGenerateJwtSecret(configPath, bootstrapConfig)
209
-
210
- const dataPath = path.resolve(config.server.dataPath)
211
- const port = config.server.port
212
- const host = config.server.host
213
-
214
- console.log(`[Phase1] Bootstrap: port=${port}, host=${host}, dataPath=${dataPath}`)
215
-
216
- // --- Phase 2: Init StorageLocationManager → ensure all dirs exist ---
217
- console.log('[Phase2] Initializing storage locations…')
218
- const locationManager = new StorageLocationManager(dataPath)
219
- await locationManager.initializeDefaults()
220
-
221
- const locationStatus = locationManager.getStatus()
222
- for (const { name, available, path: locPath } of locationStatus) {
223
- console.log(`[Phase2] Location "${name}": ${available ? 'OK' : 'UNAVAILABLE'} → ${locPath}`)
224
- }
225
-
226
- // --- Phase 2c: TLS certificate setup ---
227
- let tlsOptions: { key: Buffer; cert: Buffer } | undefined
228
-
229
- if (config.tls.enabled) {
230
- // Use require() instead of import() — the ESM build of @camstack/core has
231
- // broken chunks with require("fs") when leaked .js files exist in core/src/.
232
- // CJS build works correctly and tsx supports require().
233
- const core = require('@camstack/core') as typeof import('@camstack/core')
234
- const { ensureTlsCert, loadTlsCert } = core
235
- if (config.tls.certPath && config.tls.keyPath) {
236
- // User-provided cert
237
- console.log(`[Phase2c] Loading custom TLS cert from ${config.tls.certPath}`)
238
- const pair = loadTlsCert(config.tls.certPath, config.tls.keyPath)
239
- tlsOptions = { key: pair.key, cert: pair.cert }
240
- } else {
241
- // Auto-generate self-signed
242
- const tlsResult = await ensureTlsCert(dataPath)
243
- if (tlsResult.generated) {
244
- console.log(`[Phase2c] Generated self-signed TLS cert at ${tlsResult.certPath}`)
245
- } else {
246
- console.log(`[Phase2c] Using existing TLS cert at ${tlsResult.certPath}`)
247
- }
248
- const pair = loadTlsCert(tlsResult.certPath, tlsResult.keyPath)
249
- tlsOptions = { key: pair.key, cert: pair.cert }
250
- }
251
- }
252
-
253
- return {
254
- bootstrapConfig: config,
255
- dataPath,
256
- locationManager,
257
- tlsOptions,
258
- }
259
- }
@@ -1,109 +0,0 @@
1
- /**
2
- * One-time integration-id backfill for devices created before the
3
- * device-manager forwarder started stamping `integrationId` (camera
4
- * providers). Maps each addon that hosts EXACTLY ONE integration to that
5
- * integration id, then stamps top-level untagged devices of those addons.
6
- * Multi-instance addons (e.g. Home Assistant with several brokers) are
7
- * ambiguous on `addonId` alone and are skipped — they stamp going forward.
8
- */
9
-
10
- export interface BackfillIntegration {
11
- readonly id: string
12
- readonly addonId: string
13
- }
14
-
15
- export interface BackfillDevice {
16
- readonly id: number
17
- readonly addonId: string
18
- readonly parentDeviceId: number | null
19
- readonly integrationId?: string
20
- }
21
-
22
- export interface BackfillStamp {
23
- readonly deviceId: number
24
- readonly integrationId: string
25
- }
26
-
27
- export function planIntegrationIdBackfill(
28
- integrations: readonly BackfillIntegration[],
29
- devices: readonly BackfillDevice[],
30
- ): readonly BackfillStamp[] {
31
- const singleByAddon = new Map<string, string>()
32
- const ambiguous = new Set<string>()
33
- for (const integration of integrations) {
34
- if (ambiguous.has(integration.addonId)) continue
35
- if (singleByAddon.has(integration.addonId)) {
36
- singleByAddon.delete(integration.addonId)
37
- ambiguous.add(integration.addonId)
38
- continue
39
- }
40
- singleByAddon.set(integration.addonId, integration.id)
41
- }
42
-
43
- const stamps: BackfillStamp[] = []
44
- for (const device of devices) {
45
- if (device.parentDeviceId !== null) continue
46
- if (device.integrationId !== undefined && device.integrationId !== '') continue
47
- const integrationId = singleByAddon.get(device.addonId)
48
- if (integrationId === undefined) continue
49
- stamps.push({ deviceId: device.id, integrationId })
50
- }
51
- return stamps
52
- }
53
-
54
- /**
55
- * Stamps to apply at integration-DELETE time so a cascade removes legacy
56
- * un-tagged devices too. The boot backfill only runs on startup; a device
57
- * created before stamping (or whose provider never stamps, e.g. `provider-rtsp`)
58
- * keeps no `integrationId`, so once its integration is deleted it would orphan
59
- * forever — `removeByIntegration` matches on `integrationId` and finds nothing.
60
- *
61
- * Run this in the delete handler BEFORE deleting the integration record (while
62
- * it is still present in `integrations`), then stamp the returned devices and
63
- * let `removeByIntegration` cascade them. Reuses the boot backfill's safety
64
- * rule (only addons hosting exactly ONE integration are unambiguous) and
65
- * filters to the integration being deleted so siblings are never touched.
66
- */
67
- export function planDeleteTimeStamps(
68
- integrationId: string,
69
- integrations: readonly BackfillIntegration[],
70
- devices: readonly BackfillDevice[],
71
- ): readonly BackfillStamp[] {
72
- return planIntegrationIdBackfill(integrations, devices).filter(
73
- (stamp) => stamp.integrationId === integrationId,
74
- )
75
- }
76
-
77
- export interface BackfillLogger {
78
- readonly info: (message: string, meta?: Record<string, unknown>) => void
79
- readonly warn: (message: string, meta?: Record<string, unknown>) => void
80
- }
81
-
82
- export interface IntegrationIdBackfillDeps {
83
- readonly listIntegrations: () => Promise<readonly BackfillIntegration[]>
84
- readonly listDevices: () => Promise<readonly BackfillDevice[]>
85
- readonly setIntegrationId: (deviceId: number, integrationId: string) => Promise<void>
86
- readonly logger: BackfillLogger
87
- }
88
-
89
- export async function runIntegrationIdBackfill(
90
- deps: IntegrationIdBackfillDeps,
91
- ): Promise<{ stamped: number }> {
92
- const [integrations, devices] = await Promise.all([deps.listIntegrations(), deps.listDevices()])
93
- const stamps = planIntegrationIdBackfill(integrations, devices)
94
- let stamped = 0
95
- for (const stamp of stamps) {
96
- try {
97
- await deps.setIntegrationId(stamp.deviceId, stamp.integrationId)
98
- stamped++
99
- } catch (err) {
100
- deps.logger.warn('integrationId backfill: stamp failed', {
101
- deviceId: stamp.deviceId,
102
- integrationId: stamp.integrationId,
103
- error: err instanceof Error ? err.message : String(err),
104
- })
105
- }
106
- }
107
- if (stamped > 0) deps.logger.info('integrationId backfill complete', { stamped })
108
- return { stamped }
109
- }
@@ -1,105 +0,0 @@
1
- import { randomUUID } from 'node:crypto'
2
- import { readPendingRestart } from '@camstack/kernel'
3
- import { EventCategory } from '@camstack/types'
4
- import type { IScopedLogger, PendingRestartMarkerPayload } from '@camstack/types'
5
- import { AddonRegistryService } from '../core/addon/addon-registry.service'
6
- import { EventBusService } from '../core/events/event-bus.service'
7
- import { LoggingService } from '../core/logging/logging.service'
8
-
9
- export interface PostBootContext {
10
- readonly port: number
11
- readonly host: string
12
- readonly dataPath: string
13
- readonly trpcRegistered: boolean
14
- }
15
-
16
- export class PostBootService {
17
- private readonly logger: IScopedLogger
18
-
19
- /**
20
- * Holds the marker that fired `system.restart-completed` on this boot
21
- * for `LAST_RESTART_RETENTION_MS`. Lets clients query the marker
22
- * after they reconnect — the live event itself is emitted before the
23
- * WS resubscribe has a chance to land.
24
- */
25
- private static lastRestart: { payload: PendingRestartMarkerPayload; expiresAt: number } | null =
26
- null
27
-
28
- /** How long the last-restart marker stays queryable after boot (5 min). */
29
- static readonly LAST_RESTART_RETENTION_MS = 5 * 60_000
30
-
31
- constructor(
32
- _addonRegistry: AddonRegistryService,
33
- private readonly eventBus: EventBusService,
34
- loggingService: LoggingService,
35
- ) {
36
- this.logger = loggingService.createLogger('PostBoot')
37
- }
38
-
39
- /**
40
- * Snapshot of the most-recent restart marker, or `null` after the
41
- * retention window. The hub's `addons.getLastRestart` cap method
42
- * delegates here.
43
- */
44
- static getLastRestart(): PendingRestartMarkerPayload | null {
45
- const entry = PostBootService.lastRestart
46
- if (entry === null) return null
47
- if (entry.expiresAt <= Date.now()) {
48
- PostBootService.lastRestart = null
49
- return null
50
- }
51
- return entry.payload
52
- }
53
-
54
- async run(context: PostBootContext): Promise<void> {
55
- const { port, host, dataPath, trpcRegistered } = context
56
-
57
- // Device stream wiring moved into `addon-stream-broker` — the addon
58
- // does its own initial sync against `ctx.deviceRegistry` plus listens
59
- // to `DeviceRegistered` events for future registrations. No kernel
60
- // orchestration shim needed here anymore.
61
-
62
- // Emit system.boot event
63
- this.eventBus.emit({
64
- id: randomUUID(),
65
- timestamp: new Date(),
66
- source: { type: 'core', id: 'system' },
67
- category: EventCategory.SystemBoot,
68
- data: { port, host, trpcRegistered, dataPath },
69
- })
70
-
71
- // If the previous shutdown was driven by RestartCoordinator, emit a
72
- // `system.restart-completed` event so the admin UI can surface a
73
- // success toast describing what changed (framework update, manual
74
- // restart, …). `readPendingRestart` clears the marker atomically so
75
- // we never re-fire on a crash-loop boot.
76
- this.emitRestartCompletedIfPending(dataPath)
77
- }
78
-
79
- private emitRestartCompletedIfPending(dataDir: string): void {
80
- const marker = readPendingRestart(dataDir)
81
- if (marker === null) return
82
-
83
- const payload: PendingRestartMarkerPayload = {
84
- kind: marker.kind,
85
- requestedAt: marker.requestedAt,
86
- ...(marker.packageName !== undefined ? { packageName: marker.packageName } : {}),
87
- ...(marker.fromVersion !== undefined ? { fromVersion: marker.fromVersion } : {}),
88
- ...(marker.toVersion !== undefined ? { toVersion: marker.toVersion } : {}),
89
- ...(marker.requestedBy !== undefined ? { requestedBy: marker.requestedBy } : {}),
90
- }
91
-
92
- this.logger.info('Restart completed', { meta: payload })
93
- PostBootService.lastRestart = {
94
- payload,
95
- expiresAt: Date.now() + PostBootService.LAST_RESTART_RETENTION_MS,
96
- }
97
- this.eventBus.emit({
98
- id: randomUUID(),
99
- timestamp: new Date(),
100
- source: { type: 'core', id: 'system' },
101
- category: EventCategory.SystemRestartCompleted,
102
- data: payload,
103
- })
104
- }
105
- }
@@ -1,62 +0,0 @@
1
- // server/backend/src/core/addon/__tests__/addon-registry-capability.test.ts
2
- import { describe, it, expect, vi, beforeEach } from 'vitest'
3
- import { CapabilityRegistry } from '@camstack/kernel'
4
- import type { IScopedLogger } from '@camstack/types'
5
-
6
- function createMockLogger(): IScopedLogger {
7
- return {
8
- error: vi.fn(),
9
- warn: vi.fn(),
10
- info: vi.fn(),
11
- debug: vi.fn(),
12
- child: vi.fn().mockReturnThis(),
13
- }
14
- }
15
-
16
- describe('AddonRegistryService -- CapabilityRegistry integration', () => {
17
- let registry: CapabilityRegistry
18
-
19
- beforeEach(() => {
20
- registry = new CapabilityRegistry(createMockLogger())
21
- registry.ready()
22
- })
23
-
24
- it('singleton provider is available after registerProvider', () => {
25
- registry.declareCapability({ name: 'storage', scope: 'system', mode: 'singleton', methods: {} })
26
-
27
- const mockStorageProvider = { getLocation: vi.fn() }
28
- registry.registerProvider('storage', 'sqlite-storage', mockStorageProvider)
29
-
30
- expect(registry.getSingleton('storage')).toBe(mockStorageProvider)
31
- })
32
-
33
- it('streaming-engine singleton wired after provider registers', () => {
34
- registry.declareCapability({
35
- name: 'streaming-engine',
36
- scope: 'system',
37
- mode: 'singleton',
38
- methods: {},
39
- })
40
-
41
- const mockEngine = { initialize: vi.fn() }
42
- registry.registerProvider('streaming-engine', 'go2rtc', mockEngine)
43
-
44
- expect(registry.getSingleton('streaming-engine')).toBe(mockEngine)
45
- })
46
-
47
- it('log-destination collection receives all providers', () => {
48
- registry.declareCapability({
49
- name: 'log-destination',
50
- scope: 'system',
51
- mode: 'collection',
52
- methods: {},
53
- })
54
-
55
- const dest1 = { id: 'winston' }
56
- const dest2 = { id: 'loki' }
57
- registry.registerProvider('log-destination', 'winston-logging', dest1)
58
- registry.registerProvider('log-destination', 'loki-logging', dest2)
59
-
60
- expect(registry.getCollection('log-destination')).toEqual([dest1, dest2])
61
- })
62
- })
@@ -1,62 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
-
3
- import { overlayDeclaration } from '../addon-row-manifest.js'
4
-
5
- /**
6
- * Regression: after the HA broker rework, redeploying provider-homeassistant
7
- * (broker + device-adoption) left the integration picker without Home
8
- * Assistant. `loadNewAddons` refreshed `entry.declaration` to the new caps but
9
- * `listAddons` built its row manifest from the STALE `entry.addon.manifest`
10
- * (still [broker, ha-discovery]), so `getAvailableTypes` filtered HA out.
11
- */
12
- interface TestManifest {
13
- id: string
14
- name: string
15
- icon?: string
16
- brokerKind?: string
17
- capabilities?: ReadonlyArray<{ name: string }>
18
- }
19
-
20
- describe('overlayDeclaration — listAddons row manifest freshness', () => {
21
- const base = { id: 'provider-homeassistant', name: 'Home Assistant' }
22
-
23
- it('prefers the fresh declaration capabilities over the stale instance manifest', () => {
24
- const instanceManifest: TestManifest = {
25
- ...base,
26
- capabilities: [{ name: 'broker' }, { name: 'ha-discovery' }],
27
- }
28
- const declaration: Partial<TestManifest> = {
29
- capabilities: [{ name: 'broker' }, { name: 'device-adoption' }],
30
- brokerKind: 'home-assistant',
31
- }
32
-
33
- const merged = overlayDeclaration(instanceManifest, declaration)
34
-
35
- expect(merged.capabilities).toEqual([{ name: 'broker' }, { name: 'device-adoption' }])
36
- expect(merged.brokerKind).toBe('home-assistant')
37
- })
38
-
39
- it('keeps the instance manifest when no fresh declaration exists', () => {
40
- const instanceManifest: TestManifest = { ...base, capabilities: [{ name: 'broker' }] }
41
-
42
- const merged = overlayDeclaration(instanceManifest, undefined)
43
-
44
- expect(merged.capabilities).toEqual([{ name: 'broker' }])
45
- })
46
-
47
- it('fills gaps from the instance manifest for keys the declaration omits', () => {
48
- const instanceManifest: TestManifest = {
49
- ...base,
50
- icon: 'assets/icon.svg',
51
- capabilities: [{ name: 'broker' }],
52
- }
53
- const declaration: Partial<TestManifest> = {
54
- capabilities: [{ name: 'broker' }, { name: 'device-adoption' }],
55
- }
56
-
57
- const merged = overlayDeclaration(instanceManifest, declaration)
58
-
59
- expect(merged.icon).toBe('assets/icon.svg')
60
- expect(merged.capabilities).toContainEqual({ name: 'device-adoption' })
61
- })
62
- })