@camstack/server 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
  2. package/dist/api/addon-upload.js +441 -0
  3. package/dist/api/addons-custom.router.js +91 -0
  4. package/dist/api/auth-whoami.js +55 -0
  5. package/dist/api/bridge-addons.router.js +109 -0
  6. package/dist/api/capabilities.router.js +229 -0
  7. package/dist/api/core/addon-settings.router.js +117 -0
  8. package/dist/api/core/agents.router.js +73 -0
  9. package/dist/api/core/auth.router.js +286 -0
  10. package/dist/api/core/bulk-update-coordinator.js +229 -0
  11. package/dist/api/core/cap-providers.js +1124 -0
  12. package/dist/api/core/capabilities.router.js +138 -0
  13. package/dist/api/core/collection-preference.js +17 -0
  14. package/dist/api/core/event-bus-proxy.router.js +45 -0
  15. package/dist/api/core/hwaccel.router.js +91 -0
  16. package/dist/api/core/live-events.router.js +61 -0
  17. package/dist/api/core/logs.router.js +172 -0
  18. package/dist/api/core/notifications.router.js +67 -0
  19. package/dist/api/core/repl.router.js +35 -0
  20. package/dist/api/core/settings-backend.router.js +121 -0
  21. package/dist/api/core/stream-probe.router.js +58 -0
  22. package/dist/api/core/system-events.router.js +100 -0
  23. package/dist/api/health/health.routes.js +68 -0
  24. package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
  25. package/dist/api/oauth2/oauth2-routes.js +219 -0
  26. package/dist/api/trpc/cap-mount-helpers.js +194 -0
  27. package/dist/api/trpc/cap-route-error-formatter.js +133 -0
  28. package/dist/api/trpc/client-ip.js +147 -0
  29. package/dist/api/trpc/core-cap-bridge.js +115 -0
  30. package/dist/api/trpc/generated-cap-mounts.js +388 -0
  31. package/dist/api/trpc/generated-cap-routers.js +7635 -0
  32. package/dist/api/trpc/scope-access.js +93 -0
  33. package/dist/api/trpc/trpc.context.js +184 -0
  34. package/dist/api/trpc/trpc.middleware.js +139 -0
  35. package/dist/api/trpc/trpc.router.js +188 -0
  36. package/dist/auth/session-cookie.js +47 -0
  37. package/dist/boot/boot-config.js +241 -0
  38. package/dist/boot/integration-id-backfill.js +76 -0
  39. package/dist/boot/post-boot.service.js +85 -0
  40. package/dist/core/addon/addon-call-gateway.js +99 -0
  41. package/dist/core/addon/addon-package.service.js +1560 -0
  42. package/dist/core/addon/addon-registry.service.js +2739 -0
  43. package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
  44. package/dist/core/addon/addon-search.service.js +62 -0
  45. package/dist/core/addon/addon-settings-provider.js +102 -0
  46. package/dist/core/addon/addon.tokens.js +5 -0
  47. package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
  48. package/dist/core/addon-pages/addon-pages.service.js +107 -0
  49. package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
  50. package/dist/core/agent/agent-registry.service.js +477 -0
  51. package/dist/core/auth/auth.service.js +10 -0
  52. package/dist/core/capability/capability.service.js +58 -0
  53. package/dist/core/config/config.schema.js +7 -0
  54. package/dist/core/config/config.service.js +10 -0
  55. package/dist/core/events/event-bus.service.js +83 -0
  56. package/dist/core/feature/feature.service.js +10 -0
  57. package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
  58. package/dist/core/logging/log-ring-buffer.js +6 -0
  59. package/dist/core/logging/logging.service.js +130 -0
  60. package/dist/core/logging/scoped-logger.js +6 -0
  61. package/dist/core/moleculer/cap-call-fn.js +50 -0
  62. package/dist/core/moleculer/cap-route-authority.js +122 -0
  63. package/dist/core/moleculer/moleculer.service.js +898 -0
  64. package/dist/core/network/network-quality.service.js +7 -0
  65. package/dist/core/notification/notification-wrapper.service.js +33 -0
  66. package/dist/core/notification/toast-wrapper.service.js +25 -0
  67. package/dist/core/provider/provider.tokens.js +4 -0
  68. package/dist/core/repl/repl-engine.service.js +140 -0
  69. package/dist/core/storage/fs-storage-backend.js +6 -0
  70. package/dist/core/storage/storage-location-manager.js +6 -0
  71. package/dist/core/storage/storage.service.js +7 -0
  72. package/dist/core/streaming/stream-probe.service.js +209 -0
  73. package/dist/core/topology/topology-emitter.service.js +106 -0
  74. package/dist/launcher.js +325 -0
  75. package/dist/main.js +1098 -0
  76. package/dist/manual-boot.js +227 -0
  77. package/package.json +5 -1
  78. package/src/__tests__/addon-install-e2e.test.ts +0 -74
  79. package/src/__tests__/addon-pages-e2e.test.ts +0 -200
  80. package/src/__tests__/addon-route-session.test.ts +0 -17
  81. package/src/__tests__/addon-settings-router.spec.ts +0 -67
  82. package/src/__tests__/addon-upload.spec.ts +0 -475
  83. package/src/__tests__/agent-registry.spec.ts +0 -179
  84. package/src/__tests__/agent-status-page.spec.ts +0 -82
  85. package/src/__tests__/auth-session-cookie.test.ts +0 -48
  86. package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
  87. package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
  88. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
  89. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
  90. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
  91. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
  92. package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
  93. package/src/__tests__/cap-route-adapter.spec.ts +0 -302
  94. package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
  95. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
  96. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
  97. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
  98. package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
  99. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
  100. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
  101. package/src/__tests__/cap-routers/harness.ts +0 -163
  102. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
  103. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
  104. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
  105. package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
  106. package/src/__tests__/capability-e2e.test.ts +0 -384
  107. package/src/__tests__/cli-e2e.test.ts +0 -150
  108. package/src/__tests__/core-cap-bridge.spec.ts +0 -91
  109. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
  110. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
  111. package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
  112. package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
  113. package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
  114. package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
  115. package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
  116. package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
  117. package/src/__tests__/framework-allowlist.spec.ts +0 -96
  118. package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
  119. package/src/__tests__/https-e2e.test.ts +0 -124
  120. package/src/__tests__/lifecycle-e2e.test.ts +0 -189
  121. package/src/__tests__/live-events-subscription.spec.ts +0 -149
  122. package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
  123. package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
  124. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
  125. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
  126. package/src/__tests__/native-cap-route.spec.ts +0 -427
  127. package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
  128. package/src/__tests__/post-boot-restart.spec.ts +0 -161
  129. package/src/__tests__/singleton-contention.test.ts +0 -499
  130. package/src/__tests__/streaming-diagnostic.test.ts +0 -615
  131. package/src/__tests__/streaming-scale.test.ts +0 -314
  132. package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
  133. package/src/__tests__/uds-log-ingest.spec.ts +0 -183
  134. package/src/api/__tests__/addons-custom.spec.ts +0 -148
  135. package/src/api/__tests__/capabilities.router.test.ts +0 -56
  136. package/src/api/addon-upload.ts +0 -529
  137. package/src/api/addons-custom.router.ts +0 -101
  138. package/src/api/auth-whoami.ts +0 -101
  139. package/src/api/bridge-addons.router.ts +0 -122
  140. package/src/api/capabilities.router.ts +0 -265
  141. package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
  142. package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
  143. package/src/api/core/addon-settings.router.ts +0 -127
  144. package/src/api/core/agents.router.ts +0 -86
  145. package/src/api/core/auth.router.ts +0 -322
  146. package/src/api/core/bulk-update-coordinator.ts +0 -305
  147. package/src/api/core/cap-providers.ts +0 -1339
  148. package/src/api/core/capabilities.router.ts +0 -149
  149. package/src/api/core/collection-preference.ts +0 -40
  150. package/src/api/core/event-bus-proxy.router.ts +0 -45
  151. package/src/api/core/hwaccel.router.ts +0 -108
  152. package/src/api/core/live-events.router.ts +0 -67
  153. package/src/api/core/logs.router.ts +0 -195
  154. package/src/api/core/notifications.router.ts +0 -66
  155. package/src/api/core/repl.router.ts +0 -39
  156. package/src/api/core/settings-backend.router.ts +0 -140
  157. package/src/api/core/stream-probe.router.ts +0 -57
  158. package/src/api/core/system-events.router.ts +0 -125
  159. package/src/api/health/health.routes.ts +0 -117
  160. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
  161. package/src/api/oauth2/oauth2-routes.ts +0 -281
  162. package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
  163. package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
  164. package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
  165. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
  166. package/src/api/trpc/cap-mount-helpers.ts +0 -245
  167. package/src/api/trpc/cap-route-error-formatter.ts +0 -171
  168. package/src/api/trpc/client-ip.ts +0 -147
  169. package/src/api/trpc/core-cap-bridge.ts +0 -154
  170. package/src/api/trpc/generated-cap-mounts.ts +0 -1240
  171. package/src/api/trpc/generated-cap-routers.ts +0 -11523
  172. package/src/api/trpc/scope-access.ts +0 -110
  173. package/src/api/trpc/trpc.context.ts +0 -258
  174. package/src/api/trpc/trpc.middleware.ts +0 -146
  175. package/src/api/trpc/trpc.router.ts +0 -389
  176. package/src/auth/session-cookie.ts +0 -54
  177. package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
  178. package/src/boot/boot-config.ts +0 -259
  179. package/src/boot/integration-id-backfill.ts +0 -109
  180. package/src/boot/post-boot.service.ts +0 -105
  181. package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
  182. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
  183. package/src/core/addon/addon-call-gateway.ts +0 -171
  184. package/src/core/addon/addon-package.service.ts +0 -1787
  185. package/src/core/addon/addon-registry.service.ts +0 -3130
  186. package/src/core/addon/addon-search.service.ts +0 -91
  187. package/src/core/addon/addon-settings-provider.ts +0 -220
  188. package/src/core/addon/addon.tokens.ts +0 -2
  189. package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
  190. package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
  191. package/src/core/addon-pages/addon-pages.service.ts +0 -82
  192. package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
  193. package/src/core/agent/agent-registry.service.ts +0 -529
  194. package/src/core/auth/auth.service.spec.ts +0 -86
  195. package/src/core/auth/auth.service.ts +0 -8
  196. package/src/core/capability/capability.service.ts +0 -66
  197. package/src/core/config/config.schema.ts +0 -3
  198. package/src/core/config/config.service.spec.ts +0 -175
  199. package/src/core/config/config.service.ts +0 -7
  200. package/src/core/events/event-bus.service.spec.ts +0 -235
  201. package/src/core/events/event-bus.service.ts +0 -89
  202. package/src/core/feature/feature.service.spec.ts +0 -99
  203. package/src/core/feature/feature.service.ts +0 -8
  204. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
  205. package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
  206. package/src/core/logging/log-ring-buffer.ts +0 -3
  207. package/src/core/logging/logging.service.spec.ts +0 -287
  208. package/src/core/logging/logging.service.ts +0 -143
  209. package/src/core/logging/scoped-logger.ts +0 -3
  210. package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
  211. package/src/core/moleculer/cap-call-fn.ts +0 -107
  212. package/src/core/moleculer/cap-route-authority.ts +0 -194
  213. package/src/core/moleculer/moleculer.service.ts +0 -1072
  214. package/src/core/network/network-quality.service.spec.ts +0 -53
  215. package/src/core/network/network-quality.service.ts +0 -5
  216. package/src/core/notification/notification-wrapper.service.ts +0 -34
  217. package/src/core/notification/toast-wrapper.service.ts +0 -27
  218. package/src/core/provider/provider.tokens.ts +0 -1
  219. package/src/core/repl/repl-engine.service.spec.ts +0 -444
  220. package/src/core/repl/repl-engine.service.ts +0 -155
  221. package/src/core/storage/fs-storage-backend.spec.ts +0 -70
  222. package/src/core/storage/fs-storage-backend.ts +0 -3
  223. package/src/core/storage/storage-location-manager.spec.ts +0 -130
  224. package/src/core/storage/storage-location-manager.ts +0 -3
  225. package/src/core/storage/storage.service.spec.ts +0 -73
  226. package/src/core/storage/storage.service.ts +0 -3
  227. package/src/core/streaming/stream-probe.service.ts +0 -221
  228. package/src/core/topology/topology-emitter.service.ts +0 -105
  229. package/src/launcher.ts +0 -314
  230. package/src/main.ts +0 -1245
  231. package/src/manual-boot.ts +0 -301
  232. package/tsconfig.build.json +0 -8
  233. package/tsconfig.json +0 -33
  234. package/vitest.config.ts +0 -26
@@ -1,110 +0,0 @@
1
- /**
2
- * Pure scope-access matcher.
3
- *
4
- * Extracted from `trpc.middleware.ts` so the spec can exercise it
5
- * without spinning up the tRPC initTRPC machinery. The function is
6
- * stateless aside from an optional device-ancestor lookup callback.
7
- *
8
- * Algorithm (v2 — four scope types):
9
- * 1. Look the tRPC `path` up in `METHOD_ACCESS_MAP`. Unknown =
10
- * FORBIDDEN (codegen drift; fail closed).
11
- * 2. For each scope on the caller, check if it matches:
12
- * - `category` — scope.target matches meta.capScope ('device'|'system')
13
- * - `capability` — scope.target matches meta.capName exactly
14
- * - `addon` — scope.target matches meta.addonId (when set)
15
- * - `device` — input.deviceId (OR any of its ancestor deviceIds
16
- * via `getDeviceAncestors`) is in scope.targets.
17
- * Auto-inheritance means granting a Reolink camera
18
- * implicitly grants its siren / floodlight / PIR
19
- * child accessories without re-listing them.
20
- * 3. On a target match, accept iff `scope.access` includes the
21
- * method's required `access` flavour.
22
- * 4. No matching scope → FORBIDDEN with a human-readable reason.
23
- */
24
- import { METHOD_ACCESS_MAP } from '@camstack/types'
25
- import type { MethodAccess, TokenScope } from '@camstack/types'
26
-
27
- export type ScopeAccessResult = { ok: true; access: MethodAccess } | { ok: false; reason: string }
28
-
29
- /**
30
- * Resolves a deviceId to its ancestor chain (parent, grandparent, …).
31
- * Empty / omitted for top-level devices. Order is irrelevant — the
32
- * matcher only checks set membership.
33
- */
34
- export type DeviceAncestorLookup = (deviceId: number) => readonly number[]
35
-
36
- /**
37
- * Pull `deviceId` off a tRPC request input. Device-scope cap methods
38
- * uniformly take `{deviceId: number, ...}` per the DeviceProxy contract,
39
- * so a single extractor covers every device-scope call. Returns null when
40
- * the input doesn't carry a deviceId (system-scope cap, void input, …).
41
- */
42
- function extractDeviceId(input: unknown): number | null {
43
- if (input === null || typeof input !== 'object') return null
44
- const candidate = (input as Record<string, unknown>)['deviceId']
45
- return typeof candidate === 'number' ? candidate : null
46
- }
47
-
48
- /**
49
- * Build the set of deviceIds that count as "this request" for the
50
- * device-scope match: the deviceId itself plus every ancestor (so a
51
- * scope on the parent camera covers accessory children).
52
- */
53
- function effectiveDeviceIds(
54
- deviceId: number,
55
- getAncestors: DeviceAncestorLookup | undefined,
56
- ): readonly string[] {
57
- if (!getAncestors) return [String(deviceId)]
58
- const out = new Set<string>([String(deviceId)])
59
- for (const ancestor of getAncestors(deviceId)) out.add(String(ancestor))
60
- return [...out]
61
- }
62
-
63
- export function checkScopeAccess(
64
- scopes: readonly TokenScope[],
65
- path: string,
66
- input?: unknown,
67
- getDeviceAncestors?: DeviceAncestorLookup,
68
- ): ScopeAccessResult {
69
- const meta = METHOD_ACCESS_MAP[path]
70
- if (!meta) {
71
- return { ok: false, reason: `Unknown method '${path}' — codegen drift` }
72
- }
73
- const deviceId = meta.capScope === 'device' ? extractDeviceId(input) : null
74
- const deviceChain = deviceId !== null ? effectiveDeviceIds(deviceId, getDeviceAncestors) : []
75
- for (const s of scopes) {
76
- let targetMatches = false
77
- switch (s.type) {
78
- case 'category':
79
- targetMatches = s.target === meta.capScope
80
- break
81
- case 'capability':
82
- targetMatches = s.target === meta.capName
83
- break
84
- case 'addon':
85
- targetMatches = meta.addonId !== null && s.target === meta.addonId
86
- break
87
- case 'device':
88
- // Match if the request's device — or any of its ancestors — is
89
- // in the grant's target list. Accessory children inherit the
90
- // parent's scope without re-enumeration.
91
- targetMatches = deviceChain.some((id) => s.targets.includes(id))
92
- break
93
- }
94
- if (!targetMatches) continue
95
- if (s.access.includes(meta.access)) return { ok: true, access: meta.access }
96
- }
97
- return {
98
- ok: false,
99
- reason: `No scope grants ${meta.access} on '${meta.capName}' (${meta.capScope}-scope cap${
100
- deviceId !== null ? `, device=${deviceId}` : ''
101
- }). Have: ${
102
- scopes
103
- .map((s) => {
104
- const target = s.type === 'device' ? `[${s.targets.join(',')}]` : s.target
105
- return `${s.type}:${target}[${s.access.join(',')}]`
106
- })
107
- .join(', ') || '(none)'
108
- }`,
109
- }
110
- }
@@ -1,258 +0,0 @@
1
- import type { FastifyRequest } from 'fastify'
2
- import type { IncomingMessage } from 'node:http'
3
- import type { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws'
4
- import type { AuthenticatedUser, TokenScope } from '@camstack/types'
5
- import type { AuthService } from '../../core/auth/auth.service'
6
- import type { AddonRegistryService } from '../../core/addon/addon-registry.service'
7
-
8
- /** AuthenticatedUser extended with agent identity + scoped-token fields. */
9
- export interface AuthenticatedAgent extends AuthenticatedUser {
10
- agentId?: string
11
- /** True iff this user was resolved from a `cst_*` scoped token. */
12
- isScoped?: boolean
13
- /**
14
- * The scope set that gated this request. Procedures with admin gates
15
- * reject `isScoped` callers outright; the `protectedProcedure`
16
- * middleware matches scopes against the request path to decide
17
- * whether the call is permitted.
18
- */
19
- scopes?: readonly TokenScope[]
20
- }
21
-
22
- interface ScopedTokenLike {
23
- readonly id: string
24
- readonly userId: string
25
- readonly tokenPrefix: string
26
- readonly scopes: readonly TokenScope[]
27
- }
28
- interface UserManagementLike {
29
- validateScopedToken(input: { token: string }): Promise<ScopedTokenLike | null>
30
- }
31
-
32
- /**
33
- * Request union accepted by the tRPC context.
34
- *
35
- * HTTP tRPC requests arrive as `FastifyRequest`, but the WS adapter provides
36
- * a raw `IncomingMessage` with no `.query`. Callers must narrow before reading
37
- * Fastify-specific properties.
38
- */
39
- export type TrpcRequest = FastifyRequest | IncomingMessage
40
-
41
- export interface TrpcContext {
42
- user: AuthenticatedAgent | null
43
- /**
44
- * The originating HTTP/WS request. Absent for mesh-originated calls
45
- * (the core-cap bridge invokes procedures via `createCaller`, with no
46
- * request behind them). No procedure reads `req` today; it is kept
47
- * for diagnostics and future request-scoped logic.
48
- */
49
- req?: TrpcRequest
50
- /**
51
- * Walks the parent-chain of a deviceId — used by the scope-access
52
- * matcher so a `device:5` grant implicitly covers every accessory
53
- * (siren / floodlight / PIR / …) whose `parentDeviceId` is 5.
54
- * Populated by `createTrpcContext` from the live device registry;
55
- * omitted on the WS / test paths where the registry isn't wired in.
56
- */
57
- getDeviceAncestors?: (deviceId: number) => readonly number[]
58
- }
59
-
60
- /** Read `req.query` if present (Fastify-only) without losing type safety. */
61
- function readQuery(req: TrpcRequest): Record<string, unknown> | null {
62
- if (!('query' in req)) return null
63
- const q: unknown = Reflect.get(req, 'query')
64
- if (q === null || typeof q !== 'object' || Array.isArray(q)) return null
65
- return { ...q }
66
- }
67
-
68
- /**
69
- * Extract a JWT token from an HTTP request.
70
- * Priority: Authorization header → Fastify req.query → URL query string
71
- */
72
- function extractTokenFromRequest(req: TrpcRequest): string | null {
73
- const authHeader = req.headers.authorization
74
- if (authHeader && typeof authHeader === 'string') {
75
- const [scheme, token] = authHeader.split(' ')
76
- if (scheme === 'Bearer' && token) return token
77
- }
78
-
79
- // Fastify-parsed query object (HTTP tRPC requests)
80
- const q = readQuery(req)
81
- if (q && typeof q.token === 'string') {
82
- return q.token
83
- }
84
-
85
- // Raw URL query string fallback (IncomingMessage or Fastify w/o parsed query)
86
- const url = req.url
87
- if (url) {
88
- const qIdx = url.indexOf('?')
89
- if (qIdx !== -1) {
90
- const t = new URLSearchParams(url.slice(qIdx + 1)).get('token')
91
- if (t) return t
92
- }
93
- }
94
-
95
- return null
96
- }
97
-
98
- /**
99
- * Resolve an AuthenticatedAgent from a raw token. Handles both JWT
100
- * (sync, via authService) and `cst_*` scoped tokens (async, via the
101
- * `user-management` cap singleton — same path addon-upload uses for
102
- * its REST auth chain).
103
- *
104
- * Returns `null` for: missing token, malformed JWT, unknown scoped
105
- * token. Caller (protectedProcedure) decides the failure response
106
- * (typically UNAUTHORIZED).
107
- */
108
- async function resolveUser(
109
- token: string | null | undefined,
110
- authService: AuthService,
111
- addonRegistry: AddonRegistryService,
112
- ): Promise<AuthenticatedAgent | null> {
113
- if (!token) return null
114
-
115
- // Scoped-token path: hit the user-management cap. Synthetic user
116
- // with `isAdmin: false` so admin-gated procedures bounce while
117
- // protectedProcedure can still gate by scope match.
118
- if (token.startsWith('cst_')) {
119
- try {
120
- const userMgmt = addonRegistry.getCapabilityRegistry().getSingleton('user-management') as
121
- | UserManagementLike
122
- | undefined
123
- if (!userMgmt) return null
124
- const record = await userMgmt.validateScopedToken({ token })
125
- if (!record) return null
126
- return {
127
- id: record.userId,
128
- // Display label — `scoped:<prefix>` makes audit logs read
129
- // naturally without exposing the token hash.
130
- username: `scoped:${record.tokenPrefix}`,
131
- isAdmin: false,
132
- permissions: {
133
- isAdmin: false,
134
- allowedProviders: '*',
135
- allowedDevices: {},
136
- },
137
- isApiKey: true,
138
- isScoped: true,
139
- scopes: record.scopes,
140
- }
141
- } catch {
142
- return null
143
- }
144
- }
145
-
146
- // JWT path.
147
- try {
148
- const payload = authService.verifyToken(token)
149
- // Reject pre-v2 JWTs at the boundary. Tokens issued before the
150
- // role → isAdmin migration don't carry the `isAdmin` field, so
151
- // letting them through would degrade silently into "non-admin
152
- // with no scopes" → locked out of every cap. Returning null forces
153
- // the client to land on 401 → re-login, where it picks up a fresh
154
- // v2 token. No back-compat shim — the role enum is gone.
155
- if (typeof payload.isAdmin !== 'boolean') {
156
- return null
157
- }
158
- return {
159
- id: payload.userId ?? payload.keyId ?? 'unknown',
160
- username: payload.username ?? 'unknown',
161
- isAdmin: payload.isAdmin,
162
- permissions: {
163
- isAdmin: payload.isAdmin,
164
- allowedProviders: payload.allowedProviders,
165
- allowedDevices: payload.allowedDevices,
166
- },
167
- isApiKey: payload.type === 'api_key',
168
- agentId: payload.agentId,
169
- // Scopes are baked into the JWT at login; the middleware uses
170
- // them to gate every call until the user re-logs.
171
- ...(payload.scopes !== undefined ? { scopes: payload.scopes } : {}),
172
- }
173
- } catch {
174
- return null
175
- }
176
- }
177
-
178
- /**
179
- * Build the parent-chain walker for the scope-access matcher. Returns
180
- * every ancestor deviceId of `deviceId` (parent, grandparent, …) so a
181
- * grant on a Reolink camera covers its accessory children without
182
- * re-enumerating them.
183
- *
184
- * Bounded by hop count (defence-in-depth — the device tree should
185
- * never exceed 2-3 levels but a corrupt registry shouldn't loop forever).
186
- */
187
- function makeAncestorLookup(
188
- addonRegistry: AddonRegistryService,
189
- ): (deviceId: number) => readonly number[] {
190
- return (deviceId: number) => {
191
- const out: number[] = []
192
- const registry = addonRegistry.getDeviceRegistry()
193
- let current = registry.getById(deviceId)
194
- for (let hop = 0; hop < 8 && current?.parentDeviceId != null; hop++) {
195
- out.push(current.parentDeviceId)
196
- current = registry.getById(current.parentDeviceId)
197
- }
198
- return out
199
- }
200
- }
201
-
202
- /**
203
- * Context factory for hub-internal calls originating from the trusted
204
- * Moleculer mesh (the `$core-caps` bridge service in `core-cap-bridge.ts`).
205
- *
206
- * Cluster membership is gated by `CAMSTACK_CLUSTER_SECRET`, so a
207
- * mesh-originated call is treated as a fully-trusted admin: it carries
208
- * a synthetic `isAdmin` user, which makes `protectedProcedure` /
209
- * `adminProcedure` pass without a JWT and skips the scope-access
210
- * matcher entirely. There is no HTTP request behind the call.
211
- */
212
- export function createMeshTrpcContext(): TrpcContext {
213
- const user: AuthenticatedAgent = {
214
- id: 'mesh',
215
- username: 'mesh',
216
- isAdmin: true,
217
- permissions: { isAdmin: true, allowedProviders: '*', allowedDevices: {} },
218
- isApiKey: true,
219
- }
220
- return { user }
221
- }
222
-
223
- /** Context factory for HTTP tRPC requests (Fastify adapter). */
224
- export async function createTrpcContext(
225
- req: TrpcRequest,
226
- authService: AuthService,
227
- addonRegistry: AddonRegistryService,
228
- ): Promise<TrpcContext> {
229
- const token = extractTokenFromRequest(req)
230
- return {
231
- user: await resolveUser(token, authService, addonRegistry),
232
- req,
233
- getDeviceAncestors: makeAncestorLookup(addonRegistry),
234
- }
235
- }
236
-
237
- /**
238
- * Context factory for WebSocket tRPC connections (applyWSSHandler).
239
- * Token is sent via tRPC connectionParams (a JSON message sent right after
240
- * the WS handshake), which is more reliable than query params through proxies.
241
- */
242
- export async function createWsTrpcContext(
243
- opts: CreateWSSContextFnOptions,
244
- authService: AuthService,
245
- addonRegistry: AddonRegistryService,
246
- ): Promise<TrpcContext> {
247
- // 1. connectionParams.token (sent by BackendClient's createWSClient)
248
- const paramToken = opts.info.connectionParams?.['token']
249
- const token =
250
- (typeof paramToken === 'string' ? paramToken : null) ?? extractTokenFromRequest(opts.req)
251
-
252
- const user = await resolveUser(token, authService, addonRegistry)
253
- return {
254
- user,
255
- req: opts.req,
256
- getDeviceAncestors: makeAncestorLookup(addonRegistry),
257
- }
258
- }
@@ -1,146 +0,0 @@
1
- import { initTRPC, TRPCError } from '@trpc/server'
2
- import superjson from 'superjson'
3
- import { METHOD_ACCESS_MAP } from '@camstack/types'
4
- import type { TrpcContext } from './trpc.context'
5
- import { checkScopeAccess } from './scope-access.js'
6
- import { formatTrpcError } from './cap-route-error-formatter.js'
7
-
8
- const t = initTRPC.context<TrpcContext>().create({
9
- transformer: superjson,
10
- errorFormatter: formatTrpcError,
11
- })
12
-
13
- // ---------------------------------------------------------------------------
14
- // Async-generator subscription helpers (tRPC v11 — replaces deprecated observable)
15
- // ---------------------------------------------------------------------------
16
-
17
- /**
18
- * Convert a push-based subscription (callback → unsubscribe) into an async generator
19
- * suitable for tRPC v11 `.subscription()`.
20
- *
21
- * @param subscribe — called once; receives a `push` callback and must return an unsubscribe fn.
22
- */
23
- export async function* iterableSubscription<T>(
24
- subscribe: (push: (value: T) => void) => () => void,
25
- ): AsyncGenerator<T> {
26
- const queue: T[] = []
27
- let resolve: (() => void) | null = null
28
-
29
- const unsub = subscribe((value) => {
30
- queue.push(value)
31
- resolve?.()
32
- })
33
-
34
- try {
35
- while (true) {
36
- while (queue.length > 0) {
37
- yield queue.shift()!
38
- }
39
- await new Promise<void>((r) => {
40
- resolve = r
41
- })
42
- }
43
- } finally {
44
- unsub()
45
- }
46
- }
47
-
48
- /**
49
- * Create an interval-based async generator that yields a value on each tick.
50
- * Useful for polling subscriptions.
51
- */
52
- export async function* iterableInterval<T>(
53
- intervalMs: number,
54
- getValue: () => T,
55
- ): AsyncGenerator<T> {
56
- try {
57
- while (true) {
58
- yield getValue()
59
- await new Promise<void>((r) => setTimeout(r, intervalMs))
60
- }
61
- } finally {
62
- // cleanup handled by generator return
63
- }
64
- }
65
-
66
- export const trpcRouter = t.router
67
- export const publicProcedure = t.procedure
68
-
69
- /**
70
- * Server-side caller factory — turns the hub appRouter into a directly
71
- * invokable record under a supplied `TrpcContext`. The core-cap bridge
72
- * (`core-cap-bridge.ts`) uses it to expose core routers over the
73
- * Moleculer mesh without an HTTP round-trip.
74
- */
75
- export const createCallerFactory = t.createCallerFactory
76
-
77
- /**
78
- * Caps-only authenticated procedure (v2).
79
- *
80
- * - `isAdmin: true` → pass-through. Admin's `scopes` field is ignored.
81
- * - `isAdmin: false` → `METHOD_ACCESS_MAP[path]` lookup + scope match.
82
- * The caller's scope set must grant the required (capName, access)
83
- * pair via one of the three forms (`category`/`capability`/`addon`).
84
- *
85
- * Hand-written core routers (`auth.*`, `system.info`, etc.) are not
86
- * codegen'd from cap definitions and therefore not in
87
- * `METHOD_ACCESS_MAP`. Those routes carry their own gating via
88
- * `adminProcedure` when destructive; the bare `protectedProcedure`
89
- * authentication check is the only gate. The middleware skips the
90
- * scope-check for unknown paths so the SDK boot probe (`auth.me`) and
91
- * `system.info` reach non-admin / scoped-token callers — without
92
- * pulling every core route into the codegen map.
93
- *
94
- * Single source of authority for caps: `isAdmin`. The legacy role enum
95
- * collapsed onto this boolean in v2.
96
- */
97
- export const protectedProcedure = t.procedure.use(async ({ ctx, next, path, getRawInput }) => {
98
- if (!ctx.user) {
99
- throw new TRPCError({ code: 'UNAUTHORIZED' })
100
- }
101
- // Spread+reassign of `user` narrows downstream ctx from `User | null`
102
- // to `User` so `adminProcedure` / `agentProcedure` can read fields
103
- // without re-checking.
104
- if (ctx.user.isAdmin) {
105
- return next({ ctx: { ...ctx, user: ctx.user } })
106
- }
107
- // Hand-written core route — no cap entry. Authentication has already
108
- // passed; defer further gating to any explicit `adminProcedure`
109
- // chained on top of this one.
110
- if (!(path in METHOD_ACCESS_MAP)) {
111
- return next({ ctx: { ...ctx, user: ctx.user } })
112
- }
113
- // Device-scope caps may be gated by a `device:N` scope. Resolve the
114
- // raw input once so the matcher can read `input.deviceId` without
115
- // re-doing the Zod parse (tRPC caches the parsed input downstream).
116
- // The `getDeviceAncestors` hook lets the matcher walk parent → child
117
- // accessory inheritance (grant on Reolink also covers its siren / PIR).
118
- const rawInput = await getRawInput()
119
- const result = checkScopeAccess(ctx.user.scopes ?? [], path, rawInput, ctx.getDeviceAncestors)
120
- if (!result.ok) {
121
- throw new TRPCError({ code: 'FORBIDDEN', message: result.reason })
122
- }
123
- return next({ ctx: { ...ctx, user: ctx.user } })
124
- })
125
-
126
- /**
127
- * Destructive-ops gate. Adds an explicit admin check on top of
128
- * `protectedProcedure`. Useful on hand-written routes whose admin-only
129
- * nature should be obvious to a code reader.
130
- */
131
- export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
132
- if (!ctx.user.isAdmin) {
133
- throw new TRPCError({ code: 'FORBIDDEN', message: 'Admin required' })
134
- }
135
- return next({ ctx })
136
- })
137
-
138
- /**
139
- * Procedure for agent service accounts. After the v2 collapse, agents
140
- * are admin sessions issued via `createServiceToken` — they all get
141
- * `isAdmin: true`. This procedure is identical to `adminProcedure`;
142
- * kept for naming clarity on agent-specific routes.
143
- */
144
- export const agentProcedure = adminProcedure.use(({ ctx, next }) => {
145
- return next({ ctx: { ...ctx, agentId: ctx.user.agentId ?? ctx.user.id } })
146
- })