@camstack/server 0.1.3

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 (133) hide show
  1. package/.env.example +17 -0
  2. package/package.json +55 -0
  3. package/src/__tests__/addon-install-e2e.test.ts +75 -0
  4. package/src/__tests__/addon-pages-e2e.test.ts +178 -0
  5. package/src/__tests__/addon-route-session.test.ts +17 -0
  6. package/src/__tests__/addon-settings-router.spec.ts +62 -0
  7. package/src/__tests__/addon-upload.spec.ts +355 -0
  8. package/src/__tests__/agent-registry.spec.ts +162 -0
  9. package/src/__tests__/agent-status-page.spec.ts +84 -0
  10. package/src/__tests__/auth-session-cookie.test.ts +21 -0
  11. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +23 -0
  12. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +64 -0
  13. package/src/__tests__/cap-routers/_meta.spec.ts +200 -0
  14. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +106 -0
  15. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +142 -0
  16. package/src/__tests__/cap-routers/harness.ts +159 -0
  17. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +119 -0
  18. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +66 -0
  19. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +135 -0
  20. package/src/__tests__/cap-routers/settings-store.router.spec.ts +247 -0
  21. package/src/__tests__/capability-e2e.test.ts +386 -0
  22. package/src/__tests__/cli-e2e.test.ts +129 -0
  23. package/src/__tests__/core-cap-bridge.spec.ts +89 -0
  24. package/src/__tests__/embedded-deps-e2e.test.ts +109 -0
  25. package/src/__tests__/event-bus-proxy-router.spec.ts +72 -0
  26. package/src/__tests__/fixtures/mock-analysis-addon-a.ts +37 -0
  27. package/src/__tests__/fixtures/mock-analysis-addon-b.ts +37 -0
  28. package/src/__tests__/fixtures/mock-log-addon.ts +37 -0
  29. package/src/__tests__/fixtures/mock-storage-addon.ts +40 -0
  30. package/src/__tests__/framework-allowlist.spec.ts +95 -0
  31. package/src/__tests__/https-e2e.test.ts +118 -0
  32. package/src/__tests__/lifecycle-e2e.test.ts +140 -0
  33. package/src/__tests__/live-events-subscription.spec.ts +150 -0
  34. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +229 -0
  35. package/src/__tests__/oauth2-account-linking.spec.ts +736 -0
  36. package/src/__tests__/post-boot-restart.spec.ts +161 -0
  37. package/src/__tests__/singleton-contention.test.ts +487 -0
  38. package/src/__tests__/streaming-diagnostic.test.ts +512 -0
  39. package/src/__tests__/streaming-scale.test.ts +280 -0
  40. package/src/agent-status-page.ts +121 -0
  41. package/src/api/__tests__/addons-custom.spec.ts +134 -0
  42. package/src/api/__tests__/capabilities.router.test.ts +47 -0
  43. package/src/api/addon-upload.ts +472 -0
  44. package/src/api/addons-custom.router.ts +100 -0
  45. package/src/api/auth-whoami.ts +99 -0
  46. package/src/api/bridge-addons.router.ts +120 -0
  47. package/src/api/capabilities.router.ts +226 -0
  48. package/src/api/core/__tests__/auth-router-totp.spec.ts +256 -0
  49. package/src/api/core/addon-settings.router.ts +124 -0
  50. package/src/api/core/agents.router.ts +87 -0
  51. package/src/api/core/auth.router.ts +303 -0
  52. package/src/api/core/cap-providers.ts +993 -0
  53. package/src/api/core/capabilities.router.ts +119 -0
  54. package/src/api/core/collection-preference.ts +40 -0
  55. package/src/api/core/event-bus-proxy.router.ts +45 -0
  56. package/src/api/core/hwaccel.router.ts +81 -0
  57. package/src/api/core/live-events.router.ts +60 -0
  58. package/src/api/core/logs.router.ts +162 -0
  59. package/src/api/core/notifications.router.ts +65 -0
  60. package/src/api/core/repl.router.ts +41 -0
  61. package/src/api/core/settings-backend.router.ts +142 -0
  62. package/src/api/core/stream-probe.router.ts +57 -0
  63. package/src/api/core/system-events.router.ts +116 -0
  64. package/src/api/health/health.routes.ts +123 -0
  65. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +52 -0
  66. package/src/api/oauth2/consent-page.ts +42 -0
  67. package/src/api/oauth2/oauth2-routes.ts +248 -0
  68. package/src/api/trpc/__tests__/scope-access-device.spec.ts +223 -0
  69. package/src/api/trpc/__tests__/scope-access.spec.ts +107 -0
  70. package/src/api/trpc/cap-mount-helpers.ts +225 -0
  71. package/src/api/trpc/core-cap-bridge.ts +152 -0
  72. package/src/api/trpc/generated-cap-mounts.ts +707 -0
  73. package/src/api/trpc/generated-cap-routers.ts +6340 -0
  74. package/src/api/trpc/scope-access.ts +110 -0
  75. package/src/api/trpc/trpc.context.ts +255 -0
  76. package/src/api/trpc/trpc.middleware.ts +140 -0
  77. package/src/api/trpc/trpc.router.ts +275 -0
  78. package/src/auth/session-cookie.ts +44 -0
  79. package/src/boot/boot-config.ts +278 -0
  80. package/src/boot/post-boot.service.ts +103 -0
  81. package/src/core/addon/__tests__/addon-registry-capability.test.ts +53 -0
  82. package/src/core/addon/addon-package.service.ts +1684 -0
  83. package/src/core/addon/addon-registry.service.ts +2926 -0
  84. package/src/core/addon/addon-search.service.ts +90 -0
  85. package/src/core/addon/addon-settings-provider.ts +276 -0
  86. package/src/core/addon/addon.tokens.ts +2 -0
  87. package/src/core/addon-bridge/addon-bridge.service.ts +125 -0
  88. package/src/core/addon-pages/addon-pages.service.spec.ts +117 -0
  89. package/src/core/addon-pages/addon-pages.service.ts +80 -0
  90. package/src/core/addon-widgets/addon-widgets.service.ts +92 -0
  91. package/src/core/agent/agent-registry.service.ts +507 -0
  92. package/src/core/auth/auth.service.spec.ts +88 -0
  93. package/src/core/auth/auth.service.ts +8 -0
  94. package/src/core/capability/capability.service.ts +57 -0
  95. package/src/core/config/config.schema.ts +3 -0
  96. package/src/core/config/config.service.spec.ts +175 -0
  97. package/src/core/config/config.service.ts +7 -0
  98. package/src/core/events/event-bus.service.spec.ts +212 -0
  99. package/src/core/events/event-bus.service.ts +85 -0
  100. package/src/core/feature/feature.service.spec.ts +96 -0
  101. package/src/core/feature/feature.service.ts +8 -0
  102. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +168 -0
  103. package/src/core/lifecycle/lifecycle-state-machine.ts +3 -0
  104. package/src/core/logging/log-ring-buffer.ts +3 -0
  105. package/src/core/logging/logging.service.spec.ts +247 -0
  106. package/src/core/logging/logging.service.ts +129 -0
  107. package/src/core/logging/scoped-logger.ts +3 -0
  108. package/src/core/moleculer/moleculer.service.ts +612 -0
  109. package/src/core/network/network-quality.service.spec.ts +47 -0
  110. package/src/core/network/network-quality.service.ts +5 -0
  111. package/src/core/notification/notification-wrapper.service.ts +36 -0
  112. package/src/core/notification/toast-wrapper.service.ts +31 -0
  113. package/src/core/provider/provider.tokens.ts +1 -0
  114. package/src/core/repl/repl-engine.service.spec.ts +417 -0
  115. package/src/core/repl/repl-engine.service.ts +156 -0
  116. package/src/core/storage/fs-storage-backend.spec.ts +70 -0
  117. package/src/core/storage/fs-storage-backend.ts +3 -0
  118. package/src/core/storage/settings-store.spec.ts +213 -0
  119. package/src/core/storage/settings-store.ts +2 -0
  120. package/src/core/storage/sql-schema.spec.ts +140 -0
  121. package/src/core/storage/sql-schema.ts +3 -0
  122. package/src/core/storage/storage-location-manager.spec.ts +121 -0
  123. package/src/core/storage/storage-location-manager.ts +3 -0
  124. package/src/core/storage/storage.service.spec.ts +73 -0
  125. package/src/core/storage/storage.service.ts +3 -0
  126. package/src/core/streaming/stream-probe.service.ts +212 -0
  127. package/src/core/topology/topology-emitter.service.ts +101 -0
  128. package/src/launcher.ts +309 -0
  129. package/src/main.ts +1049 -0
  130. package/src/manual-boot.ts +322 -0
  131. package/tsconfig.build.json +8 -0
  132. package/tsconfig.json +21 -0
  133. package/vitest.config.ts +26 -0
@@ -0,0 +1,275 @@
1
+ import { trpcRouter } from './trpc.middleware'
2
+ // All capability tRPC routers are auto-mounted via `mountAllCaps()` below.
3
+ // Only the OVERRIDES (service-backed providers, custom collection
4
+ // dispatch logic, hub-only remote-proxy caps) and the CORE (non-cap)
5
+ // routers are wired manually in this file. Tracking comment for the
6
+ // historical migration index (every `create<X>Router` that used to live
7
+ // here has been replaced by its codegen'd cap-router counterpart):
8
+ // auth → auth (kept manual for AuthService DI), users/system →
9
+ // `userMgmt`/`system` caps, settings → `system-config` builtin,
10
+ // devices → `deviceManager` cap, integrations → `integrations` cap,
11
+ // streaming → `streamingManagement` cap, events → `eventQuery` cap,
12
+ // logs → kept manual, live → kept manual, processes → `processMgmt`
13
+ // cap, agents → `nodes` cap, sessions → `session` cap, trackMedia /
14
+ // trackTrail → caps, recording → `recordingEngine` cap, network →
15
+ // `networkQuality` cap, addons → `addons` cap, bridgePipeline removed
16
+ // (legacy), detection → `detectionConfig` cap, capabilities → kept
17
+ // manual, update → addons cap, addonPages → cap, notification →
18
+ // `notifications` (kept manual for NotificationService DI),
19
+ // scopedToken → `userManagement`, toast → `toast` cap, inference →
20
+ // `pipelineExecutor` cap, pipeline → `pipelineConfig` cap,
21
+ // systemEvents → kept manual.
22
+ import type { CapabilityRegistry } from '@camstack/kernel'
23
+ import type { InferProvider } from '@camstack/types'
24
+ import {
25
+ pipelineExecutorCapability,
26
+ pipelineRunnerCapability,
27
+ pipelineOrchestratorCapability,
28
+ audioAnalyzerCapability,
29
+ audioCodecCapability,
30
+ platformProbeCapability,
31
+ decoderCapability,
32
+ localNetworkCapability,
33
+ } from '@camstack/types'
34
+ import {
35
+ // The auto-mount covers ~75 caps. The handful re-imported below back
36
+ // the manual overrides that need service-backed providers or custom
37
+ // collection dispatch logic; the auto-mount entry is replaced by the
38
+ // override line via spread precedence.
39
+ createCapRouter_pipelineExecutor,
40
+ createCapRouter_pipelineRunner,
41
+ createCapRouter_pipelineOrchestrator,
42
+ createCapRouter_audioAnalyzer,
43
+ createCapRouter_audioCodec,
44
+ createCapRouter_decoder,
45
+ createCapRouter_platformProbe,
46
+ createCapRouter_localNetwork,
47
+ createCapRouter_snapshotProvider,
48
+ createCapRouter_networkQuality,
49
+ createCapRouter_system,
50
+ createCapRouter_toast,
51
+ createCapRouter_integrations,
52
+ createCapRouter_nodes,
53
+ createCapRouter_addons,
54
+ } from './generated-cap-routers'
55
+ import { mountAllCaps } from './generated-cap-mounts.js'
56
+ import {
57
+ buildSystemProvider,
58
+ buildNetworkQualityProvider,
59
+ buildToastProvider,
60
+ buildNodesProvider,
61
+ buildIntegrationsProvider,
62
+ buildAddonsProvider,
63
+ } from '../core/cap-providers.js'
64
+ import { createAuthRouter } from '../core/auth.router.js'
65
+ import { createAddonSettingsRouter } from '../core/addon-settings.router.js'
66
+ import { createSettingsBackendRouter } from '../core/settings-backend.router.js'
67
+ import { createEventBusProxyRouter } from '../core/event-bus-proxy.router.js'
68
+ import { createReplRouter } from '../core/repl.router.js'
69
+ import { createNotificationsRouter } from '../core/notifications.router.js'
70
+ import { createLogsRouter } from '../core/logs.router.js'
71
+ import { createSystemEventsRouter } from '../core/system-events.router.js'
72
+ import { createLiveEventsRouter } from '../core/live-events.router.js'
73
+ import { createCapabilitiesRouter } from '../core/capabilities.router.js'
74
+ import { createStreamProbeRouter } from '../core/stream-probe.router.js'
75
+ import { createHwAccelRouter } from '../core/hwaccel.router.js'
76
+ import { requireSingleton, firstSupported, anySupports } from './cap-mount-helpers.js'
77
+ import type { AuthService } from '../../core/auth/auth.service'
78
+ import type { ConfigService } from '../../core/config/config.service'
79
+ import type { FeatureService } from '../../core/feature/feature.service'
80
+ import type { LoggingService } from '../../core/logging/logging.service'
81
+ import type { EventBusService } from '../../core/events/event-bus.service'
82
+ import type { AgentRegistryService } from '../../core/agent/agent-registry.service'
83
+ import type { MoleculerService } from '../../core/moleculer/moleculer.service'
84
+ import type { AddonRegistryService } from '../../core/addon/addon-registry.service'
85
+ import type { AddonPackageService } from '../../core/addon/addon-package.service'
86
+ import type { ReplEngineService } from '../../core/repl/repl-engine.service'
87
+ import type { NetworkQualityService } from '../../core/network/network-quality.service'
88
+ import type { AddonBridgeService } from '../../core/addon-bridge/addon-bridge.service'
89
+ import type { NotificationService, ToastService } from '@camstack/core'
90
+ import type { StreamProbeService } from '../../core/streaming/stream-probe.service'
91
+
92
+ export interface RouterServices {
93
+ authService: AuthService
94
+ configService: ConfigService
95
+ featureService: FeatureService
96
+ loggingService: LoggingService
97
+ eventBus: EventBusService
98
+ agentRegistry: AgentRegistryService
99
+ moleculer: MoleculerService
100
+ addonRegistry: AddonRegistryService
101
+ replEngine: ReplEngineService
102
+ networkQualityService: NetworkQualityService
103
+ addonBridge: AddonBridgeService
104
+ addonPackageService: AddonPackageService
105
+ notificationService: NotificationService | null
106
+ toastService: ToastService | null
107
+ capabilityRegistry: CapabilityRegistry | null
108
+ streamProbe: StreamProbeService | null
109
+ }
110
+
111
+ /**
112
+ * Build the AppRouter. Mounts every codegen'd cap router via the auto-
113
+ * mount entrypoint and overrides the handful that need service-backed
114
+ * providers or custom collection dispatch. Non-cap (core) routers ride
115
+ * alongside in the same root object.
116
+ *
117
+ * Override-by-spread: spread `mountAllCaps(services)` first, then the
118
+ * overrides AFTER — the later property wins. The drift guard in
119
+ * `scripts/codegen.ts` ensures every codegen'd `createCapRouter_X` is
120
+ * present in the auto-mount inventory (or explicitly in the legacy
121
+ * skip-list), so the override list below NEVER needs to add a new entry
122
+ * just to mount a new cap — only to swap in a custom provider.
123
+ */
124
+ function buildCapabilityRouters(services: RouterServices) {
125
+ return {
126
+ // ── Auto-mount: every codegen'd cap router with a canonical
127
+ // provider shape. Everything below this line OVERRIDES the
128
+ // auto-mount entry for caps with service-backed providers,
129
+ // custom collection routing, or a hub-only `null` remote proxy.
130
+ ...mountAllCaps(services),
131
+
132
+ // ── Non-cap (core) routers — hand-written, single-impl ──────────
133
+ notifications: createNotificationsRouter(services.notificationService),
134
+ // Raw DB proxy for forked workers to read/write addon store.
135
+ // Workers use ctx.api.addonSettingsRaw.getGlobal.query({...}).
136
+ // NOT the three-level settings gateway — that's the codegen'd
137
+ // `addonSettings` cap router (mounted via auto-mount above).
138
+ addonSettingsRaw: createAddonSettingsRouter(services.configService),
139
+ settingsBackend: createSettingsBackendRouter(() => services.addonRegistry.getSettingsBackend()),
140
+ eventBusProxy: createEventBusProxyRouter(services.eventBus),
141
+ repl: createReplRouter(services.replEngine),
142
+ systemEvents: createSystemEventsRouter(services.eventBus),
143
+ capabilities: createCapabilitiesRouter(services.capabilityRegistry, services.configService),
144
+ logs: createLogsRouter(services.loggingService),
145
+ live: createLiveEventsRouter(services.eventBus, services.addonRegistry),
146
+ // stream-probe — fixed core API (ffprobe wrapper), not a cap.
147
+ streamProbe: createStreamProbeRouter(services.streamProbe),
148
+ // hwaccel — fixed core API, wraps the per-node `$hwaccel` Moleculer
149
+ // service. UI pipeline / NodeDetail pages query per-node to show
150
+ // which hardware backend each agent will use.
151
+ hwaccel: createHwAccelRouter(services.moleculer?.broker ?? null),
152
+ auth: createAuthRouter(services.authService, services.capabilityRegistry),
153
+
154
+ // ── Cap overrides: service-backed providers ─────────────────────
155
+ // These caps don't have an addon registering a provider in the
156
+ // CapabilityRegistry — the provider is built on-demand from
157
+ // backend services. `mountAllCaps` would return `null` for them
158
+ // (registry lookup miss), so we re-mount with `buildXProvider`.
159
+ networkQuality: createCapRouter_networkQuality(
160
+ (_ctx) => buildNetworkQualityProvider(services.networkQualityService),
161
+ ),
162
+ system: createCapRouter_system(
163
+ (_ctx) => buildSystemProvider(services.featureService, services.capabilityRegistry),
164
+ ),
165
+ toast: createCapRouter_toast(
166
+ (ctx) => buildToastProvider(services.toastService, ctx),
167
+ ),
168
+ integrations: createCapRouter_integrations(
169
+ (_ctx) => buildIntegrationsProvider(services.addonRegistry, services.eventBus, services.loggingService),
170
+ ),
171
+ nodes: createCapRouter_nodes(
172
+ (_ctx) => buildNodesProvider(
173
+ services.agentRegistry,
174
+ services.moleculer,
175
+ services.addonRegistry,
176
+ ),
177
+ ),
178
+ addons: createCapRouter_addons(
179
+ (ctx) => buildAddonsProvider(
180
+ services.addonRegistry,
181
+ services.addonPackageService,
182
+ services.loggingService,
183
+ services.moleculer,
184
+ services.configService,
185
+ ctx,
186
+ ),
187
+ ),
188
+
189
+ // ── Cap overrides: cross-node remote-proxy cast ─────────────────
190
+ // These caps' providers have manual interface types that pre-date
191
+ // `InferProvider<typeof xCap>` — structurally identical, nominally
192
+ // distinct. Casting at the override site is cheaper than reworking
193
+ // the provider declarations. Auto-mount can't infer the cast.
194
+ pipelineExecutor: createCapRouter_pipelineExecutor(
195
+ (_ctx) => requireSingleton(services.capabilityRegistry, 'pipeline-executor') as InferProvider<typeof pipelineExecutorCapability> | null,
196
+ (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<typeof pipelineExecutorCapability> | null,
197
+ ),
198
+ pipelineRunner: createCapRouter_pipelineRunner(
199
+ (_ctx) => requireSingleton(services.capabilityRegistry, 'pipeline-runner') as InferProvider<typeof pipelineRunnerCapability> | null,
200
+ (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<typeof pipelineRunnerCapability> | null,
201
+ ),
202
+ pipelineOrchestrator: createCapRouter_pipelineOrchestrator(
203
+ (_ctx) => requireSingleton(services.capabilityRegistry, 'pipeline-orchestrator') as InferProvider<typeof pipelineOrchestratorCapability> | null,
204
+ (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<typeof pipelineOrchestratorCapability> | null,
205
+ ),
206
+ audioAnalyzer: createCapRouter_audioAnalyzer(
207
+ (_ctx) => requireSingleton(services.capabilityRegistry, 'audio-analyzer'),
208
+ (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<typeof audioAnalyzerCapability> | null,
209
+ ),
210
+ audioCodec: createCapRouter_audioCodec(
211
+ (_ctx) => requireSingleton(services.capabilityRegistry, 'audio-codec') as InferProvider<typeof audioCodecCapability> | null,
212
+ (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<typeof audioCodecCapability> | null,
213
+ ),
214
+ decoder: createCapRouter_decoder(
215
+ (_ctx) => requireSingleton(services.capabilityRegistry, 'decoder') as InferProvider<typeof decoderCapability> | null,
216
+ (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<typeof decoderCapability> | null,
217
+ ),
218
+ platformProbe: createCapRouter_platformProbe(
219
+ (_ctx) => requireSingleton(services.capabilityRegistry, 'platform-probe'),
220
+ (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId) as InferProvider<typeof platformProbeCapability> | null,
221
+ ),
222
+
223
+ // ── Cap overrides: hub-only, no remote fallback ─────────────────
224
+ // The cap is intentionally single-node; agents are not directly
225
+ // addressable. Auto-mount would still set up a proxy factory; we
226
+ // explicitly return `null` to short-circuit any cross-node attempt.
227
+ localNetwork: createCapRouter_localNetwork(
228
+ (_ctx) => requireSingleton(services.capabilityRegistry, 'local-network'),
229
+ (_capName, _nodeId) => null as InferProvider<typeof localNetworkCapability> | null,
230
+ ),
231
+
232
+ // ── Cap overrides: collection dispatch (contribution / probe) ──
233
+ // `turn-provider.getTurnServers` is now handled generically by the
234
+ // auto-mount: it's an array-output method on a `collection` cap, so
235
+ // `mountAllCaps` fans it across every enabled provider via
236
+ // `concatCollection` when no `addonId` is supplied. No hand-written
237
+ // override needed.
238
+ //
239
+ // `snapshot-provider.supportsDevice` is an OR across providers;
240
+ // `getSnapshot` picks the first one that claims the device. The
241
+ // generic first-provider resolver from the auto-mount can't model
242
+ // this — we hand-write the probe + fan-out logic.
243
+ snapshotProvider: createCapRouter_snapshotProvider(
244
+ (_ctx) => {
245
+ const reg = services.capabilityRegistry
246
+ if (!reg) return null
247
+ type SnapshotProviderInferred = import('@camstack/types').CapabilityProviderMap['snapshot-provider']
248
+ const providers = reg.getCollection<SnapshotProviderInferred>('snapshot-provider')
249
+ if (!providers || providers.length === 0) return null
250
+ const supportsDevice = anySupports(providers, 'supportsDevice')
251
+ const getSnapshot = firstSupported(providers, 'supportsDevice', 'getSnapshot')
252
+ return { supportsDevice, getSnapshot }
253
+ },
254
+ ),
255
+
256
+ // NOT MOUNTED — legacy provider shapes (positional args / sync
257
+ // returns) that don't match the codegen routers' {input}-object +
258
+ // Promise<T> contract. Tracked by `LEGACY_SHAPE_SKIP` in
259
+ // `generated-cap-mounts.ts` until the provider refactor (task #195):
260
+ // - addon-routes (IAddonRouteProvider: getRoutes sync)
261
+ // - auth-provider (IAuthProvider: positional credentials)
262
+ // - log-destination (ILogDestination: positional + extra lifecycle)
263
+ // - restreamer (IRestreamer: registerDevice positional)
264
+ // - streaming-engine (IStreamingEngine: registerStream positional)
265
+ // - webrtc (IWebRtcProvider: missing hasAdaptiveBitrate)
266
+ }
267
+ }
268
+
269
+ export function buildAppRouter(services: RouterServices) {
270
+ return trpcRouter({
271
+ ...buildCapabilityRouters(services),
272
+ })
273
+ }
274
+
275
+ export type AppRouter = ReturnType<typeof buildAppRouter>
@@ -0,0 +1,44 @@
1
+ /** Browser session cookie carrying the hub JWT. Set by POST /api/auth/session
2
+ * after a tRPC login; read by the addon-route catch-all for `authenticated`
3
+ * routes hit by a plain browser navigation. */
4
+ export const SESSION_COOKIE = 'camstack_session'
5
+
6
+ interface CookieSpec {
7
+ readonly name: string
8
+ readonly value: string
9
+ readonly options: {
10
+ httpOnly: boolean
11
+ sameSite: 'lax'
12
+ secure: boolean
13
+ path: string
14
+ maxAge: number
15
+ }
16
+ }
17
+
18
+ export function buildSessionCookie(token: string, ttlSec: number): CookieSpec {
19
+ return {
20
+ name: SESSION_COOKIE,
21
+ value: token,
22
+ options: { httpOnly: true, sameSite: 'lax', secure: true, path: '/', maxAge: ttlSec },
23
+ }
24
+ }
25
+
26
+ export function clearSessionCookie(): CookieSpec {
27
+ return {
28
+ name: SESSION_COOKIE,
29
+ value: '',
30
+ options: { httpOnly: true, sameSite: 'lax', secure: true, path: '/', maxAge: 0 },
31
+ }
32
+ }
33
+
34
+ /** A browser navigation we can bounce to the login page: a top-level GET
35
+ * that wants HTML. Anything else (API call, POST, non-HTML) keeps the
36
+ * 401 behavior so programmatic clients get a clean error. */
37
+ export function shouldRedirectToLogin(method: string, accept: string | undefined): boolean {
38
+ return method === 'GET' && typeof accept === 'string' && accept.includes('text/html')
39
+ }
40
+
41
+ /** Build the `/login?next=…` URL for an unauthenticated browser request. */
42
+ export function loginRedirectUrl(originalUrl: string): string {
43
+ return `/login?next=${encodeURIComponent(originalUrl)}`
44
+ }
@@ -0,0 +1,278 @@
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(
108
+ `[Phase1] Updated config.yaml with missing bootstrap defaults`,
109
+ );
110
+ } catch (err) {
111
+ console.warn(`[Phase1] Could not update config.yaml:`, err);
112
+ }
113
+ }
114
+ console.log(`[Phase1] Loaded bootstrap config from: ${configPath}`);
115
+ } else {
116
+ console.log(
117
+ `[Phase1] Config file not found at: ${configPath} — writing defaults`,
118
+ );
119
+ const defaults = { ...CONFIG_DEFAULTS };
120
+ try {
121
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
122
+ const tmpPath = `${configPath}.tmp`;
123
+ fs.writeFileSync(
124
+ tmpPath,
125
+ yaml.dump(defaults, { lineWidth: 120, indent: 2, quotingType: '"' }),
126
+ "utf-8",
127
+ );
128
+ fs.renameSync(tmpPath, configPath);
129
+ console.log(`[Phase1] Default config.yaml written to: ${configPath}`);
130
+ } catch (err) {
131
+ console.warn(`[Phase1] Could not write default config.yaml:`, err);
132
+ }
133
+ raw = defaults;
134
+ }
135
+
136
+ // Apply env var overrides for bootstrap keys
137
+ for (const [envKey, configPath_] of Object.entries(ENV_VAR_MAP)) {
138
+ const envValue = process.env[envKey];
139
+ if (envValue === undefined || envValue === "") continue;
140
+ const coerced: unknown =
141
+ configPath_ === "server.port"
142
+ ? Number(envValue)
143
+ : configPath_ === "tls.enabled"
144
+ ? envValue === "true"
145
+ : envValue;
146
+ raw = setNested(raw, configPath_, coerced);
147
+ console.log(`[Phase1] Env override: ${envKey} → ${configPath_}`);
148
+ }
149
+
150
+ return bootstrapSchema.parse(raw);
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // autoGenerateJwtSecret
155
+ // ---------------------------------------------------------------------------
156
+
157
+ /**
158
+ * If `jwtSecret` is null in the parsed config, generate a random secret,
159
+ * persist it to the YAML file, and return an updated config.
160
+ */
161
+ export function autoGenerateJwtSecret(
162
+ configPath: string,
163
+ bootstrapConfig: BootstrapConfig,
164
+ ): BootstrapConfig {
165
+ if (bootstrapConfig.auth.jwtSecret !== null) {
166
+ return bootstrapConfig;
167
+ }
168
+
169
+ const secret = randomBytes(32).toString("hex");
170
+ console.log(
171
+ "[Phase1] jwtSecret is null — auto-generating and writing to config.yaml",
172
+ );
173
+
174
+ let raw: Record<string, unknown> = {};
175
+ if (fs.existsSync(configPath)) {
176
+ raw = asJsonObject(yaml.load(fs.readFileSync(configPath, "utf-8"))) ?? {};
177
+ }
178
+
179
+ const authSection = asJsonObject(raw.auth) ?? {};
180
+ raw.auth = { ...authSection, jwtSecret: secret };
181
+
182
+ const tmpPath = `${configPath}.tmp`;
183
+ try {
184
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
185
+ fs.writeFileSync(
186
+ tmpPath,
187
+ yaml.dump(raw, { lineWidth: 120, indent: 2, quotingType: '"' }),
188
+ "utf-8",
189
+ );
190
+ fs.renameSync(tmpPath, configPath);
191
+ } catch (err) {
192
+ console.warn(
193
+ "[Phase1] Could not write auto-generated jwtSecret to config.yaml:",
194
+ err,
195
+ );
196
+ }
197
+
198
+ return {
199
+ ...bootstrapConfig,
200
+ auth: { ...bootstrapConfig.auth, jwtSecret: secret },
201
+ };
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // setupInfra
206
+ // ---------------------------------------------------------------------------
207
+
208
+ /**
209
+ * Phase 2: Create StorageLocationManager, ensure directories, configure TLS.
210
+ * Returns the full InfraContext needed by subsequent boot phases.
211
+ */
212
+ export async function setupInfra(
213
+ configPath: string,
214
+ bootstrapConfig: BootstrapConfig,
215
+ ): Promise<InfraContext> {
216
+ // Auto-generate jwtSecret if not set
217
+ const config = autoGenerateJwtSecret(configPath, bootstrapConfig);
218
+
219
+ const dataPath = path.resolve(config.server.dataPath);
220
+ const port = config.server.port;
221
+ const host = config.server.host;
222
+
223
+ console.log(
224
+ `[Phase1] Bootstrap: port=${port}, host=${host}, dataPath=${dataPath}`,
225
+ );
226
+
227
+ // --- Phase 2: Init StorageLocationManager → ensure all dirs exist ---
228
+ console.log("[Phase2] Initializing storage locations…");
229
+ const locationManager = new StorageLocationManager(dataPath);
230
+ await locationManager.initializeDefaults();
231
+
232
+ const locationStatus = locationManager.getStatus();
233
+ for (const { name, available, path: locPath } of locationStatus) {
234
+ console.log(
235
+ `[Phase2] Location "${name}": ${available ? "OK" : "UNAVAILABLE"} → ${locPath}`,
236
+ );
237
+ }
238
+
239
+ // --- Phase 2c: TLS certificate setup ---
240
+ let tlsOptions: { key: Buffer; cert: Buffer } | undefined;
241
+
242
+ if (config.tls.enabled) {
243
+ // Use require() instead of import() — the ESM build of @camstack/core has
244
+ // broken chunks with require("fs") when leaked .js files exist in core/src/.
245
+ // CJS build works correctly and tsx supports require().
246
+ const core = require("@camstack/core") as typeof import("@camstack/core");
247
+ const { ensureTlsCert, loadTlsCert } = core;
248
+ if (config.tls.certPath && config.tls.keyPath) {
249
+ // User-provided cert
250
+ console.log(
251
+ `[Phase2c] Loading custom TLS cert from ${config.tls.certPath}`,
252
+ );
253
+ const pair = loadTlsCert(config.tls.certPath, config.tls.keyPath);
254
+ tlsOptions = { key: pair.key, cert: pair.cert };
255
+ } else {
256
+ // Auto-generate self-signed
257
+ const tlsResult = await ensureTlsCert(dataPath);
258
+ if (tlsResult.generated) {
259
+ console.log(
260
+ `[Phase2c] Generated self-signed TLS cert at ${tlsResult.certPath}`,
261
+ );
262
+ } else {
263
+ console.log(
264
+ `[Phase2c] Using existing TLS cert at ${tlsResult.certPath}`,
265
+ );
266
+ }
267
+ const pair = loadTlsCert(tlsResult.certPath, tlsResult.keyPath);
268
+ tlsOptions = { key: pair.key, cert: pair.cert };
269
+ }
270
+ }
271
+
272
+ return {
273
+ bootstrapConfig: config,
274
+ dataPath,
275
+ locationManager,
276
+ tlsOptions,
277
+ };
278
+ }