@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
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return -- mock factories cross typed boundaries */
|
|
2
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
3
|
-
import { createAuthRouter } from '../auth.router.js'
|
|
4
|
-
import { AuthService } from '../../../core/auth/auth.service.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Two-phase TOTP login flow tests for `auth.router`. Focus on the
|
|
8
|
-
* cap-router contracts the SDK + admin-ui depend on:
|
|
9
|
-
*
|
|
10
|
-
* • `login` returns `requiresTotp: true` + a short-lived challenge
|
|
11
|
-
* token when the user has TOTP enrolled.
|
|
12
|
-
* • `login` returns a regular session token when the user has NO
|
|
13
|
-
* TOTP (backwards compat).
|
|
14
|
-
* • `loginVerifyTotp` validates the challenge token + code and
|
|
15
|
-
* mints the real session JWT.
|
|
16
|
-
* • Failure paths: wrong code, expired/forged challenge, missing
|
|
17
|
-
* user, missing user-management cap.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
interface MockUser {
|
|
21
|
-
id: string
|
|
22
|
-
username: string
|
|
23
|
-
isAdmin: boolean
|
|
24
|
-
passwordHash: string
|
|
25
|
-
allowedProviders: string | string[]
|
|
26
|
-
allowedDevices: Record<string, unknown>
|
|
27
|
-
scopes: unknown[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface MockUserManagement {
|
|
31
|
-
validateCredentials: ReturnType<typeof vi.fn>
|
|
32
|
-
getTotpStatus: ReturnType<typeof vi.fn>
|
|
33
|
-
verifyTotp: ReturnType<typeof vi.fn>
|
|
34
|
-
listUsers: ReturnType<typeof vi.fn>
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function makeUser(overrides: Partial<MockUser> = {}): MockUser {
|
|
38
|
-
return {
|
|
39
|
-
id: 'u-1',
|
|
40
|
-
username: 'alice',
|
|
41
|
-
isAdmin: false,
|
|
42
|
-
passwordHash: 'hashed',
|
|
43
|
-
allowedProviders: '*',
|
|
44
|
-
allowedDevices: {},
|
|
45
|
-
scopes: [],
|
|
46
|
-
...overrides,
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function makeUserMgmt(overrides: Partial<MockUserManagement> = {}): MockUserManagement {
|
|
51
|
-
return {
|
|
52
|
-
validateCredentials: vi.fn(),
|
|
53
|
-
getTotpStatus: vi.fn(async () => ({ enabled: false, confirmedAt: null })),
|
|
54
|
-
verifyTotp: vi.fn(async () => ({ valid: true })),
|
|
55
|
-
listUsers: vi.fn(async () => [makeUser()]),
|
|
56
|
-
...overrides,
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function makeConfig(overrides: Record<string, unknown> = {}): {
|
|
61
|
-
get<T>(p: string): T
|
|
62
|
-
update(s: string, d: Record<string, unknown>): void
|
|
63
|
-
} {
|
|
64
|
-
const store: Record<string, unknown> = { 'auth.jwtSecret': 'unit-test-secret', ...overrides }
|
|
65
|
-
return {
|
|
66
|
-
get<T>(path: string): T {
|
|
67
|
-
return store[path] as T
|
|
68
|
-
},
|
|
69
|
-
update(_section: string, _data: Record<string, unknown>): void {
|
|
70
|
-
/* no-op */
|
|
71
|
-
},
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function makeRegistry(userMgmt: MockUserManagement): {
|
|
76
|
-
getSingleton: (name: string) => unknown | null
|
|
77
|
-
} {
|
|
78
|
-
return {
|
|
79
|
-
getSingleton: (name: string) => (name === 'user-management' ? userMgmt : null),
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Invoke a tRPC procedure directly via the router's internal API. */
|
|
84
|
-
async function callProc(
|
|
85
|
-
router: ReturnType<typeof createAuthRouter>,
|
|
86
|
-
name: 'login' | 'loginVerifyTotp',
|
|
87
|
-
input: unknown,
|
|
88
|
-
): Promise<unknown> {
|
|
89
|
-
const caller = router.createCaller({} as never)
|
|
90
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
-
const fn = (caller as any)[name]
|
|
92
|
-
return fn(input)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
describe('auth.router — TOTP gate', () => {
|
|
96
|
-
let auth: AuthService
|
|
97
|
-
let userMgmt: MockUserManagement
|
|
98
|
-
let registry: ReturnType<typeof makeRegistry>
|
|
99
|
-
|
|
100
|
-
beforeEach(() => {
|
|
101
|
-
auth = new AuthService(makeConfig() as never)
|
|
102
|
-
userMgmt = makeUserMgmt()
|
|
103
|
-
registry = makeRegistry(userMgmt)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
describe('login (phase 1)', () => {
|
|
107
|
-
it('returns a regular session token + requiresTotp:false when user has NO TOTP', async () => {
|
|
108
|
-
const u = makeUser()
|
|
109
|
-
userMgmt.validateCredentials.mockResolvedValueOnce(u)
|
|
110
|
-
userMgmt.getTotpStatus.mockResolvedValueOnce({ enabled: false, confirmedAt: null })
|
|
111
|
-
|
|
112
|
-
const router = createAuthRouter(auth, registry as never)
|
|
113
|
-
const result = (await callProc(router, 'login', { username: 'alice', password: 'pw' })) as {
|
|
114
|
-
token: string
|
|
115
|
-
user: { id: string; username: string; isAdmin: boolean }
|
|
116
|
-
requiresTotp?: boolean
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
expect(result.requiresTotp).toBe(false)
|
|
120
|
-
expect(result.user).toEqual({ id: 'u-1', username: 'alice', isAdmin: false })
|
|
121
|
-
// The token verifies as a regular session token (NOT a challenge).
|
|
122
|
-
const verified = auth.verifyToken(result.token)
|
|
123
|
-
expect(verified.userId).toBe('u-1')
|
|
124
|
-
// verifyTotpChallengeToken should NOT recognize a regular session token.
|
|
125
|
-
expect(auth.verifySsoBridgeToken(result.token)).toBeNull()
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('returns a challenge token + requiresTotp:true when user HAS TOTP', async () => {
|
|
129
|
-
const u = makeUser()
|
|
130
|
-
userMgmt.validateCredentials.mockResolvedValueOnce(u)
|
|
131
|
-
userMgmt.getTotpStatus.mockResolvedValueOnce({ enabled: true, confirmedAt: Date.now() })
|
|
132
|
-
|
|
133
|
-
const router = createAuthRouter(auth, registry as never)
|
|
134
|
-
const result = (await callProc(router, 'login', { username: 'alice', password: 'pw' })) as {
|
|
135
|
-
token: string
|
|
136
|
-
user: { id: string; username: string; isAdmin: boolean }
|
|
137
|
-
requiresTotp?: boolean
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
expect(result.requiresTotp).toBe(true)
|
|
141
|
-
// The challenge token MUST NOT verify as a session JWT — it has
|
|
142
|
-
// `kind: 'totp-challenge'` and the standard verifyToken expects
|
|
143
|
-
// the session shape (it'd throw or return garbage).
|
|
144
|
-
const challengeClaims = auth.verifyTotpChallengeToken(result.token)
|
|
145
|
-
expect(challengeClaims).not.toBeNull()
|
|
146
|
-
expect(challengeClaims!.userId).toBe('u-1')
|
|
147
|
-
expect(challengeClaims!.isAdmin).toBe(false)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('rejects invalid credentials', async () => {
|
|
151
|
-
userMgmt.validateCredentials.mockResolvedValueOnce(null)
|
|
152
|
-
const router = createAuthRouter(auth, registry as never)
|
|
153
|
-
await expect(
|
|
154
|
-
callProc(router, 'login', { username: 'alice', password: 'wrong' }),
|
|
155
|
-
).rejects.toThrow('Invalid credentials')
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('throws when user-management cap is unregistered (boot failure)', async () => {
|
|
159
|
-
const emptyRegistry = { getSingleton: () => null } as never
|
|
160
|
-
const router = createAuthRouter(auth, emptyRegistry)
|
|
161
|
-
await expect(
|
|
162
|
-
callProc(router, 'login', { username: 'alice', password: 'pw' }),
|
|
163
|
-
).rejects.toThrow(/user-management.*capability not registered/)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('falls through to session-token branch when getTotpStatus method is absent (older user-mgmt builds)', async () => {
|
|
167
|
-
const u = makeUser()
|
|
168
|
-
const legacyMgmt = {
|
|
169
|
-
validateCredentials: vi.fn().mockResolvedValueOnce(u),
|
|
170
|
-
// getTotpStatus deliberately undefined
|
|
171
|
-
verifyTotp: vi.fn(),
|
|
172
|
-
listUsers: vi.fn(),
|
|
173
|
-
}
|
|
174
|
-
const router = createAuthRouter(auth, makeRegistry(legacyMgmt as never) as never)
|
|
175
|
-
const result = (await callProc(router, 'login', { username: 'alice', password: 'pw' })) as {
|
|
176
|
-
requiresTotp?: boolean
|
|
177
|
-
}
|
|
178
|
-
expect(result.requiresTotp).toBe(false)
|
|
179
|
-
})
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
describe('loginVerifyTotp (phase 2)', () => {
|
|
183
|
-
it('mints the real session JWT when challenge + code are both valid', async () => {
|
|
184
|
-
const u = makeUser({ scopes: [{ type: 'category', target: 'storage', access: ['view'] }] })
|
|
185
|
-
userMgmt.verifyTotp.mockResolvedValueOnce({ valid: true })
|
|
186
|
-
userMgmt.listUsers.mockResolvedValueOnce([u])
|
|
187
|
-
|
|
188
|
-
const challengeToken = auth.signTotpChallengeToken({
|
|
189
|
-
userId: u.id,
|
|
190
|
-
username: u.username,
|
|
191
|
-
isAdmin: u.isAdmin,
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
const router = createAuthRouter(auth, registry as never)
|
|
195
|
-
const result = (await callProc(router, 'loginVerifyTotp', {
|
|
196
|
-
challengeToken,
|
|
197
|
-
code: '123456',
|
|
198
|
-
})) as {
|
|
199
|
-
token: string
|
|
200
|
-
user: { id: string; username: string; isAdmin: boolean }
|
|
201
|
-
requiresTotp?: boolean
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
expect(result.requiresTotp).toBe(false)
|
|
205
|
-
expect(result.user.id).toBe('u-1')
|
|
206
|
-
// Session JWT verifies + carries the user's scopes snapshot.
|
|
207
|
-
const decoded = auth.verifyToken(result.token)
|
|
208
|
-
expect(decoded.userId).toBe('u-1')
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it('rejects an invalid/expired challenge token', async () => {
|
|
212
|
-
const router = createAuthRouter(auth, registry as never)
|
|
213
|
-
await expect(
|
|
214
|
-
callProc(router, 'loginVerifyTotp', { challengeToken: 'not.a.real.jwt', code: '123456' }),
|
|
215
|
-
).rejects.toThrow(/Invalid or expired TOTP challenge/)
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('rejects a forged challenge token (signed with wrong secret)', async () => {
|
|
219
|
-
const other = new AuthService(makeConfig({ 'auth.jwtSecret': 'attacker-secret' }) as never)
|
|
220
|
-
const forged = other.signTotpChallengeToken({
|
|
221
|
-
userId: 'u-1',
|
|
222
|
-
username: 'alice',
|
|
223
|
-
isAdmin: true,
|
|
224
|
-
})
|
|
225
|
-
const router = createAuthRouter(auth, registry as never)
|
|
226
|
-
await expect(
|
|
227
|
-
callProc(router, 'loginVerifyTotp', { challengeToken: forged, code: '000000' }),
|
|
228
|
-
).rejects.toThrow(/Invalid or expired TOTP challenge/)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
it('rejects a session token reused as a challenge token (wrong kind)', async () => {
|
|
232
|
-
const sessionTok = auth.signToken({
|
|
233
|
-
userId: 'u-1',
|
|
234
|
-
username: 'alice',
|
|
235
|
-
isAdmin: true,
|
|
236
|
-
allowedProviders: '*',
|
|
237
|
-
allowedDevices: {},
|
|
238
|
-
})
|
|
239
|
-
const router = createAuthRouter(auth, registry as never)
|
|
240
|
-
await expect(
|
|
241
|
-
callProc(router, 'loginVerifyTotp', { challengeToken: sessionTok, code: '000000' }),
|
|
242
|
-
).rejects.toThrow(/Invalid or expired TOTP challenge/)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
it('rejects a wrong 6-digit code', async () => {
|
|
246
|
-
userMgmt.verifyTotp.mockResolvedValueOnce({ valid: false })
|
|
247
|
-
const challengeToken = auth.signTotpChallengeToken({
|
|
248
|
-
userId: 'u-1',
|
|
249
|
-
username: 'alice',
|
|
250
|
-
isAdmin: false,
|
|
251
|
-
})
|
|
252
|
-
const router = createAuthRouter(auth, registry as never)
|
|
253
|
-
await expect(
|
|
254
|
-
callProc(router, 'loginVerifyTotp', { challengeToken, code: '000000' }),
|
|
255
|
-
).rejects.toThrow(/Invalid TOTP code/)
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('rejects when the user vanishes between legs (deleted concurrently)', async () => {
|
|
259
|
-
userMgmt.verifyTotp.mockResolvedValueOnce({ valid: true })
|
|
260
|
-
// The fresh listUsers() call returns nothing — user was deleted.
|
|
261
|
-
userMgmt.listUsers.mockResolvedValueOnce([])
|
|
262
|
-
const challengeToken = auth.signTotpChallengeToken({
|
|
263
|
-
userId: 'ghost',
|
|
264
|
-
username: 'alice',
|
|
265
|
-
isAdmin: false,
|
|
266
|
-
})
|
|
267
|
-
const router = createAuthRouter(auth, registry as never)
|
|
268
|
-
await expect(
|
|
269
|
-
callProc(router, 'loginVerifyTotp', { challengeToken, code: '123456' }),
|
|
270
|
-
).rejects.toThrow(/User no longer exists/)
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
it('uses the FRESHLY fetched user record (scope changes pick up immediately)', async () => {
|
|
274
|
-
// Issue challenge for u-1 with empty scopes (the in-token snapshot).
|
|
275
|
-
const challengeToken = auth.signTotpChallengeToken({
|
|
276
|
-
userId: 'u-1',
|
|
277
|
-
username: 'alice',
|
|
278
|
-
isAdmin: false,
|
|
279
|
-
})
|
|
280
|
-
// Between the two legs, an admin granted u-1 a new scope.
|
|
281
|
-
userMgmt.verifyTotp.mockResolvedValueOnce({ valid: true })
|
|
282
|
-
userMgmt.listUsers.mockResolvedValueOnce([
|
|
283
|
-
makeUser({ scopes: [{ type: 'category', target: 'addon', access: ['view'] }] }),
|
|
284
|
-
])
|
|
285
|
-
const router = createAuthRouter(auth, registry as never)
|
|
286
|
-
const result = (await callProc(router, 'loginVerifyTotp', {
|
|
287
|
-
challengeToken,
|
|
288
|
-
code: '123456',
|
|
289
|
-
})) as { token: string }
|
|
290
|
-
const decoded = auth.verifyToken(result.token)
|
|
291
|
-
// The fresh scope is in the minted session JWT, not the snapshot
|
|
292
|
-
// from the (potentially stale) password leg.
|
|
293
|
-
const scopes = (decoded as { scopes?: unknown[] }).scopes
|
|
294
|
-
expect(scopes).toHaveLength(1)
|
|
295
|
-
})
|
|
296
|
-
})
|
|
297
|
-
})
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { INTEGRATION_CAP_MARKERS } from '../cap-providers.js'
|
|
3
|
-
|
|
4
|
-
describe('integration cap markers', () => {
|
|
5
|
-
it('recognises device-adoption (not the old ha-discovery)', () => {
|
|
6
|
-
expect(INTEGRATION_CAP_MARKERS.has('device-adoption')).toBe(true)
|
|
7
|
-
expect(INTEGRATION_CAP_MARKERS.has('ha-discovery')).toBe(false)
|
|
8
|
-
expect(INTEGRATION_CAP_MARKERS.has('device-provider')).toBe(true)
|
|
9
|
-
})
|
|
10
|
-
})
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Addon settings router — raw DB proxy for the common settings API.
|
|
3
|
-
*
|
|
4
|
-
* Exposes four protected procedures consumed by:
|
|
5
|
-
* 1. Forked addons (via the tRPC WSS client in `WorkerBootstrapService`)
|
|
6
|
-
* to read/write their 3-level settings chain from the worker process.
|
|
7
|
-
* 2. Future UI flows that want to inspect/mutate addon settings through
|
|
8
|
-
* a single well-typed endpoint.
|
|
9
|
-
*
|
|
10
|
-
* The router is deliberately thin — it does NOT perform schema-based
|
|
11
|
-
* resolver merging (defaults → global → per-device). That happens on the
|
|
12
|
-
* consumer side where the addon's `ConfigUISchema` is available:
|
|
13
|
-
* - In-process addons: handled by `SettingsResolverService.createView()`
|
|
14
|
-
* wired into `AddonContext.settings` during `createAddonContext()`.
|
|
15
|
-
* - Forked addons: handled by the `AddonSettingsView` constructed inside
|
|
16
|
-
* `WorkerBootstrapService`, which has access to the worker's local
|
|
17
|
-
* addon schema.
|
|
18
|
-
*
|
|
19
|
-
* Introduced in session 5 Sprint 3a (worker-bootstrap cap-aware wiring).
|
|
20
|
-
*/
|
|
21
|
-
import { z } from 'zod'
|
|
22
|
-
import type { ConfigService } from '../../core/config/config.service.js'
|
|
23
|
-
import { trpcRouter, protectedProcedure } from '../trpc/trpc.middleware.js'
|
|
24
|
-
|
|
25
|
-
const AddonSettingsRecordSchema = z.record(z.string(), z.unknown())
|
|
26
|
-
|
|
27
|
-
const AddonIdInputSchema = z.object({
|
|
28
|
-
addonId: z.string(),
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const AddonDeviceInputSchema = z.object({
|
|
32
|
-
addonId: z.string(),
|
|
33
|
-
deviceId: z.string(),
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const UpdateGlobalInputSchema = z.object({
|
|
37
|
-
addonId: z.string(),
|
|
38
|
-
field: z.string(),
|
|
39
|
-
value: z.unknown(),
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const UpdateDeviceInputSchema = z.object({
|
|
43
|
-
addonId: z.string(),
|
|
44
|
-
deviceId: z.string(),
|
|
45
|
-
field: z.string(),
|
|
46
|
-
value: z.unknown(),
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
const SuccessSchema = z.object({ success: z.literal(true) })
|
|
50
|
-
|
|
51
|
-
const ReplaceGlobalInputSchema = z.object({
|
|
52
|
-
addonId: z.string(),
|
|
53
|
-
config: z.record(z.string(), z.unknown()),
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
export function createAddonSettingsRouter(cfg: ConfigService) {
|
|
57
|
-
return trpcRouter({
|
|
58
|
-
/**
|
|
59
|
-
* Read the addon-global settings record for the given addon.
|
|
60
|
-
* Returns the raw stored values (no defaults, no device overrides).
|
|
61
|
-
*/
|
|
62
|
-
getGlobal: protectedProcedure
|
|
63
|
-
.input(AddonIdInputSchema)
|
|
64
|
-
.output(AddonSettingsRecordSchema)
|
|
65
|
-
.query(({ input }) => cfg.getAddonConfig(input.addonId)),
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Read the per-device override record for the given addon × device.
|
|
69
|
-
* Returns the raw stored values (schema filtering happens on the
|
|
70
|
-
* consumer side at merge time).
|
|
71
|
-
*/
|
|
72
|
-
getDeviceOverrides: protectedProcedure
|
|
73
|
-
.input(AddonDeviceInputSchema)
|
|
74
|
-
.output(AddonSettingsRecordSchema)
|
|
75
|
-
.query(({ input }) => cfg.getAddonDevice(input.addonId, input.deviceId)),
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Update a single field in the addon-global settings record.
|
|
79
|
-
* Reads the current record, merges the new value, and writes back
|
|
80
|
-
* via `setAddonConfig` (bulk replace). Intended for small per-field
|
|
81
|
-
* writes from addon code; bulk updates should use a dedicated admin
|
|
82
|
-
* endpoint (not exposed here).
|
|
83
|
-
*/
|
|
84
|
-
updateGlobal: protectedProcedure
|
|
85
|
-
.input(UpdateGlobalInputSchema)
|
|
86
|
-
.output(SuccessSchema)
|
|
87
|
-
.mutation(({ input }) => {
|
|
88
|
-
const current = cfg.getAddonConfig(input.addonId)
|
|
89
|
-
cfg.setAddonConfig(input.addonId, { ...current, [input.field]: input.value })
|
|
90
|
-
return { success: true as const }
|
|
91
|
-
}),
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Update a single field in the per-device override record for the
|
|
95
|
-
* given addon × device. Merges with the existing overrides and
|
|
96
|
-
* writes back via `setAddonDevice`. Scope enforcement (dropping
|
|
97
|
-
* fields not declared as `scope: 'device'`) is the consumer's
|
|
98
|
-
* responsibility — we preserve the raw shape at this layer so the
|
|
99
|
-
* resolver contract remains symmetric with `getDeviceOverrides`.
|
|
100
|
-
*/
|
|
101
|
-
updateDevice: protectedProcedure
|
|
102
|
-
.input(UpdateDeviceInputSchema)
|
|
103
|
-
.output(SuccessSchema)
|
|
104
|
-
.mutation(({ input }) => {
|
|
105
|
-
const current = cfg.getAddonDevice(input.addonId, input.deviceId)
|
|
106
|
-
cfg.setAddonDevice(input.addonId, input.deviceId, {
|
|
107
|
-
...current,
|
|
108
|
-
[input.field]: input.value,
|
|
109
|
-
})
|
|
110
|
-
return { success: true as const }
|
|
111
|
-
}),
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Replace the entire addon-global settings record in one call.
|
|
115
|
-
* Used by forked workers for `context.config.setAll()`. Unlike
|
|
116
|
-
* `updateGlobal` (single-field merge), this overwrites the full record.
|
|
117
|
-
* Admin-level write: only workers with valid hub tokens can call this.
|
|
118
|
-
*/
|
|
119
|
-
replaceGlobal: protectedProcedure
|
|
120
|
-
.input(ReplaceGlobalInputSchema)
|
|
121
|
-
.output(SuccessSchema)
|
|
122
|
-
.mutation(({ input }) => {
|
|
123
|
-
cfg.setAddonConfig(input.addonId, input.config)
|
|
124
|
-
return { success: true as const }
|
|
125
|
-
}),
|
|
126
|
-
})
|
|
127
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agents router — fixed core API (not a capability).
|
|
3
|
-
*
|
|
4
|
-
* Thin binding over AgentRegistryService which now delegates to Moleculer
|
|
5
|
-
* for node discovery and health. Only role-assignment management and
|
|
6
|
-
* node listing remain; protocol endpoints (register, heartbeat, task
|
|
7
|
-
* dispatch) are handled natively by the Moleculer service mesh.
|
|
8
|
-
*/
|
|
9
|
-
import { z } from 'zod'
|
|
10
|
-
import type { AgentRegistryService } from '../../core/agent/agent-registry.service.js'
|
|
11
|
-
import type { MoleculerService } from '../../core/moleculer/moleculer.service.js'
|
|
12
|
-
import { trpcRouter, adminProcedure } from '../trpc/trpc.middleware.js'
|
|
13
|
-
|
|
14
|
-
const AgentRoleSchema = z.enum(['decoder', 'transcoder', 'detector', 'recorder'])
|
|
15
|
-
|
|
16
|
-
export function createAgentsRouter(ar: AgentRegistryService, moleculer: MoleculerService) {
|
|
17
|
-
return trpcRouter({
|
|
18
|
-
// ── Node listing (replaces listAgents / listConnected) ────────────
|
|
19
|
-
listNodes: adminProcedure.input(z.void()).query(async () => {
|
|
20
|
-
const items = await ar.listNodes()
|
|
21
|
-
// Spread to mutable copies: AgentListItem uses readonly arrays; Zod output schema uses mutable.
|
|
22
|
-
return items.map((a) => ({
|
|
23
|
-
...a,
|
|
24
|
-
info: {
|
|
25
|
-
...a.info,
|
|
26
|
-
capabilities: [...a.info.capabilities],
|
|
27
|
-
},
|
|
28
|
-
status: {
|
|
29
|
-
...a.status,
|
|
30
|
-
fps: { ...a.status.fps },
|
|
31
|
-
errors: [...a.status.errors],
|
|
32
|
-
},
|
|
33
|
-
subProcesses: [...a.subProcesses],
|
|
34
|
-
}))
|
|
35
|
-
}),
|
|
36
|
-
|
|
37
|
-
// ── Capability discovery (via Moleculer service list) ─────────────
|
|
38
|
-
getAgentCapabilities: adminProcedure.input(z.void()).query(async () => {
|
|
39
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access -- moleculer.broker.call typing chain unresolvable; runtime shape validated by the cast below
|
|
40
|
-
const services = (await moleculer.broker.call('$node.services', {})) as Array<{
|
|
41
|
-
name: string
|
|
42
|
-
}>
|
|
43
|
-
const capSet = new Set<string>()
|
|
44
|
-
for (const svc of services) {
|
|
45
|
-
if (svc.name.startsWith('$')) continue
|
|
46
|
-
capSet.add(svc.name)
|
|
47
|
-
}
|
|
48
|
-
return [...capSet].toSorted()
|
|
49
|
-
}),
|
|
50
|
-
|
|
51
|
-
// ── Role assignments ──────────────────────────────────────────────
|
|
52
|
-
getAssignments: adminProcedure
|
|
53
|
-
.input(z.object({ cameraId: z.number().optional() }))
|
|
54
|
-
.query(({ input }) => ar.getAssignments(input.cameraId)),
|
|
55
|
-
|
|
56
|
-
setAssignment: adminProcedure
|
|
57
|
-
.input(
|
|
58
|
-
z.object({
|
|
59
|
-
cameraId: z.number(),
|
|
60
|
-
role: AgentRoleSchema,
|
|
61
|
-
agentId: z.string(),
|
|
62
|
-
priority: z.enum(['primary', 'backup', 'overflow']),
|
|
63
|
-
rtspUrl: z.string().optional(),
|
|
64
|
-
}),
|
|
65
|
-
)
|
|
66
|
-
.mutation(({ input }) => ar.setAssignment(input as never)),
|
|
67
|
-
|
|
68
|
-
removeAssignment: adminProcedure
|
|
69
|
-
.input(
|
|
70
|
-
z.object({
|
|
71
|
-
cameraId: z.number(),
|
|
72
|
-
role: AgentRoleSchema,
|
|
73
|
-
}),
|
|
74
|
-
)
|
|
75
|
-
.mutation(({ input }) => ar.removeAssignment(input.cameraId, input.role as never)),
|
|
76
|
-
|
|
77
|
-
activateBackup: adminProcedure
|
|
78
|
-
.input(
|
|
79
|
-
z.object({
|
|
80
|
-
cameraId: z.number(),
|
|
81
|
-
role: AgentRoleSchema,
|
|
82
|
-
}),
|
|
83
|
-
)
|
|
84
|
-
.mutation(({ input }) => ar.activateBackup(input.cameraId, input.role as never)),
|
|
85
|
-
})
|
|
86
|
-
}
|