@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
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAuthRouter = createAuthRouter;
4
+ /**
5
+ * Auth router — core API for login/logout/me.
6
+ *
7
+ * Login validates credentials via the `user-management` capability
8
+ * (owned by the local-auth addon) and signs a JWT.
9
+ *
10
+ * JWT signing stays in the server — it's a transport-level concern
11
+ * (the server owns the secret and the HTTP session).
12
+ *
13
+ * External auth providers are listed via the `auth-provider` cap collection.
14
+ */
15
+ const zod_1 = require("zod");
16
+ const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
17
+ /**
18
+ * Login response — discriminated on `requiresTotp`.
19
+ *
20
+ * • `requiresTotp: false` (or absent in the legacy path): the
21
+ * credentials were sufficient, `token` is the real session JWT.
22
+ * • `requiresTotp: true`: the user has TOTP enrolled. `token` is a
23
+ * short-lived (5 min) challenge token, NOT a session. The client
24
+ * prompts for a 6-digit code and submits to `loginVerifyTotp`
25
+ * with `{challengeToken, code}`. Only on success does the server
26
+ * mint the real session.
27
+ */
28
+ const LoginResultSchema = zod_1.z.object({
29
+ token: zod_1.z.string(),
30
+ user: zod_1.z.object({
31
+ id: zod_1.z.string(),
32
+ username: zod_1.z.string(),
33
+ isAdmin: zod_1.z.boolean(),
34
+ }),
35
+ requiresTotp: zod_1.z.boolean().optional(),
36
+ });
37
+ const AuthProviderSummarySchema = zod_1.z.object({
38
+ id: zod_1.z.string(),
39
+ name: zod_1.z.string(),
40
+ icon: zod_1.z.string(),
41
+ flowType: zod_1.z.string(),
42
+ });
43
+ /** Wire shape of the authenticated user returned by `auth.me`. */
44
+ const MeSchema = zod_1.z
45
+ .object({
46
+ id: zod_1.z.string(),
47
+ username: zod_1.z.string(),
48
+ isAdmin: zod_1.z.boolean(),
49
+ permissions: zod_1.z.object({
50
+ isAdmin: zod_1.z.boolean(),
51
+ allowedProviders: zod_1.z.union([zod_1.z.literal('*'), zod_1.z.array(zod_1.z.string())]),
52
+ allowedDevices: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
53
+ }),
54
+ isApiKey: zod_1.z.boolean(),
55
+ agentId: zod_1.z.string().optional(),
56
+ })
57
+ .nullable();
58
+ function createAuthRouter(auth, registry) {
59
+ return (0, trpc_middleware_js_1.trpcRouter)({
60
+ login: trpc_middleware_js_1.publicProcedure
61
+ .input(zod_1.z.object({ username: zod_1.z.string(), password: zod_1.z.string() }))
62
+ .output(LoginResultSchema)
63
+ .mutation(async ({ input }) => {
64
+ const userMgmt = registry?.getSingleton('user-management');
65
+ if (!userMgmt) {
66
+ // The local-auth addon owns this cap. When this fires the
67
+ // addon either failed to initialize or is still booting.
68
+ // Check the server log for `[hub/local-auth]` errors —
69
+ // common cause is a settings-store regression that breaks
70
+ // the `users` collection query path.
71
+ throw new Error('Login unavailable — `user-management` capability not registered. ' +
72
+ 'The `local-auth` addon failed to initialize; check server logs ' +
73
+ 'for an error tagged `[hub/local-auth]`.');
74
+ }
75
+ const user = await userMgmt.validateCredentials({
76
+ username: input.username,
77
+ password: input.password,
78
+ });
79
+ if (!user)
80
+ throw new Error('Invalid credentials');
81
+ // ── TOTP gate ────────────────────────────────────────────────
82
+ // After credentials validate, check whether the user has
83
+ // active 2FA enrollment. If yes, mint a SHORT-LIVED challenge
84
+ // token instead of the real session — the client must follow
85
+ // up via `loginVerifyTotp` with a valid 6-digit code before
86
+ // we hand out the actual JWT. The challenge token carries
87
+ // `kind: 'totp-challenge'` so it can't be replayed against
88
+ // protected endpoints (the auth middleware rejects anything
89
+ // without the standard session shape).
90
+ const totpStatus = typeof userMgmt.getTotpStatus === 'function'
91
+ ? await userMgmt.getTotpStatus({ userId: user.id })
92
+ : { enabled: false };
93
+ if (totpStatus.enabled) {
94
+ const challengeToken = auth.signTotpChallengeToken({
95
+ userId: user.id,
96
+ username: user.username,
97
+ isAdmin: user.isAdmin,
98
+ });
99
+ return {
100
+ token: challengeToken,
101
+ user: { id: user.id, username: user.username, isAdmin: user.isAdmin },
102
+ requiresTotp: true,
103
+ };
104
+ }
105
+ // Snapshot `scopes` into the JWT payload. Admins ignore the
106
+ // field (middleware bypass); for non-admins this drives the
107
+ // entire scope-access check until the user logs out + back in.
108
+ // setUserScopes mutations take effect on next login — old JWTs
109
+ // carry the snapshot from issue time. This is the standard JWT
110
+ // staleness tradeoff; if you need immediate revocation, force a
111
+ // logout from the admin UI.
112
+ const token = auth.signToken({
113
+ userId: user.id,
114
+ username: user.username,
115
+ isAdmin: user.isAdmin,
116
+ allowedProviders: user.allowedProviders ?? '*',
117
+ allowedDevices: user.allowedDevices ?? {},
118
+ scopes: user.scopes ?? [],
119
+ });
120
+ return {
121
+ token,
122
+ user: { id: user.id, username: user.username, isAdmin: user.isAdmin },
123
+ requiresTotp: false,
124
+ };
125
+ }),
126
+ /**
127
+ * Second leg of the 2FA login. Accepts the challenge token minted
128
+ * by `login` (when `requiresTotp: true`) plus the operator-entered
129
+ * 6-digit code. Re-validates both, then mints the real session.
130
+ *
131
+ * Failure modes:
132
+ * • Invalid/expired challenge token → 401 (session timed out,
133
+ * restart login).
134
+ * • Code doesn't match → 401 (try again with a fresh code from
135
+ * the authenticator app).
136
+ * • User vanished between leg 1 and leg 2 → 401 (operator was
137
+ * deleted concurrently — rare).
138
+ */
139
+ loginVerifyTotp: trpc_middleware_js_1.publicProcedure
140
+ .input(zod_1.z.object({ challengeToken: zod_1.z.string(), code: zod_1.z.string() }))
141
+ .output(LoginResultSchema)
142
+ .mutation(async ({ input }) => {
143
+ const claims = auth.verifyTotpChallengeToken(input.challengeToken);
144
+ if (!claims) {
145
+ throw new Error('Invalid or expired TOTP challenge — please re-enter your password');
146
+ }
147
+ const userMgmt = registry?.getSingleton('user-management');
148
+ if (!userMgmt) {
149
+ throw new Error('Login unavailable — `user-management` capability not registered');
150
+ }
151
+ const verify = typeof userMgmt.verifyTotp === 'function'
152
+ ? await userMgmt.verifyTotp({ userId: claims.userId, code: input.code.trim() })
153
+ : { valid: false };
154
+ if (!verify.valid) {
155
+ throw new Error('Invalid TOTP code');
156
+ }
157
+ // Re-fetch the user record so scopes / allowedDevices reflect
158
+ // any admin change made between the two legs. Without this we
159
+ // could mint a stale-scope session.
160
+ const fresh = typeof userMgmt.listUsers === 'function'
161
+ ? (await userMgmt.listUsers()).find((u) => u.id === claims.userId)
162
+ : null;
163
+ if (!fresh) {
164
+ throw new Error('User no longer exists');
165
+ }
166
+ const sessionToken = auth.signToken({
167
+ userId: fresh.id,
168
+ username: fresh.username,
169
+ isAdmin: fresh.isAdmin,
170
+ allowedProviders: fresh.allowedProviders ?? '*',
171
+ allowedDevices: fresh.allowedDevices ?? {},
172
+ scopes: fresh.scopes ?? [],
173
+ });
174
+ return {
175
+ token: sessionToken,
176
+ user: { id: fresh.id, username: fresh.username, isAdmin: fresh.isAdmin },
177
+ requiresTotp: false,
178
+ };
179
+ }),
180
+ me: trpc_middleware_js_1.protectedProcedure
181
+ .input(zod_1.z.void())
182
+ .output(MeSchema)
183
+ .query(({ ctx }) => ctx.user),
184
+ // ── Self-service profile operations ───────────────────────────────
185
+ //
186
+ // These route through the `user-management` capability provider but
187
+ // bind `userId` to the CALLER's session identity (`ctx.user.id`)
188
+ // — i.e. every authenticated user can manage THEIR OWN password and
189
+ // TOTP enrollment, without the admin gate that the corresponding
190
+ // cap methods enforce on the trpc layer.
191
+ //
192
+ // Provider methods themselves don't check admin status, so calling
193
+ // them directly here is fine. The trpc admin-gate on the cap router
194
+ // exists only to block third-party tampering — operators acting on
195
+ // themselves bypass it intentionally.
196
+ changeOwnPassword: trpc_middleware_js_1.protectedProcedure
197
+ .input(zod_1.z.object({
198
+ currentPassword: zod_1.z.string().min(1),
199
+ newPassword: zod_1.z.string().min(8),
200
+ }))
201
+ .output(zod_1.z.object({ success: zod_1.z.literal(true) }))
202
+ .mutation(async ({ input, ctx }) => {
203
+ const userMgmt = registry?.getSingleton('user-management');
204
+ if (!userMgmt)
205
+ throw new Error('user-management capability not available');
206
+ if (!ctx.user)
207
+ throw new Error('Not authenticated');
208
+ // Re-validate the current password against the live store to
209
+ // confirm session identity → password ownership match. Stops a
210
+ // stolen session-token from rotating credentials silently.
211
+ const ok = await userMgmt.validateCredentials({
212
+ username: ctx.user.username,
213
+ password: input.currentPassword,
214
+ });
215
+ if (!ok)
216
+ throw new Error('Current password is incorrect');
217
+ await userMgmt.resetPassword({ id: ctx.user.id, newPassword: input.newPassword });
218
+ return { success: true };
219
+ }),
220
+ setupOwnTotp: trpc_middleware_js_1.protectedProcedure
221
+ .input(zod_1.z.void())
222
+ .output(zod_1.z.object({ secret: zod_1.z.string(), otpauthUrl: zod_1.z.string() }))
223
+ .mutation(async ({ ctx }) => {
224
+ const userMgmt = registry?.getSingleton('user-management');
225
+ if (!userMgmt)
226
+ throw new Error('user-management capability not available');
227
+ if (!ctx.user)
228
+ throw new Error('Not authenticated');
229
+ return userMgmt.setupTotp({ userId: ctx.user.id });
230
+ }),
231
+ confirmOwnTotp: trpc_middleware_js_1.protectedProcedure
232
+ .input(zod_1.z.object({ code: zod_1.z.string().min(1) }))
233
+ .output(zod_1.z.object({ success: zod_1.z.literal(true) }))
234
+ .mutation(async ({ input, ctx }) => {
235
+ const userMgmt = registry?.getSingleton('user-management');
236
+ if (!userMgmt)
237
+ throw new Error('user-management capability not available');
238
+ if (!ctx.user)
239
+ throw new Error('Not authenticated');
240
+ return userMgmt.confirmTotp({ userId: ctx.user.id, code: input.code });
241
+ }),
242
+ disableOwnTotp: trpc_middleware_js_1.protectedProcedure
243
+ .input(zod_1.z.void())
244
+ .output(zod_1.z.object({ success: zod_1.z.literal(true) }))
245
+ .mutation(async ({ ctx }) => {
246
+ const userMgmt = registry?.getSingleton('user-management');
247
+ if (!userMgmt)
248
+ throw new Error('user-management capability not available');
249
+ if (!ctx.user)
250
+ throw new Error('Not authenticated');
251
+ return userMgmt.disableTotp({ userId: ctx.user.id });
252
+ }),
253
+ getOwnTotpStatus: trpc_middleware_js_1.protectedProcedure
254
+ .input(zod_1.z.void())
255
+ .output(zod_1.z.object({ enabled: zod_1.z.boolean(), confirmedAt: zod_1.z.number().nullable() }))
256
+ .query(async ({ ctx }) => {
257
+ const userMgmt = registry?.getSingleton('user-management');
258
+ if (!userMgmt || !ctx.user)
259
+ return { enabled: false, confirmedAt: null };
260
+ return userMgmt.getTotpStatus({ userId: ctx.user.id });
261
+ }),
262
+ logout: trpc_middleware_js_1.protectedProcedure
263
+ .input(zod_1.z.void())
264
+ .output(zod_1.z.object({ success: zod_1.z.literal(true) }))
265
+ .mutation(() => ({ success: true })),
266
+ listProviders: trpc_middleware_js_1.publicProcedure
267
+ .input(zod_1.z.void())
268
+ .output(zod_1.z.array(AuthProviderSummarySchema).readonly())
269
+ .query(() => {
270
+ if (!registry)
271
+ return [];
272
+ // Validate each auth-provider entry independently. A single
273
+ // malformed entry (e.g. an auth addon that registers without an
274
+ // `icon`) must NOT sink the whole array through the tRPC output
275
+ // validator — that would 500 the query and lock every login
276
+ // method out of the UI. Drop the bad entry, keep the rest.
277
+ const out = [];
278
+ for (const entry of registry.getCollection('auth-provider')) {
279
+ const parsed = AuthProviderSummarySchema.safeParse(entry);
280
+ if (parsed.success)
281
+ out.push(parsed.data);
282
+ }
283
+ return out;
284
+ }),
285
+ });
286
+ }
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BulkUpdateCoordinator = void 0;
4
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument -- The server installs @camstack/types@0.1.38 (last published) in server/backend/node_modules, while the workspace has 0.1.39 with the new BulkUpdate* types. ESLint's type-checker resolves against 0.1.38 and treats the new imports as `any`. Runtime is correct because Node module resolution walks up to root node_modules → workspace symlink. This disable mirrors the pattern in cap-providers.ts (same root cause). Will resolve when 0.1.39 is published and the local dist is synced. */
5
+ const node_crypto_1 = require("node:crypto");
6
+ const types_1 = require("@camstack/types");
7
+ const DEFAULT_CLEANUP_AFTER_MS = 5 * 60 * 1_000;
8
+ class BulkUpdateCoordinator {
9
+ deps;
10
+ states = new Map();
11
+ cancelFlags = new Map();
12
+ /**
13
+ * Tracks wall-clock time (ms) when each bulk completed. Used for lazy
14
+ * cleanup in `get()` — avoids scheduling a fake-timer `setTimeout` that
15
+ * would be eagerly fired by `vi.runAllTimersAsync()` in tests.
16
+ */
17
+ completedWallMs = new Map();
18
+ /** Tracks which nodeIds currently have an active (non-completed) bulk update. */
19
+ activeNodeIds = new Set();
20
+ now;
21
+ cleanupAfterMs;
22
+ /** Wall-clock source. Fake timers intercept `Date.now`, so tests can advance via `advanceTimersByTimeAsync`. */
23
+ wallNow;
24
+ constructor(deps) {
25
+ this.deps = deps;
26
+ this.now = deps.clock ?? (() => Date.now());
27
+ this.cleanupAfterMs = deps.cleanupAfterMs ?? DEFAULT_CLEANUP_AFTER_MS;
28
+ this.wallNow = () => Date.now();
29
+ }
30
+ // ── Public API ────────────────────────────────────────────────────
31
+ start(input) {
32
+ if (this.activeNodeIds.has(input.nodeId)) {
33
+ throw new Error(`Bulk update already in progress for node ${input.nodeId}`);
34
+ }
35
+ const id = (0, node_crypto_1.randomUUID)();
36
+ const items = input.items.map((i) => ({
37
+ name: i.name,
38
+ isSystem: i.isSystem,
39
+ // fromVersion: the cap interface receives name+version+isSystem only;
40
+ // the caller (cap-providers.ts) may enrich this with the current version
41
+ // if available. Empty string is acceptable per plan spec.
42
+ fromVersion: '',
43
+ toVersion: i.version,
44
+ status: 'queued',
45
+ }));
46
+ const state = {
47
+ id,
48
+ nodeId: input.nodeId,
49
+ startedAtMs: this.now(),
50
+ total: items.length,
51
+ completed: 0,
52
+ failed: 0,
53
+ current: null,
54
+ phase: 'regular',
55
+ cancelled: false,
56
+ items,
57
+ };
58
+ this.states.set(id, state);
59
+ this.activeNodeIds.add(input.nodeId);
60
+ const cancelFlag = { cancelled: false };
61
+ this.cancelFlags.set(id, cancelFlag);
62
+ // Emit initial state so clients see the bulk as started immediately
63
+ this.emit(state);
64
+ void this.runLoop(id, cancelFlag).catch((err) => {
65
+ this.deps.logger.error('BulkUpdateCoordinator: loop crashed unexpectedly', err);
66
+ });
67
+ return { id };
68
+ }
69
+ get(id) {
70
+ const state = this.states.get(id);
71
+ if (state === undefined)
72
+ return null;
73
+ // Lazy cleanup: purge if the wall-clock elapsed since completion exceeds threshold.
74
+ // This avoids scheduling a long-lived setTimeout that would be eagerly fired
75
+ // by vi.runAllTimersAsync() in tests.
76
+ const completedWall = this.completedWallMs.get(id);
77
+ if (completedWall !== undefined && this.wallNow() - completedWall >= this.cleanupAfterMs) {
78
+ this.purge(id);
79
+ return null;
80
+ }
81
+ return state;
82
+ }
83
+ list(nodeId) {
84
+ const all = [...this.states.keys()]
85
+ .map((id) => this.get(id)) // get() applies lazy-cleanup
86
+ .filter((s) => s !== null);
87
+ return nodeId === undefined ? all : all.filter((s) => s.nodeId === nodeId);
88
+ }
89
+ cancel(id) {
90
+ const state = this.states.get(id);
91
+ const flag = this.cancelFlags.get(id);
92
+ if (state === undefined || flag === undefined)
93
+ return { cancelled: false };
94
+ // Once restarting, the hub restart is committed — cancel has no effect.
95
+ if (state.phase === 'restarting')
96
+ return { cancelled: false };
97
+ // Already completed.
98
+ if (state.completedAtMs !== undefined)
99
+ return { cancelled: false };
100
+ flag.cancelled = true;
101
+ this.mutate(id, (s) => ({ ...s, cancelled: true }));
102
+ return { cancelled: true };
103
+ }
104
+ // ── Internal loop ─────────────────────────────────────────────────
105
+ async runLoop(id, cancelFlag) {
106
+ const initial = this.states.get(id);
107
+ // ── Phase 1: regular addons ──────────────────────────────────────
108
+ this.transitionPhase(id, 'regular');
109
+ for (const item of initial.items.filter((i) => !i.isSystem)) {
110
+ if (cancelFlag.cancelled)
111
+ break;
112
+ await this.processItem(id, item, false);
113
+ }
114
+ // ── Phase 2: system packages (deferRestart: true) ────────────────
115
+ if (!cancelFlag.cancelled && initial.items.some((i) => i.isSystem)) {
116
+ this.transitionPhase(id, 'system');
117
+ for (const item of initial.items.filter((i) => i.isSystem)) {
118
+ if (cancelFlag.cancelled)
119
+ break;
120
+ await this.processItem(id, item, true);
121
+ }
122
+ // ── Phase 3: single restart ──────────────────────────────────
123
+ const anySystemPendingRestart = this.states
124
+ .get(id)
125
+ .items.some((i) => i.isSystem && i.status === 'done-pending-restart');
126
+ if (anySystemPendingRestart && !cancelFlag.cancelled) {
127
+ this.transitionPhase(id, 'restarting');
128
+ try {
129
+ await this.deps.restartServer({ confirm: true });
130
+ // NOTE: In production, restartServer kills+respawns the hub process.
131
+ // Code below this point will not execute in that scenario.
132
+ // If the mock/stub returns (e.g. in tests), we fall through to finalizing.
133
+ }
134
+ catch (err) {
135
+ // Restart failed but the npm installs already completed. Promote all
136
+ // done-pending-restart items to done with a caveat error so the UI
137
+ // can inform the user that a manual restart is needed.
138
+ this.deps.logger.error('BulkUpdateCoordinator: restart failed', err);
139
+ const errMsg = err instanceof Error ? err.message : String(err);
140
+ for (const it of this.states.get(id).items) {
141
+ if (it.status === 'done-pending-restart') {
142
+ this.setItemStatus(id, it.name, 'done', {
143
+ error: `Restart failed; manual restart required (${errMsg})`,
144
+ });
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ // ── Phase 4: finalize ────────────────────────────────────────────
151
+ // Reached when:
152
+ // a) no system packages at all, OR
153
+ // b) restart failed (process continued), OR
154
+ // c) cancelled before the restart phase.
155
+ this.transitionPhase(id, 'finalizing');
156
+ this.completeBulk(id);
157
+ }
158
+ async processItem(id, item, isSystem) {
159
+ this.setItemStatus(id, item.name, 'updating', { startedAtMs: this.now() });
160
+ this.mutate(id, (s) => ({ ...s, current: item.name }));
161
+ this.emit(this.states.get(id));
162
+ try {
163
+ if (isSystem) {
164
+ await this.deps.updateFrameworkPackage({
165
+ packageName: item.name,
166
+ version: item.toVersion,
167
+ deferRestart: true,
168
+ });
169
+ this.setItemStatus(id, item.name, 'done-pending-restart', { completedAtMs: this.now() });
170
+ }
171
+ else {
172
+ await this.deps.updateAddon({ name: item.name, version: item.toVersion });
173
+ this.setItemStatus(id, item.name, 'done', { completedAtMs: this.now() });
174
+ }
175
+ }
176
+ catch (err) {
177
+ const msg = err instanceof Error ? err.message : String(err);
178
+ this.setItemStatus(id, item.name, 'failed', { error: msg, completedAtMs: this.now() });
179
+ }
180
+ this.mutate(id, (s) => ({ ...s, current: null }));
181
+ this.emit(this.states.get(id));
182
+ }
183
+ // ── State mutation helpers ────────────────────────────────────────
184
+ setItemStatus(id, name, status, fields = {}) {
185
+ this.mutate(id, (s) => {
186
+ const items = s.items.map((it) => (it.name === name ? { ...it, status, ...fields } : it));
187
+ // completed = all terminal states: done | done-pending-restart | failed
188
+ const completed = items.filter((it) => it.status === 'done' || it.status === 'done-pending-restart' || it.status === 'failed').length;
189
+ const failed = items.filter((it) => it.status === 'failed').length;
190
+ return { ...s, items, completed, failed };
191
+ });
192
+ }
193
+ transitionPhase(id, phase) {
194
+ this.mutate(id, (s) => ({ ...s, phase }));
195
+ this.emit(this.states.get(id));
196
+ }
197
+ completeBulk(id) {
198
+ this.mutate(id, (s) => ({ ...s, completedAtMs: this.now(), current: null }));
199
+ this.emit(this.states.get(id));
200
+ // Free the nodeId slot so a new bulk for the same node can be started
201
+ const nodeId = this.states.get(id).nodeId;
202
+ this.activeNodeIds.delete(nodeId);
203
+ // Record wall-clock completion time for lazy cleanup in `get()`.
204
+ // We intentionally avoid scheduling a setTimeout here: a long-lived
205
+ // setTimeout (5 min) would be eagerly fired by vi.runAllTimersAsync()
206
+ // in tests, causing `get()` to return null immediately after the run.
207
+ // Instead, `get()` lazily checks whether the cleanup threshold has
208
+ // elapsed using Date.now() — which fake timers DO advance via
209
+ // advanceTimersByTimeAsync(), making the cleanup testable without
210
+ // a long-running timer.
211
+ this.completedWallMs.set(id, this.wallNow());
212
+ }
213
+ purge(id) {
214
+ this.states.delete(id);
215
+ this.cancelFlags.delete(id);
216
+ this.completedWallMs.delete(id);
217
+ }
218
+ /** Immutably update the state for the given id. No-op if id is unknown. */
219
+ mutate(id, update) {
220
+ const current = this.states.get(id);
221
+ if (current === undefined)
222
+ return;
223
+ this.states.set(id, update(current));
224
+ }
225
+ emit(state) {
226
+ this.deps.eventBus.emit(types_1.EventCategory.AddonsBulkUpdateProgress, state);
227
+ }
228
+ }
229
+ exports.BulkUpdateCoordinator = BulkUpdateCoordinator;