@camstack/server 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
  2. package/dist/api/addon-upload.js +441 -0
  3. package/dist/api/addons-custom.router.js +91 -0
  4. package/dist/api/auth-whoami.js +55 -0
  5. package/dist/api/bridge-addons.router.js +109 -0
  6. package/dist/api/capabilities.router.js +229 -0
  7. package/dist/api/core/addon-settings.router.js +117 -0
  8. package/dist/api/core/agents.router.js +73 -0
  9. package/dist/api/core/auth.router.js +286 -0
  10. package/dist/api/core/bulk-update-coordinator.js +229 -0
  11. package/dist/api/core/cap-providers.js +1124 -0
  12. package/dist/api/core/capabilities.router.js +138 -0
  13. package/dist/api/core/collection-preference.js +17 -0
  14. package/dist/api/core/event-bus-proxy.router.js +45 -0
  15. package/dist/api/core/hwaccel.router.js +91 -0
  16. package/dist/api/core/live-events.router.js +61 -0
  17. package/dist/api/core/logs.router.js +172 -0
  18. package/dist/api/core/notifications.router.js +67 -0
  19. package/dist/api/core/repl.router.js +35 -0
  20. package/dist/api/core/settings-backend.router.js +121 -0
  21. package/dist/api/core/stream-probe.router.js +58 -0
  22. package/dist/api/core/system-events.router.js +100 -0
  23. package/dist/api/health/health.routes.js +68 -0
  24. package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
  25. package/dist/api/oauth2/oauth2-routes.js +219 -0
  26. package/dist/api/trpc/cap-mount-helpers.js +194 -0
  27. package/dist/api/trpc/cap-route-error-formatter.js +133 -0
  28. package/dist/api/trpc/client-ip.js +147 -0
  29. package/dist/api/trpc/core-cap-bridge.js +115 -0
  30. package/dist/api/trpc/generated-cap-mounts.js +388 -0
  31. package/dist/api/trpc/generated-cap-routers.js +7635 -0
  32. package/dist/api/trpc/scope-access.js +93 -0
  33. package/dist/api/trpc/trpc.context.js +184 -0
  34. package/dist/api/trpc/trpc.middleware.js +139 -0
  35. package/dist/api/trpc/trpc.router.js +188 -0
  36. package/dist/auth/session-cookie.js +47 -0
  37. package/dist/boot/boot-config.js +241 -0
  38. package/dist/boot/integration-id-backfill.js +76 -0
  39. package/dist/boot/post-boot.service.js +85 -0
  40. package/dist/core/addon/addon-call-gateway.js +99 -0
  41. package/dist/core/addon/addon-package.service.js +1560 -0
  42. package/dist/core/addon/addon-registry.service.js +2739 -0
  43. package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
  44. package/dist/core/addon/addon-search.service.js +62 -0
  45. package/dist/core/addon/addon-settings-provider.js +102 -0
  46. package/dist/core/addon/addon.tokens.js +5 -0
  47. package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
  48. package/dist/core/addon-pages/addon-pages.service.js +107 -0
  49. package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
  50. package/dist/core/agent/agent-registry.service.js +477 -0
  51. package/dist/core/auth/auth.service.js +10 -0
  52. package/dist/core/capability/capability.service.js +58 -0
  53. package/dist/core/config/config.schema.js +7 -0
  54. package/dist/core/config/config.service.js +10 -0
  55. package/dist/core/events/event-bus.service.js +83 -0
  56. package/dist/core/feature/feature.service.js +10 -0
  57. package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
  58. package/dist/core/logging/log-ring-buffer.js +6 -0
  59. package/dist/core/logging/logging.service.js +130 -0
  60. package/dist/core/logging/scoped-logger.js +6 -0
  61. package/dist/core/moleculer/cap-call-fn.js +50 -0
  62. package/dist/core/moleculer/cap-route-authority.js +122 -0
  63. package/dist/core/moleculer/moleculer.service.js +898 -0
  64. package/dist/core/network/network-quality.service.js +7 -0
  65. package/dist/core/notification/notification-wrapper.service.js +33 -0
  66. package/dist/core/notification/toast-wrapper.service.js +25 -0
  67. package/dist/core/provider/provider.tokens.js +4 -0
  68. package/dist/core/repl/repl-engine.service.js +140 -0
  69. package/dist/core/storage/fs-storage-backend.js +6 -0
  70. package/dist/core/storage/storage-location-manager.js +6 -0
  71. package/dist/core/storage/storage.service.js +7 -0
  72. package/dist/core/streaming/stream-probe.service.js +209 -0
  73. package/dist/core/topology/topology-emitter.service.js +106 -0
  74. package/dist/launcher.js +325 -0
  75. package/dist/main.js +1098 -0
  76. package/dist/manual-boot.js +227 -0
  77. package/package.json +5 -1
  78. package/src/__tests__/addon-install-e2e.test.ts +0 -74
  79. package/src/__tests__/addon-pages-e2e.test.ts +0 -200
  80. package/src/__tests__/addon-route-session.test.ts +0 -17
  81. package/src/__tests__/addon-settings-router.spec.ts +0 -67
  82. package/src/__tests__/addon-upload.spec.ts +0 -475
  83. package/src/__tests__/agent-registry.spec.ts +0 -179
  84. package/src/__tests__/agent-status-page.spec.ts +0 -82
  85. package/src/__tests__/auth-session-cookie.test.ts +0 -48
  86. package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
  87. package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
  88. package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
  89. package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
  90. package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
  91. package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
  92. package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
  93. package/src/__tests__/cap-route-adapter.spec.ts +0 -302
  94. package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
  95. package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
  96. package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
  97. package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
  98. package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
  99. package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
  100. package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
  101. package/src/__tests__/cap-routers/harness.ts +0 -163
  102. package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
  103. package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
  104. package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
  105. package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
  106. package/src/__tests__/capability-e2e.test.ts +0 -384
  107. package/src/__tests__/cli-e2e.test.ts +0 -150
  108. package/src/__tests__/core-cap-bridge.spec.ts +0 -91
  109. package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
  110. package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
  111. package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
  112. package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
  113. package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
  114. package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
  115. package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
  116. package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
  117. package/src/__tests__/framework-allowlist.spec.ts +0 -96
  118. package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
  119. package/src/__tests__/https-e2e.test.ts +0 -124
  120. package/src/__tests__/lifecycle-e2e.test.ts +0 -189
  121. package/src/__tests__/live-events-subscription.spec.ts +0 -149
  122. package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
  123. package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
  124. package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
  125. package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
  126. package/src/__tests__/native-cap-route.spec.ts +0 -427
  127. package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
  128. package/src/__tests__/post-boot-restart.spec.ts +0 -161
  129. package/src/__tests__/singleton-contention.test.ts +0 -499
  130. package/src/__tests__/streaming-diagnostic.test.ts +0 -615
  131. package/src/__tests__/streaming-scale.test.ts +0 -314
  132. package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
  133. package/src/__tests__/uds-log-ingest.spec.ts +0 -183
  134. package/src/api/__tests__/addons-custom.spec.ts +0 -148
  135. package/src/api/__tests__/capabilities.router.test.ts +0 -56
  136. package/src/api/addon-upload.ts +0 -529
  137. package/src/api/addons-custom.router.ts +0 -101
  138. package/src/api/auth-whoami.ts +0 -101
  139. package/src/api/bridge-addons.router.ts +0 -122
  140. package/src/api/capabilities.router.ts +0 -265
  141. package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
  142. package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
  143. package/src/api/core/addon-settings.router.ts +0 -127
  144. package/src/api/core/agents.router.ts +0 -86
  145. package/src/api/core/auth.router.ts +0 -322
  146. package/src/api/core/bulk-update-coordinator.ts +0 -305
  147. package/src/api/core/cap-providers.ts +0 -1339
  148. package/src/api/core/capabilities.router.ts +0 -149
  149. package/src/api/core/collection-preference.ts +0 -40
  150. package/src/api/core/event-bus-proxy.router.ts +0 -45
  151. package/src/api/core/hwaccel.router.ts +0 -108
  152. package/src/api/core/live-events.router.ts +0 -67
  153. package/src/api/core/logs.router.ts +0 -195
  154. package/src/api/core/notifications.router.ts +0 -66
  155. package/src/api/core/repl.router.ts +0 -39
  156. package/src/api/core/settings-backend.router.ts +0 -140
  157. package/src/api/core/stream-probe.router.ts +0 -57
  158. package/src/api/core/system-events.router.ts +0 -125
  159. package/src/api/health/health.routes.ts +0 -117
  160. package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
  161. package/src/api/oauth2/oauth2-routes.ts +0 -281
  162. package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
  163. package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
  164. package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
  165. package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
  166. package/src/api/trpc/cap-mount-helpers.ts +0 -245
  167. package/src/api/trpc/cap-route-error-formatter.ts +0 -171
  168. package/src/api/trpc/client-ip.ts +0 -147
  169. package/src/api/trpc/core-cap-bridge.ts +0 -154
  170. package/src/api/trpc/generated-cap-mounts.ts +0 -1240
  171. package/src/api/trpc/generated-cap-routers.ts +0 -11523
  172. package/src/api/trpc/scope-access.ts +0 -110
  173. package/src/api/trpc/trpc.context.ts +0 -258
  174. package/src/api/trpc/trpc.middleware.ts +0 -146
  175. package/src/api/trpc/trpc.router.ts +0 -389
  176. package/src/auth/session-cookie.ts +0 -54
  177. package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
  178. package/src/boot/boot-config.ts +0 -259
  179. package/src/boot/integration-id-backfill.ts +0 -109
  180. package/src/boot/post-boot.service.ts +0 -105
  181. package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
  182. package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
  183. package/src/core/addon/addon-call-gateway.ts +0 -171
  184. package/src/core/addon/addon-package.service.ts +0 -1787
  185. package/src/core/addon/addon-registry.service.ts +0 -3130
  186. package/src/core/addon/addon-search.service.ts +0 -91
  187. package/src/core/addon/addon-settings-provider.ts +0 -220
  188. package/src/core/addon/addon.tokens.ts +0 -2
  189. package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
  190. package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
  191. package/src/core/addon-pages/addon-pages.service.ts +0 -82
  192. package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
  193. package/src/core/agent/agent-registry.service.ts +0 -529
  194. package/src/core/auth/auth.service.spec.ts +0 -86
  195. package/src/core/auth/auth.service.ts +0 -8
  196. package/src/core/capability/capability.service.ts +0 -66
  197. package/src/core/config/config.schema.ts +0 -3
  198. package/src/core/config/config.service.spec.ts +0 -175
  199. package/src/core/config/config.service.ts +0 -7
  200. package/src/core/events/event-bus.service.spec.ts +0 -235
  201. package/src/core/events/event-bus.service.ts +0 -89
  202. package/src/core/feature/feature.service.spec.ts +0 -99
  203. package/src/core/feature/feature.service.ts +0 -8
  204. package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
  205. package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
  206. package/src/core/logging/log-ring-buffer.ts +0 -3
  207. package/src/core/logging/logging.service.spec.ts +0 -287
  208. package/src/core/logging/logging.service.ts +0 -143
  209. package/src/core/logging/scoped-logger.ts +0 -3
  210. package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
  211. package/src/core/moleculer/cap-call-fn.ts +0 -107
  212. package/src/core/moleculer/cap-route-authority.ts +0 -194
  213. package/src/core/moleculer/moleculer.service.ts +0 -1072
  214. package/src/core/network/network-quality.service.spec.ts +0 -53
  215. package/src/core/network/network-quality.service.ts +0 -5
  216. package/src/core/notification/notification-wrapper.service.ts +0 -34
  217. package/src/core/notification/toast-wrapper.service.ts +0 -27
  218. package/src/core/provider/provider.tokens.ts +0 -1
  219. package/src/core/repl/repl-engine.service.spec.ts +0 -444
  220. package/src/core/repl/repl-engine.service.ts +0 -155
  221. package/src/core/storage/fs-storage-backend.spec.ts +0 -70
  222. package/src/core/storage/fs-storage-backend.ts +0 -3
  223. package/src/core/storage/storage-location-manager.spec.ts +0 -130
  224. package/src/core/storage/storage-location-manager.ts +0 -3
  225. package/src/core/storage/storage.service.spec.ts +0 -73
  226. package/src/core/storage/storage.service.ts +0 -3
  227. package/src/core/streaming/stream-probe.service.ts +0 -221
  228. package/src/core/topology/topology-emitter.service.ts +0 -105
  229. package/src/launcher.ts +0 -314
  230. package/src/main.ts +0 -1245
  231. package/src/manual-boot.ts +0 -301
  232. package/tsconfig.build.json +0 -8
  233. package/tsconfig.json +0 -33
  234. package/vitest.config.ts +0 -26
@@ -1,91 +0,0 @@
1
- import { LoggingService } from '../logging/logging.service'
2
- import { errMsg } from '@camstack/types'
3
-
4
- interface NpmSearchResult {
5
- name: string
6
- version: string
7
- description: string
8
- keywords: string[]
9
- date: string
10
- publisher: { username: string }
11
- }
12
-
13
- interface AddonSearchResult {
14
- name: string
15
- version: string
16
- description: string
17
- keywords: string[]
18
- publishedAt: string
19
- author: string
20
- installed: boolean
21
- installedVersion?: string
22
- }
23
-
24
- export class AddonSearchService {
25
- private cache: { results: NpmSearchResult[]; timestamp: number } | null = null
26
- private readonly CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
27
-
28
- constructor(private readonly loggingService: LoggingService) {}
29
-
30
- async searchAddons(query?: string): Promise<AddonSearchResult[]> {
31
- const npmResults = await this.fetchFromNpm()
32
-
33
- // Filter by additional query if provided
34
- let filtered = npmResults
35
- if (query) {
36
- const q = query.toLowerCase()
37
- filtered = npmResults.filter(
38
- (r) =>
39
- r.name.toLowerCase().includes(q) ||
40
- r.description?.toLowerCase().includes(q) ||
41
- r.keywords?.some((k) => k.toLowerCase().includes(q)),
42
- )
43
- }
44
-
45
- // Return without install status — the caller can merge with installed list
46
- return filtered.map((r) => ({
47
- name: r.name,
48
- version: r.version,
49
- description: r.description ?? '',
50
- keywords: r.keywords ?? [],
51
- publishedAt: r.date ?? '',
52
- author: r.publisher?.username ?? '',
53
- installed: false, // caller will enrich this
54
- installedVersion: undefined,
55
- }))
56
- }
57
-
58
- private async fetchFromNpm(): Promise<NpmSearchResult[]> {
59
- // Return from cache if still fresh
60
- if (this.cache && Date.now() - this.cache.timestamp < this.CACHE_TTL_MS) {
61
- return this.cache.results
62
- }
63
-
64
- // npm registry v1 search: packages with BOTH "camstack" and "addon" keywords
65
- const url =
66
- 'https://registry.npmjs.org/-/v1/search?text=keywords:camstack+keywords:addon&size=250'
67
-
68
- try {
69
- const response = await fetch(url, {
70
- headers: { Accept: 'application/json' },
71
- signal: AbortSignal.timeout(10000),
72
- })
73
-
74
- if (!response.ok) {
75
- throw new Error(`npm search failed: ${response.status}`)
76
- }
77
-
78
- const data = (await response.json()) as { objects: Array<{ package: NpmSearchResult }> }
79
- const results = data.objects.map((o) => o.package)
80
-
81
- this.cache = { results, timestamp: Date.now() }
82
-
83
- return results
84
- } catch (err) {
85
- const logger = this.loggingService.createLogger('addon-search')
86
- logger.warn('npm search failed', { meta: { error: errMsg(err) } })
87
- // Stale cache is better than nothing on transient network failure
88
- return this.cache?.results ?? []
89
- }
90
- }
91
- }
@@ -1,220 +0,0 @@
1
- /**
2
- * Hub-side singleton provider for the `addon-settings` capability.
3
- *
4
- * Resolves `addonId` and delegates the settings call. In-process hub builtins
5
- * are called directly; every FORKED addon (hub-local child over UDS, or remote
6
- * agent over Moleculer) routes through the shared {@link AddonCallGateway} —
7
- * the SAME centralised router routes/custom-actions use. Settings used to live
8
- * on its own Moleculer-only path here, which is why forked hub-local addons'
9
- * panels went empty after the UDS migration; that path is gone.
10
- */
11
- import type { ICamstackAddon, ConfigUISchemaWithValues } from '@camstack/types'
12
- import type { IAddonSettingsProvider } from '@camstack/types'
13
- import type { AddonSettingsMethod } from '@camstack/kernel'
14
- import type { AddonCallGateway } from './addon-call-gateway.js'
15
-
16
- // ── Dependency interfaces ────────────────────────────────────────────
17
-
18
- /** Resolve an addon by id. Returns null if not loaded on this node. */
19
- type AddonLookup = (addonId: string) => ICamstackAddon | null
20
-
21
- // ── Input types (match the cap definition's z.infer) ─────────────────
22
-
23
- interface AddonIdInput {
24
- readonly addonId: string
25
- readonly nodeId?: string
26
- readonly overlay?: Record<string, unknown>
27
- readonly cap?: string
28
- }
29
-
30
- interface AddonPatchInput {
31
- readonly addonId: string
32
- readonly nodeId?: string
33
- readonly patch: Record<string, unknown>
34
- }
35
-
36
- interface DeviceGetInput {
37
- readonly addonId: string
38
- readonly deviceId: number
39
- readonly nodeId?: string
40
- }
41
-
42
- interface DeviceUpdateInput {
43
- readonly addonId: string
44
- readonly deviceId: number
45
- readonly nodeId?: string
46
- readonly patch: Record<string, unknown>
47
- }
48
-
49
- interface SettingsUpdateResult {
50
- readonly success: true
51
- }
52
-
53
- // ── Reshape helper ───────────────────────────────────────────────────
54
-
55
- /**
56
- * Deep-copy a `ConfigUISchemaWithValues` into mutable-array shape for
57
- * Zod output. The domain type uses `readonly` arrays; Zod's inferred
58
- * output uses mutable arrays. Structurally identical at runtime.
59
- */
60
- interface ReshapedSchema {
61
- tabs?: Array<{ id: string; label: string; icon: string; order?: number }>
62
- sections: Array<{
63
- id: string
64
- title: string
65
- description?: string
66
- style?: 'card' | 'accordion'
67
- defaultCollapsed?: boolean
68
- columns?: 1 | 2 | 3 | 4
69
- tab?: string
70
- order?: number
71
- fields: unknown[]
72
- }>
73
- }
74
-
75
- function reshapeForOutput(schema: ConfigUISchemaWithValues): ReshapedSchema {
76
- return {
77
- tabs: schema.tabs
78
- ? schema.tabs.map((t) => ({ id: t.id, label: t.label, icon: t.icon, order: t.order }))
79
- : undefined,
80
- sections: schema.sections.map((s) => ({
81
- id: s.id,
82
- title: s.title,
83
- description: s.description,
84
- style: s.style,
85
- defaultCollapsed: s.defaultCollapsed,
86
- columns: s.columns,
87
- tab: s.tab,
88
- order: s.order,
89
- fields: [...s.fields],
90
- })),
91
- }
92
- }
93
-
94
- /**
95
- * True for Moleculer's `ServiceNotFoundError` — raised when a
96
- * `<addonId>.settings.*` action is called before the owning forked
97
- * addon has registered its service (still booting, or wedged in
98
- * `onInitialize`). Matched by `name` so we don't depend on Moleculer's
99
- * error class being importable here.
100
- */
101
- function isServiceNotFoundError(err: unknown): boolean {
102
- return err instanceof Error && err.name === 'ServiceNotFoundError'
103
- }
104
-
105
- // ── Provider factory ─────────────────────────────────────────────────
106
-
107
- interface AddonSettingsProviderDeps {
108
- /** Look up a loaded IN-PROCESS addon by id (the `@camstack/core` builtins). */
109
- readonly getAddon: AddonLookup
110
- /** Shared router for forked addon-level calls (hub-local-child / remote). */
111
- readonly gateway: AddonCallGateway
112
- }
113
-
114
- function createAddonSettingsProvider(deps: AddonSettingsProviderDeps): IAddonSettingsProvider {
115
- const { getAddon, gateway } = deps
116
-
117
- // Forked READ — route through the shared gateway (UDS hub-local-child OR
118
- // Moleculer remote agent). Degrades to null when the forked addon can't
119
- // answer yet (mid-boot / missing service) so the panel shows its empty state
120
- // instead of bubbling a 500.
121
- async function forkedGet(
122
- addonId: string,
123
- method: AddonSettingsMethod,
124
- args: Record<string, unknown>,
125
- nodeId?: string,
126
- ): Promise<ReshapedSchema | null> {
127
- try {
128
- const result = await gateway.callForked(addonId, { target: 'settings', method, args }, nodeId)
129
- return result ? reshapeForOutput(result as ConfigUISchemaWithValues) : null
130
- } catch (err) {
131
- if (isServiceNotFoundError(err)) return null
132
- throw err
133
- }
134
- }
135
-
136
- // Forked UPDATE — route through the shared gateway; returns the ack.
137
- async function forkedUpdate(
138
- addonId: string,
139
- method: AddonSettingsMethod,
140
- args: Record<string, unknown>,
141
- nodeId?: string,
142
- ): Promise<SettingsUpdateResult> {
143
- await gateway.callForked(addonId, { target: 'settings', method, args }, nodeId)
144
- return { success: true as const }
145
- }
146
-
147
- // ── Provider implementation ──────────────────────────────────────
148
-
149
- return {
150
- async getGlobalSettings(input: AddonIdInput) {
151
- if (gateway.isInProcess(input.addonId, input.nodeId)) {
152
- const addon = getAddon(input.addonId)
153
- if (!addon || typeof addon.getGlobalSettings !== 'function') return null
154
- const result = await addon.getGlobalSettings(input.overlay, input.cap)
155
- return result ? reshapeForOutput(result) : null
156
- }
157
- return forkedGet(
158
- input.addonId,
159
- 'getGlobalSettings',
160
- {
161
- ...(input.overlay ? { overlay: input.overlay } : {}),
162
- ...(input.cap ? { cap: input.cap } : {}),
163
- },
164
- input.nodeId,
165
- )
166
- },
167
-
168
- async updateGlobalSettings(input: AddonPatchInput) {
169
- if (gateway.isInProcess(input.addonId, input.nodeId)) {
170
- const addon = getAddon(input.addonId)
171
- if (!addon || typeof addon.updateGlobalSettings !== 'function') {
172
- throw new Error(`Addon "${input.addonId}" does not implement updateGlobalSettings`)
173
- }
174
- await addon.updateGlobalSettings(input.patch)
175
- return { success: true as const }
176
- }
177
- return forkedUpdate(
178
- input.addonId,
179
- 'updateGlobalSettings',
180
- { patch: input.patch },
181
- input.nodeId,
182
- )
183
- },
184
-
185
- async getDeviceSettings(input: DeviceGetInput) {
186
- if (gateway.isInProcess(input.addonId, input.nodeId)) {
187
- const addon = getAddon(input.addonId)
188
- if (!addon || typeof addon.getDeviceSettings !== 'function') return null
189
- const result = await addon.getDeviceSettings(input.deviceId)
190
- return result ? reshapeForOutput(result) : null
191
- }
192
- return forkedGet(
193
- input.addonId,
194
- 'getDeviceSettings',
195
- { deviceId: input.deviceId },
196
- input.nodeId,
197
- )
198
- },
199
-
200
- async updateDeviceSettings(input: DeviceUpdateInput) {
201
- if (gateway.isInProcess(input.addonId, input.nodeId)) {
202
- const addon = getAddon(input.addonId)
203
- if (!addon || typeof addon.updateDeviceSettings !== 'function') {
204
- throw new Error(`Addon "${input.addonId}" does not implement updateDeviceSettings`)
205
- }
206
- await addon.updateDeviceSettings(input.deviceId, input.patch)
207
- return { success: true as const }
208
- }
209
- return forkedUpdate(
210
- input.addonId,
211
- 'updateDeviceSettings',
212
- { deviceId: input.deviceId, patch: input.patch },
213
- input.nodeId,
214
- )
215
- },
216
- }
217
- }
218
-
219
- export { createAddonSettingsProvider }
220
- export type { AddonSettingsProviderDeps }
@@ -1,2 +0,0 @@
1
- export const ADDON_REGISTRY = Symbol('ADDON_REGISTRY')
2
- export const BUILTIN_ADDONS = Symbol('BUILTIN_ADDONS')
@@ -1,130 +0,0 @@
1
- import * as path from 'node:path'
2
- import { LoggingService } from '../logging/logging.service'
3
- import type { IScopedLogger } from '@camstack/types'
4
- import { errMsg } from '@camstack/types'
5
-
6
- // Dynamically imported to tolerate missing / unbuilt packages at startup
7
- type AddonLoader = import('@camstack/kernel').AddonLoader
8
- type AddonInstaller = import('@camstack/kernel').AddonInstaller
9
- type RegisteredAddon = import('@camstack/kernel').RegisteredAddon
10
-
11
- /**
12
- * AddonBridgeService — slim package-management surface.
13
- *
14
- * Historically this service also cached per-camera pipeline configs + per-
15
- * addon config records + hosted an in-process `PipelineRunner` that consumers
16
- * called via `processFrame` / `processMotionFrame`. Every piece of that
17
- * plumbing was dead post commit 2c (the runtime scheduler moved to
18
- * `addon-pipeline-runner`) and the pipeline page redesign finishes the job
19
- * by deleting the cache, the AddonEngineManager wiring, the bridge-pipeline
20
- * tRPC router, and the PipelineWiringModule that bolted the legacy addon
21
- * resolver onto this service.
22
- *
23
- * What's left is ONLY the addon package management surface: the installer
24
- * + the loader + `reloadPackages` + `listAvailableAddons`. These are still
25
- * used by:
26
- * - `server/backend/src/api/addon-upload.ts` (multipart upload → install
27
- * → loader reload)
28
- * - `server/backend/src/api/bridge-addons.router.ts` (install/uninstall
29
- * package lifecycle, addon list via the loader)
30
- */
31
- export class AddonBridgeService {
32
- private readonly logger: IScopedLogger
33
-
34
- private loader!: AddonLoader
35
- private installer: AddonInstaller | null = null
36
-
37
- /** Whether the bridge initialised successfully */
38
- private available = false
39
-
40
- constructor(private readonly loggingService: LoggingService) {
41
- this.logger = this.loggingService.createLogger('AddonBridge')
42
-
43
- // Initialize installer eagerly in constructor (no async needed).
44
- // This ensures install/uninstall works even before onModuleInit completes.
45
- try {
46
- const kernel = require('@camstack/kernel') as typeof import('@camstack/kernel')
47
- const dataDir = process.env.CAMSTACK_DATA ?? 'camstack-data'
48
- const addonsDir = path.resolve(dataDir, 'addons')
49
- const workspacePackagesDir = kernel.detectWorkspacePackagesDir(__dirname)
50
- this.installer = new kernel.AddonInstaller({
51
- addonsDir,
52
- workspacePackagesDir: workspacePackagesDir ?? undefined,
53
- })
54
- kernel.ensureDir(addonsDir)
55
- } catch (error: unknown) {
56
- const msg = errMsg(error)
57
- this.logger.warn('Installer init failed', { meta: { error: msg } })
58
- }
59
- }
60
-
61
- async onModuleInit(): Promise<void> {
62
- this.logger.info('Initializing addon bridge (package management only)...')
63
-
64
- try {
65
- const kernel = await import('@camstack/kernel')
66
-
67
- this.loader = new kernel.AddonLoader(this.logger.child('AddonLoader'))
68
-
69
- const dataDir = process.env.CAMSTACK_DATA ?? 'camstack-data'
70
- const addonsDir = path.resolve(dataDir, 'addons')
71
- await this.loader.loadFromDirectory(addonsDir)
72
-
73
- this.available = true
74
- this.logger.info('Addon bridge initialized', {
75
- meta: { count: this.loader.listAddons().length },
76
- })
77
- } catch (error: unknown) {
78
- const msg = errMsg(error)
79
- this.logger.warn('Addon bridge loader failed — install/uninstall still available', {
80
- meta: { error: msg },
81
- })
82
- }
83
- }
84
-
85
- async onModuleDestroy(): Promise<void> {
86
- this.logger.info('Addon bridge shut down')
87
- }
88
-
89
- // ---------------------------------------------------------------------------
90
- // Public API
91
- // ---------------------------------------------------------------------------
92
-
93
- /** Whether the bridge is ready for use */
94
- isAvailable(): boolean {
95
- return this.available
96
- }
97
-
98
- /** Return the underlying AddonLoader (for querying / introspection) */
99
- getLoader(): AddonLoader {
100
- return this.loader
101
- }
102
-
103
- /** List IDs of all successfully loaded addons */
104
- listAvailableAddons(): string[] {
105
- if (!this.available) return []
106
- return this.loader.listAddons().map((a: RegisteredAddon) => a.declaration.id)
107
- }
108
-
109
- /** Return the AddonInstaller instance (may be null if bridge failed to init) */
110
- getInstaller(): AddonInstaller | null {
111
- return this.installer
112
- }
113
-
114
- /** Re-discover addons from the addons directory (call after install/uninstall) */
115
- async reloadPackages(): Promise<void> {
116
- if (!this.available) {
117
- this.logger.warn('reloadPackages called but bridge is unavailable — skipping')
118
- return
119
- }
120
-
121
- const kernel = await import('@camstack/kernel')
122
- this.loader = new kernel.AddonLoader(this.logger.child('AddonLoader'))
123
-
124
- const dataDir = process.env.CAMSTACK_DATA ?? 'camstack-data'
125
- const addonsDir = path.resolve(dataDir, 'addons')
126
- await this.loader.loadFromDirectory(addonsDir)
127
-
128
- this.logger.info('Reloaded', { meta: { count: this.loader.listAddons().length } })
129
- }
130
- }
@@ -1,117 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
-
3
- vi.mock('node:fs', () => ({
4
- existsSync: vi.fn(),
5
- }))
6
-
7
- import * as fs from 'node:fs'
8
- import { AddonPagesService } from './addon-pages.service'
9
- import type { CapabilityService } from '../capability/capability.service'
10
- import type { LoggingService } from '../logging/logging.service'
11
- import type { ConfigService } from '../config/config.service'
12
- import type { IAddonPageProvider, AddonPageDeclaration, IScopedLogger } from '@camstack/types'
13
-
14
- function createMockLogger(): IScopedLogger {
15
- return {
16
- debug: vi.fn(),
17
- info: vi.fn(),
18
- warn: vi.fn(),
19
- error: vi.fn(),
20
- child: vi.fn().mockReturnThis(),
21
- }
22
- }
23
-
24
- function createMockLoggingService(): LoggingService {
25
- return {
26
- createLogger: vi.fn().mockReturnValue(createMockLogger()),
27
- } as unknown as LoggingService
28
- }
29
-
30
- function createMockConfigService(): ConfigService {
31
- return {
32
- get: vi.fn().mockReturnValue('./data'),
33
- } as unknown as ConfigService
34
- }
35
-
36
- function createMockPageProvider(
37
- id: string,
38
- pages: readonly AddonPageDeclaration[],
39
- ): IAddonPageProvider {
40
- return {
41
- id,
42
- listPages: () => pages,
43
- }
44
- }
45
-
46
- /**
47
- * AddonPagesService now resolves bundles via the `addon-pages-source`
48
- * collection cap (split from the public `addon-pages` singleton cap
49
- * earlier this session).
50
- */
51
- function createMockCapabilityService(providers: IAddonPageProvider[] = []): CapabilityService {
52
- return {
53
- getSingleton: vi.fn(() => null),
54
- getCollection: vi.fn((capability: string) => {
55
- if (capability === 'addon-pages-source') return providers
56
- return []
57
- }),
58
- resolveForDevice: vi.fn(() => null),
59
- resolveCollectionForDevice: vi.fn(() => []),
60
- setRegistry: vi.fn(),
61
- getRegistry: vi.fn(() => null),
62
- } as unknown as CapabilityService
63
- }
64
-
65
- function buildService(providers: IAddonPageProvider[] = []): AddonPagesService {
66
- return new AddonPagesService(
67
- createMockLoggingService(),
68
- createMockConfigService(),
69
- createMockCapabilityService(providers),
70
- )
71
- }
72
-
73
- describe('AddonPagesService.resolveBundle', () => {
74
- it('should return null for unregistered addon', () => {
75
- const service = buildService([])
76
- const result = service.resolveBundle('nonexistent', 'dist/pages/main.js')
77
- expect(result).toBeNull()
78
- })
79
-
80
- it('should return null when file does not exist', () => {
81
- const provider = createMockPageProvider('benchmark', [])
82
- const service = buildService([provider])
83
-
84
- const existsMock = fs.existsSync as unknown as ReturnType<typeof vi.fn>
85
- existsMock.mockReturnValue(false)
86
-
87
- const result = service.resolveBundle('benchmark', 'dist/pages/benchmark.js')
88
- expect(result).toBeNull()
89
- })
90
-
91
- it('should return resolved path when file exists', () => {
92
- const provider = createMockPageProvider('benchmark', [])
93
- const service = buildService([provider])
94
-
95
- const existsMock = fs.existsSync as unknown as ReturnType<typeof vi.fn>
96
- existsMock.mockReturnValue(true)
97
-
98
- const result = service.resolveBundle('benchmark', 'dist/pages/benchmark.js')
99
- expect(result).not.toBeNull()
100
- expect(result).toContain('addon-benchmark')
101
- expect(result).toContain('dist/pages/benchmark.js')
102
- })
103
-
104
- it('should deny path traversal attempts', () => {
105
- const provider = createMockPageProvider('benchmark', [])
106
- const service = buildService([provider])
107
- const result = service.resolveBundle('benchmark', '../../etc/passwd')
108
- expect(result).toBeNull()
109
- })
110
-
111
- it('should deny absolute path traversal', () => {
112
- const provider = createMockPageProvider('benchmark', [])
113
- const service = buildService([provider])
114
- const result = service.resolveBundle('benchmark', '../../../etc/passwd')
115
- expect(result).toBeNull()
116
- })
117
- })
@@ -1,82 +0,0 @@
1
- import * as path from 'node:path'
2
- import * as fs from 'node:fs'
3
- import { LoggingService } from '../logging/logging.service'
4
- import { ConfigService } from '../config/config.service'
5
- import { CapabilityService } from '../capability/capability.service'
6
- import type { IScopedLogger } from '@camstack/types'
7
- import type { IAddonPageProvider } from '@camstack/types'
8
-
9
- /**
10
- * AddonPagesService — server-side helper that backs the static file
11
- * route `/api/addon-pages/:addonId/*` (registered in `main.ts`).
12
- *
13
- * The public listing surface (`addonPages.listPages` on the AppRouter)
14
- * has been split out of this class — it now lives in the
15
- * `addon-pages-aggregator` builtin (`@camstack/core/builtins/...`)
16
- * which walks every `addon-pages-source` collection provider and
17
- * stamps versioned `bundleUrl`s. The split lets both ends flow through
18
- * codegen instead of relying on a hand-written wrapper.
19
- *
20
- * What stays here: filesystem path resolution + traversal protection
21
- * for the static file route. Both rely on `CapabilityService` to
22
- * enumerate registered page providers (so unknown / unregistered
23
- * addons can't be probed via path traversal tricks) and on
24
- * `ConfigService` to locate the addons directory.
25
- */
26
- export class AddonPagesService {
27
- private readonly logger: IScopedLogger
28
-
29
- constructor(
30
- private readonly loggingService: LoggingService,
31
- private readonly configService: ConfigService,
32
- private readonly caps: CapabilityService,
33
- ) {
34
- this.logger = this.loggingService.createLogger('AddonPagesService')
35
- }
36
-
37
- /**
38
- * Resolve the filesystem path to an addon's page bundle file.
39
- * Returns null if the addon is not registered, the file doesn't exist,
40
- * or the path would escape the addon directory (path traversal protection).
41
- */
42
- resolveBundle(addonId: string, filePath: string): string | null {
43
- // Check if the addon is a registered page provider via the
44
- // collection cap. `addon-pages-source` is the new home for the raw
45
- // per-provider declarations (the public `addon-pages` cap is the
46
- // aggregated singleton — different shape, different surface).
47
- const providers = this.caps.getCollection<IAddonPageProvider>('addon-pages-source')
48
- const isRegistered = providers.some((p) => p.id === addonId)
49
- if (!isRegistered) {
50
- this.logger.warn('Bundle resolve failed: addon not registered as page provider', {
51
- tags: { addonId },
52
- })
53
- return null
54
- }
55
-
56
- const addonsDir = this.resolveAddonsDir()
57
- // Addon packages are at addons/@camstack/addon-<id>/dist/
58
- const addonDistPath = path.join(addonsDir, '@camstack', `addon-${addonId}`, 'dist')
59
-
60
- const resolvedBase = path.resolve(addonDistPath)
61
- const resolvedFile = path.resolve(addonDistPath, filePath)
62
-
63
- // Path traversal protection
64
- if (!resolvedFile.startsWith(resolvedBase + path.sep) && resolvedFile !== resolvedBase) {
65
- this.logger.warn('Path traversal denied for addon', { tags: { addonId }, meta: { filePath } })
66
- return null
67
- }
68
-
69
- if (!fs.existsSync(resolvedFile)) {
70
- this.logger.debug('Bundle file not found', { meta: { resolvedFile } })
71
- return null
72
- }
73
-
74
- return resolvedFile
75
- }
76
-
77
- /** Resolve the addons directory from config */
78
- private resolveAddonsDir(): string {
79
- const dataPath = this.configService.get<string>('server.dataPath') ?? 'camstack-data'
80
- return path.resolve(dataPath, 'addons')
81
- }
82
- }