@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,54 +1,39 @@
1
- export interface AgentStatusData {
2
- readonly agentId: string
3
- readonly agentName: string
4
- readonly hubUrl: string
5
- readonly connected: boolean
6
- readonly activeTaskCount: number
7
- readonly taskTypes: readonly string[]
8
- readonly platform: string
9
- readonly arch: string
10
- readonly cpuCores: number
11
- readonly memoryTotalMB: number
12
- readonly memoryFreeMB: number
13
- readonly uptime: number
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderAgentStatusPage = renderAgentStatusPage;
4
+ function formatUptime(seconds) {
5
+ const days = Math.floor(seconds / 86400);
6
+ const hours = Math.floor((seconds % 86400) / 3600);
7
+ const mins = Math.floor((seconds % 3600) / 60);
8
+ const parts = [];
9
+ if (days > 0)
10
+ parts.push(`${days}d`);
11
+ if (hours > 0)
12
+ parts.push(`${hours}h`);
13
+ parts.push(`${mins}m`);
14
+ return parts.join(' ');
14
15
  }
15
-
16
- function formatUptime(seconds: number): string {
17
- const days = Math.floor(seconds / 86400)
18
- const hours = Math.floor((seconds % 86400) / 3600)
19
- const mins = Math.floor((seconds % 3600) / 60)
20
- const parts: string[] = []
21
- if (days > 0) parts.push(`${days}d`)
22
- if (hours > 0) parts.push(`${hours}h`)
23
- parts.push(`${mins}m`)
24
- return parts.join(' ')
25
- }
26
-
27
- function escapeHtml(str: string): string {
28
- return str
29
- .replace(/&/g, '&')
30
- .replace(/</g, '&lt;')
31
- .replace(/>/g, '&gt;')
32
- .replace(/"/g, '&quot;')
16
+ function escapeHtml(str) {
17
+ return str
18
+ .replace(/&/g, '&amp;')
19
+ .replace(/</g, '&lt;')
20
+ .replace(/>/g, '&gt;')
21
+ .replace(/"/g, '&quot;');
33
22
  }
34
-
35
23
  /**
36
24
  * Render a minimal server-rendered HTML status page for the agent.
37
25
  * No React, no build system. Pure HTML + inline CSS.
38
26
  * Designed for local debugging only.
39
27
  */
40
- export function renderAgentStatusPage(data: AgentStatusData): string {
41
- const statusColor = data.connected ? '#22c55e' : '#ef4444'
42
- const statusText = data.connected ? 'Connected' : 'Disconnected'
43
- const memUsedMB = data.memoryTotalMB - data.memoryFreeMB
44
- const memPercent = Math.round((memUsedMB / data.memoryTotalMB) * 100)
45
-
46
- const taskTypesList =
47
- data.taskTypes.length > 0
48
- ? data.taskTypes.map((t) => `<li>${escapeHtml(t)}</li>`).join('')
49
- : '<li class="muted">No task handlers registered</li>'
50
-
51
- return `<!DOCTYPE html>
28
+ function renderAgentStatusPage(data) {
29
+ const statusColor = data.connected ? '#22c55e' : '#ef4444';
30
+ const statusText = data.connected ? 'Connected' : 'Disconnected';
31
+ const memUsedMB = data.memoryTotalMB - data.memoryFreeMB;
32
+ const memPercent = Math.round((memUsedMB / data.memoryTotalMB) * 100);
33
+ const taskTypesList = data.taskTypes.length > 0
34
+ ? data.taskTypes.map((t) => `<li>${escapeHtml(t)}</li>`).join('')
35
+ : '<li class="muted">No task handlers registered</li>';
36
+ return `<!DOCTYPE html>
52
37
  <html lang="en">
53
38
  <head>
54
39
  <meta charset="utf-8">
@@ -118,5 +103,5 @@ export function renderAgentStatusPage(data: AgentStatusData): string {
118
103
  </div>
119
104
  </div>
120
105
  </body>
121
- </html>`
106
+ </html>`;
122
107
  }
@@ -0,0 +1,441 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerAddonUploadRoute = registerAddonUploadRoute;
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const os = __importStar(require("node:os"));
40
+ const node_child_process_1 = require("node:child_process");
41
+ const scope_access_js_1 = require("./trpc/scope-access.js");
42
+ const addon_package_service_js_1 = require("../core/addon/addon-package.service.js");
43
+ /**
44
+ * Validate a `cst_*` scoped token via the `user-management` cap singleton.
45
+ *
46
+ * The local `AuthService.validateScopedToken` indirection is unused (the
47
+ * `setScopedTokenManager` wire-up was never invoked), so we go straight
48
+ * to the cap registry — same path the generic addon-route handler in
49
+ * `main.ts` uses (the one that actually works end-to-end).
50
+ *
51
+ * Returns null when the singleton isn't mounted yet (boot race) or the
52
+ * token doesn't validate. Caller treats both as auth failure.
53
+ */
54
+ async function validateScopedTokenViaCap(addonRegistry, token) {
55
+ const capRegistry = addonRegistry.getCapabilityRegistry();
56
+ const userMgmt = capRegistry.getSingleton('user-management');
57
+ if (!userMgmt)
58
+ return null;
59
+ return userMgmt.validateScopedToken({ token });
60
+ }
61
+ /**
62
+ * REST endpoint `/api/addons/upload` shares the scope-gate of the
63
+ * `addons.installPackage` cap method (semantically equivalent — both
64
+ * install a tarball under the system-scope `addons` cap with `create`
65
+ * access). Reuse the shared scope matcher so the gate stays in sync
66
+ * with the tRPC middleware — no parallel REST-only ACL.
67
+ *
68
+ * Why not `addons.upload`? There is no `upload` method on the cap
69
+ * definition (this endpoint is Fastify-only), so `METHOD_ACCESS_MAP`
70
+ * has no row for it and `checkScopeAccess` falls through to deny.
71
+ */
72
+ const UPLOAD_TRPC_PATH = 'addons.installPackage';
73
+ function isUploadAllowed(scoped) {
74
+ return (0, scope_access_js_1.checkScopeAccess)(scoped.scopes, UPLOAD_TRPC_PATH).ok;
75
+ }
76
+ const TARBALL_EXTENSIONS = ['.tgz', '.tar.gz'];
77
+ const MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
78
+ const AGENT_DEPLOY_TIMEOUT_MS = 60_000;
79
+ function isTarball(filename) {
80
+ return TARBALL_EXTENSIONS.some((ext) => filename.endsWith(ext));
81
+ }
82
+ /**
83
+ * Validate an uploaded tarball by extracting its `package.json` and
84
+ * checking it declares `name` + `version`. Runs BEFORE the hub/agent
85
+ * branch so broken archives are rejected at the gateway instead of
86
+ * failing mid-extraction on the agent (which has no clean rollback).
87
+ *
88
+ * The routine writes the buffer to a scratch path and invokes `tar` with
89
+ * `-xzO` to stream-extract just `package/package.json` to stdout. No full
90
+ * unpack is needed — npm-pack layout always puts the manifest at that
91
+ * fixed inner path. Returns null (not throw) on malformed archives so
92
+ * the caller can surface a precise 400 response.
93
+ */
94
+ function validateTarball(buffer, filename) {
95
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'camstack-tarball-check-'));
96
+ const tgzPath = path.join(tmpDir, filename);
97
+ try {
98
+ fs.writeFileSync(tgzPath, buffer);
99
+ const output = (0, node_child_process_1.execFileSync)('tar', ['-xzO', '-f', tgzPath, 'package/package.json'], {
100
+ timeout: 5_000,
101
+ stdio: ['ignore', 'pipe', 'ignore'],
102
+ });
103
+ const parsed = JSON.parse(output.toString('utf8'));
104
+ if (!parsed || typeof parsed !== 'object')
105
+ return null;
106
+ const pkg = parsed;
107
+ if (typeof pkg['name'] !== 'string' || typeof pkg['version'] !== 'string')
108
+ return null;
109
+ return { name: pkg['name'], version: pkg['version'] };
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ finally {
115
+ fs.rmSync(tmpDir, { recursive: true, force: true });
116
+ }
117
+ }
118
+ async function registerAddonUploadRoute(fastify, addonBridge, authService, moleculer, addonRegistry, addonPackageService, logger) {
119
+ await fastify.register(Promise.resolve().then(() => __importStar(require('@fastify/multipart'))), {
120
+ limits: { fileSize: MAX_UPLOAD_BYTES },
121
+ });
122
+ fastify.post('/api/addons/upload', async (request, reply) => {
123
+ const authHeader = request.headers.authorization;
124
+ if (!authHeader) {
125
+ return reply.status(401).send({ error: 'Unauthorized' });
126
+ }
127
+ // Auth chain: JWT (isAdmin) OR scoped token whose scopes grant
128
+ // `create` access on the `addons` capability. The scoped path is the
129
+ // CLI's `camstack login` flow — fetches a long-lived upload-scoped
130
+ // token so headless deploys don't need an admin password on disk.
131
+ const token = authHeader.replace('Bearer ', '');
132
+ let authOk = false;
133
+ let authReason;
134
+ // Try JWT first — fastest path + carries isAdmin flag directly.
135
+ try {
136
+ const payload = authService.verifyToken(token);
137
+ if (payload.isAdmin) {
138
+ authOk = true;
139
+ }
140
+ else {
141
+ authReason = 'JWT is not admin';
142
+ }
143
+ }
144
+ catch {
145
+ // Not a JWT (or invalid signature) — fall through to scoped-token path.
146
+ }
147
+ if (!authOk) {
148
+ // `cst_*` scoped tokens — only the cap-registry singleton actually
149
+ // validates; the local AuthService bridge was never wired. See main.ts:652.
150
+ try {
151
+ const record = await validateScopedTokenViaCap(addonRegistry, token);
152
+ if (!record) {
153
+ authReason = authReason ?? 'token not recognised';
154
+ }
155
+ else if (isUploadAllowed(record)) {
156
+ authOk = true;
157
+ }
158
+ else {
159
+ authReason = `scoped token lacks create access on '${UPLOAD_TRPC_PATH}'`;
160
+ }
161
+ }
162
+ catch (err) {
163
+ authReason = `scoped token validation failed: ${err instanceof Error ? err.message : String(err)}`;
164
+ }
165
+ }
166
+ if (!authOk) {
167
+ return reply
168
+ .status(403)
169
+ .send({ error: `Forbidden: ${authReason ?? 'admin or upload-scoped token required'}` });
170
+ }
171
+ const data = await request.file();
172
+ if (!data) {
173
+ return reply.status(400).send({ error: 'No file uploaded' });
174
+ }
175
+ if (!isTarball(data.filename)) {
176
+ return reply.status(400).send({ error: 'File must be a .tgz or .tar.gz archive' });
177
+ }
178
+ // `nodeId` and `addonId` come through as multipart text fields.
179
+ // `data.fields[X].value` is the parsed string; we narrow defensively
180
+ // because the multipart plugin types it as `unknown`.
181
+ const nodeIdField = data.fields['nodeId'];
182
+ const addonIdField = data.fields['addonId'];
183
+ const nodeId = typeof nodeIdField === 'object' &&
184
+ nodeIdField !== null &&
185
+ 'value' in nodeIdField &&
186
+ typeof nodeIdField.value === 'string'
187
+ ? nodeIdField.value
188
+ : null;
189
+ const addonIdHint = typeof addonIdField === 'object' &&
190
+ addonIdField !== null &&
191
+ 'value' in addonIdField &&
192
+ typeof addonIdField.value === 'string'
193
+ ? addonIdField.value
194
+ : null;
195
+ const buffer = await data.toBuffer();
196
+ // Gate: reject archives that don't expose a parseable package.json with
197
+ // name + version. The hub installer did this implicitly via npm; the
198
+ // agent path would otherwise fail mid-extraction with no clean rollback.
199
+ const manifest = validateTarball(buffer, data.filename);
200
+ if (!manifest) {
201
+ return reply.status(400).send({
202
+ error: 'Tarball missing or malformed package/package.json (name + version required)',
203
+ });
204
+ }
205
+ // Branch by deployment target. `nodeId === 'hub'` (or absent) installs on
206
+ // the hub AND broadcasts to every connected agent that can run any of
207
+ // the package's addons (i.e. anything not marked hub-only). Any other
208
+ // explicit `nodeId` value routes only to that agent via `$agent.deploy`.
209
+ if (!nodeId || nodeId === 'hub') {
210
+ return installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, data.filename, buffer);
211
+ }
212
+ const agentAddonId = addonIdHint ?? manifest.name;
213
+ return deployToAgent(reply, moleculer, nodeId, agentAddonId, buffer);
214
+ });
215
+ }
216
+ /**
217
+ * Read the on-disk package.json and decide whether the package contains
218
+ * anything that needs to land on agents. We deliberately read from disk
219
+ * (rather than `addonRegistry.listAddons()`) so the function is safe to
220
+ * call before `loadNewAddons()` and so `agent-only` addons — which the
221
+ * hub registry intentionally skips — still show up here.
222
+ *
223
+ * Returns `true` if at least one addon's `execution.placement` is anything
224
+ * other than `'hub-only'`. Absence of `execution` defaults to hub-only so
225
+ * legacy packages don't suddenly broadcast cluster-wide.
226
+ */
227
+ function packageHasAgentDeployable(addonsDir, packageName) {
228
+ try {
229
+ const pkgPath = path.join(addonsDir, packageName, 'package.json');
230
+ const parsed = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
231
+ if (parsed === null || typeof parsed !== 'object')
232
+ return false;
233
+ const camstack = parsed
234
+ .camstack;
235
+ const addons = camstack?.addons ?? [];
236
+ return addons.some((a) => {
237
+ const placement = a.execution?.placement ?? 'hub-only';
238
+ return placement === 'agent-only' || placement === 'any-node';
239
+ });
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ }
245
+ /**
246
+ * Push `buffer` (a tarball) to every connected non-hub node and trigger a
247
+ * `$agent.reload` so the freshly extracted dist is picked up immediately
248
+ * without an agent restart. Agents that fail are reported per-node — one
249
+ * unreachable agent must not block the others or the hub install.
250
+ */
251
+ async function propagateToAgents(moleculer, logger, packageName, buffer) {
252
+ const broker = moleculer.broker;
253
+ const nodes = broker.registry?.getNodeList?.({ onlyAvailable: true }) ?? [];
254
+ // Moleculer reports both top-level nodes (`hub`, `dev-agent-0`, …) AND
255
+ // their child per-addon runner processes (`hub/detection-pipeline`, `dev-agent-0/foo`).
256
+ // Only the top-level agent nodes have the `$agent` service — calling
257
+ // `$agent.deploy` on a child would hang until timeout. The hub itself
258
+ // is excluded (it was already installed via `installToHub`).
259
+ const allNodeIds = nodes.map((n) => n.id);
260
+ const agentNodeIds = allNodeIds.filter((id) => id !== 'hub' && !id.includes('/'));
261
+ logger.info('propagate: enumerated broker nodes', {
262
+ meta: {
263
+ packageName,
264
+ allNodeIds,
265
+ targetAgents: agentNodeIds,
266
+ },
267
+ });
268
+ if (agentNodeIds.length === 0)
269
+ return [];
270
+ const results = [];
271
+ for (const nodeId of agentNodeIds) {
272
+ try {
273
+ const deployRaw = await broker.call('$agent.deploy', { addonId: packageName, bundle: buffer }, { nodeID: nodeId, timeout: AGENT_DEPLOY_TIMEOUT_MS });
274
+ if (!isAgentDeployResponse(deployRaw)) {
275
+ results.push({ nodeId, success: false, error: 'malformed deploy response' });
276
+ continue;
277
+ }
278
+ const reloadRaw = await broker.call('$agent.reload', {}, { nodeID: nodeId, timeout: AGENT_DEPLOY_TIMEOUT_MS });
279
+ const reloaded = reloadRaw !== null &&
280
+ typeof reloadRaw === 'object' &&
281
+ 'loaded' in reloadRaw
282
+ ? reloadRaw.loaded
283
+ : [];
284
+ results.push({ nodeId, success: true, loaded: reloaded });
285
+ }
286
+ catch (err) {
287
+ results.push({
288
+ nodeId,
289
+ success: false,
290
+ error: err instanceof Error ? err.message : String(err),
291
+ });
292
+ }
293
+ }
294
+ return results;
295
+ }
296
+ /**
297
+ * Hub install path: write the tgz, refresh the manifest list, then drive the
298
+ * registry through (a) `loadNewAddons` for brand-new addonIds and (b)
299
+ * `restartAddon` for each addonId already tied to this package. The restart
300
+ * branch is the hot-update story — group-hosted addons re-fork their child
301
+ * process (fresh dist code is loaded) and in-process addons re-init in place.
302
+ * Without this the CLI push was write-to-disk-only and required a server
303
+ * restart to actually run the new code.
304
+ */
305
+ async function installToHub(reply, addonBridge, addonRegistry, addonPackageService, moleculer, logger, filename, buffer) {
306
+ const tmpDir = path.join(os.tmpdir(), `camstack-addon-upload-${Date.now()}`);
307
+ fs.mkdirSync(tmpDir, { recursive: true });
308
+ const tgzPath = path.join(tmpDir, filename);
309
+ fs.writeFileSync(tgzPath, buffer);
310
+ try {
311
+ const installer = addonBridge.getInstaller();
312
+ if (!installer) {
313
+ return reply.status(500).send({ error: 'Addon installer not available' });
314
+ }
315
+ // Snapshot addonIds tied to this package BEFORE installation. Used after
316
+ // reload to distinguish "new" vs "updated". installFromTgz only writes
317
+ // to disk; `addonRegistry.listAddons()` reflects in-memory state, so the
318
+ // order (snapshot first vs install first) doesn't matter here — kept
319
+ // pre-install for readability.
320
+ const result = await installer.installFromTgz(tgzPath);
321
+ const preInstallAddonIds = addonRegistry
322
+ .listAddons()
323
+ .filter((row) => row.manifest.packageName === result.name)
324
+ .map((row) => row.manifest.id);
325
+ const addonsDir = path.resolve(process.env['CAMSTACK_DATA'] ?? 'camstack-data', 'addons');
326
+ fs.writeFileSync(path.join(addonsDir, result.name, '.install-source'), 'upload');
327
+ // Framework / system packages (`camstack.system: true`, e.g. @camstack/core)
328
+ // ship hub builtins (sqlite-settings, filesystem-storage, …) that CANNOT be
329
+ // hot-reloaded in place — an in-process `restartAddon` leaves them
330
+ // uninitialized ("SqliteSettingsBackend not initialized"), breaking auth +
331
+ // settings. The new code is now on disk; a clean server restart reloads it
332
+ // and re-runs boot initialization. The 10s restart grace lets this response
333
+ // flush before the process exits, so the CLI sees a clean confirmation.
334
+ if ((0, addon_package_service_js_1.isFrameworkPackage)(result.name)) {
335
+ logger.info('framework package deployed — scheduling server restart', {
336
+ meta: { packageName: result.name, packageVersion: result.version },
337
+ });
338
+ addonPackageService.restartServer(`addon-upload: ${result.name}@${result.version}`);
339
+ return reply.send({
340
+ success: true,
341
+ name: result.name,
342
+ version: result.version,
343
+ requiresRestart: true,
344
+ restarting: true,
345
+ message: 'Framework package installed — server is restarting to load it',
346
+ });
347
+ }
348
+ // `addonRegistry.loadNewAddons()` already runs its own fresh filesystem
349
+ // scan, diff'ing against existing `addonEntries` — it updates metadata
350
+ // for known addons without touching their live instances + initializes
351
+ // only entries it hasn't seen before. The previous `addonBridge.
352
+ // reloadPackages()` call here was redundant AND destructive: it built
353
+ // a brand-new `AddonLoader` and replaced the global, which knocked
354
+ // every isolated group-runner's IPC channel offline (visible as
355
+ // `Group-runner hub/<X>-isolated disconnected` for 6+ addons after
356
+ // a single `camstack deploy`). `loadNewAddons` alone is enough.
357
+ const loadResult = await addonRegistry.loadNewAddons();
358
+ // Hot-update + cluster propagation are FIRE-AND-FORGET. The upload
359
+ // route must respond quickly so the CLI / UI sees confirmation; the
360
+ // capability re-registration after a forked group-runner respawn can
361
+ // legitimately take 30s–several minutes (Python pool warmup, native
362
+ // module init, broken external deps) and we don't want operator
363
+ // tooling to hang waiting for it. `restartAddon` and
364
+ // `propagateToAgents` each log their own progress; operators monitor
365
+ // status through the topology cap or the addons UI.
366
+ const propagatable = packageHasAgentDeployable(addonsDir, result.name);
367
+ logger.info('hub install OK', {
368
+ meta: {
369
+ packageName: result.name,
370
+ packageVersion: result.version,
371
+ loaded: loadResult.loaded,
372
+ restartTargets: preInstallAddonIds,
373
+ propagatable,
374
+ },
375
+ });
376
+ for (const id of preInstallAddonIds) {
377
+ void addonRegistry.restartAddon(id).then((r) => {
378
+ if (!r.success) {
379
+ logger.warn('background restart failed', {
380
+ tags: { addonId: id },
381
+ meta: { error: r.error ?? 'unknown' },
382
+ });
383
+ }
384
+ else {
385
+ logger.info('background restart OK', { tags: { addonId: id } });
386
+ }
387
+ });
388
+ }
389
+ if (propagatable) {
390
+ void propagateToAgents(moleculer, logger, result.name, buffer).then((agentResults) => {
391
+ logger.info('propagation done', {
392
+ meta: { packageName: result.name, agents: agentResults },
393
+ });
394
+ });
395
+ }
396
+ return reply.send({
397
+ success: true,
398
+ target: 'hub',
399
+ packageName: result.name,
400
+ version: result.version,
401
+ loaded: loadResult.loaded,
402
+ restartingInBackground: preInstallAddonIds,
403
+ propagatingToAgentsInBackground: propagatable,
404
+ failures: loadResult.failed,
405
+ });
406
+ }
407
+ finally {
408
+ fs.rmSync(tmpDir, { recursive: true, force: true });
409
+ }
410
+ }
411
+ function isAgentDeployResponse(value) {
412
+ if (value === null || typeof value !== 'object')
413
+ return false;
414
+ const v = value;
415
+ if (typeof v.success !== 'boolean')
416
+ return false;
417
+ if (typeof v.addonId !== 'string')
418
+ return false;
419
+ if (v.path !== undefined && typeof v.path !== 'string')
420
+ return false;
421
+ return true;
422
+ }
423
+ async function deployToAgent(reply, moleculer, nodeId, addonId, buffer) {
424
+ try {
425
+ const broker = moleculer.broker;
426
+ const raw = await broker.call('$agent.deploy', { addonId, bundle: buffer }, { nodeID: nodeId, timeout: AGENT_DEPLOY_TIMEOUT_MS });
427
+ if (!isAgentDeployResponse(raw)) {
428
+ return reply.status(502).send({ error: 'Agent deploy returned malformed response' });
429
+ }
430
+ return reply.send({
431
+ success: true,
432
+ target: nodeId,
433
+ addonId: raw.addonId,
434
+ path: raw.path,
435
+ });
436
+ }
437
+ catch (err) {
438
+ const msg = err instanceof Error ? err.message : String(err);
439
+ return reply.status(502).send({ error: `Agent deploy failed: ${msg}` });
440
+ }
441
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAddonsCustomProcedures = createAddonsCustomProcedures;
4
+ /**
5
+ * `api.addons.custom` — generic dispatcher for addon-defined custom actions.
6
+ *
7
+ * Task 7.2 of the device-proxy redesign. Addons declare a catalog of
8
+ * custom actions at boot via `AddonInitResult.customActions` + a single
9
+ * `handleCustomAction(action, input)` handler. The catalog is registered
10
+ * with a per-process `CustomActionRegistry` (Task 7.1). This endpoint is
11
+ * the single tRPC entry point that resolves an `(addonId, action)` pair,
12
+ * validates input + output against the action's Zod schemas, enforces the
13
+ * action's declared auth level, and dispatches to the addon handler.
14
+ *
15
+ * The factory returns a record of procedures (not a router) so the caller
16
+ * can spread it into the existing `addons` namespace:
17
+ *
18
+ * trpcRouter({
19
+ * ...existingAddonsProcedures,
20
+ * ...createAddonsCustomProcedures({ getCustomActionRegistry: ... }),
21
+ * })
22
+ *
23
+ * This avoids `mergeRouters` (which requires sharing the `t` instance
24
+ * across modules) while still mounting the procedure at `api.addons.custom`.
25
+ */
26
+ const zod_1 = require("zod");
27
+ const server_1 = require("@trpc/server");
28
+ const trpc_middleware_js_1 = require("./trpc/trpc.middleware.js");
29
+ /**
30
+ * Build the procedure record for the `custom` endpoint.
31
+ *
32
+ * The OUTER procedure is `protectedProcedure` — every caller must be
33
+ * authenticated. The INNER per-action auth declared in `spec.auth` is
34
+ * enforced manually by `ensureAuth` because the auth level is not known
35
+ * until after the registry lookup.
36
+ */
37
+ function createAddonsCustomProcedures(deps) {
38
+ return {
39
+ custom: trpc_middleware_js_1.protectedProcedure
40
+ .input(zod_1.z.object({
41
+ addonId: zod_1.z.string().min(1),
42
+ action: zod_1.z.string().min(1),
43
+ input: zod_1.z.unknown(),
44
+ }))
45
+ .output(zod_1.z.unknown())
46
+ .mutation(async ({ input, ctx }) => {
47
+ const registry = deps.getCustomActionRegistry();
48
+ const entry = registry.resolve(input.addonId, input.action);
49
+ if (!entry) {
50
+ throw new server_1.TRPCError({
51
+ code: 'NOT_FOUND',
52
+ message: `addon '${input.addonId}' has no custom action '${input.action}'`,
53
+ });
54
+ }
55
+ // Per-action authorization. The outer procedure already requires
56
+ // authentication; here we additionally enforce the declared role
57
+ // when it's stricter than 'protected'.
58
+ ensureAuth(ctx, entry.spec.auth);
59
+ // Validate input against the action's declared Zod schema.
60
+ const parsedInput = entry.spec.input.parse(input.input);
61
+ // Dispatch through the addon handler.
62
+ const result = await entry.handler(parsedInput);
63
+ // Validate the addon's output. Crash-early on misbehaving addons.
64
+ return entry.spec.output.parse(result);
65
+ }),
66
+ };
67
+ }
68
+ /**
69
+ * Enforce the action's declared auth level.
70
+ *
71
+ * Mirrors the role checks performed by `protectedProcedure` and
72
+ * `adminProcedure` in trpc.middleware.ts:
73
+ * - public: no auth
74
+ * - protected: any authenticated user
75
+ * - admin: isAdmin only (scoped tokens bounce)
76
+ */
77
+ function ensureAuth(ctx, level) {
78
+ if (level === 'public')
79
+ return;
80
+ if (!ctx.user) {
81
+ throw new server_1.TRPCError({ code: 'UNAUTHORIZED' });
82
+ }
83
+ if (level === 'protected')
84
+ return;
85
+ if (level === 'admin') {
86
+ if (!ctx.user.isAdmin) {
87
+ throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'custom action requires admin' });
88
+ }
89
+ return;
90
+ }
91
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAuthWhoamiRoute = registerAuthWhoamiRoute;
4
+ async function registerAuthWhoamiRoute(fastify, authService, addonRegistry) {
5
+ fastify.get('/api/auth/whoami', async (request, reply) => {
6
+ const authHeader = request.headers.authorization;
7
+ if (!authHeader) {
8
+ return reply.status(401).send({ ok: false, error: 'No Authorization header' });
9
+ }
10
+ const token = authHeader.replace('Bearer ', '');
11
+ // JWT fast path — same logic as addon-upload auth chain.
12
+ try {
13
+ const payload = authService.verifyToken(token);
14
+ const ok = {
15
+ ok: true,
16
+ kind: 'jwt',
17
+ userId: payload.userId ?? payload.keyId ?? 'unknown',
18
+ username: payload.username ?? 'unknown',
19
+ isAdmin: payload.isAdmin,
20
+ };
21
+ return reply.send(ok);
22
+ }
23
+ catch {
24
+ // Not a JWT — try scoped token via cap registry.
25
+ }
26
+ try {
27
+ const capRegistry = addonRegistry.getCapabilityRegistry();
28
+ const userMgmt = capRegistry.getSingleton('user-management');
29
+ if (!userMgmt) {
30
+ return reply.status(503).send({ ok: false, error: 'user-management cap not mounted' });
31
+ }
32
+ const record = await userMgmt.validateScopedToken({ token });
33
+ if (!record) {
34
+ return reply
35
+ .status(401)
36
+ .send({ ok: false, error: 'Token not recognised (revoked, expired, or never issued)' });
37
+ }
38
+ const ok = {
39
+ ok: true,
40
+ kind: 'scoped',
41
+ userId: record.userId,
42
+ username: record.userId,
43
+ tokenPrefix: record.tokenPrefix,
44
+ scopes: record.scopes,
45
+ expiresAt: record.expiresAt ?? null,
46
+ createdAt: record.createdAt,
47
+ };
48
+ return reply.send(ok);
49
+ }
50
+ catch (err) {
51
+ const msg = err instanceof Error ? err.message : String(err);
52
+ return reply.status(500).send({ ok: false, error: `Validation failed: ${msg}` });
53
+ }
54
+ });
55
+ }