@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.
- package/.env.example +17 -0
- package/package.json +55 -0
- package/src/__tests__/addon-install-e2e.test.ts +75 -0
- package/src/__tests__/addon-pages-e2e.test.ts +178 -0
- package/src/__tests__/addon-route-session.test.ts +17 -0
- package/src/__tests__/addon-settings-router.spec.ts +62 -0
- package/src/__tests__/addon-upload.spec.ts +355 -0
- package/src/__tests__/agent-registry.spec.ts +162 -0
- package/src/__tests__/agent-status-page.spec.ts +84 -0
- package/src/__tests__/auth-session-cookie.test.ts +21 -0
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +23 -0
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +64 -0
- package/src/__tests__/cap-routers/_meta.spec.ts +200 -0
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +106 -0
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +142 -0
- package/src/__tests__/cap-routers/harness.ts +159 -0
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +119 -0
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +66 -0
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +135 -0
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +247 -0
- package/src/__tests__/capability-e2e.test.ts +386 -0
- package/src/__tests__/cli-e2e.test.ts +129 -0
- package/src/__tests__/core-cap-bridge.spec.ts +89 -0
- package/src/__tests__/embedded-deps-e2e.test.ts +109 -0
- package/src/__tests__/event-bus-proxy-router.spec.ts +72 -0
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +37 -0
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +37 -0
- package/src/__tests__/fixtures/mock-log-addon.ts +37 -0
- package/src/__tests__/fixtures/mock-storage-addon.ts +40 -0
- package/src/__tests__/framework-allowlist.spec.ts +95 -0
- package/src/__tests__/https-e2e.test.ts +118 -0
- package/src/__tests__/lifecycle-e2e.test.ts +140 -0
- package/src/__tests__/live-events-subscription.spec.ts +150 -0
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +229 -0
- package/src/__tests__/oauth2-account-linking.spec.ts +736 -0
- package/src/__tests__/post-boot-restart.spec.ts +161 -0
- package/src/__tests__/singleton-contention.test.ts +487 -0
- package/src/__tests__/streaming-diagnostic.test.ts +512 -0
- package/src/__tests__/streaming-scale.test.ts +280 -0
- package/src/agent-status-page.ts +121 -0
- package/src/api/__tests__/addons-custom.spec.ts +134 -0
- package/src/api/__tests__/capabilities.router.test.ts +47 -0
- package/src/api/addon-upload.ts +472 -0
- package/src/api/addons-custom.router.ts +100 -0
- package/src/api/auth-whoami.ts +99 -0
- package/src/api/bridge-addons.router.ts +120 -0
- package/src/api/capabilities.router.ts +226 -0
- package/src/api/core/__tests__/auth-router-totp.spec.ts +256 -0
- package/src/api/core/addon-settings.router.ts +124 -0
- package/src/api/core/agents.router.ts +87 -0
- package/src/api/core/auth.router.ts +303 -0
- package/src/api/core/cap-providers.ts +993 -0
- package/src/api/core/capabilities.router.ts +119 -0
- package/src/api/core/collection-preference.ts +40 -0
- package/src/api/core/event-bus-proxy.router.ts +45 -0
- package/src/api/core/hwaccel.router.ts +81 -0
- package/src/api/core/live-events.router.ts +60 -0
- package/src/api/core/logs.router.ts +162 -0
- package/src/api/core/notifications.router.ts +65 -0
- package/src/api/core/repl.router.ts +41 -0
- package/src/api/core/settings-backend.router.ts +142 -0
- package/src/api/core/stream-probe.router.ts +57 -0
- package/src/api/core/system-events.router.ts +116 -0
- package/src/api/health/health.routes.ts +123 -0
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +52 -0
- package/src/api/oauth2/consent-page.ts +42 -0
- package/src/api/oauth2/oauth2-routes.ts +248 -0
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +223 -0
- package/src/api/trpc/__tests__/scope-access.spec.ts +107 -0
- package/src/api/trpc/cap-mount-helpers.ts +225 -0
- package/src/api/trpc/core-cap-bridge.ts +152 -0
- package/src/api/trpc/generated-cap-mounts.ts +707 -0
- package/src/api/trpc/generated-cap-routers.ts +6340 -0
- package/src/api/trpc/scope-access.ts +110 -0
- package/src/api/trpc/trpc.context.ts +255 -0
- package/src/api/trpc/trpc.middleware.ts +140 -0
- package/src/api/trpc/trpc.router.ts +275 -0
- package/src/auth/session-cookie.ts +44 -0
- package/src/boot/boot-config.ts +278 -0
- package/src/boot/post-boot.service.ts +103 -0
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +53 -0
- package/src/core/addon/addon-package.service.ts +1684 -0
- package/src/core/addon/addon-registry.service.ts +2926 -0
- package/src/core/addon/addon-search.service.ts +90 -0
- package/src/core/addon/addon-settings-provider.ts +276 -0
- package/src/core/addon/addon.tokens.ts +2 -0
- package/src/core/addon-bridge/addon-bridge.service.ts +125 -0
- package/src/core/addon-pages/addon-pages.service.spec.ts +117 -0
- package/src/core/addon-pages/addon-pages.service.ts +80 -0
- package/src/core/addon-widgets/addon-widgets.service.ts +92 -0
- package/src/core/agent/agent-registry.service.ts +507 -0
- package/src/core/auth/auth.service.spec.ts +88 -0
- package/src/core/auth/auth.service.ts +8 -0
- package/src/core/capability/capability.service.ts +57 -0
- package/src/core/config/config.schema.ts +3 -0
- package/src/core/config/config.service.spec.ts +175 -0
- package/src/core/config/config.service.ts +7 -0
- package/src/core/events/event-bus.service.spec.ts +212 -0
- package/src/core/events/event-bus.service.ts +85 -0
- package/src/core/feature/feature.service.spec.ts +96 -0
- package/src/core/feature/feature.service.ts +8 -0
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +168 -0
- package/src/core/lifecycle/lifecycle-state-machine.ts +3 -0
- package/src/core/logging/log-ring-buffer.ts +3 -0
- package/src/core/logging/logging.service.spec.ts +247 -0
- package/src/core/logging/logging.service.ts +129 -0
- package/src/core/logging/scoped-logger.ts +3 -0
- package/src/core/moleculer/moleculer.service.ts +612 -0
- package/src/core/network/network-quality.service.spec.ts +47 -0
- package/src/core/network/network-quality.service.ts +5 -0
- package/src/core/notification/notification-wrapper.service.ts +36 -0
- package/src/core/notification/toast-wrapper.service.ts +31 -0
- package/src/core/provider/provider.tokens.ts +1 -0
- package/src/core/repl/repl-engine.service.spec.ts +417 -0
- package/src/core/repl/repl-engine.service.ts +156 -0
- package/src/core/storage/fs-storage-backend.spec.ts +70 -0
- package/src/core/storage/fs-storage-backend.ts +3 -0
- package/src/core/storage/settings-store.spec.ts +213 -0
- package/src/core/storage/settings-store.ts +2 -0
- package/src/core/storage/sql-schema.spec.ts +140 -0
- package/src/core/storage/sql-schema.ts +3 -0
- package/src/core/storage/storage-location-manager.spec.ts +121 -0
- package/src/core/storage/storage-location-manager.ts +3 -0
- package/src/core/storage/storage.service.spec.ts +73 -0
- package/src/core/storage/storage.service.ts +3 -0
- package/src/core/streaming/stream-probe.service.ts +212 -0
- package/src/core/topology/topology-emitter.service.ts +101 -0
- package/src/launcher.ts +309 -0
- package/src/main.ts +1049 -0
- package/src/manual-boot.ts +322 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +21 -0
- 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
|
+
}
|