@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,101 +0,0 @@
1
- /**
2
- * `GET /api/auth/whoami` — validate the caller's token and surface the
3
- * canonical identity + scope summary the server sees.
4
- *
5
- * Designed for the CLI `camstack whoami` command, which used to trust
6
- * the local cache blindly. Hitting this endpoint catches:
7
- * • revocation (token was deleted server-side after the local cache)
8
- * • expiry
9
- * • orphan-cleanup (e.g. owner deleted, see local-auth boot migration)
10
- *
11
- * Accepts both JWT and `cst_*` scoped tokens. Returns 401 on auth fail;
12
- * 200 with the resolved identity otherwise. We intentionally do NOT
13
- * expose `tokenHash` or the raw token — only `tokenPrefix` + scopes for
14
- * the client to confirm the cached blob matches what the server has.
15
- */
16
- import type { FastifyInstance } from 'fastify'
17
- import type { AuthService } from '../core/auth/auth.service'
18
- import type { AddonRegistryService } from '../core/addon/addon-registry.service'
19
-
20
- interface ScopedTokenLike {
21
- readonly id: string
22
- readonly userId: string
23
- readonly name: string
24
- readonly tokenPrefix: string
25
- readonly scopes: readonly { type: string; target: string }[]
26
- readonly expiresAt?: number | null
27
- readonly lastUsedAt?: number | null
28
- readonly createdAt: number
29
- }
30
- interface UserManagementLike {
31
- validateScopedToken(input: { token: string }): Promise<ScopedTokenLike | null>
32
- }
33
-
34
- interface WhoamiOk {
35
- readonly ok: true
36
- readonly kind: 'jwt' | 'scoped'
37
- readonly userId: string
38
- readonly username: string
39
- readonly isAdmin?: boolean
40
- readonly tokenPrefix?: string
41
- readonly scopes?: readonly { type: string; target: string }[]
42
- readonly expiresAt?: number | null
43
- readonly createdAt?: number
44
- }
45
-
46
- export async function registerAuthWhoamiRoute(
47
- fastify: FastifyInstance,
48
- authService: AuthService,
49
- addonRegistry: AddonRegistryService,
50
- ): Promise<void> {
51
- fastify.get('/api/auth/whoami', async (request, reply) => {
52
- const authHeader = request.headers.authorization
53
- if (!authHeader) {
54
- return reply.status(401).send({ ok: false, error: 'No Authorization header' })
55
- }
56
- const token = authHeader.replace('Bearer ', '')
57
-
58
- // JWT fast path — same logic as addon-upload auth chain.
59
- try {
60
- const payload = authService.verifyToken(token)
61
- const ok: WhoamiOk = {
62
- ok: true,
63
- kind: 'jwt',
64
- userId: payload.userId ?? payload.keyId ?? 'unknown',
65
- username: payload.username ?? 'unknown',
66
- isAdmin: payload.isAdmin,
67
- }
68
- return reply.send(ok)
69
- } catch {
70
- // Not a JWT — try scoped token via cap registry.
71
- }
72
-
73
- try {
74
- const capRegistry = addonRegistry.getCapabilityRegistry()
75
- const userMgmt = capRegistry.getSingleton('user-management') as UserManagementLike | undefined
76
- if (!userMgmt) {
77
- return reply.status(503).send({ ok: false, error: 'user-management cap not mounted' })
78
- }
79
- const record = await userMgmt.validateScopedToken({ token })
80
- if (!record) {
81
- return reply
82
- .status(401)
83
- .send({ ok: false, error: 'Token not recognised (revoked, expired, or never issued)' })
84
- }
85
- const ok: WhoamiOk = {
86
- ok: true,
87
- kind: 'scoped',
88
- userId: record.userId,
89
- username: record.userId,
90
- tokenPrefix: record.tokenPrefix,
91
- scopes: record.scopes,
92
- expiresAt: record.expiresAt ?? null,
93
- createdAt: record.createdAt,
94
- }
95
- return reply.send(ok)
96
- } catch (err) {
97
- const msg = err instanceof Error ? err.message : String(err)
98
- return reply.status(500).send({ ok: false, error: `Validation failed: ${msg}` })
99
- }
100
- })
101
- }
@@ -1,122 +0,0 @@
1
- import { z } from 'zod'
2
- import { TRPCError } from '@trpc/server'
3
- import { protectedProcedure, adminProcedure, trpcRouter } from './trpc/trpc.middleware'
4
- import type { AddonBridgeService } from '../core/addon-bridge/addon-bridge.service'
5
- import type { AddonSearchService } from '../core/addon/addon-search.service'
6
- import type { AddonRegistryService } from '../core/addon/addon-registry.service'
7
- import type { ToastService } from '@camstack/core'
8
-
9
- export function createBridgeAddonsRouter(
10
- bridge: AddonBridgeService,
11
- addonSearch: AddonSearchService,
12
- addonRegistry?: AddonRegistryService,
13
- toastService?: ToastService,
14
- ) {
15
- return trpcRouter({
16
- /** List all addon packages installed in the addons directory */
17
- listPackages: protectedProcedure.query(() => {
18
- const installer = bridge.getInstaller()
19
- if (!installer) return []
20
- return installer.listInstalled()
21
- }),
22
-
23
- /** List all available addons across all loaded packages */
24
- listAddons: protectedProcedure.query(() => {
25
- return bridge.listAvailableAddons().map((id) => {
26
- const addon = bridge.getLoader().getAddon(id)
27
- return {
28
- id,
29
- packageName: addon?.packageName ?? 'unknown',
30
- slot: addon?.declaration.slot ?? null,
31
- }
32
- })
33
- }),
34
-
35
- /** Install a community addon package from npm */
36
- installPackage: adminProcedure
37
- .input(z.object({ packageName: z.string(), version: z.string().optional() }))
38
- .mutation(async ({ input }) => {
39
- const installer = bridge.getInstaller()
40
- if (!installer) {
41
- throw new TRPCError({
42
- code: 'PRECONDITION_FAILED',
43
- message: 'Addon installer not available — bridge may have failed to initialize',
44
- })
45
- }
46
- await installer.install(input.packageName, input.version)
47
- await bridge.reloadPackages()
48
- const result = addonRegistry
49
- ? await addonRegistry.loadNewAddons()
50
- : { loaded: [], failed: [] }
51
- toastService?.broadcast({
52
- title: 'Addon Installed',
53
- message: `${input.packageName} installed successfully${result.loaded.length ? ` (${result.loaded.join(', ')})` : ''}`,
54
- severity: result.failed.length ? 'warning' : 'info',
55
- })
56
- return { success: true, loaded: result.loaded, failed: result.failed }
57
- }),
58
-
59
- /** Uninstall a community addon package */
60
- uninstallPackage: adminProcedure
61
- .input(z.object({ packageName: z.string() }))
62
- .mutation(async ({ input }) => {
63
- // Server-side guard: prevent uninstalling required packages.
64
- // After Phase D bundle merge, the pipeline-related packages
65
- // (stream-broker, detection-pipeline, motion-wasm, decoders,
66
- // audio) all live in @camstack/addon-pipeline.
67
- const REQUIRED = new Set([
68
- '@camstack/core',
69
- '@camstack/addon-pipeline',
70
- '@camstack/addon-pipeline-orchestrator',
71
- '@camstack/addon-post-analysis',
72
- '@camstack/addon-admin-ui',
73
- ])
74
- if (REQUIRED.has(input.packageName)) {
75
- throw new TRPCError({
76
- code: 'FORBIDDEN',
77
- message: `Package ${input.packageName} is required and cannot be uninstalled`,
78
- })
79
- }
80
-
81
- const installer = bridge.getInstaller()
82
- if (!installer) {
83
- throw new TRPCError({
84
- code: 'PRECONDITION_FAILED',
85
- message: 'Addon installer not available — bridge may have failed to initialize',
86
- })
87
- }
88
- await installer.uninstall(input.packageName)
89
- await bridge.reloadPackages()
90
- if (addonRegistry) await addonRegistry.loadNewAddons()
91
- toastService?.broadcast({
92
- title: 'Addon Uninstalled',
93
- message: `${input.packageName} has been removed`,
94
- severity: 'info',
95
- })
96
- return { success: true }
97
- }),
98
-
99
- /** Force reload all addon packages (re-scan directories, re-import modules) */
100
- reloadPackages: adminProcedure.mutation(async () => {
101
- await bridge.reloadPackages()
102
- return { success: true, message: 'Addon packages reloaded' }
103
- }),
104
-
105
- /** Search npm for available CamStack addons */
106
- searchAvailable: protectedProcedure
107
- .input(z.object({ query: z.string().optional() }).optional())
108
- .query(async ({ input }) => {
109
- const results = await addonSearch.searchAddons(input?.query)
110
-
111
- // Enrich with install status from locally installed packages
112
- const installed = bridge.getInstaller()?.listInstalled() ?? []
113
- const installedMap = new Map(installed.map((p) => [p.name, p.version]))
114
-
115
- return results.map((r) => ({
116
- ...r,
117
- installed: installedMap.has(r.name),
118
- installedVersion: installedMap.get(r.name),
119
- }))
120
- }),
121
- })
122
- }
@@ -1,265 +0,0 @@
1
- import { z } from 'zod'
2
- import { TRPCError } from '@trpc/server'
3
- import { adminProcedure, trpcRouter } from './trpc/trpc.middleware'
4
- import type { AddonRegistryService } from '../core/addon/addon-registry.service'
5
- import type { ConfigService } from '../core/config/config.service'
6
- import type { LoggingService } from '../core/logging/logging.service'
7
- import { isInfraCapability } from '@camstack/kernel'
8
- import { collectionPreferenceKey, persistCollectionDisabled } from './core/collection-preference'
9
-
10
- // ─── Zod Schemas ───────────────────────────────────────────────────────────
11
-
12
- const setPreferenceInput = z.discriminatedUnion('mode', [
13
- z.object({
14
- mode: z.literal('singleton'),
15
- capability: z.string(),
16
- addonId: z.string(),
17
- }),
18
- z.object({
19
- mode: z.literal('collection'),
20
- capability: z.string(),
21
- addonId: z.string(),
22
- enabled: z.boolean(),
23
- }),
24
- ])
25
-
26
- // ─── Router ────────────────────────────────────────────────────────────────
27
-
28
- export function createCapabilitiesRouter(
29
- addonRegistry: AddonRegistryService,
30
- configService: ConfigService,
31
- loggingService: LoggingService,
32
- ) {
33
- const logger = loggingService.createLogger('CapabilitiesRouter')
34
-
35
- /** Build enriched provider details from addon metadata */
36
- function getProviderDetails(addonIds: readonly string[]) {
37
- const allAddons = addonRegistry.listAllAddons()
38
- return addonIds.map((addonId) => {
39
- const addon = allAddons.find((a) => a.manifest.id === addonId)
40
- return {
41
- addonId,
42
- displayName: addon?.manifest.name ?? addonId,
43
- packageName: addon?.manifest.packageName ?? addonId,
44
- }
45
- })
46
- }
47
-
48
- return trpcRouter({
49
- // ─── List all capabilities with enriched metadata ──────────────────
50
-
51
- list: adminProcedure.query(() => {
52
- const registry = addonRegistry.getCapabilityRegistry()
53
- const caps = registry.listCapabilities()
54
- return caps.map((cap) => ({
55
- ...cap,
56
- providerDetails: getProviderDetails(cap.providers),
57
- isInfra: isInfraCapability(cap.name),
58
- }))
59
- }),
60
-
61
- // ─── Get current preference for a capability ──────────────────────
62
-
63
- getPreference: adminProcedure.input(z.object({ capability: z.string() })).query(({ input }) => {
64
- const registry = addonRegistry.getCapabilityRegistry()
65
- const mode = registry.getMode(input.capability)
66
- if (!mode) return null
67
-
68
- if (mode === 'singleton') {
69
- const addonId = configService.get<string>(`capabilities.singleton.${input.capability}`)
70
- return {
71
- capability: input.capability,
72
- mode: mode as 'singleton',
73
- preference: addonId ? { addonId } : null,
74
- }
75
- }
76
-
77
- // collection
78
- const raw = configService.get<string>(collectionPreferenceKey(input.capability))
79
- let disabled: string[] = []
80
- if (raw) {
81
- try {
82
- const parsed = JSON.parse(raw) as { disabled?: string[] }
83
- disabled = Array.isArray(parsed.disabled) ? parsed.disabled : []
84
- } catch {
85
- /* ignore malformed */
86
- }
87
- }
88
- return {
89
- capability: input.capability,
90
- mode: mode as 'collection',
91
- preference: { disabled },
92
- }
93
- }),
94
-
95
- // ─── Set preference (singleton switch or collection toggle) ───────
96
-
97
- setPreference: adminProcedure.input(setPreferenceInput).mutation(async ({ input }) => {
98
- const registry = addonRegistry.getCapabilityRegistry()
99
-
100
- if (input.mode === 'singleton') {
101
- const { capability, addonId } = input
102
- const caps = registry.listCapabilities()
103
- const cap = caps.find((c) => c.name === capability)
104
- if (!cap)
105
- throw new TRPCError({ code: 'NOT_FOUND', message: `Unknown capability: ${capability}` })
106
- if (cap.mode !== 'singleton')
107
- throw new TRPCError({
108
- code: 'BAD_REQUEST',
109
- message: `"${capability}" is not a singleton`,
110
- })
111
- if (!cap.providers.includes(addonId)) {
112
- throw new TRPCError({
113
- code: 'BAD_REQUEST',
114
- message: `Provider "${addonId}" is not registered for "${capability}"`,
115
- })
116
- }
117
-
118
- const requiresRestart = isInfraCapability(capability)
119
-
120
- if (!requiresRestart) {
121
- // Hot-swap at runtime
122
- await registry.setActiveSingleton(capability, addonId)
123
- }
124
-
125
- // Persist preference
126
- configService.set(`capabilities.singleton.${capability}`, addonId)
127
- logger.info('Singleton preference set', {
128
- tags: { addonId },
129
- meta: { capability, requiresRestart },
130
- })
131
-
132
- return { success: true, requiresRestart }
133
- }
134
-
135
- // collection toggle
136
- const { capability, addonId, enabled } = input
137
- const caps = registry.listCapabilities()
138
- const cap = caps.find((c) => c.name === capability)
139
- if (!cap)
140
- throw new TRPCError({ code: 'NOT_FOUND', message: `Unknown capability: ${capability}` })
141
- if (cap.mode !== 'collection')
142
- throw new TRPCError({ code: 'BAD_REQUEST', message: `"${capability}" is not a collection` })
143
- if (!cap.providers.includes(addonId)) {
144
- throw new TRPCError({
145
- code: 'BAD_REQUEST',
146
- message: `Provider "${addonId}" is not registered for "${capability}"`,
147
- })
148
- }
149
-
150
- if (enabled) {
151
- registry.enableCollectionProvider(capability, addonId)
152
- } else {
153
- registry.disableCollectionProvider(capability, addonId)
154
- }
155
-
156
- // Persist disabled list via the shared canonical writer.
157
- const updatedCap = registry.listCapabilities().find((c) => c.name === capability)
158
- persistCollectionDisabled(configService, capability, updatedCap?.disabledProviders ?? [])
159
- logger.info('Collection provider toggled', {
160
- tags: { addonId },
161
- meta: { capability, enabled },
162
- })
163
-
164
- return { success: true, requiresRestart: false }
165
- }),
166
-
167
- // ─── Reset preference to default ──────────────────────────────────
168
-
169
- resetPreference: adminProcedure
170
- .input(z.object({ capability: z.string() }))
171
- .mutation(({ input }) => {
172
- const registry = addonRegistry.getCapabilityRegistry()
173
- const mode = registry.getMode(input.capability)
174
- if (!mode)
175
- throw new TRPCError({
176
- code: 'NOT_FOUND',
177
- message: `Unknown capability: ${input.capability}`,
178
- })
179
-
180
- if (mode === 'singleton') {
181
- configService.set(`capabilities.singleton.${input.capability}`, null)
182
- logger.info('Singleton preference reset (takes effect on restart)', {
183
- meta: { capability: input.capability },
184
- })
185
- return { success: true, requiresRestart: true }
186
- }
187
-
188
- // collection: re-enable all disabled providers
189
- const caps = registry.listCapabilities()
190
- const cap = caps.find((c) => c.name === input.capability)
191
- if (cap) {
192
- for (const addonId of cap.disabledProviders) {
193
- registry.enableCollectionProvider(input.capability, addonId)
194
- }
195
- }
196
- configService.set(`capabilities.collection.${input.capability}`, null)
197
- logger.info('Collection preference reset (all providers re-enabled)', {
198
- meta: { capability: input.capability },
199
- })
200
- return { success: true, requiresRestart: false }
201
- }),
202
-
203
- // ─── Per-device overrides (existing, unchanged) ───────────────────
204
-
205
- listCapabilities: adminProcedure.query(() => {
206
- const registry = addonRegistry.getCapabilityRegistry()
207
- return registry.listCapabilities()
208
- }),
209
-
210
- setDeviceCapability: adminProcedure
211
- .input(z.object({ deviceId: z.string(), capability: z.string(), addonId: z.string() }))
212
- .mutation(({ input }) => {
213
- const registry = addonRegistry.getCapabilityRegistry()
214
- registry.setDeviceOverride(input.deviceId, input.capability, input.addonId)
215
- logger.info('Device capability override set', {
216
- tags: { deviceId: Number(input.deviceId), addonId: input.addonId },
217
- meta: { capability: input.capability },
218
- })
219
- }),
220
-
221
- clearDeviceCapability: adminProcedure
222
- .input(z.object({ deviceId: z.string(), capability: z.string() }))
223
- .mutation(({ input }) => {
224
- const registry = addonRegistry.getCapabilityRegistry()
225
- registry.clearDeviceOverride(input.deviceId, input.capability)
226
- logger.info('Device capability override cleared', {
227
- tags: { deviceId: Number(input.deviceId) },
228
- meta: { capability: input.capability },
229
- })
230
- }),
231
-
232
- getDeviceCapabilities: adminProcedure
233
- .input(z.object({ deviceId: z.string() }))
234
- .output(z.record(z.string(), z.string()))
235
- .query(({ input }): Record<string, string> => {
236
- const registry = addonRegistry.getCapabilityRegistry()
237
- const overrides = registry.getDeviceOverrides(input.deviceId)
238
- return Object.fromEntries(overrides) as Record<string, string>
239
- }),
240
-
241
- setDeviceCollectionFilter: adminProcedure
242
- .input(
243
- z.object({ deviceId: z.string(), capability: z.string(), addonIds: z.array(z.string()) }),
244
- )
245
- .mutation(({ input }) => {
246
- const registry = addonRegistry.getCapabilityRegistry()
247
- registry.setDeviceCollectionFilter(input.deviceId, input.capability, input.addonIds)
248
- logger.info('Device collection filter set', {
249
- tags: { deviceId: Number(input.deviceId) },
250
- meta: { capability: input.capability, addonIds: input.addonIds },
251
- })
252
- }),
253
-
254
- clearDeviceCollectionFilter: adminProcedure
255
- .input(z.object({ deviceId: z.string(), capability: z.string() }))
256
- .mutation(({ input }) => {
257
- const registry = addonRegistry.getCapabilityRegistry()
258
- registry.clearDeviceCollectionFilter(input.deviceId, input.capability)
259
- logger.info('Device collection filter cleared', {
260
- tags: { deviceId: Number(input.deviceId) },
261
- meta: { capability: input.capability },
262
- })
263
- }),
264
- })
265
- }