@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
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkScopeAccess = checkScopeAccess;
4
+ /**
5
+ * Pure scope-access matcher.
6
+ *
7
+ * Extracted from `trpc.middleware.ts` so the spec can exercise it
8
+ * without spinning up the tRPC initTRPC machinery. The function is
9
+ * stateless aside from an optional device-ancestor lookup callback.
10
+ *
11
+ * Algorithm (v2 — four scope types):
12
+ * 1. Look the tRPC `path` up in `METHOD_ACCESS_MAP`. Unknown =
13
+ * FORBIDDEN (codegen drift; fail closed).
14
+ * 2. For each scope on the caller, check if it matches:
15
+ * - `category` — scope.target matches meta.capScope ('device'|'system')
16
+ * - `capability` — scope.target matches meta.capName exactly
17
+ * - `addon` — scope.target matches meta.addonId (when set)
18
+ * - `device` — input.deviceId (OR any of its ancestor deviceIds
19
+ * via `getDeviceAncestors`) is in scope.targets.
20
+ * Auto-inheritance means granting a Reolink camera
21
+ * implicitly grants its siren / floodlight / PIR
22
+ * child accessories without re-listing them.
23
+ * 3. On a target match, accept iff `scope.access` includes the
24
+ * method's required `access` flavour.
25
+ * 4. No matching scope → FORBIDDEN with a human-readable reason.
26
+ */
27
+ const types_1 = require("@camstack/types");
28
+ /**
29
+ * Pull `deviceId` off a tRPC request input. Device-scope cap methods
30
+ * uniformly take `{deviceId: number, ...}` per the DeviceProxy contract,
31
+ * so a single extractor covers every device-scope call. Returns null when
32
+ * the input doesn't carry a deviceId (system-scope cap, void input, …).
33
+ */
34
+ function extractDeviceId(input) {
35
+ if (input === null || typeof input !== 'object')
36
+ return null;
37
+ const candidate = input['deviceId'];
38
+ return typeof candidate === 'number' ? candidate : null;
39
+ }
40
+ /**
41
+ * Build the set of deviceIds that count as "this request" for the
42
+ * device-scope match: the deviceId itself plus every ancestor (so a
43
+ * scope on the parent camera covers accessory children).
44
+ */
45
+ function effectiveDeviceIds(deviceId, getAncestors) {
46
+ if (!getAncestors)
47
+ return [String(deviceId)];
48
+ const out = new Set([String(deviceId)]);
49
+ for (const ancestor of getAncestors(deviceId))
50
+ out.add(String(ancestor));
51
+ return [...out];
52
+ }
53
+ function checkScopeAccess(scopes, path, input, getDeviceAncestors) {
54
+ const meta = types_1.METHOD_ACCESS_MAP[path];
55
+ if (!meta) {
56
+ return { ok: false, reason: `Unknown method '${path}' — codegen drift` };
57
+ }
58
+ const deviceId = meta.capScope === 'device' ? extractDeviceId(input) : null;
59
+ const deviceChain = deviceId !== null ? effectiveDeviceIds(deviceId, getDeviceAncestors) : [];
60
+ for (const s of scopes) {
61
+ let targetMatches = false;
62
+ switch (s.type) {
63
+ case 'category':
64
+ targetMatches = s.target === meta.capScope;
65
+ break;
66
+ case 'capability':
67
+ targetMatches = s.target === meta.capName;
68
+ break;
69
+ case 'addon':
70
+ targetMatches = meta.addonId !== null && s.target === meta.addonId;
71
+ break;
72
+ case 'device':
73
+ // Match if the request's device — or any of its ancestors — is
74
+ // in the grant's target list. Accessory children inherit the
75
+ // parent's scope without re-enumeration.
76
+ targetMatches = deviceChain.some((id) => s.targets.includes(id));
77
+ break;
78
+ }
79
+ if (!targetMatches)
80
+ continue;
81
+ if (s.access.includes(meta.access))
82
+ return { ok: true, access: meta.access };
83
+ }
84
+ return {
85
+ ok: false,
86
+ reason: `No scope grants ${meta.access} on '${meta.capName}' (${meta.capScope}-scope cap${deviceId !== null ? `, device=${deviceId}` : ''}). Have: ${scopes
87
+ .map((s) => {
88
+ const target = s.type === 'device' ? `[${s.targets.join(',')}]` : s.target;
89
+ return `${s.type}:${target}[${s.access.join(',')}]`;
90
+ })
91
+ .join(', ') || '(none)'}`,
92
+ };
93
+ }
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMeshTrpcContext = createMeshTrpcContext;
4
+ exports.createTrpcContext = createTrpcContext;
5
+ exports.createWsTrpcContext = createWsTrpcContext;
6
+ /** Read `req.query` if present (Fastify-only) without losing type safety. */
7
+ function readQuery(req) {
8
+ if (!('query' in req))
9
+ return null;
10
+ const q = Reflect.get(req, 'query');
11
+ if (q === null || typeof q !== 'object' || Array.isArray(q))
12
+ return null;
13
+ return { ...q };
14
+ }
15
+ /**
16
+ * Extract a JWT token from an HTTP request.
17
+ * Priority: Authorization header → Fastify req.query → URL query string
18
+ */
19
+ function extractTokenFromRequest(req) {
20
+ const authHeader = req.headers.authorization;
21
+ if (authHeader && typeof authHeader === 'string') {
22
+ const [scheme, token] = authHeader.split(' ');
23
+ if (scheme === 'Bearer' && token)
24
+ return token;
25
+ }
26
+ // Fastify-parsed query object (HTTP tRPC requests)
27
+ const q = readQuery(req);
28
+ if (q && typeof q.token === 'string') {
29
+ return q.token;
30
+ }
31
+ // Raw URL query string fallback (IncomingMessage or Fastify w/o parsed query)
32
+ const url = req.url;
33
+ if (url) {
34
+ const qIdx = url.indexOf('?');
35
+ if (qIdx !== -1) {
36
+ const t = new URLSearchParams(url.slice(qIdx + 1)).get('token');
37
+ if (t)
38
+ return t;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ /**
44
+ * Resolve an AuthenticatedAgent from a raw token. Handles both JWT
45
+ * (sync, via authService) and `cst_*` scoped tokens (async, via the
46
+ * `user-management` cap singleton — same path addon-upload uses for
47
+ * its REST auth chain).
48
+ *
49
+ * Returns `null` for: missing token, malformed JWT, unknown scoped
50
+ * token. Caller (protectedProcedure) decides the failure response
51
+ * (typically UNAUTHORIZED).
52
+ */
53
+ async function resolveUser(token, authService, addonRegistry) {
54
+ if (!token)
55
+ return null;
56
+ // Scoped-token path: hit the user-management cap. Synthetic user
57
+ // with `isAdmin: false` so admin-gated procedures bounce while
58
+ // protectedProcedure can still gate by scope match.
59
+ if (token.startsWith('cst_')) {
60
+ try {
61
+ const userMgmt = addonRegistry.getCapabilityRegistry().getSingleton('user-management');
62
+ if (!userMgmt)
63
+ return null;
64
+ const record = await userMgmt.validateScopedToken({ token });
65
+ if (!record)
66
+ return null;
67
+ return {
68
+ id: record.userId,
69
+ // Display label — `scoped:<prefix>` makes audit logs read
70
+ // naturally without exposing the token hash.
71
+ username: `scoped:${record.tokenPrefix}`,
72
+ isAdmin: false,
73
+ permissions: {
74
+ isAdmin: false,
75
+ allowedProviders: '*',
76
+ allowedDevices: {},
77
+ },
78
+ isApiKey: true,
79
+ isScoped: true,
80
+ scopes: record.scopes,
81
+ };
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ // JWT path.
88
+ try {
89
+ const payload = authService.verifyToken(token);
90
+ // Reject pre-v2 JWTs at the boundary. Tokens issued before the
91
+ // role → isAdmin migration don't carry the `isAdmin` field, so
92
+ // letting them through would degrade silently into "non-admin
93
+ // with no scopes" → locked out of every cap. Returning null forces
94
+ // the client to land on 401 → re-login, where it picks up a fresh
95
+ // v2 token. No back-compat shim — the role enum is gone.
96
+ if (typeof payload.isAdmin !== 'boolean') {
97
+ return null;
98
+ }
99
+ return {
100
+ id: payload.userId ?? payload.keyId ?? 'unknown',
101
+ username: payload.username ?? 'unknown',
102
+ isAdmin: payload.isAdmin,
103
+ permissions: {
104
+ isAdmin: payload.isAdmin,
105
+ allowedProviders: payload.allowedProviders,
106
+ allowedDevices: payload.allowedDevices,
107
+ },
108
+ isApiKey: payload.type === 'api_key',
109
+ agentId: payload.agentId,
110
+ // Scopes are baked into the JWT at login; the middleware uses
111
+ // them to gate every call until the user re-logs.
112
+ ...(payload.scopes !== undefined ? { scopes: payload.scopes } : {}),
113
+ };
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ /**
120
+ * Build the parent-chain walker for the scope-access matcher. Returns
121
+ * every ancestor deviceId of `deviceId` (parent, grandparent, …) so a
122
+ * grant on a Reolink camera covers its accessory children without
123
+ * re-enumerating them.
124
+ *
125
+ * Bounded by hop count (defence-in-depth — the device tree should
126
+ * never exceed 2-3 levels but a corrupt registry shouldn't loop forever).
127
+ */
128
+ function makeAncestorLookup(addonRegistry) {
129
+ return (deviceId) => {
130
+ const out = [];
131
+ const registry = addonRegistry.getDeviceRegistry();
132
+ let current = registry.getById(deviceId);
133
+ for (let hop = 0; hop < 8 && current?.parentDeviceId != null; hop++) {
134
+ out.push(current.parentDeviceId);
135
+ current = registry.getById(current.parentDeviceId);
136
+ }
137
+ return out;
138
+ };
139
+ }
140
+ /**
141
+ * Context factory for hub-internal calls originating from the trusted
142
+ * Moleculer mesh (the `$core-caps` bridge service in `core-cap-bridge.ts`).
143
+ *
144
+ * Cluster membership is gated by `CAMSTACK_CLUSTER_SECRET`, so a
145
+ * mesh-originated call is treated as a fully-trusted admin: it carries
146
+ * a synthetic `isAdmin` user, which makes `protectedProcedure` /
147
+ * `adminProcedure` pass without a JWT and skips the scope-access
148
+ * matcher entirely. There is no HTTP request behind the call.
149
+ */
150
+ function createMeshTrpcContext() {
151
+ const user = {
152
+ id: 'mesh',
153
+ username: 'mesh',
154
+ isAdmin: true,
155
+ permissions: { isAdmin: true, allowedProviders: '*', allowedDevices: {} },
156
+ isApiKey: true,
157
+ };
158
+ return { user };
159
+ }
160
+ /** Context factory for HTTP tRPC requests (Fastify adapter). */
161
+ async function createTrpcContext(req, authService, addonRegistry) {
162
+ const token = extractTokenFromRequest(req);
163
+ return {
164
+ user: await resolveUser(token, authService, addonRegistry),
165
+ req,
166
+ getDeviceAncestors: makeAncestorLookup(addonRegistry),
167
+ };
168
+ }
169
+ /**
170
+ * Context factory for WebSocket tRPC connections (applyWSSHandler).
171
+ * Token is sent via tRPC connectionParams (a JSON message sent right after
172
+ * the WS handshake), which is more reliable than query params through proxies.
173
+ */
174
+ async function createWsTrpcContext(opts, authService, addonRegistry) {
175
+ // 1. connectionParams.token (sent by BackendClient's createWSClient)
176
+ const paramToken = opts.info.connectionParams?.['token'];
177
+ const token = (typeof paramToken === 'string' ? paramToken : null) ?? extractTokenFromRequest(opts.req);
178
+ const user = await resolveUser(token, authService, addonRegistry);
179
+ return {
180
+ user,
181
+ req: opts.req,
182
+ getDeviceAncestors: makeAncestorLookup(addonRegistry),
183
+ };
184
+ }
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.agentProcedure = exports.adminProcedure = exports.protectedProcedure = exports.createCallerFactory = exports.publicProcedure = exports.trpcRouter = void 0;
7
+ exports.iterableSubscription = iterableSubscription;
8
+ exports.iterableInterval = iterableInterval;
9
+ const server_1 = require("@trpc/server");
10
+ const superjson_1 = __importDefault(require("superjson"));
11
+ const types_1 = require("@camstack/types");
12
+ const scope_access_js_1 = require("./scope-access.js");
13
+ const cap_route_error_formatter_js_1 = require("./cap-route-error-formatter.js");
14
+ const t = server_1.initTRPC.context().create({
15
+ transformer: superjson_1.default,
16
+ errorFormatter: cap_route_error_formatter_js_1.formatTrpcError,
17
+ });
18
+ // ---------------------------------------------------------------------------
19
+ // Async-generator subscription helpers (tRPC v11 — replaces deprecated observable)
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Convert a push-based subscription (callback → unsubscribe) into an async generator
23
+ * suitable for tRPC v11 `.subscription()`.
24
+ *
25
+ * @param subscribe — called once; receives a `push` callback and must return an unsubscribe fn.
26
+ */
27
+ async function* iterableSubscription(subscribe) {
28
+ const queue = [];
29
+ let resolve = null;
30
+ const unsub = subscribe((value) => {
31
+ queue.push(value);
32
+ resolve?.();
33
+ });
34
+ try {
35
+ while (true) {
36
+ while (queue.length > 0) {
37
+ yield queue.shift();
38
+ }
39
+ await new Promise((r) => {
40
+ resolve = r;
41
+ });
42
+ }
43
+ }
44
+ finally {
45
+ unsub();
46
+ }
47
+ }
48
+ /**
49
+ * Create an interval-based async generator that yields a value on each tick.
50
+ * Useful for polling subscriptions.
51
+ */
52
+ async function* iterableInterval(intervalMs, getValue) {
53
+ try {
54
+ while (true) {
55
+ yield getValue();
56
+ await new Promise((r) => setTimeout(r, intervalMs));
57
+ }
58
+ }
59
+ finally {
60
+ // cleanup handled by generator return
61
+ }
62
+ }
63
+ exports.trpcRouter = t.router;
64
+ exports.publicProcedure = t.procedure;
65
+ /**
66
+ * Server-side caller factory — turns the hub appRouter into a directly
67
+ * invokable record under a supplied `TrpcContext`. The core-cap bridge
68
+ * (`core-cap-bridge.ts`) uses it to expose core routers over the
69
+ * Moleculer mesh without an HTTP round-trip.
70
+ */
71
+ exports.createCallerFactory = t.createCallerFactory;
72
+ /**
73
+ * Caps-only authenticated procedure (v2).
74
+ *
75
+ * - `isAdmin: true` → pass-through. Admin's `scopes` field is ignored.
76
+ * - `isAdmin: false` → `METHOD_ACCESS_MAP[path]` lookup + scope match.
77
+ * The caller's scope set must grant the required (capName, access)
78
+ * pair via one of the three forms (`category`/`capability`/`addon`).
79
+ *
80
+ * Hand-written core routers (`auth.*`, `system.info`, etc.) are not
81
+ * codegen'd from cap definitions and therefore not in
82
+ * `METHOD_ACCESS_MAP`. Those routes carry their own gating via
83
+ * `adminProcedure` when destructive; the bare `protectedProcedure`
84
+ * authentication check is the only gate. The middleware skips the
85
+ * scope-check for unknown paths so the SDK boot probe (`auth.me`) and
86
+ * `system.info` reach non-admin / scoped-token callers — without
87
+ * pulling every core route into the codegen map.
88
+ *
89
+ * Single source of authority for caps: `isAdmin`. The legacy role enum
90
+ * collapsed onto this boolean in v2.
91
+ */
92
+ exports.protectedProcedure = t.procedure.use(async ({ ctx, next, path, getRawInput }) => {
93
+ if (!ctx.user) {
94
+ throw new server_1.TRPCError({ code: 'UNAUTHORIZED' });
95
+ }
96
+ // Spread+reassign of `user` narrows downstream ctx from `User | null`
97
+ // to `User` so `adminProcedure` / `agentProcedure` can read fields
98
+ // without re-checking.
99
+ if (ctx.user.isAdmin) {
100
+ return next({ ctx: { ...ctx, user: ctx.user } });
101
+ }
102
+ // Hand-written core route — no cap entry. Authentication has already
103
+ // passed; defer further gating to any explicit `adminProcedure`
104
+ // chained on top of this one.
105
+ if (!(path in types_1.METHOD_ACCESS_MAP)) {
106
+ return next({ ctx: { ...ctx, user: ctx.user } });
107
+ }
108
+ // Device-scope caps may be gated by a `device:N` scope. Resolve the
109
+ // raw input once so the matcher can read `input.deviceId` without
110
+ // re-doing the Zod parse (tRPC caches the parsed input downstream).
111
+ // The `getDeviceAncestors` hook lets the matcher walk parent → child
112
+ // accessory inheritance (grant on Reolink also covers its siren / PIR).
113
+ const rawInput = await getRawInput();
114
+ const result = (0, scope_access_js_1.checkScopeAccess)(ctx.user.scopes ?? [], path, rawInput, ctx.getDeviceAncestors);
115
+ if (!result.ok) {
116
+ throw new server_1.TRPCError({ code: 'FORBIDDEN', message: result.reason });
117
+ }
118
+ return next({ ctx: { ...ctx, user: ctx.user } });
119
+ });
120
+ /**
121
+ * Destructive-ops gate. Adds an explicit admin check on top of
122
+ * `protectedProcedure`. Useful on hand-written routes whose admin-only
123
+ * nature should be obvious to a code reader.
124
+ */
125
+ exports.adminProcedure = exports.protectedProcedure.use(({ ctx, next }) => {
126
+ if (!ctx.user.isAdmin) {
127
+ throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Admin required' });
128
+ }
129
+ return next({ ctx });
130
+ });
131
+ /**
132
+ * Procedure for agent service accounts. After the v2 collapse, agents
133
+ * are admin sessions issued via `createServiceToken` — they all get
134
+ * `isAdmin: true`. This procedure is identical to `adminProcedure`;
135
+ * kept for naming clarity on agent-specific routes.
136
+ */
137
+ exports.agentProcedure = exports.adminProcedure.use(({ ctx, next }) => {
138
+ return next({ ctx: { ...ctx, agentId: ctx.user.agentId ?? ctx.user.id } });
139
+ });
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enrichInputWithUserAgent = enrichInputWithUserAgent;
4
+ exports.wrapWebrtcSessionProviderWithRelay = wrapWebrtcSessionProviderWithRelay;
5
+ exports.buildAppRouter = buildAppRouter;
6
+ const trpc_middleware_1 = require("./trpc.middleware");
7
+ const generated_cap_routers_1 = require("./generated-cap-routers");
8
+ const generated_cap_mounts_js_1 = require("./generated-cap-mounts.js");
9
+ const cap_providers_js_1 = require("../core/cap-providers.js");
10
+ const auth_router_js_1 = require("../core/auth.router.js");
11
+ const addon_settings_router_js_1 = require("../core/addon-settings.router.js");
12
+ const settings_backend_router_js_1 = require("../core/settings-backend.router.js");
13
+ const event_bus_proxy_router_js_1 = require("../core/event-bus-proxy.router.js");
14
+ const repl_router_js_1 = require("../core/repl.router.js");
15
+ const notifications_router_js_1 = require("../core/notifications.router.js");
16
+ const logs_router_js_1 = require("../core/logs.router.js");
17
+ const system_events_router_js_1 = require("../core/system-events.router.js");
18
+ const live_events_router_js_1 = require("../core/live-events.router.js");
19
+ const capabilities_router_js_1 = require("../core/capabilities.router.js");
20
+ const stream_probe_router_js_1 = require("../core/stream-probe.router.js");
21
+ const hwaccel_router_js_1 = require("../core/hwaccel.router.js");
22
+ const cap_mount_helpers_js_1 = require("./cap-mount-helpers.js");
23
+ const client_ip_js_1 = require("./client-ip.js");
24
+ /**
25
+ * Merge the server-read User-Agent into a signaling call's
26
+ * `consumerAttribution`, building a NEW input object (immutable — never
27
+ * mutates the caller's input). When `userAgent` is null (mesh-originated
28
+ * call, or a client that omits the header) the input passes through
29
+ * unchanged. Any client-supplied `userAgent` is OVERWRITTEN — the hub
30
+ * trusts only the request context, never the client.
31
+ */
32
+ function enrichInputWithUserAgent(input, userAgent) {
33
+ if (userAgent === null)
34
+ return input;
35
+ const base = input.consumerAttribution ?? { kind: 'webrtc-browser' };
36
+ return {
37
+ ...input,
38
+ consumerAttribution: { ...base, userAgent },
39
+ };
40
+ }
41
+ /**
42
+ * Relay-only forcing for remote viewers is DISABLED (2026-05-26).
43
+ *
44
+ * It was meant to give CGNAT/4G viewers a clean relay↔relay path, but werift's
45
+ * TURN media-forward is unreliable between two real TURN servers (relay↔relay
46
+ * connects yet media never arrives → connected-but-black), and forcing relay
47
+ * ALSO kills the direct LAN/Tailscale host pair — which carries full native
48
+ * quality with no relay. We now offer ALL candidates (host incl. the hub's
49
+ * advertised Tailscale address, srflx, relay) and let ICE nominate the best
50
+ * reachable pair: direct when possible, relay only as a fallback. The
51
+ * `relayOnly` cap field + broker support remain for when relay media-forward
52
+ * is fixed.
53
+ *
54
+ * The wrapper additionally enriches the `createSession` / `handleOffer`
55
+ * subscriber attribution with the originating client's User-Agent, read
56
+ * from the tRPC request context (browser sessions). All OTHER methods
57
+ * delegate straight through — auth, the remote-proxy factory and every
58
+ * signaling behaviour are untouched.
59
+ */
60
+ function wrapWebrtcSessionProviderWithRelay(provider, ctx) {
61
+ const userAgent = (0, client_ip_js_1.extractUserAgent)(ctx.req);
62
+ return {
63
+ ...provider,
64
+ createSession: (input) => provider.createSession(enrichInputWithUserAgent(input, userAgent)),
65
+ handleOffer: (input) => provider.handleOffer(enrichInputWithUserAgent(input, userAgent)),
66
+ };
67
+ }
68
+ /**
69
+ * Build the AppRouter. Mounts every codegen'd cap router via the auto-
70
+ * mount entrypoint and overrides the handful that need service-backed
71
+ * providers or custom collection dispatch. Non-cap (core) routers ride
72
+ * alongside in the same root object.
73
+ *
74
+ * Override-by-spread: spread `mountAllCaps(services)` first, then the
75
+ * overrides AFTER — the later property wins. The drift guard in
76
+ * `scripts/codegen.ts` ensures every codegen'd `createCapRouter_X` is
77
+ * present in the auto-mount inventory (or explicitly in the legacy
78
+ * skip-list), so the override list below NEVER needs to add a new entry
79
+ * just to mount a new cap — only to swap in a custom provider.
80
+ */
81
+ function buildCapabilityRouters(services) {
82
+ return {
83
+ // ── Auto-mount: every codegen'd cap router with a canonical
84
+ // provider shape. Everything below this line OVERRIDES the
85
+ // auto-mount entry for caps with service-backed providers,
86
+ // custom collection routing, or a hub-only `null` remote proxy.
87
+ ...(0, generated_cap_mounts_js_1.mountAllCaps)(services),
88
+ // ── Non-cap (core) routers — hand-written, single-impl ──────────
89
+ notifications: (0, notifications_router_js_1.createNotificationsRouter)(services.notificationService),
90
+ // Raw DB proxy for forked workers to read/write addon store.
91
+ // Workers use ctx.api.addonSettingsRaw.getGlobal.query({...}).
92
+ // NOT the three-level settings gateway — that's the codegen'd
93
+ // `addonSettings` cap router (mounted via auto-mount above).
94
+ addonSettingsRaw: (0, addon_settings_router_js_1.createAddonSettingsRouter)(services.configService),
95
+ settingsBackend: (0, settings_backend_router_js_1.createSettingsBackendRouter)(() => services.addonRegistry.getSettingsBackend()),
96
+ eventBusProxy: (0, event_bus_proxy_router_js_1.createEventBusProxyRouter)(services.eventBus),
97
+ repl: (0, repl_router_js_1.createReplRouter)(services.replEngine),
98
+ systemEvents: (0, system_events_router_js_1.createSystemEventsRouter)(services.eventBus),
99
+ capabilities: (0, capabilities_router_js_1.createCapabilitiesRouter)(services.capabilityRegistry, services.configService),
100
+ logs: (0, logs_router_js_1.createLogsRouter)(services.loggingService),
101
+ live: (0, live_events_router_js_1.createLiveEventsRouter)(services.eventBus, services.addonRegistry),
102
+ // stream-probe — fixed core API (ffprobe wrapper), not a cap.
103
+ streamProbe: (0, stream_probe_router_js_1.createStreamProbeRouter)(services.streamProbe),
104
+ // hwaccel — fixed core API, wraps the per-node `$hwaccel` Moleculer
105
+ // service. UI pipeline / NodeDetail pages query per-node to show
106
+ // which hardware backend each agent will use.
107
+ hwaccel: (0, hwaccel_router_js_1.createHwAccelRouter)(services.moleculer?.broker ?? null),
108
+ auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry),
109
+ // ── Cap overrides: service-backed providers ─────────────────────
110
+ // These caps don't have an addon registering a provider in the
111
+ // CapabilityRegistry — the provider is built on-demand from
112
+ // backend services. `mountAllCaps` would return `null` for them
113
+ // (registry lookup miss), so we re-mount with `buildXProvider`.
114
+ networkQuality: (0, generated_cap_routers_1.createCapRouter_networkQuality)((_ctx) => (0, cap_providers_js_1.buildNetworkQualityProvider)(services.networkQualityService)),
115
+ system: (0, generated_cap_routers_1.createCapRouter_system)((_ctx) => (0, cap_providers_js_1.buildSystemProvider)(services.featureService, services.capabilityRegistry)),
116
+ toast: (0, generated_cap_routers_1.createCapRouter_toast)((ctx) => (0, cap_providers_js_1.buildToastProvider)(services.toastService, ctx)),
117
+ integrations: (0, generated_cap_routers_1.createCapRouter_integrations)((_ctx) => (0, cap_providers_js_1.buildIntegrationsProvider)(services.addonRegistry, services.eventBus, services.loggingService, services.capabilityRegistry)),
118
+ nodes: (0, generated_cap_routers_1.createCapRouter_nodes)((_ctx) => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry)),
119
+ addons: (0, generated_cap_routers_1.createCapRouter_addons)((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx, services.eventBus)),
120
+ // ── Cap overrides: cross-node remote-proxy cast ─────────────────
121
+ // These caps' providers have manual interface types that pre-date
122
+ // `InferProvider<typeof xCap>` — structurally identical, nominally
123
+ // distinct. Casting at the override site is cheaper than reworking
124
+ // the provider declarations. Auto-mount can't infer the cast.
125
+ pipelineExecutor: (0, generated_cap_routers_1.createCapRouter_pipelineExecutor)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'pipeline-executor'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
126
+ pipelineRunner: (0, generated_cap_routers_1.createCapRouter_pipelineRunner)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'pipeline-runner'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
127
+ pipelineOrchestrator: (0, generated_cap_routers_1.createCapRouter_pipelineOrchestrator)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'pipeline-orchestrator'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
128
+ audioAnalyzer: (0, generated_cap_routers_1.createCapRouter_audioAnalyzer)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'audio-analyzer'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
129
+ audioCodec: (0, generated_cap_routers_1.createCapRouter_audioCodec)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'audio-codec'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
130
+ decoder: (0, generated_cap_routers_1.createCapRouter_decoder)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'decoder'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
131
+ platformProbe: (0, generated_cap_routers_1.createCapRouter_platformProbe)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'platform-probe'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
132
+ // ── Cap overrides: hub-only, no remote fallback ─────────────────
133
+ // The cap is intentionally single-node; agents are not directly
134
+ // addressable. Auto-mount would still set up a proxy factory; we
135
+ // explicitly return `null` to short-circuit any cross-node attempt.
136
+ localNetwork: (0, generated_cap_routers_1.createCapRouter_localNetwork)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'local-network'), (_capName, _nodeId) => null),
137
+ // ── Cap overrides: collection dispatch (contribution / probe) ──
138
+ // `turn-provider.getTurnServers` is now handled generically by the
139
+ // auto-mount: it's an array-output method on a `collection` cap, so
140
+ // `mountAllCaps` fans it across every enabled provider via
141
+ // `concatCollection` when no `addonId` is supplied. No hand-written
142
+ // override needed.
143
+ //
144
+ // `snapshot-provider.supportsDevice` is an OR across providers;
145
+ // `getSnapshot` picks the first one that claims the device. The
146
+ // generic first-provider resolver from the auto-mount can't model
147
+ // this — we hand-write the probe + fan-out logic.
148
+ snapshotProvider: (0, generated_cap_routers_1.createCapRouter_snapshotProvider)((_ctx) => {
149
+ const reg = services.capabilityRegistry;
150
+ if (!reg)
151
+ return null;
152
+ const providers = reg.getCollection('snapshot-provider');
153
+ if (!providers || providers.length === 0)
154
+ return null;
155
+ const supportsDevice = (0, cap_mount_helpers_js_1.anySupports)(providers, 'supportsDevice');
156
+ const getSnapshot = (0, cap_mount_helpers_js_1.firstSupported)(providers, 'supportsDevice', 'getSnapshot');
157
+ return { supportsDevice, getSnapshot };
158
+ }),
159
+ // ── Cap override: server-detected remote → relay-only ────────────
160
+ // The broker (a forked addon) can't see the HTTP request, so it
161
+ // can't tell a LAN viewer from a remote one. We override only the
162
+ // `getProvider` accessor to return a per-request provider whose
163
+ // `createSession` carries a server-computed `relayOnly` flag derived
164
+ // from the client IP in `ctx.req`. Remote (CGNAT/4G) viewers force
165
+ // TURN-relay-only ICE; LAN viewers keep the direct host/srflx path.
166
+ // All other methods delegate straight through, and the cross-node
167
+ // remote-proxy routing is preserved (forked/agent-hosted brokers).
168
+ webrtcSession: (0, generated_cap_routers_1.createCapRouter_webrtcSession)((ctx) => {
169
+ const provider = services.capabilityRegistry?.getSingleton('webrtc-session') ?? null;
170
+ return provider ? wrapWebrtcSessionProviderWithRelay(provider, ctx) : null;
171
+ }, (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
172
+ // NOT MOUNTED — legacy provider shapes (positional args / sync
173
+ // returns) that don't match the codegen routers' {input}-object +
174
+ // Promise<T> contract. Tracked by `LEGACY_SHAPE_SKIP` in
175
+ // `generated-cap-mounts.ts` until the provider refactor (task #195):
176
+ // - addon-routes (IAddonRouteProvider: getRoutes sync)
177
+ // - auth-provider (IAuthProvider: positional credentials)
178
+ // - log-destination (ILogDestination: positional + extra lifecycle)
179
+ // - restreamer (IRestreamer: registerDevice positional)
180
+ // - streaming-engine (IStreamingEngine: registerStream positional)
181
+ // - webrtc (IWebRtcProvider: missing hasAdaptiveBitrate)
182
+ };
183
+ }
184
+ function buildAppRouter(services) {
185
+ return (0, trpc_middleware_1.trpcRouter)({
186
+ ...buildCapabilityRouters(services),
187
+ });
188
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SESSION_COOKIE = void 0;
4
+ exports.buildSessionCookie = buildSessionCookie;
5
+ exports.clearSessionCookie = clearSessionCookie;
6
+ exports.shouldRedirectToLogin = shouldRedirectToLogin;
7
+ exports.loginRedirectUrl = loginRedirectUrl;
8
+ exports.isEmbedRedirectTarget = isEmbedRedirectTarget;
9
+ /** Browser session cookie carrying the hub JWT. Set by POST /api/auth/session
10
+ * after a tRPC login; read by the addon-route catch-all for `authenticated`
11
+ * routes hit by a plain browser navigation. */
12
+ exports.SESSION_COOKIE = 'camstack_session';
13
+ function buildSessionCookie(token, ttlSec) {
14
+ return {
15
+ name: exports.SESSION_COOKIE,
16
+ value: token,
17
+ options: { httpOnly: true, sameSite: 'lax', secure: true, path: '/', maxAge: ttlSec },
18
+ };
19
+ }
20
+ function clearSessionCookie() {
21
+ return {
22
+ name: exports.SESSION_COOKIE,
23
+ value: '',
24
+ options: { httpOnly: true, sameSite: 'lax', secure: true, path: '/', maxAge: 0 },
25
+ };
26
+ }
27
+ /** A browser navigation we can bounce to the login page: a top-level GET
28
+ * that wants HTML. Anything else (API call, POST, non-HTML) keeps the
29
+ * 401 behavior so programmatic clients get a clean error. */
30
+ function shouldRedirectToLogin(method, accept) {
31
+ return method === 'GET' && typeof accept === 'string' && accept.includes('text/html');
32
+ }
33
+ /** Build the `/login?next=…` URL for an unauthenticated browser request. */
34
+ function loginRedirectUrl(originalUrl) {
35
+ return `/login?next=${encodeURIComponent(originalUrl)}`;
36
+ }
37
+ /** Allowed redirect target for `GET /api/embed-auth`: a same-origin RELATIVE
38
+ * path to a stream-broker embed page. Defeats open-redirects — the endpoint
39
+ * sets the session cookie from a Bearer token, so the `next` must be safe to
40
+ * bounce to. Rejects absolute/protocol-relative URLs, backslashes, and `..`. */
41
+ function isEmbedRedirectTarget(next) {
42
+ if (!next.startsWith('/addon/stream-broker/embed/'))
43
+ return false;
44
+ if (next.includes('\\') || next.includes('://') || next.includes('..'))
45
+ return false;
46
+ return true;
47
+ }