@camstack/server 0.2.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NetworkQualityService = void 0;
|
|
4
|
+
const core_1 = require("@camstack/core");
|
|
5
|
+
class NetworkQualityService extends core_1.NetworkQualityTracker {
|
|
6
|
+
}
|
|
7
|
+
exports.NetworkQualityService = NetworkQualityService;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotificationServiceWrapper = void 0;
|
|
4
|
+
const core_1 = require("@camstack/core");
|
|
5
|
+
/**
|
|
6
|
+
* NestJS-injectable wrapper around the core NotificationService.
|
|
7
|
+
*
|
|
8
|
+
* Lazily creates the NotificationService on first use, wiring in the
|
|
9
|
+
* CapabilityRegistry for proxy-based output resolution.
|
|
10
|
+
*/
|
|
11
|
+
class NotificationServiceWrapper {
|
|
12
|
+
caps;
|
|
13
|
+
logging;
|
|
14
|
+
_service = null;
|
|
15
|
+
constructor(caps, logging) {
|
|
16
|
+
this.caps = caps;
|
|
17
|
+
this.logging = logging;
|
|
18
|
+
}
|
|
19
|
+
get service() {
|
|
20
|
+
if (!this._service) {
|
|
21
|
+
this._service = new core_1.NotificationService(this.logging.createLogger('notifications'));
|
|
22
|
+
const registry = this.caps.getRegistry();
|
|
23
|
+
if (registry) {
|
|
24
|
+
this._service.setRegistry(registry);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return this._service;
|
|
28
|
+
}
|
|
29
|
+
async notify(notification) {
|
|
30
|
+
return this.service.notify(notification);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.NotificationServiceWrapper = NotificationServiceWrapper;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ToastServiceWrapper = void 0;
|
|
4
|
+
const core_1 = require("@camstack/core");
|
|
5
|
+
/**
|
|
6
|
+
* NestJS-injectable wrapper around the core ToastService.
|
|
7
|
+
*
|
|
8
|
+
* Provides toast broadcasting to connected UI clients.
|
|
9
|
+
*/
|
|
10
|
+
class ToastServiceWrapper {
|
|
11
|
+
_service = new core_1.ToastService();
|
|
12
|
+
get service() {
|
|
13
|
+
return this._service;
|
|
14
|
+
}
|
|
15
|
+
broadcast(toast) {
|
|
16
|
+
this._service.broadcast(toast);
|
|
17
|
+
}
|
|
18
|
+
sendToUser(userId, toast) {
|
|
19
|
+
this._service.sendToUser(userId, toast);
|
|
20
|
+
}
|
|
21
|
+
subscribe(connectionId, userId, callback) {
|
|
22
|
+
return this._service.subscribe(connectionId, userId, callback);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.ToastServiceWrapper = ToastServiceWrapper;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReplEngineService = void 0;
|
|
4
|
+
const core_1 = require("@camstack/core");
|
|
5
|
+
const types_1 = require("@camstack/types");
|
|
6
|
+
class ReplEngineService extends core_1.ReplEngine {
|
|
7
|
+
/**
|
|
8
|
+
* Lazily-instantiated `SystemMirror` shared across REPL sessions.
|
|
9
|
+
* Holds a single warm-boot mirror that every `sm.getDeviceById(id)`
|
|
10
|
+
* lookup serves from. Init runs at first access (Promise cached so
|
|
11
|
+
* concurrent sessions don't double-fetch).
|
|
12
|
+
*/
|
|
13
|
+
static systemMirror = null;
|
|
14
|
+
static systemMirrorInit = null;
|
|
15
|
+
/**
|
|
16
|
+
* Build a `SystemMirrorApi` for in-process server use. The cap-method
|
|
17
|
+
* surface (deviceManager / deviceState queries) goes through the
|
|
18
|
+
* standard broker tRPC client — those resolve locally via
|
|
19
|
+
* `localProviderLink`. The `live.onEvent` channel is NOT a cap, so
|
|
20
|
+
* the broker can't route it; we synthesize the same shape over the
|
|
21
|
+
* local `EventBusService` so SystemMirror subscriptions work without
|
|
22
|
+
* crossing the network boundary or polling the broker for a
|
|
23
|
+
* non-existent `live` service.
|
|
24
|
+
*/
|
|
25
|
+
static buildInProcessApi(addonRegistry, eventBus) {
|
|
26
|
+
const baseApi = addonRegistry.getBrokerApi();
|
|
27
|
+
return {
|
|
28
|
+
...baseApi,
|
|
29
|
+
live: {
|
|
30
|
+
onEvent: {
|
|
31
|
+
subscribe: (input, opts) => {
|
|
32
|
+
const off = eventBus.subscribe({ category: input.category }, (evt) => {
|
|
33
|
+
try {
|
|
34
|
+
opts.onData({ data: evt.data });
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
opts.onError?.(err);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return { unsubscribe: off };
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
static getOrInitSystemMirror(addonRegistry, eventBus) {
|
|
47
|
+
if (this.systemMirror)
|
|
48
|
+
return Promise.resolve(this.systemMirror);
|
|
49
|
+
if (!this.systemMirrorInit) {
|
|
50
|
+
const api = this.buildInProcessApi(addonRegistry, eventBus);
|
|
51
|
+
const sm = new types_1.SystemMirror(api);
|
|
52
|
+
this.systemMirrorInit = sm.init().then(() => {
|
|
53
|
+
this.systemMirror = sm;
|
|
54
|
+
return sm;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return this.systemMirrorInit;
|
|
58
|
+
}
|
|
59
|
+
constructor(addonRegistry, eventBus, _loggingService) {
|
|
60
|
+
const contextProvider = {
|
|
61
|
+
async getSystemSandbox() {
|
|
62
|
+
const integrationRegistry = addonRegistry.getIntegrationRegistry();
|
|
63
|
+
const deviceRegistry = addonRegistry.getDeviceRegistry();
|
|
64
|
+
// Warm-boot the SystemMirror so `sm.getDeviceById(id)` is sync
|
|
65
|
+
// for the first user expression.
|
|
66
|
+
const sm = await ReplEngineService.getOrInitSystemMirror(addonRegistry, eventBus);
|
|
67
|
+
return {
|
|
68
|
+
// ── New canonical API ───────────────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* SystemMirror — the cap-driven, reactive view of every
|
|
71
|
+
* device. Sync `getDeviceById(id)`, typed `state.<cap>.value`
|
|
72
|
+
* reads, full method dispatch, query helpers.
|
|
73
|
+
*/
|
|
74
|
+
sm,
|
|
75
|
+
// ── Legacy (kept for backward-compat) ──────────────────────
|
|
76
|
+
addonRegistry,
|
|
77
|
+
eventBus,
|
|
78
|
+
integrationRegistry,
|
|
79
|
+
devices: () => deviceRegistry.getAll(),
|
|
80
|
+
integrations: async () => (await integrationRegistry?.listIntegrations()) ?? [],
|
|
81
|
+
addons: () => addonRegistry.listAddons(),
|
|
82
|
+
getDevice: (id) => deviceRegistry.getById(id),
|
|
83
|
+
getIntegration: async (id) => (await integrationRegistry?.getIntegration(id)) ?? null,
|
|
84
|
+
getSystemMirror: () => Promise.resolve(sm),
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
async getDeviceSandbox(deviceId) {
|
|
88
|
+
const deviceRegistry = addonRegistry.getDeviceRegistry();
|
|
89
|
+
const rawDevice = deviceRegistry.getById(deviceId);
|
|
90
|
+
const sm = await ReplEngineService.getOrInitSystemMirror(addonRegistry, eventBus);
|
|
91
|
+
// `device` is the typed DeviceProxy backed by the SystemMirror
|
|
92
|
+
// mirror — same shape as `sm.getDeviceById(deviceId)`. Sync
|
|
93
|
+
// state reads + cap-method dispatch via the wrapper chain.
|
|
94
|
+
const device = sm.getDeviceById(deviceId);
|
|
95
|
+
return {
|
|
96
|
+
/** SystemMirror — full cluster view. */
|
|
97
|
+
sm,
|
|
98
|
+
/**
|
|
99
|
+
* The current device as a DeviceProxy. Sync state reads
|
|
100
|
+
* (`device.state.battery.value`) + async cap methods
|
|
101
|
+
* (`await device.snapshot.getSnapshot({})`).
|
|
102
|
+
*/
|
|
103
|
+
device,
|
|
104
|
+
/** Numeric device id (same as URL). */
|
|
105
|
+
deviceId,
|
|
106
|
+
/** Device metadata (name, addonId, type, online, …). */
|
|
107
|
+
info: sm.getDeviceInfo(deviceId),
|
|
108
|
+
/** Raw IDevice instance — escape hatch for legacy access.
|
|
109
|
+
* Prefer `device` (DeviceProxy) for new code. */
|
|
110
|
+
rawDevice,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
getProviderSandbox(addonId) {
|
|
114
|
+
const integrationRegistry = addonRegistry.getIntegrationRegistry();
|
|
115
|
+
const deviceRegistry = addonRegistry.getDeviceRegistry();
|
|
116
|
+
const devices = deviceRegistry.getAllForAddon(addonId);
|
|
117
|
+
return {
|
|
118
|
+
getIntegration: () => integrationRegistry?.getIntegration(addonId) ?? Promise.resolve(null),
|
|
119
|
+
devices,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
getAddonSandbox(addonId) {
|
|
123
|
+
// REPL exposes addon metadata only — never the live in-process
|
|
124
|
+
// instance. Direct addon access only works for hub-local addons
|
|
125
|
+
// and breaks for remote-agent addons. To invoke addon behaviour
|
|
126
|
+
// from the REPL, use the cap router via tRPC instead.
|
|
127
|
+
const entry = addonRegistry.listAddons().find((e) => e.manifest.id === addonId);
|
|
128
|
+
return {
|
|
129
|
+
manifest: entry?.manifest,
|
|
130
|
+
declaration: entry?.declaration,
|
|
131
|
+
source: entry?.source,
|
|
132
|
+
installSource: entry?.installSource,
|
|
133
|
+
process: entry?.process,
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
super(contextProvider);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.ReplEngineService = ReplEngineService;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FsStorageBackend = void 0;
|
|
4
|
+
// Re-export from @camstack/core
|
|
5
|
+
var core_1 = require("@camstack/core");
|
|
6
|
+
Object.defineProperty(exports, "FsStorageBackend", { enumerable: true, get: function () { return core_1.FsStorageBackend; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StorageLocationManager = void 0;
|
|
4
|
+
// Re-export from @camstack/core
|
|
5
|
+
var core_1 = require("@camstack/core");
|
|
6
|
+
Object.defineProperty(exports, "StorageLocationManager", { enumerable: true, get: function () { return core_1.StorageLocationManager; } });
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StreamProbeService = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const util_1 = require("util");
|
|
6
|
+
const types_1 = require("@camstack/types");
|
|
7
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
8
|
+
const CACHE_TTL_MS = 3_600_000; // 1 hour
|
|
9
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
10
|
+
/** Codec aliases normalised to canonical names. */
|
|
11
|
+
const CODEC_ALIASES = {
|
|
12
|
+
hevc: 'h265',
|
|
13
|
+
};
|
|
14
|
+
class StreamProbeService {
|
|
15
|
+
logger;
|
|
16
|
+
cache = new Map();
|
|
17
|
+
constructor(loggingService) {
|
|
18
|
+
this.logger = loggingService.createLogger('StreamProbeService');
|
|
19
|
+
}
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Public API
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/** Probe a stream URL and return its metadata (cached for 1 hour). */
|
|
24
|
+
async probe(url, options) {
|
|
25
|
+
const force = options?.force ?? false;
|
|
26
|
+
if (!force) {
|
|
27
|
+
const cached = this.cache.get(url);
|
|
28
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
29
|
+
return cached.metadata;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const metadata = await this.runProbe(url);
|
|
33
|
+
this.cache.set(url, { metadata, timestamp: Date.now() });
|
|
34
|
+
return metadata;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generic field probe: given a field key and value, decides how to probe.
|
|
38
|
+
* Stream fields (stream_*) → ffprobe, other URLs → HTTP HEAD check.
|
|
39
|
+
*/
|
|
40
|
+
async probeField(key, value) {
|
|
41
|
+
const url = String(value ?? '').trim();
|
|
42
|
+
if (!url) {
|
|
43
|
+
return { status: 'error', error: 'No URL provided' };
|
|
44
|
+
}
|
|
45
|
+
// Stream fields: use ffprobe
|
|
46
|
+
if (key.startsWith('stream')) {
|
|
47
|
+
const meta = await this.probe(url, { force: true });
|
|
48
|
+
const labels = [];
|
|
49
|
+
if (meta.width && meta.height)
|
|
50
|
+
labels.push(`${meta.width}\u00d7${meta.height}`);
|
|
51
|
+
if (meta.codec) {
|
|
52
|
+
const codecDisplay = { h265: 'H.265', h264: 'H.264', hevc: 'H.265' };
|
|
53
|
+
labels.push(codecDisplay[meta.codec] ?? meta.codec.toUpperCase());
|
|
54
|
+
}
|
|
55
|
+
if (meta.fps)
|
|
56
|
+
labels.push(`${Math.round(meta.fps)}fps`);
|
|
57
|
+
if (meta.audio?.codec) {
|
|
58
|
+
const audioCodec = meta.audio.codec.toUpperCase();
|
|
59
|
+
const audioBits = [audioCodec];
|
|
60
|
+
if (meta.audio.sampleRate)
|
|
61
|
+
audioBits.push(`${Math.round(meta.audio.sampleRate / 1000)}kHz`);
|
|
62
|
+
if (meta.audio.channels === 1)
|
|
63
|
+
audioBits.push('mono');
|
|
64
|
+
else if (meta.audio.channels === 2)
|
|
65
|
+
audioBits.push('stereo');
|
|
66
|
+
else if (meta.audio.channels)
|
|
67
|
+
audioBits.push(`${meta.audio.channels}ch`);
|
|
68
|
+
labels.push(`audio: ${audioBits.join(' ')}`);
|
|
69
|
+
}
|
|
70
|
+
if (labels.length === 0) {
|
|
71
|
+
return { status: 'error', error: 'No video streams found at URL' };
|
|
72
|
+
}
|
|
73
|
+
return { status: 'ok', labels };
|
|
74
|
+
}
|
|
75
|
+
// Other URL fields (snapshot, etc.): GET with early abort to check reachability + content-type
|
|
76
|
+
try {
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timeout = AbortSignal.timeout(5000);
|
|
79
|
+
timeout.addEventListener('abort', () => controller.abort(timeout.reason));
|
|
80
|
+
const response = await fetch(url, { method: 'GET', signal: controller.signal });
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
controller.abort();
|
|
83
|
+
return { status: 'error', error: `HTTP ${response.status} ${response.statusText}` };
|
|
84
|
+
}
|
|
85
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
86
|
+
controller.abort(); // Don't download the full body
|
|
87
|
+
const labels = ['Reachable'];
|
|
88
|
+
if (contentType.startsWith('image/')) {
|
|
89
|
+
labels.push(contentType.replace('image/', '').toUpperCase());
|
|
90
|
+
}
|
|
91
|
+
return { status: 'ok', labels };
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
95
|
+
// We aborted intentionally after reading headers — that's success
|
|
96
|
+
return { status: 'ok', labels: ['Reachable'] };
|
|
97
|
+
}
|
|
98
|
+
const msg = (0, types_1.errMsg)(err);
|
|
99
|
+
return { status: 'error', error: `Unreachable: ${msg}` };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Clear cached metadata for a single URL or all URLs. */
|
|
103
|
+
clearCache(url) {
|
|
104
|
+
if (url) {
|
|
105
|
+
this.cache.delete(url);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.cache.clear();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Internal
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
async runProbe(url) {
|
|
115
|
+
try {
|
|
116
|
+
// Query first video + first audio stream in one ffprobe pass.
|
|
117
|
+
// `-show_streams` returns every stream in the container; we filter
|
|
118
|
+
// by codec_type when parsing. Fields cover both kinds.
|
|
119
|
+
const { stdout } = await execFileAsync('ffprobe', [
|
|
120
|
+
'-v',
|
|
121
|
+
'error',
|
|
122
|
+
'-rtsp_transport',
|
|
123
|
+
'tcp',
|
|
124
|
+
'-timeout',
|
|
125
|
+
'5000000',
|
|
126
|
+
'-show_entries',
|
|
127
|
+
'stream=codec_type,codec_name,profile,width,height,r_frame_rate,bit_rate,sample_rate,channels',
|
|
128
|
+
'-of',
|
|
129
|
+
'json',
|
|
130
|
+
url,
|
|
131
|
+
], { timeout: PROBE_TIMEOUT_MS });
|
|
132
|
+
return this.parseOutput(stdout);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
this.logger.error('ffprobe failed', { meta: { url, error: String(err) } });
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
parseOutput(stdout) {
|
|
140
|
+
const parsed = JSON.parse(stdout);
|
|
141
|
+
const streams = parsed.streams;
|
|
142
|
+
if (!Array.isArray(streams) || streams.length === 0) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
const meta = {};
|
|
146
|
+
for (const raw of streams) {
|
|
147
|
+
const stream = raw;
|
|
148
|
+
const codecType = typeof stream.codec_type === 'string' ? stream.codec_type : undefined;
|
|
149
|
+
if (codecType === 'video' && meta.codec === undefined) {
|
|
150
|
+
const rawCodec = typeof stream.codec_name === 'string' ? stream.codec_name : undefined;
|
|
151
|
+
meta.codec = rawCodec ? (CODEC_ALIASES[rawCodec] ?? rawCodec) : undefined;
|
|
152
|
+
if (typeof stream.width === 'number')
|
|
153
|
+
meta.width = stream.width;
|
|
154
|
+
if (typeof stream.height === 'number')
|
|
155
|
+
meta.height = stream.height;
|
|
156
|
+
const fps = this.parseFps(stream.r_frame_rate);
|
|
157
|
+
if (fps !== undefined)
|
|
158
|
+
meta.fps = fps;
|
|
159
|
+
const bitrateKbps = this.parseBitrateKbps(stream.bit_rate);
|
|
160
|
+
if (bitrateKbps !== undefined)
|
|
161
|
+
meta.bitrateKbps = bitrateKbps;
|
|
162
|
+
}
|
|
163
|
+
else if (codecType === 'audio' && meta.audio === undefined) {
|
|
164
|
+
const audio = {};
|
|
165
|
+
if (typeof stream.codec_name === 'string')
|
|
166
|
+
audio.codec = stream.codec_name;
|
|
167
|
+
if (typeof stream.profile === 'string')
|
|
168
|
+
audio.profile = stream.profile;
|
|
169
|
+
const sampleRate = this.parseIntField(stream.sample_rate);
|
|
170
|
+
if (sampleRate !== undefined)
|
|
171
|
+
audio.sampleRate = sampleRate;
|
|
172
|
+
if (typeof stream.channels === 'number')
|
|
173
|
+
audio.channels = stream.channels;
|
|
174
|
+
const audioBitrateKbps = this.parseBitrateKbps(stream.bit_rate);
|
|
175
|
+
if (audioBitrateKbps !== undefined)
|
|
176
|
+
audio.bitrateKbps = audioBitrateKbps;
|
|
177
|
+
meta.audio = audio;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return meta;
|
|
181
|
+
}
|
|
182
|
+
parseIntField(value) {
|
|
183
|
+
const n = typeof value === 'string' ? Number(value) : typeof value === 'number' ? value : NaN;
|
|
184
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
185
|
+
return undefined;
|
|
186
|
+
return Math.round(n);
|
|
187
|
+
}
|
|
188
|
+
/** Parse fractional frame rate string like "25/1" → 25. */
|
|
189
|
+
parseFps(value) {
|
|
190
|
+
if (typeof value !== 'string')
|
|
191
|
+
return undefined;
|
|
192
|
+
const parts = value.split('/');
|
|
193
|
+
if (parts.length !== 2)
|
|
194
|
+
return undefined;
|
|
195
|
+
const num = Number(parts[0]);
|
|
196
|
+
const den = Number(parts[1]);
|
|
197
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || den === 0)
|
|
198
|
+
return undefined;
|
|
199
|
+
return Math.round((num / den) * 100) / 100;
|
|
200
|
+
}
|
|
201
|
+
/** Parse bit_rate string (bps) → kbps. */
|
|
202
|
+
parseBitrateKbps(value) {
|
|
203
|
+
const n = typeof value === 'string' ? Number(value) : typeof value === 'number' ? value : NaN;
|
|
204
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
205
|
+
return undefined;
|
|
206
|
+
return Math.round(n / 1000);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
exports.StreamProbeService = StreamProbeService;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TopologyEmitterService = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* TopologyEmitterService — push the cluster topology over the event bus.
|
|
6
|
+
*
|
|
7
|
+
* Subscribes to agent connect/disconnect + addon lifecycle events,
|
|
8
|
+
* recomputes the topology snapshot on each change (debounced), and
|
|
9
|
+
* emits `cluster.topology-snapshot`. Also fires a periodic safety-net
|
|
10
|
+
* snapshot every TOPOLOGY_HEARTBEAT_MS so consumers that boot mid-
|
|
11
|
+
* stream get a fresh payload without waiting for the next lifecycle
|
|
12
|
+
* change.
|
|
13
|
+
*
|
|
14
|
+
* Replaces UI polling on `nodes.topology`. Admin-ui dashboards
|
|
15
|
+
* subscribe to the snapshot category and read state directly from the
|
|
16
|
+
* payload — zero round-trip.
|
|
17
|
+
*/
|
|
18
|
+
const node_crypto_1 = require("node:crypto");
|
|
19
|
+
const types_1 = require("@camstack/types");
|
|
20
|
+
const cap_providers_1 = require("../../api/core/cap-providers");
|
|
21
|
+
const DEBOUNCE_MS = 200;
|
|
22
|
+
const HEARTBEAT_MS = 30_000;
|
|
23
|
+
const LIFECYCLE_CATEGORIES = [
|
|
24
|
+
types_1.EventCategory.AgentOnline,
|
|
25
|
+
types_1.EventCategory.AgentOffline,
|
|
26
|
+
types_1.EventCategory.WorkerOnline,
|
|
27
|
+
types_1.EventCategory.WorkerOffline,
|
|
28
|
+
types_1.EventCategory.AgentUnregistered,
|
|
29
|
+
types_1.EventCategory.AddonStarted,
|
|
30
|
+
types_1.EventCategory.AddonStopped,
|
|
31
|
+
types_1.EventCategory.AddonRestarted,
|
|
32
|
+
types_1.EventCategory.AddonInstalled,
|
|
33
|
+
types_1.EventCategory.AddonUninstalled,
|
|
34
|
+
types_1.EventCategory.AddonUpdated,
|
|
35
|
+
types_1.EventCategory.AddonCrashed,
|
|
36
|
+
];
|
|
37
|
+
class TopologyEmitterService {
|
|
38
|
+
eventBus;
|
|
39
|
+
agentRegistry;
|
|
40
|
+
addonRegistry;
|
|
41
|
+
unsubscribers = [];
|
|
42
|
+
debounceTimer = null;
|
|
43
|
+
heartbeatTimer = null;
|
|
44
|
+
emitting = false;
|
|
45
|
+
constructor(eventBus, agentRegistry, addonRegistry) {
|
|
46
|
+
this.eventBus = eventBus;
|
|
47
|
+
this.agentRegistry = agentRegistry;
|
|
48
|
+
this.addonRegistry = addonRegistry;
|
|
49
|
+
}
|
|
50
|
+
onModuleInit() {
|
|
51
|
+
for (const category of LIFECYCLE_CATEGORIES) {
|
|
52
|
+
const unsub = this.eventBus.subscribe({ category }, () => this.scheduleEmit());
|
|
53
|
+
this.unsubscribers.push(unsub);
|
|
54
|
+
}
|
|
55
|
+
// Heartbeat fires the first emit ~immediately so consumers that
|
|
56
|
+
// mount before any lifecycle change still see a snapshot.
|
|
57
|
+
void this.emitNow();
|
|
58
|
+
this.heartbeatTimer = setInterval(() => this.scheduleEmit(), HEARTBEAT_MS);
|
|
59
|
+
}
|
|
60
|
+
onModuleDestroy() {
|
|
61
|
+
if (this.debounceTimer)
|
|
62
|
+
clearTimeout(this.debounceTimer);
|
|
63
|
+
if (this.heartbeatTimer)
|
|
64
|
+
clearInterval(this.heartbeatTimer);
|
|
65
|
+
for (const unsub of this.unsubscribers) {
|
|
66
|
+
try {
|
|
67
|
+
unsub();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* idempotent */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
this.unsubscribers.length = 0;
|
|
74
|
+
}
|
|
75
|
+
scheduleEmit() {
|
|
76
|
+
if (this.debounceTimer)
|
|
77
|
+
clearTimeout(this.debounceTimer);
|
|
78
|
+
this.debounceTimer = setTimeout(() => {
|
|
79
|
+
this.debounceTimer = null;
|
|
80
|
+
void this.emitNow();
|
|
81
|
+
}, DEBOUNCE_MS);
|
|
82
|
+
}
|
|
83
|
+
async emitNow() {
|
|
84
|
+
if (this.emitting)
|
|
85
|
+
return;
|
|
86
|
+
this.emitting = true;
|
|
87
|
+
try {
|
|
88
|
+
const nodes = await (0, cap_providers_1.computeTopology)(this.agentRegistry, this.addonRegistry);
|
|
89
|
+
this.eventBus.emit({
|
|
90
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
91
|
+
timestamp: new Date(),
|
|
92
|
+
source: { type: 'core', id: 'topology-emitter' },
|
|
93
|
+
category: types_1.EventCategory.ClusterTopologySnapshot,
|
|
94
|
+
data: { nodes, timestamp: Date.now() },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Best-effort observability path. Next lifecycle event or the
|
|
99
|
+
// heartbeat will retry — no need to log here.
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
this.emitting = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.TopologyEmitterService = TopologyEmitterService;
|