@camstack/server 0.1.6 → 0.1.7

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/addon-upload.spec.ts +58 -0
  3. package/src/__tests__/bulk-update-coordinator.spec.ts +286 -0
  4. package/src/__tests__/cap-ownership-authority.spec.ts +400 -0
  5. package/src/__tests__/cap-providers-bulk-update.spec.ts +388 -0
  6. package/src/__tests__/cap-route-adapter.spec.ts +289 -0
  7. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +123 -0
  8. package/src/__tests__/cap-routers/capabilities-node.spec.ts +55 -0
  9. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +30 -0
  10. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +249 -0
  11. package/src/__tests__/framework-installer-defer-restart.spec.ts +165 -0
  12. package/src/__tests__/moleculer/uds-readiness.spec.ts +143 -0
  13. package/src/__tests__/moleculer/uds-topology.spec.ts +390 -0
  14. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +123 -0
  15. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +39 -4
  16. package/src/__tests__/native-cap-route.spec.ts +404 -0
  17. package/src/__tests__/oauth2-account-linking.spec.ts +85 -0
  18. package/src/__tests__/uds-addon-call-wiring.spec.ts +237 -0
  19. package/src/__tests__/uds-log-ingest.spec.ts +183 -0
  20. package/src/api/addon-upload.ts +27 -1
  21. package/src/api/capabilities.router.ts +1 -1
  22. package/src/api/core/bulk-update-coordinator.ts +302 -0
  23. package/src/api/core/cap-providers.ts +59 -6
  24. package/src/api/core/capabilities.router.ts +26 -3
  25. package/src/api/oauth2/oauth2-routes.ts +5 -1
  26. package/src/api/trpc/__tests__/client-ip.spec.ts +120 -0
  27. package/src/api/trpc/cap-route-error-formatter.ts +163 -0
  28. package/src/api/trpc/client-ip.ts +130 -0
  29. package/src/api/trpc/generated-cap-mounts.ts +19 -1
  30. package/src/api/trpc/generated-cap-routers.ts +180 -1
  31. package/src/api/trpc/trpc.middleware.ts +5 -1
  32. package/src/api/trpc/trpc.router.ts +45 -0
  33. package/src/core/addon/addon-call-gateway.ts +157 -0
  34. package/src/core/addon/addon-package.service.ts +9 -0
  35. package/src/core/addon/addon-registry.service.ts +364 -105
  36. package/src/core/addon/addon-settings-provider.ts +40 -116
  37. package/src/core/capability/capability.service.ts +9 -0
  38. package/src/core/moleculer/cap-call-fn.spec.ts +166 -0
  39. package/src/core/moleculer/cap-call-fn.ts +103 -0
  40. package/src/core/moleculer/cap-route-authority.ts +182 -0
  41. package/src/core/moleculer/moleculer.service.ts +380 -36
  42. package/src/main.ts +45 -12
@@ -0,0 +1,163 @@
1
+ /**
2
+ * tRPC error formatter that serializes CapRouteError diagnostic fields
3
+ * across the server→client boundary.
4
+ *
5
+ * tRPC only carries `error.message` by default. This formatter augments
6
+ * the default shape's `data` block with typed CapRouteError fields so the
7
+ * admin-UI can read `capRouteReason` instead of substring-matching message text.
8
+ *
9
+ * Fields added (all optional — absent when the error is not a CapRouteError):
10
+ * - `capRouteReason` — 'no-provider' | 'node-offline' | 'cap-unknown' | 'transport-failed'
11
+ * - `capRouteRejected` — array of `{ kind: string; why: string }` route-rejection descriptors
12
+ * - `capRouteNodeId` — the target node id, when known
13
+ *
14
+ * The formatter is EXPORTED for unit testing (no side-effects, pure function).
15
+ */
16
+ import { CapRouteError } from '@camstack/kernel'
17
+ import type { RejectedRoute } from '@camstack/kernel'
18
+ import type { TRPCError } from '@trpc/server'
19
+ import type { DefaultErrorShape } from '@trpc/server/unstable-core-do-not-import'
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** The augmented data block we attach when a CapRouteError is present. */
26
+ export interface CapRouteErrorData {
27
+ readonly capRouteReason: string
28
+ readonly capRouteRejected: readonly RejectedRoute[]
29
+ readonly capRouteNodeId?: string
30
+ }
31
+
32
+ export interface AugmentedErrorShape extends DefaultErrorShape {
33
+ readonly data: DefaultErrorShape['data'] & Partial<CapRouteErrorData>
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Type guards
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /** Known CapRouteError reason values — used as a runtime safety rail. */
41
+ const KNOWN_REASONS = new Set<string>(['no-provider', 'node-offline', 'cap-unknown', 'transport-failed'])
42
+
43
+ /** Narrows a plain string to the `CapRouteError['reason']` union. */
44
+ function isCapRouteReason(r: string): r is CapRouteError['reason'] {
45
+ return KNOWN_REASONS.has(r)
46
+ }
47
+
48
+ /** Narrows an `unknown` value to `RejectedRoute` by checking structural shape. */
49
+ function isRejectedRoute(r: unknown): r is RejectedRoute {
50
+ if (typeof r !== 'object' || r === null) return false
51
+ const kind: unknown = Reflect.get(r, 'kind')
52
+ const why: unknown = Reflect.get(r, 'why')
53
+ return typeof kind === 'string' && typeof why === 'string'
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // CapRouteError extraction helpers
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Walks the `.cause` chain of an error to find a CapRouteError.
62
+ * Returns the first one found, or null.
63
+ *
64
+ * Detection is dual-mode:
65
+ * 1. `instanceof CapRouteError` — works when the same module is loaded.
66
+ * 2. Duck-type: `name === 'CapRouteError'` + `typeof reason === 'string'`
67
+ * — robust against module-boundary issues (self-contained addons).
68
+ */
69
+ function extractCapRouteError(err: unknown): CapRouteError | null {
70
+ let current: unknown = err
71
+ for (let depth = 0; depth < 8; depth++) {
72
+ if (current === null || current === undefined) return null
73
+
74
+ if (current instanceof CapRouteError) {
75
+ return current
76
+ }
77
+
78
+ // Duck-type fallback: err.name + err.reason field present
79
+ if (typeof current === 'object' && Reflect.get(current, 'name') === 'CapRouteError') {
80
+ const rawReason: unknown = Reflect.get(current, 'reason')
81
+ if (typeof rawReason === 'string') {
82
+ // Runtime safety: reject unrecognised reason strings so the formatter
83
+ // only promotes values it knows are valid CapRouteError reasons.
84
+ if (!isCapRouteReason(rawReason)) {
85
+ // Unrecognised reason — treat as a non-CapRouteError and keep walking
86
+ const cause: unknown = Reflect.get(current, 'cause')
87
+ if (cause === current) return null
88
+ current = cause
89
+ continue
90
+ }
91
+ const reason: CapRouteError['reason'] = rawReason
92
+
93
+ const rawRejected: unknown = Reflect.get(current, 'rejected')
94
+ const rejected: readonly RejectedRoute[] = Array.isArray(rawRejected)
95
+ ? rawRejected.filter(isRejectedRoute)
96
+ : []
97
+
98
+ const nodeId: unknown = Reflect.get(current, 'nodeId')
99
+ const message: unknown = Reflect.get(current, 'message')
100
+ const rawCapName: unknown = Reflect.get(current, 'capName')
101
+ const capName: string = typeof rawCapName === 'string' ? rawCapName : '(unknown)'
102
+
103
+ // Build a minimal object with the same shape — enough for the formatter.
104
+ const synthetic = Object.assign(new CapRouteError(capName, undefined, {
105
+ reason,
106
+ rejected,
107
+ ...(typeof nodeId === 'string' ? { nodeId } : {}),
108
+ }), {
109
+ // Override message from the original if available
110
+ message: typeof message === 'string' ? message : '(duck-typed CapRouteError)',
111
+ })
112
+ return synthetic
113
+ }
114
+ }
115
+
116
+ // Walk the cause chain
117
+ if (typeof current !== 'object') return null
118
+ const cause: unknown = Reflect.get(current, 'cause')
119
+ if (cause === current) return null // Guard against circular refs
120
+ current = cause
121
+ }
122
+ return null
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Formatter — exported for unit tests
127
+ // ---------------------------------------------------------------------------
128
+
129
+ export interface FormatTrpcErrorOpts {
130
+ readonly error: TRPCError
131
+ readonly shape: DefaultErrorShape
132
+ }
133
+
134
+ /**
135
+ * Augments the default tRPC error shape with CapRouteError diagnostic fields
136
+ * when the thrown error (or any error in its `.cause` chain) is a CapRouteError.
137
+ * Returns the shape unchanged for all other errors.
138
+ */
139
+ export function formatTrpcError(opts: FormatTrpcErrorOpts): AugmentedErrorShape {
140
+ const { error, shape } = opts
141
+
142
+ // extractCapRouteError already walks the full .cause chain, so a single call
143
+ // starting from `error` covers both `error instanceof CapRouteError` and
144
+ // `error.cause` (and deeper nesting). No second call needed.
145
+ const capRouteError = extractCapRouteError(error)
146
+ if (capRouteError === null) {
147
+ return { ...shape, data: { ...shape.data } }
148
+ }
149
+
150
+ const extraData: CapRouteErrorData = {
151
+ capRouteReason: capRouteError.reason,
152
+ capRouteRejected: capRouteError.rejected,
153
+ ...(capRouteError.nodeId !== undefined ? { capRouteNodeId: capRouteError.nodeId } : {}),
154
+ }
155
+
156
+ return {
157
+ ...shape,
158
+ data: {
159
+ ...shape.data,
160
+ ...extraData,
161
+ },
162
+ }
163
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Client-IP extraction + LAN/remote classification for the tRPC layer.
3
+ *
4
+ * Used by the `webrtcSession.createSession` override to decide whether a
5
+ * live-view session should force TURN-relay-only ICE: a viewer behind
6
+ * CGNAT/4G (a non-LAN source IP) only ever offers a relay candidate, so
7
+ * the broker must offer a genuinely relay-only SDP (the patched werift
8
+ * emits one under `iceTransportPolicy:'relay'`) to get a clean
9
+ * relay↔relay media path. LAN viewers (private/loopback source IP) keep
10
+ * the low-latency direct host/srflx path. The SERVER computes this — the
11
+ * client never sends a relay-only flag.
12
+ */
13
+ import type { FastifyRequest } from 'fastify'
14
+ import type { IncomingMessage } from 'node:http'
15
+
16
+ export type ClientRequest = FastifyRequest | IncomingMessage
17
+
18
+ /**
19
+ * Best-effort extraction of the originating client IP from a tRPC
20
+ * request. Order of preference:
21
+ * 1. `X-Forwarded-For` first hop (when behind a reverse proxy — the
22
+ * hub is typically fronted by Caddy/nginx/Cloudflare for remote
23
+ * access, so the socket peer is the proxy, not the viewer).
24
+ * 2. Fastify's `req.ip` (already proxy-aware when `trustProxy` is on).
25
+ * 3. The raw socket remote address.
26
+ *
27
+ * Returns `null` when no address can be determined (e.g. mesh-originated
28
+ * calls that carry no HTTP request) — the caller treats `null` as "not
29
+ * remote" so the LAN/direct path stays the safe default.
30
+ */
31
+ export function extractClientIp(req: ClientRequest | undefined): string | null {
32
+ if (!req) return null
33
+
34
+ // 1. X-Forwarded-For — comma-separated list, client is the FIRST entry.
35
+ const xff = req.headers['x-forwarded-for']
36
+ const xffValue = Array.isArray(xff) ? xff[0] : xff
37
+ if (typeof xffValue === 'string' && xffValue.length > 0) {
38
+ const first = xffValue.split(',')[0]?.trim()
39
+ if (first) return normalizeIp(first)
40
+ }
41
+
42
+ // 2. Fastify's parsed `req.ip` (honours `trustProxy` config).
43
+ if ('ip' in req && typeof req.ip === 'string' && req.ip.length > 0) {
44
+ return normalizeIp(req.ip)
45
+ }
46
+
47
+ // 3. Raw socket peer (WS / IncomingMessage path).
48
+ const remote = req.socket?.remoteAddress
49
+ if (typeof remote === 'string' && remote.length > 0) {
50
+ return normalizeIp(remote)
51
+ }
52
+
53
+ return null
54
+ }
55
+
56
+ /**
57
+ * Strip an IPv4-mapped IPv6 prefix (`::ffff:192.168.1.5` → `192.168.1.5`)
58
+ * and any zone id (`fe80::1%en0` → `fe80::1`) so the range checks below
59
+ * see a clean address.
60
+ */
61
+ function normalizeIp(ip: string): string {
62
+ let out = ip.trim()
63
+ if (out.startsWith('::ffff:')) out = out.slice('::ffff:'.length)
64
+ const pct = out.indexOf('%')
65
+ if (pct !== -1) out = out.slice(0, pct)
66
+ return out
67
+ }
68
+
69
+ /**
70
+ * True when the address is NOT in a private / loopback / link-local /
71
+ * Tailscale range — i.e. an internet (remote) viewer. Covers IPv4
72
+ * (10/8, 172.16/12, 192.168/16, 127/8, 100.64/10 Tailscale CGNAT) and
73
+ * IPv6 (::1, fc00::/7 unique-local, fe80::/10 link-local — fd7a::/16
74
+ * Tailscale ULA is a subset of fc00::/7 and is therefore already
75
+ * covered).
76
+ *
77
+ * Tailscale clients (the 100.64.0.0/10 CGNAT overlay or the fd7a::/16
78
+ * ULA overlay) are deliberately classified LOCAL: over the Tailscale
79
+ * mesh BOTH peers sit on the 100.x / fd7a:: overlay and are mutually
80
+ * reachable, so the broker offers ALL candidates (host/srflx/relay)
81
+ * — including the hub's Tailscale host candidate — and a direct
82
+ * host↔host pair wins ICE with native (non-re-encoded) media. Forcing
83
+ * relay-only for Tailscale would push them onto the broken relay path.
84
+ *
85
+ * Unparseable or null addresses return `false` (treated as LAN — the
86
+ * safe default that preserves the existing direct path).
87
+ */
88
+ export function isRemoteClientIp(ip: string | null): boolean {
89
+ if (!ip) return false
90
+ if (ip.includes(':')) return !isPrivateIpv6(ip)
91
+ if (isIpv4(ip)) return !isPrivateIpv4(ip)
92
+ // Not a recognisable IP literal — be conservative, treat as LAN.
93
+ return false
94
+ }
95
+
96
+ function isIpv4(ip: string): boolean {
97
+ const parts = ip.split('.')
98
+ if (parts.length !== 4) return false
99
+ return parts.every((p) => {
100
+ if (!/^\d{1,3}$/.test(p)) return false
101
+ const n = Number.parseInt(p, 10)
102
+ return n >= 0 && n <= 255
103
+ })
104
+ }
105
+
106
+ function isPrivateIpv4(ip: string): boolean {
107
+ const parts = ip.split('.').map((p) => Number.parseInt(p, 10))
108
+ const [a, b] = parts
109
+ if (a === undefined || b === undefined) return false
110
+ if (a === 10) return true // 10.0.0.0/8
111
+ if (a === 127) return true // 127.0.0.0/8 loopback
112
+ if (a === 172 && b >= 16 && b <= 31) return true // 172.16.0.0/12
113
+ if (a === 192 && b === 168) return true // 192.168.0.0/16
114
+ if (a === 169 && b === 254) return true // 169.254.0.0/16 link-local
115
+ // 100.64.0.0/10 — Tailscale CGNAT overlay (100.64.0.0 – 100.127.255.255).
116
+ // Both Tailscale peers are mutually reachable on this overlay, so treat
117
+ // it as local (direct host↔host pair, no relay).
118
+ if (a === 100 && b >= 64 && b <= 127) return true
119
+ return false
120
+ }
121
+
122
+ function isPrivateIpv6(ip: string): boolean {
123
+ const lower = ip.toLowerCase()
124
+ if (lower === '::1') return true // loopback
125
+ if (lower === '::') return true // unspecified
126
+ if (lower.startsWith('fe80')) return true // fe80::/10 link-local
127
+ // fc00::/7 unique-local — covers fc.. and fd..
128
+ if (lower.startsWith('fc') || lower.startsWith('fd')) return true
129
+ return false
130
+ }
@@ -1,7 +1,7 @@
1
1
  // AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
2
2
  // Re-run: npx tsx scripts/generate-cap-mounts.ts
3
3
  //
4
- // Mounted: 78 Skipped (legacy): 6
4
+ // Mounted: 81 Skipped (legacy): 6
5
5
 
6
6
  /**
7
7
  * Single auto-mount entrypoint for every codegen'd cap router.
@@ -48,6 +48,7 @@ import {
48
48
  batteryCapability,
49
49
  brightnessCapability,
50
50
  cameraCredentialsCapability,
51
+ cameraPipelineConfigCapability,
51
52
  cameraStreamsCapability,
52
53
  decoderCapability,
53
54
  detectionPipelineCapability,
@@ -84,6 +85,7 @@ import {
84
85
  pipelineOrchestratorCapability,
85
86
  pipelineRunnerCapability,
86
87
  platformProbeCapability,
88
+ privacyMaskCapability,
87
89
  ptzAutotrackCapability,
88
90
  ptzCapability,
89
91
  rebootCapability,
@@ -97,6 +99,7 @@ import {
97
99
  storageCapability,
98
100
  storageProviderCapability,
99
101
  streamBrokerCapability,
102
+ streamCatalogCapability,
100
103
  streamParamsCapability,
101
104
  switchCapability,
102
105
  systemCapability,
@@ -130,6 +133,7 @@ import {
130
133
  createCapRouter_battery,
131
134
  createCapRouter_brightness,
132
135
  createCapRouter_cameraCredentials,
136
+ createCapRouter_cameraPipelineConfig,
133
137
  createCapRouter_cameraStreams,
134
138
  createCapRouter_decoder,
135
139
  createCapRouter_detectionPipeline,
@@ -166,6 +170,7 @@ import {
166
170
  createCapRouter_pipelineOrchestrator,
167
171
  createCapRouter_pipelineRunner,
168
172
  createCapRouter_platformProbe,
173
+ createCapRouter_privacyMask,
169
174
  createCapRouter_ptz,
170
175
  createCapRouter_ptzAutotrack,
171
176
  createCapRouter_reboot,
@@ -179,6 +184,7 @@ import {
179
184
  createCapRouter_storage,
180
185
  createCapRouter_storageProvider,
181
186
  createCapRouter_streamBroker,
187
+ createCapRouter_streamCatalog,
182
188
  createCapRouter_streamParams,
183
189
  createCapRouter_switch,
184
190
  createCapRouter_system,
@@ -328,6 +334,10 @@ export function mountAllCaps(services: MountAllCapsServices) {
328
334
  (_ctx) => requireDeviceScoped(reg, 'camera-credentials') as InferProvider<typeof cameraCredentialsCapability> | null,
329
335
  remoteCapProxy,
330
336
  ),
337
+ cameraPipelineConfig: createCapRouter_cameraPipelineConfig(
338
+ (_ctx) => reg?.getSingleton<InferProvider<typeof cameraPipelineConfigCapability>>('camera-pipeline-config') ?? null,
339
+ remoteCapProxy,
340
+ ),
331
341
  cameraStreams: createCapRouter_cameraStreams(
332
342
  (_ctx) => reg?.getSingleton<InferProvider<typeof cameraStreamsCapability>>('camera-streams') ?? null,
333
343
  remoteCapProxy,
@@ -554,6 +564,10 @@ export function mountAllCaps(services: MountAllCapsServices) {
554
564
  (_ctx) => reg?.getSingleton<InferProvider<typeof platformProbeCapability>>('platform-probe') ?? null,
555
565
  remoteCapProxy,
556
566
  ),
567
+ privacyMask: createCapRouter_privacyMask(
568
+ (_ctx) => requireDeviceScoped(reg, 'privacy-mask') as InferProvider<typeof privacyMaskCapability> | null,
569
+ remoteCapProxy,
570
+ ),
557
571
  ptz: createCapRouter_ptz(
558
572
  (_ctx) => requireDeviceScoped(reg, 'ptz') as InferProvider<typeof ptzCapability> | null,
559
573
  remoteCapProxy,
@@ -633,6 +647,10 @@ export function mountAllCaps(services: MountAllCapsServices) {
633
647
  (_ctx) => reg?.getSingleton<InferProvider<typeof streamBrokerCapability>>('stream-broker') ?? null,
634
648
  remoteCapProxy,
635
649
  ),
650
+ streamCatalog: createCapRouter_streamCatalog(
651
+ (_ctx) => requireDeviceScoped(reg, 'stream-catalog') as InferProvider<typeof streamCatalogCapability> | null,
652
+ remoteCapProxy,
653
+ ),
636
654
  streamParams: createCapRouter_streamParams(
637
655
  (_ctx) => requireDeviceScoped(reg, 'stream-params') as InferProvider<typeof streamParamsCapability> | null,
638
656
  remoteCapProxy,
@@ -1,9 +1,11 @@
1
1
  // AUTO-GENERATED by scripts/generate-cap-routers.ts — DO NOT EDIT
2
2
  // Re-run: npx tsx scripts/generate-cap-routers.ts
3
3
  //
4
- // Capabilities: 84
4
+ // Capabilities: 87
5
+ /* eslint-disable */
5
6
 
6
7
  import { TRPCError } from '@trpc/server'
8
+ import { CapRouteError } from '@camstack/kernel'
7
9
  import { z } from 'zod'
8
10
  import { adminProcedure, protectedProcedure, iterableSubscription, trpcRouter } from './trpc.middleware.js'
9
11
  import type { InferProvider } from '@camstack/types'
@@ -31,6 +33,7 @@ import { backupCapability } from '@camstack/types'
31
33
  import { batteryCapability } from '@camstack/types'
32
34
  import { brightnessCapability } from '@camstack/types'
33
35
  import { cameraCredentialsCapability } from '@camstack/types'
36
+ import { cameraPipelineConfigCapability } from '@camstack/types'
34
37
  import { cameraStreamsCapability } from '@camstack/types'
35
38
  import { decoderCapability } from '@camstack/types'
36
39
  import { detectionPipelineCapability } from '@camstack/types'
@@ -68,6 +71,7 @@ import { pipelineExecutorCapability } from '@camstack/types'
68
71
  import { pipelineOrchestratorCapability } from '@camstack/types'
69
72
  import { pipelineRunnerCapability } from '@camstack/types'
70
73
  import { platformProbeCapability } from '@camstack/types'
74
+ import { privacyMaskCapability } from '@camstack/types'
71
75
  import { ptzCapability } from '@camstack/types'
72
76
  import { ptzAutotrackCapability } from '@camstack/types'
73
77
  import { rebootCapability } from '@camstack/types'
@@ -82,6 +86,7 @@ import { ssoBridgeCapability } from '@camstack/types'
82
86
  import { storageCapability } from '@camstack/types'
83
87
  import { storageProviderCapability } from '@camstack/types'
84
88
  import { streamBrokerCapability } from '@camstack/types'
89
+ import { streamCatalogCapability } from '@camstack/types'
85
90
  import { streamParamsCapability } from '@camstack/types'
86
91
  import { streamingEngineCapability } from '@camstack/types'
87
92
  import { switchCapability } from '@camstack/types'
@@ -104,6 +109,10 @@ export function requireCapProvider<T>(name: string, getProvider: () => T | null)
104
109
  throw new TRPCError({
105
110
  code: 'PRECONDITION_FAILED',
106
111
  message: `Capability "${name}" provider not available`,
112
+ cause: new CapRouteError(name, undefined, {
113
+ reason: 'no-provider',
114
+ rejected: [{ kind: 'hub-in-process', why: 'no provider bound for this cap' }],
115
+ }),
107
116
  })
108
117
  }
109
118
  return p
@@ -134,6 +143,10 @@ function resolveProvider<T>(
134
143
  throw new TRPCError({
135
144
  code: 'PRECONDITION_FAILED',
136
145
  message: `Capability "${capName}" provider not available`,
146
+ cause: new CapRouteError(capName, undefined, {
147
+ reason: 'no-provider',
148
+ rejected: [{ kind: 'hub-in-process', why: 'no provider bound for this cap/device' }],
149
+ }),
137
150
  })
138
151
  }
139
152
 
@@ -148,6 +161,11 @@ function resolveProvider<T>(
148
161
  throw new TRPCError({
149
162
  code: 'PRECONDITION_FAILED',
150
163
  message: `Capability "${capName}" not available on node "${nodeId}"`,
164
+ cause: new CapRouteError(capName, undefined, {
165
+ reason: 'no-provider',
166
+ nodeId,
167
+ rejected: [{ kind: 'remote-moleculer', why: `cap not available on node "${nodeId}"` }],
168
+ }),
151
169
  })
152
170
  }
153
171
  return proxy
@@ -493,6 +511,38 @@ export function createCapRouter_addons(
493
511
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
494
512
  return p.updateFrameworkPackage(input as any)
495
513
  }),
514
+ startBulkUpdate: adminProcedure
515
+ .input(addonsCapability.methods.startBulkUpdate.input.loose())
516
+ .output(addonsCapability.methods.startBulkUpdate.output)
517
+ .mutation(async ({ input, ctx }) => {
518
+ const p = requireCapProvider('addons', () => getProvider(ctx))
519
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
520
+ return p.startBulkUpdate(input as any)
521
+ }),
522
+ getBulkUpdateState: adminProcedure
523
+ .input(addonsCapability.methods.getBulkUpdateState.input.loose())
524
+ .output(addonsCapability.methods.getBulkUpdateState.output)
525
+ .query(async ({ input, ctx }) => {
526
+ const p = requireCapProvider('addons', () => getProvider(ctx))
527
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
528
+ return p.getBulkUpdateState(input as any)
529
+ }),
530
+ cancelBulkUpdate: adminProcedure
531
+ .input(addonsCapability.methods.cancelBulkUpdate.input.loose())
532
+ .output(addonsCapability.methods.cancelBulkUpdate.output)
533
+ .mutation(async ({ input, ctx }) => {
534
+ const p = requireCapProvider('addons', () => getProvider(ctx))
535
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
536
+ return p.cancelBulkUpdate(input as any)
537
+ }),
538
+ listActiveBulkUpdates: adminProcedure
539
+ .input(addonsCapability.methods.listActiveBulkUpdates.input.loose())
540
+ .output(addonsCapability.methods.listActiveBulkUpdates.output)
541
+ .query(async ({ input, ctx }) => {
542
+ const p = requireCapProvider('addons', () => getProvider(ctx))
543
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
544
+ return p.listActiveBulkUpdates(input as any)
545
+ }),
496
546
  getVersions: protectedProcedure
497
547
  .input(addonsCapability.methods.getVersions.input.loose())
498
548
  .output(addonsCapability.methods.getVersions.output)
@@ -1183,6 +1233,45 @@ export function createCapRouter_cameraCredentials(
1183
1233
  })
1184
1234
  }
1185
1235
 
1236
+ // ── camera-pipeline-config (singleton) ──────────────────────────────
1237
+
1238
+ type CameraPipelineConfigProvider = InferProvider<typeof cameraPipelineConfigCapability>
1239
+
1240
+ export function createCapRouter_cameraPipelineConfig(
1241
+ getProvider: (ctx: TrpcContext) => CameraPipelineConfigProvider | null,
1242
+ createRemoteProxy?: (capName: string, nodeId: string) => CameraPipelineConfigProvider | null,
1243
+ ) {
1244
+ return trpcRouter({
1245
+ getDeviceSettingsContribution: protectedProcedure
1246
+ .input(DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceSettingsContribution.input.loose())
1247
+ .output(DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceSettingsContribution.output)
1248
+ .query(async ({ input, ctx }) => {
1249
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
1250
+ const p = resolveProvider('camera-pipeline-config', nodeId, () => getProvider(ctx), createRemoteProxy)
1251
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
1252
+ return p.getDeviceSettingsContribution(methodInput as any)
1253
+ }),
1254
+ getDeviceLiveContribution: protectedProcedure
1255
+ .input(DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceLiveContribution.input.loose())
1256
+ .output(DEVICE_SETTINGS_CONTRIBUTION_METHODS.getDeviceLiveContribution.output)
1257
+ .query(async ({ input, ctx }) => {
1258
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
1259
+ const p = resolveProvider('camera-pipeline-config', nodeId, () => getProvider(ctx), createRemoteProxy)
1260
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
1261
+ return p.getDeviceLiveContribution(methodInput as any)
1262
+ }),
1263
+ applyDeviceSettingsPatch: adminProcedure
1264
+ .input(DEVICE_SETTINGS_CONTRIBUTION_METHODS.applyDeviceSettingsPatch.input.loose())
1265
+ .output(DEVICE_SETTINGS_CONTRIBUTION_METHODS.applyDeviceSettingsPatch.output)
1266
+ .mutation(async ({ input, ctx }) => {
1267
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
1268
+ const p = resolveProvider('camera-pipeline-config', nodeId, () => getProvider(ctx), createRemoteProxy)
1269
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
1270
+ return p.applyDeviceSettingsPatch(methodInput as any)
1271
+ }),
1272
+ })
1273
+ }
1274
+
1186
1275
  // ── camera-streams (singleton) ──────────────────────────────────────
1187
1276
 
1188
1277
  type CameraStreamsProvider = InferProvider<typeof cameraStreamsCapability>
@@ -2976,6 +3065,15 @@ export function createCapRouter_nativeObjectDetection(
2976
3065
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
2977
3066
  return p.getStatus(methodInput as any)
2978
3067
  }),
3068
+ setEnabled: adminProcedure
3069
+ .input(nativeObjectDetectionCapability.methods.setEnabled.input.loose())
3070
+ .output(nativeObjectDetectionCapability.methods.setEnabled.output)
3071
+ .mutation(async ({ input, ctx }) => {
3072
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
3073
+ const p = resolveProvider('native-object-detection', nodeId, () => getProvider(ctx), createRemoteProxy)
3074
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3075
+ return p.setEnabled(methodInput as any)
3076
+ }),
2979
3077
  })
2980
3078
  }
2981
3079
 
@@ -4123,6 +4221,45 @@ export function createCapRouter_platformProbe(
4123
4221
  })
4124
4222
  }
4125
4223
 
4224
+ // ── privacy-mask (singleton) ────────────────────────────────────────
4225
+
4226
+ type PrivacyMaskProvider = InferProvider<typeof privacyMaskCapability>
4227
+
4228
+ export function createCapRouter_privacyMask(
4229
+ getProvider: (ctx: TrpcContext) => PrivacyMaskProvider | null,
4230
+ createRemoteProxy?: (capName: string, nodeId: string) => PrivacyMaskProvider | null,
4231
+ ) {
4232
+ return trpcRouter({
4233
+ getStatus: protectedProcedure
4234
+ .input(DEVICE_STATUS_METHOD.getStatus.input.loose())
4235
+ .output(DEVICE_STATUS_METHOD.getStatus.output)
4236
+ .query(async ({ input, ctx }) => {
4237
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
4238
+ const p = resolveProvider('privacy-mask', nodeId, () => getProvider(ctx), createRemoteProxy)
4239
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4240
+ return p.getStatus(methodInput as any)
4241
+ }),
4242
+ getOptions: protectedProcedure
4243
+ .input(privacyMaskCapability.methods.getOptions.input.loose())
4244
+ .output(privacyMaskCapability.methods.getOptions.output)
4245
+ .query(async ({ input, ctx }) => {
4246
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
4247
+ const p = resolveProvider('privacy-mask', nodeId, () => getProvider(ctx), createRemoteProxy)
4248
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4249
+ return p.getOptions(methodInput as any)
4250
+ }),
4251
+ setMask: adminProcedure
4252
+ .input(privacyMaskCapability.methods.setMask.input.loose())
4253
+ .output(privacyMaskCapability.methods.setMask.output)
4254
+ .mutation(async ({ input, ctx }) => {
4255
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
4256
+ const p = resolveProvider('privacy-mask', nodeId, () => getProvider(ctx), createRemoteProxy)
4257
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
4258
+ return p.setMask(methodInput as any)
4259
+ }),
4260
+ })
4261
+ }
4262
+
4126
4263
  // ── ptz (singleton) ─────────────────────────────────────────────────
4127
4264
 
4128
4265
  type PtzProvider = InferProvider<typeof ptzCapability>
@@ -5443,6 +5580,27 @@ export function createCapRouter_streamBroker(
5443
5580
  })
5444
5581
  }
5445
5582
 
5583
+ // ── stream-catalog (singleton) ──────────────────────────────────────
5584
+
5585
+ type StreamCatalogProvider = InferProvider<typeof streamCatalogCapability>
5586
+
5587
+ export function createCapRouter_streamCatalog(
5588
+ getProvider: (ctx: TrpcContext) => StreamCatalogProvider | null,
5589
+ createRemoteProxy?: (capName: string, nodeId: string) => StreamCatalogProvider | null,
5590
+ ) {
5591
+ return trpcRouter({
5592
+ getCatalog: protectedProcedure
5593
+ .input(streamCatalogCapability.methods.getCatalog.input.loose())
5594
+ .output(streamCatalogCapability.methods.getCatalog.output)
5595
+ .query(async ({ input, ctx }) => {
5596
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
5597
+ const p = resolveProvider('stream-catalog', nodeId, () => getProvider(ctx), createRemoteProxy)
5598
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
5599
+ return p.getCatalog(methodInput as any)
5600
+ }),
5601
+ })
5602
+ }
5603
+
5446
5604
  // ── stream-params (singleton) ───────────────────────────────────────
5447
5605
 
5448
5606
  type StreamParamsProvider = InferProvider<typeof streamParamsCapability>
@@ -6102,6 +6260,24 @@ export function createCapRouter_webrtcSession(
6102
6260
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
6103
6261
  return p.handleAnswer(methodInput as any)
6104
6262
  }),
6263
+ addIceCandidate: protectedProcedure
6264
+ .input(webrtcSessionCapability.methods.addIceCandidate.input.loose())
6265
+ .output(webrtcSessionCapability.methods.addIceCandidate.output)
6266
+ .mutation(async ({ input, ctx }) => {
6267
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
6268
+ const p = resolveProvider('webrtc-session', nodeId, () => getProvider(ctx), createRemoteProxy)
6269
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
6270
+ return p.addIceCandidate(methodInput as any)
6271
+ }),
6272
+ getIceCandidates: protectedProcedure
6273
+ .input(webrtcSessionCapability.methods.getIceCandidates.input.loose())
6274
+ .output(webrtcSessionCapability.methods.getIceCandidates.output)
6275
+ .query(async ({ input, ctx }) => {
6276
+ const { nodeId, ...methodInput } = input as { nodeId?: string } & Record<string, unknown>
6277
+ const p = resolveProvider('webrtc-session', nodeId, () => getProvider(ctx), createRemoteProxy)
6278
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
6279
+ return p.getIceCandidates(methodInput as any)
6280
+ }),
6105
6281
  closeSession: protectedProcedure
6106
6282
  .input(webrtcSessionCapability.methods.closeSession.input.loose())
6107
6283
  .output(webrtcSessionCapability.methods.closeSession.output)
@@ -6273,6 +6449,7 @@ export interface GeneratedCapabilityRouterMap {
6273
6449
  readonly battery: ReturnType<typeof createCapRouter_battery>
6274
6450
  readonly brightness: ReturnType<typeof createCapRouter_brightness>
6275
6451
  readonly cameraCredentials: ReturnType<typeof createCapRouter_cameraCredentials>
6452
+ readonly cameraPipelineConfig: ReturnType<typeof createCapRouter_cameraPipelineConfig>
6276
6453
  readonly cameraStreams: ReturnType<typeof createCapRouter_cameraStreams>
6277
6454
  readonly decoder: ReturnType<typeof createCapRouter_decoder>
6278
6455
  readonly detectionPipeline: ReturnType<typeof createCapRouter_detectionPipeline>
@@ -6310,6 +6487,7 @@ export interface GeneratedCapabilityRouterMap {
6310
6487
  readonly pipelineOrchestrator: ReturnType<typeof createCapRouter_pipelineOrchestrator>
6311
6488
  readonly pipelineRunner: ReturnType<typeof createCapRouter_pipelineRunner>
6312
6489
  readonly platformProbe: ReturnType<typeof createCapRouter_platformProbe>
6490
+ readonly privacyMask: ReturnType<typeof createCapRouter_privacyMask>
6313
6491
  readonly ptz: ReturnType<typeof createCapRouter_ptz>
6314
6492
  readonly ptzAutotrack: ReturnType<typeof createCapRouter_ptzAutotrack>
6315
6493
  readonly reboot: ReturnType<typeof createCapRouter_reboot>
@@ -6324,6 +6502,7 @@ export interface GeneratedCapabilityRouterMap {
6324
6502
  readonly storage: ReturnType<typeof createCapRouter_storage>
6325
6503
  readonly storageProvider: ReturnType<typeof createCapRouter_storageProvider>
6326
6504
  readonly streamBroker: ReturnType<typeof createCapRouter_streamBroker>
6505
+ readonly streamCatalog: ReturnType<typeof createCapRouter_streamCatalog>
6327
6506
  readonly streamParams: ReturnType<typeof createCapRouter_streamParams>
6328
6507
  readonly streamingEngine: ReturnType<typeof createCapRouter_streamingEngine>
6329
6508
  readonly switch: ReturnType<typeof createCapRouter_switch>