@camstack/server 0.2.2 → 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,102 +0,0 @@
1
- /**
2
- * Matrix tests for `checkScopeAccess`. Drives the scope-gate in
3
- * `protectedProcedure`.
4
- *
5
- * The map (`METHOD_ACCESS_MAP`) is codegen-emitted; the test imports
6
- * it indirectly via the helper so any drift between cap definitions
7
- * and runtime would surface here as a test failure (e.g. a renamed
8
- * method would no longer resolve to a known entry).
9
- */
10
- import { describe, it, expect } from 'vitest'
11
- import { checkScopeAccess } from '../scope-access.js'
12
- import type { TokenScope } from '@camstack/types'
13
-
14
- function scope(
15
- type: 'addon' | 'capability',
16
- target: string,
17
- access: TokenScope['access'],
18
- ): TokenScope {
19
- return { type, target, access }
20
- }
21
-
22
- describe('checkScopeAccess', () => {
23
- // ── Sanity: known paths resolve, unknown paths fail closed ──────
24
-
25
- it('returns FORBIDDEN with codegen-drift reason for unknown paths', () => {
26
- const result = checkScopeAccess([scope('capability', 'backup', ['view'])], 'no-such.method')
27
- expect(result.ok).toBe(false)
28
- if (!result.ok) {
29
- expect(result.reason).toContain('codegen drift')
30
- }
31
- })
32
-
33
- // ── Capability-typed scopes ─────────────────────────────────────
34
-
35
- it('accepts when capability scope matches target + access', () => {
36
- const result = checkScopeAccess([scope('capability', 'backup', ['view'])], 'backup.list')
37
- expect(result.ok).toBe(true)
38
- if (result.ok) expect(result.access).toBe('view')
39
- })
40
-
41
- it('rejects when capability scope matches target but lacks the required access', () => {
42
- // backup.trigger requires `create`; the scope only grants `view`.
43
- const result = checkScopeAccess([scope('capability', 'backup', ['view'])], 'backup.trigger')
44
- expect(result.ok).toBe(false)
45
- if (!result.ok) {
46
- // Matcher's reason format: "No scope grants <access> on '<cap>' (<scope>-scope cap)"
47
- expect(result.reason).toMatch(/No scope grants create on 'backup'/)
48
- }
49
- })
50
-
51
- it('rejects when capability scope targets a different cap entirely', () => {
52
- const result = checkScopeAccess(
53
- [scope('capability', 'devices', ['view', 'create', 'delete'])],
54
- 'backup.list',
55
- )
56
- expect(result.ok).toBe(false)
57
- })
58
-
59
- it('accepts destructive method when scope includes delete', () => {
60
- const result = checkScopeAccess(
61
- [scope('capability', 'backup', ['view', 'delete'])],
62
- 'backup.delete',
63
- )
64
- expect(result.ok).toBe(true)
65
- if (result.ok) expect(result.access).toBe('delete')
66
- })
67
-
68
- // ── Scope unions ────────────────────────────────────────────────
69
-
70
- it('union of access flavours satisfies any single requirement', () => {
71
- const scopes: TokenScope[] = [
72
- scope('capability', 'backup', ['view']),
73
- scope('capability', 'backup', ['create']),
74
- ]
75
- // view request → first scope matches
76
- expect(checkScopeAccess(scopes, 'backup.list').ok).toBe(true)
77
- // create request → second scope matches
78
- expect(checkScopeAccess(scopes, 'backup.trigger').ok).toBe(true)
79
- })
80
-
81
- // ── Empty scope set ─────────────────────────────────────────────
82
-
83
- it('rejects when no scopes are granted at all', () => {
84
- const result = checkScopeAccess([], 'backup.list')
85
- expect(result.ok).toBe(false)
86
- if (!result.ok) {
87
- expect(result.reason).toContain('Have: (none)')
88
- }
89
- })
90
-
91
- // ── Reason string contains debug-friendly diff ──────────────────
92
-
93
- it('reason string surfaces what the caller actually has', () => {
94
- const result = checkScopeAccess([scope('capability', 'devices', ['view'])], 'backup.trigger')
95
- expect(result.ok).toBe(false)
96
- if (!result.ok) {
97
- // Format: "No scope grants create on 'backup' (system-scope cap). Have: capability:devices[view]"
98
- expect(result.reason).toMatch(/No scope grants create on 'backup'/)
99
- expect(result.reason).toContain('capability:devices[view]')
100
- }
101
- })
102
- })
@@ -1,136 +0,0 @@
1
- /**
2
- * Unit tests for the User-Agent enrichment the hub applies to the
3
- * `webrtc-session` mount. The hub reads the UA from the tRPC request
4
- * context and merges it into the subscriber attribution so the
5
- * stream-broker SUBSCRIBERS panel can identify a browser viewer. Any
6
- * client-supplied `userAgent` is OVERWRITTEN — the server trusts only the
7
- * request context, never the client.
8
- */
9
- import { describe, it, expect, vi } from 'vitest'
10
- import type { IncomingMessage } from 'node:http'
11
- import type { InferProvider, BrokerConsumerAttribution } from '@camstack/types'
12
- import { webrtcSessionCapability } from '@camstack/types'
13
- import { enrichInputWithUserAgent, wrapWebrtcSessionProviderWithRelay } from '../trpc.router.js'
14
- import type { TrpcContext } from '../trpc.context.js'
15
-
16
- type WebrtcSessionProvider = InferProvider<typeof webrtcSessionCapability>
17
-
18
- function reqWithUa(userAgent?: string): IncomingMessage {
19
- const headers: Record<string, string | string[]> = {}
20
- if (userAgent !== undefined) headers['user-agent'] = userAgent
21
- return { headers, socket: {} } as unknown as IncomingMessage
22
- }
23
-
24
- describe('enrichInputWithUserAgent', () => {
25
- it('passes input through unchanged when userAgent is null', () => {
26
- const input = { deviceId: 1, consumerAttribution: { kind: 'webrtc-browser' } as const }
27
- expect(enrichInputWithUserAgent(input, null)).toBe(input)
28
- })
29
-
30
- it('merges userAgent into an existing attribution (new object)', () => {
31
- const attribution: BrokerConsumerAttribution = { kind: 'webrtc-browser', label: 'alice' }
32
- const input = { deviceId: 1, consumerAttribution: attribution }
33
- const out = enrichInputWithUserAgent(input, 'Chrome/120')
34
- expect(out.consumerAttribution).toEqual({
35
- kind: 'webrtc-browser',
36
- label: 'alice',
37
- userAgent: 'Chrome/120',
38
- })
39
- // Immutability: the original attribution is untouched.
40
- expect(attribution.userAgent).toBeUndefined()
41
- })
42
-
43
- it('defaults to webrtc-browser when no attribution was supplied', () => {
44
- const out = enrichInputWithUserAgent({ deviceId: 1 }, 'Firefox/121')
45
- expect(out.consumerAttribution).toEqual({ kind: 'webrtc-browser', userAgent: 'Firefox/121' })
46
- })
47
-
48
- it('OVERWRITES a client-supplied userAgent (never trust the client)', () => {
49
- const input = {
50
- deviceId: 1,
51
- consumerAttribution: { kind: 'webrtc-browser', userAgent: 'spoofed' } as const,
52
- }
53
- const out = enrichInputWithUserAgent(input, 'Safari/17')
54
- expect(out.consumerAttribution?.userAgent).toBe('Safari/17')
55
- })
56
- })
57
-
58
- describe('wrapWebrtcSessionProviderWithRelay — UA enrichment', () => {
59
- function mockProvider(): WebrtcSessionProvider {
60
- const createSession = vi.fn().mockResolvedValue({ sessionId: 's', sdpOffer: 'o' })
61
- const handleOffer = vi.fn().mockResolvedValue({ sessionId: 's', sdpAnswer: 'a' })
62
- const passthrough = vi.fn().mockResolvedValue(undefined)
63
- return {
64
- createSession,
65
- handleOffer,
66
- listStreams: vi.fn().mockResolvedValue([]),
67
- handleAnswer: passthrough,
68
- addIceCandidate: passthrough,
69
- getIceCandidates: vi.fn().mockResolvedValue({ candidates: [], done: true }),
70
- closeSession: passthrough,
71
- hasAdaptiveBitrate: vi.fn().mockResolvedValue(false),
72
- getSessionState: vi.fn().mockResolvedValue({ pendingRenegotiation: null }),
73
- }
74
- }
75
-
76
- function ctxWith(userAgent?: string): TrpcContext {
77
- return { user: null, req: reqWithUa(userAgent) }
78
- }
79
-
80
- it('injects the request UA into createSession attribution', async () => {
81
- const provider = mockProvider()
82
- const wrapped = wrapWebrtcSessionProviderWithRelay(provider, ctxWith('Edg/120'))
83
-
84
- await wrapped.createSession({ deviceId: 1, target: { kind: 'adaptive' } })
85
-
86
- expect(provider.createSession).toHaveBeenCalledTimes(1)
87
- const arg = vi.mocked(provider.createSession).mock.calls[0]![0]
88
- expect(arg.consumerAttribution).toEqual({ kind: 'webrtc-browser', userAgent: 'Edg/120' })
89
- })
90
-
91
- it('injects the request UA into handleOffer attribution', async () => {
92
- const provider = mockProvider()
93
- const wrapped = wrapWebrtcSessionProviderWithRelay(provider, ctxWith('CriOS/120'))
94
-
95
- await wrapped.handleOffer({ deviceId: 1, sdpOffer: 'x' })
96
-
97
- const arg = vi.mocked(provider.handleOffer).mock.calls[0]![0]
98
- expect(arg.consumerAttribution).toEqual({ kind: 'webrtc-browser', userAgent: 'CriOS/120' })
99
- })
100
-
101
- it('overwrites a client-supplied UA on createSession', async () => {
102
- const provider = mockProvider()
103
- const wrapped = wrapWebrtcSessionProviderWithRelay(provider, ctxWith('TrustedUA/1'))
104
-
105
- await wrapped.createSession({
106
- deviceId: 1,
107
- target: { kind: 'adaptive' },
108
- consumerAttribution: { kind: 'webrtc-browser', userAgent: 'spoofed', label: 'bob' },
109
- })
110
-
111
- const arg = vi.mocked(provider.createSession).mock.calls[0]![0]
112
- expect(arg.consumerAttribution).toEqual({
113
- kind: 'webrtc-browser',
114
- label: 'bob',
115
- userAgent: 'TrustedUA/1',
116
- })
117
- })
118
-
119
- it('does not alter the call when no UA header is present', async () => {
120
- const provider = mockProvider()
121
- const wrapped = wrapWebrtcSessionProviderWithRelay(provider, ctxWith(undefined))
122
-
123
- await wrapped.createSession({ deviceId: 1, target: { kind: 'adaptive' } })
124
-
125
- const arg = vi.mocked(provider.createSession).mock.calls[0]![0]
126
- expect(arg.consumerAttribution).toBeUndefined()
127
- })
128
-
129
- it('delegates other methods straight through', async () => {
130
- const provider = mockProvider()
131
- const wrapped = wrapWebrtcSessionProviderWithRelay(provider, ctxWith('Chrome/120'))
132
-
133
- await wrapped.closeSession({ deviceId: 1, sessionId: 's' })
134
- expect(provider.closeSession).toHaveBeenCalledWith({ deviceId: 1, sessionId: 's' })
135
- })
136
- })
@@ -1,245 +0,0 @@
1
- /**
2
- * Small helpers for wiring capability routers to the `CapabilityRegistry`
3
- * in `trpc.router.ts`. These functions don't generate code — they just
4
- * remove boilerplate from the mount lambdas we pass to `createCapRouter_X`.
5
- *
6
- * When to use what:
7
- * - `requireSingleton(registry, name)` — singleton caps with no custom
8
- * composition. Returns the active provider or null (the codegen'd
9
- * router itself throws PRECONDITION_FAILED when null).
10
- * - `concatCollection(providers, method)` — collection caps whose
11
- * methods return arrays and where the desired behaviour is "union of
12
- * every provider's contribution" (e.g. `turn-provider.getTurnServers`).
13
- * - `firstSupported(providers, probe, action)` — collection caps where
14
- * exactly one provider should handle each request, selected by a
15
- * probe method (e.g. `snapshot-provider.supportsDevice`).
16
- *
17
- * Collections that route by an input key (e.g. `webrtc` picking the
18
- * provider responsible for a given `streamId`) are NOT covered here —
19
- * they have app-specific routing logic that belongs in the mount.
20
- */
21
- import { TRPCError } from '@trpc/server'
22
- import type { CapabilityRegistry } from '@camstack/kernel'
23
- import type { CapabilityProviderMap } from '@camstack/types'
24
-
25
- /**
26
- * Fetch the currently active singleton provider for a capability.
27
- * Returns null if no provider is registered; the downstream codegen'd
28
- * router surfaces this as `PRECONDITION_FAILED` to the caller.
29
- */
30
- export function requireSingleton<K extends keyof CapabilityProviderMap>(
31
- registry: CapabilityRegistry | null,
32
- capName: K,
33
- ): CapabilityProviderMap[K] | null {
34
- return registry?.getSingleton(capName) ?? null
35
- }
36
-
37
- /**
38
- * Build a per-device dispatcher that satisfies the singleton-provider
39
- * shape but resolves the actual implementation lazily via
40
- * `registry.getNativeProvider(capName, deviceId)` on every method call.
41
- *
42
- * Use for device-scoped caps that have NO system-level wrapper (PTZ,
43
- * reboot, doorbell, brightness, motion-trigger, switch, …) — drivers
44
- * register per-device native providers via
45
- * `DeviceContext.registerNativeCap`, and this helper bridges the cap-
46
- * router's "fetch a singleton then call methods on it" flow into a
47
- * "resolve native by deviceId per call" flow.
48
- *
49
- * Method input MUST carry `deviceId: number`. Methods without that
50
- * field (auto-injected `getStatus({deviceId})` for caps with a status
51
- * block, every business method that follows the cap-definition
52
- * convention) work transparently.
53
- *
54
- * Throws PRECONDITION_FAILED with a device-specific message when no
55
- * native provider exists for the requested deviceId — much friendlier
56
- * than the singleton fallthrough's "no provider" generic error.
57
- */
58
- export function requireDeviceScoped<K extends keyof CapabilityProviderMap>(
59
- registry: CapabilityRegistry | null,
60
- capName: K,
61
- ): CapabilityProviderMap[K] | null {
62
- if (!registry) return null
63
- // The Proxy is the singleton stand-in. Each property access returns
64
- // a function that, on call, looks up the per-device native and
65
- // forwards the call. No caching — the lookup is cheap (Map.get) and
66
- // re-doing it per call lets devices come/go without stale refs.
67
- const dispatcher = new Proxy(
68
- {},
69
- {
70
- get(_target, prop: string | symbol) {
71
- if (typeof prop !== 'string') return undefined
72
- return async (input: { deviceId?: number } & Record<string, unknown>) => {
73
- const deviceId = input?.deviceId
74
- if (typeof deviceId !== 'number') {
75
- throw new TRPCError({
76
- code: 'BAD_REQUEST',
77
- message: `${String(capName)}.${prop}: input must carry numeric "deviceId"`,
78
- })
79
- }
80
- const native = registry.getNativeProvider<Record<string, (i: unknown) => unknown>>(
81
- capName,
82
- deviceId,
83
- )
84
- if (!native) {
85
- throw new TRPCError({
86
- code: 'PRECONDITION_FAILED',
87
- message: `Capability "${String(capName)}" not registered for device ${deviceId}`,
88
- })
89
- }
90
- const fn = native[prop]
91
- if (typeof fn !== 'function') {
92
- throw new TRPCError({
93
- code: 'NOT_IMPLEMENTED',
94
- message: `Capability "${String(capName)}" provider for device ${deviceId} does not implement "${prop}"`,
95
- })
96
- }
97
- const result = await fn.call(native, input)
98
- // Device-property-wiring overlay (read-time): only `getStatus`, and only
99
- // when the device has links for this cap (resolveLinkedStatus returns
100
- // null otherwise → base result untouched). One in-process singleton hop.
101
- if (prop === 'getStatus') {
102
- const deviceManager = registry.getSingleton<{
103
- resolveLinkedStatus?: (i: {
104
- deviceId: number
105
- cap: string
106
- baseStatus: unknown
107
- }) => Promise<Record<string, unknown> | null>
108
- }>('device-manager')
109
- const overlaid = await deviceManager?.resolveLinkedStatus?.({
110
- deviceId,
111
- cap: String(capName),
112
- baseStatus: result,
113
- })
114
- if (overlaid != null) return overlaid
115
- }
116
- return result
117
- }
118
- },
119
- },
120
- )
121
- return dispatcher as unknown as CapabilityProviderMap[K]
122
- }
123
-
124
- // ── Method-on-provider callable types ────────────────────────────────
125
-
126
- /** A key on T whose value is a function with array / promise-array return. */
127
- type ArrayReturningMethodKey<T> = {
128
- [K in keyof T]: T[K] extends (
129
- ...args: infer _A
130
- ) => readonly unknown[] | Promise<readonly unknown[]>
131
- ? K
132
- : never
133
- }[keyof T]
134
-
135
- /** A key on T whose value is a function returning boolean / promise-boolean. */
136
- type BoolReturningMethodKey<T> = {
137
- [K in keyof T]: T[K] extends (...args: infer _A) => boolean | Promise<boolean> ? K : never
138
- }[keyof T]
139
-
140
- /**
141
- * Build a method that fan-outs a call to every provider in a collection
142
- * and concatenates their array results. Useful for contribution-style
143
- * caps where each provider adds to a shared pool.
144
- */
145
- export function concatCollection<T extends object, K extends ArrayReturningMethodKey<T>>(
146
- providers: readonly T[],
147
- method: K,
148
- ): T[K] extends (...args: infer A) => readonly (infer R)[] | Promise<readonly (infer R)[]>
149
- ? (...args: A) => Promise<readonly R[]>
150
- : never {
151
- const wrapper = async (...args: unknown[]): Promise<readonly unknown[]> => {
152
- const results = await Promise.all(
153
- providers.map(async (p): Promise<readonly unknown[]> => {
154
- const member = Reflect.get(p, method)
155
- if (typeof member !== 'function') return []
156
- // `Reflect.apply` returns `any`; funnel through unknown.
157
- const out: unknown = await Reflect.apply(member, p, args)
158
- if (!Array.isArray(out)) return []
159
- const arr: readonly unknown[] = out
160
- return arr
161
- }),
162
- )
163
- return results.flat()
164
- }
165
- // Type-level bridge: the runtime wrapper signature (unknown → Promise<unknown[]>)
166
- // matches the declared generic conditional return; TypeScript's
167
- // conditional types can't be narrowed inside a function body, so this
168
- // boundary assertion is required.
169
- return wrapper as T[K] extends (
170
- ...args: infer A
171
- ) => readonly (infer R)[] | Promise<readonly (infer R)[]>
172
- ? (...args: A) => Promise<readonly R[]>
173
- : never
174
- }
175
-
176
- /**
177
- * Iterate a collection asking each provider "do you handle this?" via a
178
- * probe method, then call an action on the first one that answers yes.
179
- * Each provider is tried in registration order; errors are swallowed and
180
- * the next provider is attempted. Returns null if no provider matches or
181
- * if every matching provider's action throws/returns null.
182
- */
183
- export function firstSupported<
184
- T extends object,
185
- Probe extends BoolReturningMethodKey<T>,
186
- Action extends keyof T,
187
- >(
188
- providers: readonly T[],
189
- probe: Probe,
190
- action: Action,
191
- ): T[Action] extends (...args: infer A) => infer R
192
- ? (...args: A) => Promise<Awaited<R> | null>
193
- : never {
194
- const wrapper = async (...args: unknown[]): Promise<unknown> => {
195
- const [first] = args
196
- for (const p of providers) {
197
- try {
198
- const probeMember = Reflect.get(p, probe)
199
- if (typeof probeMember !== 'function') continue
200
- const supported: unknown = await Reflect.apply(probeMember, p, [first])
201
- if (supported !== true) continue
202
- const actionMember = Reflect.get(p, action)
203
- if (typeof actionMember !== 'function') continue
204
- const result: unknown = await Reflect.apply(actionMember, p, args)
205
- if (result !== null && result !== undefined) return result
206
- } catch {
207
- // try next provider
208
- }
209
- }
210
- return null
211
- }
212
- // Type-level bridge — see concatCollection for the same pattern.
213
- return wrapper as T[Action] extends (...args: infer A) => infer R
214
- ? (...args: A) => Promise<Awaited<R> | null>
215
- : never
216
- }
217
-
218
- /**
219
- * Convenience for collection caps that want a "logical OR of probes"
220
- * (e.g. `supportsDevice` across every snapshot-provider).
221
- */
222
- export function anySupports<T extends object, K extends BoolReturningMethodKey<T>>(
223
- providers: readonly T[],
224
- probe: K,
225
- ): T[K] extends (...args: infer A) => boolean | Promise<boolean>
226
- ? (...args: A) => Promise<boolean>
227
- : never {
228
- const wrapper = async (...args: unknown[]): Promise<boolean> => {
229
- for (const p of providers) {
230
- const member = Reflect.get(p, probe)
231
- if (typeof member !== 'function') continue
232
- try {
233
- const result: unknown = await Reflect.apply(member, p, args)
234
- if (result === true) return true
235
- } catch {
236
- /* next */
237
- }
238
- }
239
- return false
240
- }
241
- // Type-level bridge — see concatCollection for the same pattern.
242
- return wrapper as T[K] extends (...args: infer A) => boolean | Promise<boolean>
243
- ? (...args: A) => Promise<boolean>
244
- : never
245
- }
@@ -1,171 +0,0 @@
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>([
42
- 'no-provider',
43
- 'node-offline',
44
- 'cap-unknown',
45
- 'transport-failed',
46
- ])
47
-
48
- /** Narrows a plain string to the `CapRouteError['reason']` union. */
49
- function isCapRouteReason(r: string): r is CapRouteError['reason'] {
50
- return KNOWN_REASONS.has(r)
51
- }
52
-
53
- /** Narrows an `unknown` value to `RejectedRoute` by checking structural shape. */
54
- function isRejectedRoute(r: unknown): r is RejectedRoute {
55
- if (typeof r !== 'object' || r === null) return false
56
- const kind: unknown = Reflect.get(r, 'kind')
57
- const why: unknown = Reflect.get(r, 'why')
58
- return typeof kind === 'string' && typeof why === 'string'
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // CapRouteError extraction helpers
63
- // ---------------------------------------------------------------------------
64
-
65
- /**
66
- * Walks the `.cause` chain of an error to find a CapRouteError.
67
- * Returns the first one found, or null.
68
- *
69
- * Detection is dual-mode:
70
- * 1. `instanceof CapRouteError` — works when the same module is loaded.
71
- * 2. Duck-type: `name === 'CapRouteError'` + `typeof reason === 'string'`
72
- * — robust against module-boundary issues (self-contained addons).
73
- */
74
- function extractCapRouteError(err: unknown): CapRouteError | null {
75
- let current: unknown = err
76
- for (let depth = 0; depth < 8; depth++) {
77
- if (current === null || current === undefined) return null
78
-
79
- if (current instanceof CapRouteError) {
80
- return current
81
- }
82
-
83
- // Duck-type fallback: err.name + err.reason field present
84
- if (typeof current === 'object' && Reflect.get(current, 'name') === 'CapRouteError') {
85
- const rawReason: unknown = Reflect.get(current, 'reason')
86
- if (typeof rawReason === 'string') {
87
- // Runtime safety: reject unrecognised reason strings so the formatter
88
- // only promotes values it knows are valid CapRouteError reasons.
89
- if (!isCapRouteReason(rawReason)) {
90
- // Unrecognised reason — treat as a non-CapRouteError and keep walking
91
- const cause: unknown = Reflect.get(current, 'cause')
92
- if (cause === current) return null
93
- current = cause
94
- continue
95
- }
96
- const reason: CapRouteError['reason'] = rawReason
97
-
98
- const rawRejected: unknown = Reflect.get(current, 'rejected')
99
- const rejected: readonly RejectedRoute[] = Array.isArray(rawRejected)
100
- ? rawRejected.filter(isRejectedRoute)
101
- : []
102
-
103
- const nodeId: unknown = Reflect.get(current, 'nodeId')
104
- const message: unknown = Reflect.get(current, 'message')
105
- const rawCapName: unknown = Reflect.get(current, 'capName')
106
- const capName: string = typeof rawCapName === 'string' ? rawCapName : '(unknown)'
107
-
108
- // Build a minimal object with the same shape — enough for the formatter.
109
- const synthetic = Object.assign(
110
- new CapRouteError(capName, undefined, {
111
- reason,
112
- rejected,
113
- ...(typeof nodeId === 'string' ? { nodeId } : {}),
114
- }),
115
- {
116
- // Override message from the original if available
117
- message: typeof message === 'string' ? message : '(duck-typed CapRouteError)',
118
- },
119
- )
120
- return synthetic
121
- }
122
- }
123
-
124
- // Walk the cause chain
125
- if (typeof current !== 'object') return null
126
- const cause: unknown = Reflect.get(current, 'cause')
127
- if (cause === current) return null // Guard against circular refs
128
- current = cause
129
- }
130
- return null
131
- }
132
-
133
- // ---------------------------------------------------------------------------
134
- // Formatter — exported for unit tests
135
- // ---------------------------------------------------------------------------
136
-
137
- export interface FormatTrpcErrorOpts {
138
- readonly error: TRPCError
139
- readonly shape: DefaultErrorShape
140
- }
141
-
142
- /**
143
- * Augments the default tRPC error shape with CapRouteError diagnostic fields
144
- * when the thrown error (or any error in its `.cause` chain) is a CapRouteError.
145
- * Returns the shape unchanged for all other errors.
146
- */
147
- export function formatTrpcError(opts: FormatTrpcErrorOpts): AugmentedErrorShape {
148
- const { error, shape } = opts
149
-
150
- // extractCapRouteError already walks the full .cause chain, so a single call
151
- // starting from `error` covers both `error instanceof CapRouteError` and
152
- // `error.cause` (and deeper nesting). No second call needed.
153
- const capRouteError = extractCapRouteError(error)
154
- if (capRouteError === null) {
155
- return { ...shape, data: { ...shape.data } }
156
- }
157
-
158
- const extraData: CapRouteErrorData = {
159
- capRouteReason: capRouteError.reason,
160
- capRouteRejected: capRouteError.rejected,
161
- ...(capRouteError.nodeId !== undefined ? { capRouteNodeId: capRouteError.nodeId } : {}),
162
- }
163
-
164
- return {
165
- ...shape,
166
- data: {
167
- ...shape.data,
168
- ...extraData,
169
- },
170
- }
171
- }