@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.
- package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
- package/dist/api/addon-upload.js +441 -0
- package/dist/api/addons-custom.router.js +91 -0
- package/dist/api/auth-whoami.js +55 -0
- package/dist/api/bridge-addons.router.js +109 -0
- package/dist/api/capabilities.router.js +229 -0
- package/dist/api/core/addon-settings.router.js +117 -0
- package/dist/api/core/agents.router.js +73 -0
- package/dist/api/core/auth.router.js +286 -0
- package/dist/api/core/bulk-update-coordinator.js +229 -0
- package/dist/api/core/cap-providers.js +1124 -0
- package/dist/api/core/capabilities.router.js +138 -0
- package/dist/api/core/collection-preference.js +17 -0
- package/dist/api/core/event-bus-proxy.router.js +45 -0
- package/dist/api/core/hwaccel.router.js +91 -0
- package/dist/api/core/live-events.router.js +61 -0
- package/dist/api/core/logs.router.js +172 -0
- package/dist/api/core/notifications.router.js +67 -0
- package/dist/api/core/repl.router.js +35 -0
- package/dist/api/core/settings-backend.router.js +121 -0
- package/dist/api/core/stream-probe.router.js +58 -0
- package/dist/api/core/system-events.router.js +100 -0
- package/dist/api/health/health.routes.js +68 -0
- package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
- package/dist/api/oauth2/oauth2-routes.js +219 -0
- package/dist/api/trpc/cap-mount-helpers.js +194 -0
- package/dist/api/trpc/cap-route-error-formatter.js +133 -0
- package/dist/api/trpc/client-ip.js +147 -0
- package/dist/api/trpc/core-cap-bridge.js +115 -0
- package/dist/api/trpc/generated-cap-mounts.js +388 -0
- package/dist/api/trpc/generated-cap-routers.js +7635 -0
- package/dist/api/trpc/scope-access.js +93 -0
- package/dist/api/trpc/trpc.context.js +184 -0
- package/dist/api/trpc/trpc.middleware.js +139 -0
- package/dist/api/trpc/trpc.router.js +188 -0
- package/dist/auth/session-cookie.js +47 -0
- package/dist/boot/boot-config.js +241 -0
- package/dist/boot/integration-id-backfill.js +76 -0
- package/dist/boot/post-boot.service.js +85 -0
- package/dist/core/addon/addon-call-gateway.js +99 -0
- package/dist/core/addon/addon-package.service.js +1560 -0
- package/dist/core/addon/addon-registry.service.js +2739 -0
- package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
- package/dist/core/addon/addon-search.service.js +62 -0
- package/dist/core/addon/addon-settings-provider.js +102 -0
- package/dist/core/addon/addon.tokens.js +5 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
- package/dist/core/addon-pages/addon-pages.service.js +107 -0
- package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
- package/dist/core/agent/agent-registry.service.js +477 -0
- package/dist/core/auth/auth.service.js +10 -0
- package/dist/core/capability/capability.service.js +58 -0
- package/dist/core/config/config.schema.js +7 -0
- package/dist/core/config/config.service.js +10 -0
- package/dist/core/events/event-bus.service.js +83 -0
- package/dist/core/feature/feature.service.js +10 -0
- package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
- package/dist/core/logging/log-ring-buffer.js +6 -0
- package/dist/core/logging/logging.service.js +130 -0
- package/dist/core/logging/scoped-logger.js +6 -0
- package/dist/core/moleculer/cap-call-fn.js +50 -0
- package/dist/core/moleculer/cap-route-authority.js +122 -0
- package/dist/core/moleculer/moleculer.service.js +898 -0
- package/dist/core/network/network-quality.service.js +7 -0
- package/dist/core/notification/notification-wrapper.service.js +33 -0
- package/dist/core/notification/toast-wrapper.service.js +25 -0
- package/dist/core/provider/provider.tokens.js +4 -0
- package/dist/core/repl/repl-engine.service.js +140 -0
- package/dist/core/storage/fs-storage-backend.js +6 -0
- package/dist/core/storage/storage-location-manager.js +6 -0
- package/dist/core/storage/storage.service.js +7 -0
- package/dist/core/streaming/stream-probe.service.js +209 -0
- package/dist/core/topology/topology-emitter.service.js +106 -0
- package/dist/launcher.js +325 -0
- package/dist/main.js +1098 -0
- package/dist/manual-boot.js +227 -0
- package/package.json +5 -1
- package/src/__tests__/addon-install-e2e.test.ts +0 -74
- package/src/__tests__/addon-pages-e2e.test.ts +0 -200
- package/src/__tests__/addon-route-session.test.ts +0 -17
- package/src/__tests__/addon-settings-router.spec.ts +0 -67
- package/src/__tests__/addon-upload.spec.ts +0 -475
- package/src/__tests__/agent-registry.spec.ts +0 -179
- package/src/__tests__/agent-status-page.spec.ts +0 -82
- package/src/__tests__/auth-session-cookie.test.ts +0 -48
- package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
- package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
- package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
- package/src/__tests__/cap-route-adapter.spec.ts +0 -302
- package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
- package/src/__tests__/cap-routers/harness.ts +0 -163
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
- package/src/__tests__/capability-e2e.test.ts +0 -384
- package/src/__tests__/cli-e2e.test.ts +0 -150
- package/src/__tests__/core-cap-bridge.spec.ts +0 -91
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
- package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
- package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
- package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
- package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
- package/src/__tests__/framework-allowlist.spec.ts +0 -96
- package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
- package/src/__tests__/https-e2e.test.ts +0 -124
- package/src/__tests__/lifecycle-e2e.test.ts +0 -189
- package/src/__tests__/live-events-subscription.spec.ts +0 -149
- package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
- package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
- package/src/__tests__/native-cap-route.spec.ts +0 -427
- package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
- package/src/__tests__/post-boot-restart.spec.ts +0 -161
- package/src/__tests__/singleton-contention.test.ts +0 -499
- package/src/__tests__/streaming-diagnostic.test.ts +0 -615
- package/src/__tests__/streaming-scale.test.ts +0 -314
- package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
- package/src/__tests__/uds-log-ingest.spec.ts +0 -183
- package/src/api/__tests__/addons-custom.spec.ts +0 -148
- package/src/api/__tests__/capabilities.router.test.ts +0 -56
- package/src/api/addon-upload.ts +0 -529
- package/src/api/addons-custom.router.ts +0 -101
- package/src/api/auth-whoami.ts +0 -101
- package/src/api/bridge-addons.router.ts +0 -122
- package/src/api/capabilities.router.ts +0 -265
- package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
- package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
- package/src/api/core/addon-settings.router.ts +0 -127
- package/src/api/core/agents.router.ts +0 -86
- package/src/api/core/auth.router.ts +0 -322
- package/src/api/core/bulk-update-coordinator.ts +0 -305
- package/src/api/core/cap-providers.ts +0 -1339
- package/src/api/core/capabilities.router.ts +0 -149
- package/src/api/core/collection-preference.ts +0 -40
- package/src/api/core/event-bus-proxy.router.ts +0 -45
- package/src/api/core/hwaccel.router.ts +0 -108
- package/src/api/core/live-events.router.ts +0 -67
- package/src/api/core/logs.router.ts +0 -195
- package/src/api/core/notifications.router.ts +0 -66
- package/src/api/core/repl.router.ts +0 -39
- package/src/api/core/settings-backend.router.ts +0 -140
- package/src/api/core/stream-probe.router.ts +0 -57
- package/src/api/core/system-events.router.ts +0 -125
- package/src/api/health/health.routes.ts +0 -117
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
- package/src/api/oauth2/oauth2-routes.ts +0 -281
- package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
- package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
- package/src/api/trpc/cap-mount-helpers.ts +0 -245
- package/src/api/trpc/cap-route-error-formatter.ts +0 -171
- package/src/api/trpc/client-ip.ts +0 -147
- package/src/api/trpc/core-cap-bridge.ts +0 -154
- package/src/api/trpc/generated-cap-mounts.ts +0 -1240
- package/src/api/trpc/generated-cap-routers.ts +0 -11523
- package/src/api/trpc/scope-access.ts +0 -110
- package/src/api/trpc/trpc.context.ts +0 -258
- package/src/api/trpc/trpc.middleware.ts +0 -146
- package/src/api/trpc/trpc.router.ts +0 -389
- package/src/auth/session-cookie.ts +0 -54
- package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
- package/src/boot/boot-config.ts +0 -259
- package/src/boot/integration-id-backfill.ts +0 -109
- package/src/boot/post-boot.service.ts +0 -105
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
- package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
- package/src/core/addon/addon-call-gateway.ts +0 -171
- package/src/core/addon/addon-package.service.ts +0 -1787
- package/src/core/addon/addon-registry.service.ts +0 -3130
- package/src/core/addon/addon-search.service.ts +0 -91
- package/src/core/addon/addon-settings-provider.ts +0 -220
- package/src/core/addon/addon.tokens.ts +0 -2
- package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
- package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
- package/src/core/addon-pages/addon-pages.service.ts +0 -82
- package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
- package/src/core/agent/agent-registry.service.ts +0 -529
- package/src/core/auth/auth.service.spec.ts +0 -86
- package/src/core/auth/auth.service.ts +0 -8
- package/src/core/capability/capability.service.ts +0 -66
- package/src/core/config/config.schema.ts +0 -3
- package/src/core/config/config.service.spec.ts +0 -175
- package/src/core/config/config.service.ts +0 -7
- package/src/core/events/event-bus.service.spec.ts +0 -235
- package/src/core/events/event-bus.service.ts +0 -89
- package/src/core/feature/feature.service.spec.ts +0 -99
- package/src/core/feature/feature.service.ts +0 -8
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
- package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
- package/src/core/logging/log-ring-buffer.ts +0 -3
- package/src/core/logging/logging.service.spec.ts +0 -287
- package/src/core/logging/logging.service.ts +0 -143
- package/src/core/logging/scoped-logger.ts +0 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
- package/src/core/moleculer/cap-call-fn.ts +0 -107
- package/src/core/moleculer/cap-route-authority.ts +0 -194
- package/src/core/moleculer/moleculer.service.ts +0 -1072
- package/src/core/network/network-quality.service.spec.ts +0 -53
- package/src/core/network/network-quality.service.ts +0 -5
- package/src/core/notification/notification-wrapper.service.ts +0 -34
- package/src/core/notification/toast-wrapper.service.ts +0 -27
- package/src/core/provider/provider.tokens.ts +0 -1
- package/src/core/repl/repl-engine.service.spec.ts +0 -444
- package/src/core/repl/repl-engine.service.ts +0 -155
- package/src/core/storage/fs-storage-backend.spec.ts +0 -70
- package/src/core/storage/fs-storage-backend.ts +0 -3
- package/src/core/storage/storage-location-manager.spec.ts +0 -130
- package/src/core/storage/storage-location-manager.ts +0 -3
- package/src/core/storage/storage.service.spec.ts +0 -73
- package/src/core/storage/storage.service.ts +0 -3
- package/src/core/streaming/stream-probe.service.ts +0 -221
- package/src/core/topology/topology-emitter.service.ts +0 -105
- package/src/launcher.ts +0 -314
- package/src/main.ts +0 -1245
- package/src/manual-boot.ts +0 -301
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -33
- 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;
|