@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
package/src/api/auth-whoami.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `GET /api/auth/whoami` — validate the caller's token and surface the
|
|
3
|
-
* canonical identity + scope summary the server sees.
|
|
4
|
-
*
|
|
5
|
-
* Designed for the CLI `camstack whoami` command, which used to trust
|
|
6
|
-
* the local cache blindly. Hitting this endpoint catches:
|
|
7
|
-
* • revocation (token was deleted server-side after the local cache)
|
|
8
|
-
* • expiry
|
|
9
|
-
* • orphan-cleanup (e.g. owner deleted, see local-auth boot migration)
|
|
10
|
-
*
|
|
11
|
-
* Accepts both JWT and `cst_*` scoped tokens. Returns 401 on auth fail;
|
|
12
|
-
* 200 with the resolved identity otherwise. We intentionally do NOT
|
|
13
|
-
* expose `tokenHash` or the raw token — only `tokenPrefix` + scopes for
|
|
14
|
-
* the client to confirm the cached blob matches what the server has.
|
|
15
|
-
*/
|
|
16
|
-
import type { FastifyInstance } from 'fastify'
|
|
17
|
-
import type { AuthService } from '../core/auth/auth.service'
|
|
18
|
-
import type { AddonRegistryService } from '../core/addon/addon-registry.service'
|
|
19
|
-
|
|
20
|
-
interface ScopedTokenLike {
|
|
21
|
-
readonly id: string
|
|
22
|
-
readonly userId: string
|
|
23
|
-
readonly name: string
|
|
24
|
-
readonly tokenPrefix: string
|
|
25
|
-
readonly scopes: readonly { type: string; target: string }[]
|
|
26
|
-
readonly expiresAt?: number | null
|
|
27
|
-
readonly lastUsedAt?: number | null
|
|
28
|
-
readonly createdAt: number
|
|
29
|
-
}
|
|
30
|
-
interface UserManagementLike {
|
|
31
|
-
validateScopedToken(input: { token: string }): Promise<ScopedTokenLike | null>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface WhoamiOk {
|
|
35
|
-
readonly ok: true
|
|
36
|
-
readonly kind: 'jwt' | 'scoped'
|
|
37
|
-
readonly userId: string
|
|
38
|
-
readonly username: string
|
|
39
|
-
readonly isAdmin?: boolean
|
|
40
|
-
readonly tokenPrefix?: string
|
|
41
|
-
readonly scopes?: readonly { type: string; target: string }[]
|
|
42
|
-
readonly expiresAt?: number | null
|
|
43
|
-
readonly createdAt?: number
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function registerAuthWhoamiRoute(
|
|
47
|
-
fastify: FastifyInstance,
|
|
48
|
-
authService: AuthService,
|
|
49
|
-
addonRegistry: AddonRegistryService,
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
fastify.get('/api/auth/whoami', async (request, reply) => {
|
|
52
|
-
const authHeader = request.headers.authorization
|
|
53
|
-
if (!authHeader) {
|
|
54
|
-
return reply.status(401).send({ ok: false, error: 'No Authorization header' })
|
|
55
|
-
}
|
|
56
|
-
const token = authHeader.replace('Bearer ', '')
|
|
57
|
-
|
|
58
|
-
// JWT fast path — same logic as addon-upload auth chain.
|
|
59
|
-
try {
|
|
60
|
-
const payload = authService.verifyToken(token)
|
|
61
|
-
const ok: WhoamiOk = {
|
|
62
|
-
ok: true,
|
|
63
|
-
kind: 'jwt',
|
|
64
|
-
userId: payload.userId ?? payload.keyId ?? 'unknown',
|
|
65
|
-
username: payload.username ?? 'unknown',
|
|
66
|
-
isAdmin: payload.isAdmin,
|
|
67
|
-
}
|
|
68
|
-
return reply.send(ok)
|
|
69
|
-
} catch {
|
|
70
|
-
// Not a JWT — try scoped token via cap registry.
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const capRegistry = addonRegistry.getCapabilityRegistry()
|
|
75
|
-
const userMgmt = capRegistry.getSingleton('user-management') as UserManagementLike | undefined
|
|
76
|
-
if (!userMgmt) {
|
|
77
|
-
return reply.status(503).send({ ok: false, error: 'user-management cap not mounted' })
|
|
78
|
-
}
|
|
79
|
-
const record = await userMgmt.validateScopedToken({ token })
|
|
80
|
-
if (!record) {
|
|
81
|
-
return reply
|
|
82
|
-
.status(401)
|
|
83
|
-
.send({ ok: false, error: 'Token not recognised (revoked, expired, or never issued)' })
|
|
84
|
-
}
|
|
85
|
-
const ok: WhoamiOk = {
|
|
86
|
-
ok: true,
|
|
87
|
-
kind: 'scoped',
|
|
88
|
-
userId: record.userId,
|
|
89
|
-
username: record.userId,
|
|
90
|
-
tokenPrefix: record.tokenPrefix,
|
|
91
|
-
scopes: record.scopes,
|
|
92
|
-
expiresAt: record.expiresAt ?? null,
|
|
93
|
-
createdAt: record.createdAt,
|
|
94
|
-
}
|
|
95
|
-
return reply.send(ok)
|
|
96
|
-
} catch (err) {
|
|
97
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
98
|
-
return reply.status(500).send({ ok: false, error: `Validation failed: ${msg}` })
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { TRPCError } from '@trpc/server'
|
|
3
|
-
import { protectedProcedure, adminProcedure, trpcRouter } from './trpc/trpc.middleware'
|
|
4
|
-
import type { AddonBridgeService } from '../core/addon-bridge/addon-bridge.service'
|
|
5
|
-
import type { AddonSearchService } from '../core/addon/addon-search.service'
|
|
6
|
-
import type { AddonRegistryService } from '../core/addon/addon-registry.service'
|
|
7
|
-
import type { ToastService } from '@camstack/core'
|
|
8
|
-
|
|
9
|
-
export function createBridgeAddonsRouter(
|
|
10
|
-
bridge: AddonBridgeService,
|
|
11
|
-
addonSearch: AddonSearchService,
|
|
12
|
-
addonRegistry?: AddonRegistryService,
|
|
13
|
-
toastService?: ToastService,
|
|
14
|
-
) {
|
|
15
|
-
return trpcRouter({
|
|
16
|
-
/** List all addon packages installed in the addons directory */
|
|
17
|
-
listPackages: protectedProcedure.query(() => {
|
|
18
|
-
const installer = bridge.getInstaller()
|
|
19
|
-
if (!installer) return []
|
|
20
|
-
return installer.listInstalled()
|
|
21
|
-
}),
|
|
22
|
-
|
|
23
|
-
/** List all available addons across all loaded packages */
|
|
24
|
-
listAddons: protectedProcedure.query(() => {
|
|
25
|
-
return bridge.listAvailableAddons().map((id) => {
|
|
26
|
-
const addon = bridge.getLoader().getAddon(id)
|
|
27
|
-
return {
|
|
28
|
-
id,
|
|
29
|
-
packageName: addon?.packageName ?? 'unknown',
|
|
30
|
-
slot: addon?.declaration.slot ?? null,
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
}),
|
|
34
|
-
|
|
35
|
-
/** Install a community addon package from npm */
|
|
36
|
-
installPackage: adminProcedure
|
|
37
|
-
.input(z.object({ packageName: z.string(), version: z.string().optional() }))
|
|
38
|
-
.mutation(async ({ input }) => {
|
|
39
|
-
const installer = bridge.getInstaller()
|
|
40
|
-
if (!installer) {
|
|
41
|
-
throw new TRPCError({
|
|
42
|
-
code: 'PRECONDITION_FAILED',
|
|
43
|
-
message: 'Addon installer not available — bridge may have failed to initialize',
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
await installer.install(input.packageName, input.version)
|
|
47
|
-
await bridge.reloadPackages()
|
|
48
|
-
const result = addonRegistry
|
|
49
|
-
? await addonRegistry.loadNewAddons()
|
|
50
|
-
: { loaded: [], failed: [] }
|
|
51
|
-
toastService?.broadcast({
|
|
52
|
-
title: 'Addon Installed',
|
|
53
|
-
message: `${input.packageName} installed successfully${result.loaded.length ? ` (${result.loaded.join(', ')})` : ''}`,
|
|
54
|
-
severity: result.failed.length ? 'warning' : 'info',
|
|
55
|
-
})
|
|
56
|
-
return { success: true, loaded: result.loaded, failed: result.failed }
|
|
57
|
-
}),
|
|
58
|
-
|
|
59
|
-
/** Uninstall a community addon package */
|
|
60
|
-
uninstallPackage: adminProcedure
|
|
61
|
-
.input(z.object({ packageName: z.string() }))
|
|
62
|
-
.mutation(async ({ input }) => {
|
|
63
|
-
// Server-side guard: prevent uninstalling required packages.
|
|
64
|
-
// After Phase D bundle merge, the pipeline-related packages
|
|
65
|
-
// (stream-broker, detection-pipeline, motion-wasm, decoders,
|
|
66
|
-
// audio) all live in @camstack/addon-pipeline.
|
|
67
|
-
const REQUIRED = new Set([
|
|
68
|
-
'@camstack/core',
|
|
69
|
-
'@camstack/addon-pipeline',
|
|
70
|
-
'@camstack/addon-pipeline-orchestrator',
|
|
71
|
-
'@camstack/addon-post-analysis',
|
|
72
|
-
'@camstack/addon-admin-ui',
|
|
73
|
-
])
|
|
74
|
-
if (REQUIRED.has(input.packageName)) {
|
|
75
|
-
throw new TRPCError({
|
|
76
|
-
code: 'FORBIDDEN',
|
|
77
|
-
message: `Package ${input.packageName} is required and cannot be uninstalled`,
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const installer = bridge.getInstaller()
|
|
82
|
-
if (!installer) {
|
|
83
|
-
throw new TRPCError({
|
|
84
|
-
code: 'PRECONDITION_FAILED',
|
|
85
|
-
message: 'Addon installer not available — bridge may have failed to initialize',
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
await installer.uninstall(input.packageName)
|
|
89
|
-
await bridge.reloadPackages()
|
|
90
|
-
if (addonRegistry) await addonRegistry.loadNewAddons()
|
|
91
|
-
toastService?.broadcast({
|
|
92
|
-
title: 'Addon Uninstalled',
|
|
93
|
-
message: `${input.packageName} has been removed`,
|
|
94
|
-
severity: 'info',
|
|
95
|
-
})
|
|
96
|
-
return { success: true }
|
|
97
|
-
}),
|
|
98
|
-
|
|
99
|
-
/** Force reload all addon packages (re-scan directories, re-import modules) */
|
|
100
|
-
reloadPackages: adminProcedure.mutation(async () => {
|
|
101
|
-
await bridge.reloadPackages()
|
|
102
|
-
return { success: true, message: 'Addon packages reloaded' }
|
|
103
|
-
}),
|
|
104
|
-
|
|
105
|
-
/** Search npm for available CamStack addons */
|
|
106
|
-
searchAvailable: protectedProcedure
|
|
107
|
-
.input(z.object({ query: z.string().optional() }).optional())
|
|
108
|
-
.query(async ({ input }) => {
|
|
109
|
-
const results = await addonSearch.searchAddons(input?.query)
|
|
110
|
-
|
|
111
|
-
// Enrich with install status from locally installed packages
|
|
112
|
-
const installed = bridge.getInstaller()?.listInstalled() ?? []
|
|
113
|
-
const installedMap = new Map(installed.map((p) => [p.name, p.version]))
|
|
114
|
-
|
|
115
|
-
return results.map((r) => ({
|
|
116
|
-
...r,
|
|
117
|
-
installed: installedMap.has(r.name),
|
|
118
|
-
installedVersion: installedMap.get(r.name),
|
|
119
|
-
}))
|
|
120
|
-
}),
|
|
121
|
-
})
|
|
122
|
-
}
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { TRPCError } from '@trpc/server'
|
|
3
|
-
import { adminProcedure, trpcRouter } from './trpc/trpc.middleware'
|
|
4
|
-
import type { AddonRegistryService } from '../core/addon/addon-registry.service'
|
|
5
|
-
import type { ConfigService } from '../core/config/config.service'
|
|
6
|
-
import type { LoggingService } from '../core/logging/logging.service'
|
|
7
|
-
import { isInfraCapability } from '@camstack/kernel'
|
|
8
|
-
import { collectionPreferenceKey, persistCollectionDisabled } from './core/collection-preference'
|
|
9
|
-
|
|
10
|
-
// ─── Zod Schemas ───────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
const setPreferenceInput = z.discriminatedUnion('mode', [
|
|
13
|
-
z.object({
|
|
14
|
-
mode: z.literal('singleton'),
|
|
15
|
-
capability: z.string(),
|
|
16
|
-
addonId: z.string(),
|
|
17
|
-
}),
|
|
18
|
-
z.object({
|
|
19
|
-
mode: z.literal('collection'),
|
|
20
|
-
capability: z.string(),
|
|
21
|
-
addonId: z.string(),
|
|
22
|
-
enabled: z.boolean(),
|
|
23
|
-
}),
|
|
24
|
-
])
|
|
25
|
-
|
|
26
|
-
// ─── Router ────────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
export function createCapabilitiesRouter(
|
|
29
|
-
addonRegistry: AddonRegistryService,
|
|
30
|
-
configService: ConfigService,
|
|
31
|
-
loggingService: LoggingService,
|
|
32
|
-
) {
|
|
33
|
-
const logger = loggingService.createLogger('CapabilitiesRouter')
|
|
34
|
-
|
|
35
|
-
/** Build enriched provider details from addon metadata */
|
|
36
|
-
function getProviderDetails(addonIds: readonly string[]) {
|
|
37
|
-
const allAddons = addonRegistry.listAllAddons()
|
|
38
|
-
return addonIds.map((addonId) => {
|
|
39
|
-
const addon = allAddons.find((a) => a.manifest.id === addonId)
|
|
40
|
-
return {
|
|
41
|
-
addonId,
|
|
42
|
-
displayName: addon?.manifest.name ?? addonId,
|
|
43
|
-
packageName: addon?.manifest.packageName ?? addonId,
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return trpcRouter({
|
|
49
|
-
// ─── List all capabilities with enriched metadata ──────────────────
|
|
50
|
-
|
|
51
|
-
list: adminProcedure.query(() => {
|
|
52
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
53
|
-
const caps = registry.listCapabilities()
|
|
54
|
-
return caps.map((cap) => ({
|
|
55
|
-
...cap,
|
|
56
|
-
providerDetails: getProviderDetails(cap.providers),
|
|
57
|
-
isInfra: isInfraCapability(cap.name),
|
|
58
|
-
}))
|
|
59
|
-
}),
|
|
60
|
-
|
|
61
|
-
// ─── Get current preference for a capability ──────────────────────
|
|
62
|
-
|
|
63
|
-
getPreference: adminProcedure.input(z.object({ capability: z.string() })).query(({ input }) => {
|
|
64
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
65
|
-
const mode = registry.getMode(input.capability)
|
|
66
|
-
if (!mode) return null
|
|
67
|
-
|
|
68
|
-
if (mode === 'singleton') {
|
|
69
|
-
const addonId = configService.get<string>(`capabilities.singleton.${input.capability}`)
|
|
70
|
-
return {
|
|
71
|
-
capability: input.capability,
|
|
72
|
-
mode: mode as 'singleton',
|
|
73
|
-
preference: addonId ? { addonId } : null,
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// collection
|
|
78
|
-
const raw = configService.get<string>(collectionPreferenceKey(input.capability))
|
|
79
|
-
let disabled: string[] = []
|
|
80
|
-
if (raw) {
|
|
81
|
-
try {
|
|
82
|
-
const parsed = JSON.parse(raw) as { disabled?: string[] }
|
|
83
|
-
disabled = Array.isArray(parsed.disabled) ? parsed.disabled : []
|
|
84
|
-
} catch {
|
|
85
|
-
/* ignore malformed */
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
capability: input.capability,
|
|
90
|
-
mode: mode as 'collection',
|
|
91
|
-
preference: { disabled },
|
|
92
|
-
}
|
|
93
|
-
}),
|
|
94
|
-
|
|
95
|
-
// ─── Set preference (singleton switch or collection toggle) ───────
|
|
96
|
-
|
|
97
|
-
setPreference: adminProcedure.input(setPreferenceInput).mutation(async ({ input }) => {
|
|
98
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
99
|
-
|
|
100
|
-
if (input.mode === 'singleton') {
|
|
101
|
-
const { capability, addonId } = input
|
|
102
|
-
const caps = registry.listCapabilities()
|
|
103
|
-
const cap = caps.find((c) => c.name === capability)
|
|
104
|
-
if (!cap)
|
|
105
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: `Unknown capability: ${capability}` })
|
|
106
|
-
if (cap.mode !== 'singleton')
|
|
107
|
-
throw new TRPCError({
|
|
108
|
-
code: 'BAD_REQUEST',
|
|
109
|
-
message: `"${capability}" is not a singleton`,
|
|
110
|
-
})
|
|
111
|
-
if (!cap.providers.includes(addonId)) {
|
|
112
|
-
throw new TRPCError({
|
|
113
|
-
code: 'BAD_REQUEST',
|
|
114
|
-
message: `Provider "${addonId}" is not registered for "${capability}"`,
|
|
115
|
-
})
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const requiresRestart = isInfraCapability(capability)
|
|
119
|
-
|
|
120
|
-
if (!requiresRestart) {
|
|
121
|
-
// Hot-swap at runtime
|
|
122
|
-
await registry.setActiveSingleton(capability, addonId)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Persist preference
|
|
126
|
-
configService.set(`capabilities.singleton.${capability}`, addonId)
|
|
127
|
-
logger.info('Singleton preference set', {
|
|
128
|
-
tags: { addonId },
|
|
129
|
-
meta: { capability, requiresRestart },
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
return { success: true, requiresRestart }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// collection toggle
|
|
136
|
-
const { capability, addonId, enabled } = input
|
|
137
|
-
const caps = registry.listCapabilities()
|
|
138
|
-
const cap = caps.find((c) => c.name === capability)
|
|
139
|
-
if (!cap)
|
|
140
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: `Unknown capability: ${capability}` })
|
|
141
|
-
if (cap.mode !== 'collection')
|
|
142
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: `"${capability}" is not a collection` })
|
|
143
|
-
if (!cap.providers.includes(addonId)) {
|
|
144
|
-
throw new TRPCError({
|
|
145
|
-
code: 'BAD_REQUEST',
|
|
146
|
-
message: `Provider "${addonId}" is not registered for "${capability}"`,
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (enabled) {
|
|
151
|
-
registry.enableCollectionProvider(capability, addonId)
|
|
152
|
-
} else {
|
|
153
|
-
registry.disableCollectionProvider(capability, addonId)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Persist disabled list via the shared canonical writer.
|
|
157
|
-
const updatedCap = registry.listCapabilities().find((c) => c.name === capability)
|
|
158
|
-
persistCollectionDisabled(configService, capability, updatedCap?.disabledProviders ?? [])
|
|
159
|
-
logger.info('Collection provider toggled', {
|
|
160
|
-
tags: { addonId },
|
|
161
|
-
meta: { capability, enabled },
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
return { success: true, requiresRestart: false }
|
|
165
|
-
}),
|
|
166
|
-
|
|
167
|
-
// ─── Reset preference to default ──────────────────────────────────
|
|
168
|
-
|
|
169
|
-
resetPreference: adminProcedure
|
|
170
|
-
.input(z.object({ capability: z.string() }))
|
|
171
|
-
.mutation(({ input }) => {
|
|
172
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
173
|
-
const mode = registry.getMode(input.capability)
|
|
174
|
-
if (!mode)
|
|
175
|
-
throw new TRPCError({
|
|
176
|
-
code: 'NOT_FOUND',
|
|
177
|
-
message: `Unknown capability: ${input.capability}`,
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
if (mode === 'singleton') {
|
|
181
|
-
configService.set(`capabilities.singleton.${input.capability}`, null)
|
|
182
|
-
logger.info('Singleton preference reset (takes effect on restart)', {
|
|
183
|
-
meta: { capability: input.capability },
|
|
184
|
-
})
|
|
185
|
-
return { success: true, requiresRestart: true }
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// collection: re-enable all disabled providers
|
|
189
|
-
const caps = registry.listCapabilities()
|
|
190
|
-
const cap = caps.find((c) => c.name === input.capability)
|
|
191
|
-
if (cap) {
|
|
192
|
-
for (const addonId of cap.disabledProviders) {
|
|
193
|
-
registry.enableCollectionProvider(input.capability, addonId)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
configService.set(`capabilities.collection.${input.capability}`, null)
|
|
197
|
-
logger.info('Collection preference reset (all providers re-enabled)', {
|
|
198
|
-
meta: { capability: input.capability },
|
|
199
|
-
})
|
|
200
|
-
return { success: true, requiresRestart: false }
|
|
201
|
-
}),
|
|
202
|
-
|
|
203
|
-
// ─── Per-device overrides (existing, unchanged) ───────────────────
|
|
204
|
-
|
|
205
|
-
listCapabilities: adminProcedure.query(() => {
|
|
206
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
207
|
-
return registry.listCapabilities()
|
|
208
|
-
}),
|
|
209
|
-
|
|
210
|
-
setDeviceCapability: adminProcedure
|
|
211
|
-
.input(z.object({ deviceId: z.string(), capability: z.string(), addonId: z.string() }))
|
|
212
|
-
.mutation(({ input }) => {
|
|
213
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
214
|
-
registry.setDeviceOverride(input.deviceId, input.capability, input.addonId)
|
|
215
|
-
logger.info('Device capability override set', {
|
|
216
|
-
tags: { deviceId: Number(input.deviceId), addonId: input.addonId },
|
|
217
|
-
meta: { capability: input.capability },
|
|
218
|
-
})
|
|
219
|
-
}),
|
|
220
|
-
|
|
221
|
-
clearDeviceCapability: adminProcedure
|
|
222
|
-
.input(z.object({ deviceId: z.string(), capability: z.string() }))
|
|
223
|
-
.mutation(({ input }) => {
|
|
224
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
225
|
-
registry.clearDeviceOverride(input.deviceId, input.capability)
|
|
226
|
-
logger.info('Device capability override cleared', {
|
|
227
|
-
tags: { deviceId: Number(input.deviceId) },
|
|
228
|
-
meta: { capability: input.capability },
|
|
229
|
-
})
|
|
230
|
-
}),
|
|
231
|
-
|
|
232
|
-
getDeviceCapabilities: adminProcedure
|
|
233
|
-
.input(z.object({ deviceId: z.string() }))
|
|
234
|
-
.output(z.record(z.string(), z.string()))
|
|
235
|
-
.query(({ input }): Record<string, string> => {
|
|
236
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
237
|
-
const overrides = registry.getDeviceOverrides(input.deviceId)
|
|
238
|
-
return Object.fromEntries(overrides) as Record<string, string>
|
|
239
|
-
}),
|
|
240
|
-
|
|
241
|
-
setDeviceCollectionFilter: adminProcedure
|
|
242
|
-
.input(
|
|
243
|
-
z.object({ deviceId: z.string(), capability: z.string(), addonIds: z.array(z.string()) }),
|
|
244
|
-
)
|
|
245
|
-
.mutation(({ input }) => {
|
|
246
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
247
|
-
registry.setDeviceCollectionFilter(input.deviceId, input.capability, input.addonIds)
|
|
248
|
-
logger.info('Device collection filter set', {
|
|
249
|
-
tags: { deviceId: Number(input.deviceId) },
|
|
250
|
-
meta: { capability: input.capability, addonIds: input.addonIds },
|
|
251
|
-
})
|
|
252
|
-
}),
|
|
253
|
-
|
|
254
|
-
clearDeviceCollectionFilter: adminProcedure
|
|
255
|
-
.input(z.object({ deviceId: z.string(), capability: z.string() }))
|
|
256
|
-
.mutation(({ input }) => {
|
|
257
|
-
const registry = addonRegistry.getCapabilityRegistry()
|
|
258
|
-
registry.clearDeviceCollectionFilter(input.deviceId, input.capability)
|
|
259
|
-
logger.info('Device collection filter cleared', {
|
|
260
|
-
tags: { deviceId: Number(input.deviceId) },
|
|
261
|
-
meta: { capability: input.capability },
|
|
262
|
-
})
|
|
263
|
-
}),
|
|
264
|
-
})
|
|
265
|
-
}
|