@camstack/server 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{src/agent-status-page.ts → dist/agent-status-page.js} +30 -45
- package/dist/api/addon-upload.js +441 -0
- package/dist/api/addons-custom.router.js +91 -0
- package/dist/api/auth-whoami.js +55 -0
- package/dist/api/bridge-addons.router.js +109 -0
- package/dist/api/capabilities.router.js +229 -0
- package/dist/api/core/addon-settings.router.js +117 -0
- package/dist/api/core/agents.router.js +73 -0
- package/dist/api/core/auth.router.js +286 -0
- package/dist/api/core/bulk-update-coordinator.js +229 -0
- package/dist/api/core/cap-providers.js +1124 -0
- package/dist/api/core/capabilities.router.js +138 -0
- package/dist/api/core/collection-preference.js +17 -0
- package/dist/api/core/event-bus-proxy.router.js +45 -0
- package/dist/api/core/hwaccel.router.js +91 -0
- package/dist/api/core/live-events.router.js +61 -0
- package/dist/api/core/logs.router.js +172 -0
- package/dist/api/core/notifications.router.js +67 -0
- package/dist/api/core/repl.router.js +35 -0
- package/dist/api/core/settings-backend.router.js +121 -0
- package/dist/api/core/stream-probe.router.js +58 -0
- package/dist/api/core/system-events.router.js +100 -0
- package/dist/api/health/health.routes.js +68 -0
- package/{src/api/oauth2/consent-page.ts → dist/api/oauth2/consent-page.js} +11 -20
- package/dist/api/oauth2/oauth2-routes.js +219 -0
- package/dist/api/trpc/cap-mount-helpers.js +194 -0
- package/dist/api/trpc/cap-route-error-formatter.js +133 -0
- package/dist/api/trpc/client-ip.js +147 -0
- package/dist/api/trpc/core-cap-bridge.js +115 -0
- package/dist/api/trpc/generated-cap-mounts.js +388 -0
- package/dist/api/trpc/generated-cap-routers.js +7635 -0
- package/dist/api/trpc/scope-access.js +93 -0
- package/dist/api/trpc/trpc.context.js +184 -0
- package/dist/api/trpc/trpc.middleware.js +139 -0
- package/dist/api/trpc/trpc.router.js +188 -0
- package/dist/auth/session-cookie.js +47 -0
- package/dist/boot/boot-config.js +241 -0
- package/dist/boot/integration-id-backfill.js +76 -0
- package/dist/boot/post-boot.service.js +85 -0
- package/dist/core/addon/addon-call-gateway.js +99 -0
- package/dist/core/addon/addon-package.service.js +1560 -0
- package/dist/core/addon/addon-registry.service.js +2739 -0
- package/{src/core/addon/addon-row-manifest.ts → dist/core/addon/addon-row-manifest.js} +5 -5
- package/dist/core/addon/addon-search.service.js +62 -0
- package/dist/core/addon/addon-settings-provider.js +102 -0
- package/dist/core/addon/addon.tokens.js +5 -0
- package/dist/core/addon-bridge/addon-bridge.service.js +145 -0
- package/dist/core/addon-pages/addon-pages.service.js +107 -0
- package/dist/core/addon-widgets/addon-widgets.service.js +120 -0
- package/dist/core/agent/agent-registry.service.js +477 -0
- package/dist/core/auth/auth.service.js +10 -0
- package/dist/core/capability/capability.service.js +58 -0
- package/dist/core/config/config.schema.js +7 -0
- package/dist/core/config/config.service.js +10 -0
- package/dist/core/events/event-bus.service.js +83 -0
- package/dist/core/feature/feature.service.js +10 -0
- package/dist/core/lifecycle/lifecycle-state-machine.js +6 -0
- package/dist/core/logging/log-ring-buffer.js +6 -0
- package/dist/core/logging/logging.service.js +130 -0
- package/dist/core/logging/scoped-logger.js +6 -0
- package/dist/core/moleculer/cap-call-fn.js +50 -0
- package/dist/core/moleculer/cap-route-authority.js +122 -0
- package/dist/core/moleculer/moleculer.service.js +898 -0
- package/dist/core/network/network-quality.service.js +7 -0
- package/dist/core/notification/notification-wrapper.service.js +33 -0
- package/dist/core/notification/toast-wrapper.service.js +25 -0
- package/dist/core/provider/provider.tokens.js +4 -0
- package/dist/core/repl/repl-engine.service.js +140 -0
- package/dist/core/storage/fs-storage-backend.js +6 -0
- package/dist/core/storage/storage-location-manager.js +6 -0
- package/dist/core/storage/storage.service.js +7 -0
- package/dist/core/streaming/stream-probe.service.js +209 -0
- package/dist/core/topology/topology-emitter.service.js +106 -0
- package/dist/launcher.js +325 -0
- package/dist/main.js +1098 -0
- package/dist/manual-boot.js +227 -0
- package/package.json +5 -1
- package/src/__tests__/addon-install-e2e.test.ts +0 -74
- package/src/__tests__/addon-pages-e2e.test.ts +0 -200
- package/src/__tests__/addon-route-session.test.ts +0 -17
- package/src/__tests__/addon-settings-router.spec.ts +0 -67
- package/src/__tests__/addon-upload.spec.ts +0 -475
- package/src/__tests__/agent-registry.spec.ts +0 -179
- package/src/__tests__/agent-status-page.spec.ts +0 -82
- package/src/__tests__/auth-session-cookie.test.ts +0 -48
- package/src/__tests__/bulk-update-coordinator.spec.ts +0 -303
- package/src/__tests__/cap-ownership-authority.spec.ts +0 -431
- package/src/__tests__/cap-providers/cap-providers-location-import.spec.ts +0 -206
- package/src/__tests__/cap-providers/cap-usage-graph.spec.ts +0 -37
- package/src/__tests__/cap-providers/compute-topology-categories.spec.ts +0 -110
- package/src/__tests__/cap-providers/integrations-delete-cascade.spec.ts +0 -292
- package/src/__tests__/cap-providers-bulk-update.spec.ts +0 -408
- package/src/__tests__/cap-route-adapter.spec.ts +0 -302
- package/src/__tests__/cap-routers/_meta.spec.ts +0 -199
- package/src/__tests__/cap-routers/addon-settings.router.spec.ts +0 -115
- package/src/__tests__/cap-routers/broker-routing.router.spec.ts +0 -177
- package/src/__tests__/cap-routers/cap-route-error-formatter.spec.ts +0 -125
- package/src/__tests__/cap-routers/capabilities-node.spec.ts +0 -68
- package/src/__tests__/cap-routers/device-link-overlay.spec.ts +0 -137
- package/src/__tests__/cap-routers/device-manager-aggregate.router.spec.ts +0 -194
- package/src/__tests__/cap-routers/harness.ts +0 -163
- package/src/__tests__/cap-routers/metrics-provider.router.spec.ts +0 -133
- package/src/__tests__/cap-routers/null-provider-guard.spec.ts +0 -64
- package/src/__tests__/cap-routers/pipeline-executor.router.spec.ts +0 -159
- package/src/__tests__/cap-routers/settings-store.router.spec.ts +0 -291
- package/src/__tests__/capability-e2e.test.ts +0 -384
- package/src/__tests__/cli-e2e.test.ts +0 -150
- package/src/__tests__/core-cap-bridge.spec.ts +0 -91
- package/src/__tests__/dev-bootstrap-shm-ring.spec.ts +0 -40
- package/src/__tests__/device-settings-contribution-dispatch.spec.ts +0 -280
- package/src/__tests__/embedded-deps-e2e.test.ts +0 -125
- package/src/__tests__/event-bus-proxy-router.spec.ts +0 -75
- package/src/__tests__/fixtures/mock-analysis-addon-a.ts +0 -37
- package/src/__tests__/fixtures/mock-analysis-addon-b.ts +0 -37
- package/src/__tests__/fixtures/mock-log-addon.ts +0 -37
- package/src/__tests__/fixtures/mock-storage-addon.ts +0 -40
- package/src/__tests__/framework-allowlist.spec.ts +0 -96
- package/src/__tests__/framework-installer-defer-restart.spec.ts +0 -165
- package/src/__tests__/https-e2e.test.ts +0 -124
- package/src/__tests__/lifecycle-e2e.test.ts +0 -189
- package/src/__tests__/live-events-subscription.spec.ts +0 -149
- package/src/__tests__/moleculer/uds-readiness.spec.ts +0 -150
- package/src/__tests__/moleculer/uds-topology.spec.ts +0 -418
- package/src/__tests__/moleculer/uds-unowned-call.spec.ts +0 -383
- package/src/__tests__/moleculer-register-node-idempotency.spec.ts +0 -273
- package/src/__tests__/native-cap-route.spec.ts +0 -427
- package/src/__tests__/oauth2-account-linking.spec.ts +0 -867
- package/src/__tests__/post-boot-restart.spec.ts +0 -161
- package/src/__tests__/singleton-contention.test.ts +0 -499
- package/src/__tests__/streaming-diagnostic.test.ts +0 -615
- package/src/__tests__/streaming-scale.test.ts +0 -314
- package/src/__tests__/uds-addon-call-wiring.spec.ts +0 -242
- package/src/__tests__/uds-log-ingest.spec.ts +0 -183
- package/src/api/__tests__/addons-custom.spec.ts +0 -148
- package/src/api/__tests__/capabilities.router.test.ts +0 -56
- package/src/api/addon-upload.ts +0 -529
- package/src/api/addons-custom.router.ts +0 -101
- package/src/api/auth-whoami.ts +0 -101
- package/src/api/bridge-addons.router.ts +0 -122
- package/src/api/capabilities.router.ts +0 -265
- package/src/api/core/__tests__/auth-router-totp.spec.ts +0 -297
- package/src/api/core/__tests__/integration-markers.spec.ts +0 -10
- package/src/api/core/addon-settings.router.ts +0 -127
- package/src/api/core/agents.router.ts +0 -86
- package/src/api/core/auth.router.ts +0 -322
- package/src/api/core/bulk-update-coordinator.ts +0 -305
- package/src/api/core/cap-providers.ts +0 -1339
- package/src/api/core/capabilities.router.ts +0 -149
- package/src/api/core/collection-preference.ts +0 -40
- package/src/api/core/event-bus-proxy.router.ts +0 -45
- package/src/api/core/hwaccel.router.ts +0 -108
- package/src/api/core/live-events.router.ts +0 -67
- package/src/api/core/logs.router.ts +0 -195
- package/src/api/core/notifications.router.ts +0 -66
- package/src/api/core/repl.router.ts +0 -39
- package/src/api/core/settings-backend.router.ts +0 -140
- package/src/api/core/stream-probe.router.ts +0 -57
- package/src/api/core/system-events.router.ts +0 -125
- package/src/api/health/health.routes.ts +0 -117
- package/src/api/oauth2/__tests__/oauth2-routes.spec.ts +0 -62
- package/src/api/oauth2/oauth2-routes.ts +0 -281
- package/src/api/trpc/__tests__/client-ip.spec.ts +0 -146
- package/src/api/trpc/__tests__/scope-access-device.spec.ts +0 -268
- package/src/api/trpc/__tests__/scope-access.spec.ts +0 -102
- package/src/api/trpc/__tests__/webrtc-session-ua-enrich.spec.ts +0 -136
- package/src/api/trpc/cap-mount-helpers.ts +0 -245
- package/src/api/trpc/cap-route-error-formatter.ts +0 -171
- package/src/api/trpc/client-ip.ts +0 -147
- package/src/api/trpc/core-cap-bridge.ts +0 -154
- package/src/api/trpc/generated-cap-mounts.ts +0 -1240
- package/src/api/trpc/generated-cap-routers.ts +0 -11523
- package/src/api/trpc/scope-access.ts +0 -110
- package/src/api/trpc/trpc.context.ts +0 -258
- package/src/api/trpc/trpc.middleware.ts +0 -146
- package/src/api/trpc/trpc.router.ts +0 -389
- package/src/auth/session-cookie.ts +0 -54
- package/src/boot/__tests__/integration-id-backfill.spec.ts +0 -131
- package/src/boot/boot-config.ts +0 -259
- package/src/boot/integration-id-backfill.ts +0 -109
- package/src/boot/post-boot.service.ts +0 -105
- package/src/core/addon/__tests__/addon-registry-capability.test.ts +0 -62
- package/src/core/addon/__tests__/addon-row-manifest.spec.ts +0 -62
- package/src/core/addon/addon-call-gateway.ts +0 -171
- package/src/core/addon/addon-package.service.ts +0 -1787
- package/src/core/addon/addon-registry.service.ts +0 -3130
- package/src/core/addon/addon-search.service.ts +0 -91
- package/src/core/addon/addon-settings-provider.ts +0 -220
- package/src/core/addon/addon.tokens.ts +0 -2
- package/src/core/addon-bridge/addon-bridge.service.ts +0 -130
- package/src/core/addon-pages/addon-pages.service.spec.ts +0 -117
- package/src/core/addon-pages/addon-pages.service.ts +0 -82
- package/src/core/addon-widgets/addon-widgets.service.ts +0 -95
- package/src/core/agent/agent-registry.service.ts +0 -529
- package/src/core/auth/auth.service.spec.ts +0 -86
- package/src/core/auth/auth.service.ts +0 -8
- package/src/core/capability/capability.service.ts +0 -66
- package/src/core/config/config.schema.ts +0 -3
- package/src/core/config/config.service.spec.ts +0 -175
- package/src/core/config/config.service.ts +0 -7
- package/src/core/events/event-bus.service.spec.ts +0 -235
- package/src/core/events/event-bus.service.ts +0 -89
- package/src/core/feature/feature.service.spec.ts +0 -99
- package/src/core/feature/feature.service.ts +0 -8
- package/src/core/lifecycle/lifecycle-state-machine.spec.ts +0 -166
- package/src/core/lifecycle/lifecycle-state-machine.ts +0 -3
- package/src/core/logging/log-ring-buffer.ts +0 -3
- package/src/core/logging/logging.service.spec.ts +0 -287
- package/src/core/logging/logging.service.ts +0 -143
- package/src/core/logging/scoped-logger.ts +0 -3
- package/src/core/moleculer/cap-call-fn.spec.ts +0 -173
- package/src/core/moleculer/cap-call-fn.ts +0 -107
- package/src/core/moleculer/cap-route-authority.ts +0 -194
- package/src/core/moleculer/moleculer.service.ts +0 -1072
- package/src/core/network/network-quality.service.spec.ts +0 -53
- package/src/core/network/network-quality.service.ts +0 -5
- package/src/core/notification/notification-wrapper.service.ts +0 -34
- package/src/core/notification/toast-wrapper.service.ts +0 -27
- package/src/core/provider/provider.tokens.ts +0 -1
- package/src/core/repl/repl-engine.service.spec.ts +0 -444
- package/src/core/repl/repl-engine.service.ts +0 -155
- package/src/core/storage/fs-storage-backend.spec.ts +0 -70
- package/src/core/storage/fs-storage-backend.ts +0 -3
- package/src/core/storage/storage-location-manager.spec.ts +0 -130
- package/src/core/storage/storage-location-manager.ts +0 -3
- package/src/core/storage/storage.service.spec.ts +0 -73
- package/src/core/storage/storage.service.ts +0 -3
- package/src/core/streaming/stream-probe.service.ts +0 -221
- package/src/core/topology/topology-emitter.service.ts +0 -105
- package/src/launcher.ts +0 -314
- package/src/main.ts +0 -1245
- package/src/manual-boot.ts +0 -301
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -33
- package/vitest.config.ts +0 -26
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkScopeAccess = checkScopeAccess;
|
|
4
|
+
/**
|
|
5
|
+
* Pure scope-access matcher.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from `trpc.middleware.ts` so the spec can exercise it
|
|
8
|
+
* without spinning up the tRPC initTRPC machinery. The function is
|
|
9
|
+
* stateless aside from an optional device-ancestor lookup callback.
|
|
10
|
+
*
|
|
11
|
+
* Algorithm (v2 — four scope types):
|
|
12
|
+
* 1. Look the tRPC `path` up in `METHOD_ACCESS_MAP`. Unknown =
|
|
13
|
+
* FORBIDDEN (codegen drift; fail closed).
|
|
14
|
+
* 2. For each scope on the caller, check if it matches:
|
|
15
|
+
* - `category` — scope.target matches meta.capScope ('device'|'system')
|
|
16
|
+
* - `capability` — scope.target matches meta.capName exactly
|
|
17
|
+
* - `addon` — scope.target matches meta.addonId (when set)
|
|
18
|
+
* - `device` — input.deviceId (OR any of its ancestor deviceIds
|
|
19
|
+
* via `getDeviceAncestors`) is in scope.targets.
|
|
20
|
+
* Auto-inheritance means granting a Reolink camera
|
|
21
|
+
* implicitly grants its siren / floodlight / PIR
|
|
22
|
+
* child accessories without re-listing them.
|
|
23
|
+
* 3. On a target match, accept iff `scope.access` includes the
|
|
24
|
+
* method's required `access` flavour.
|
|
25
|
+
* 4. No matching scope → FORBIDDEN with a human-readable reason.
|
|
26
|
+
*/
|
|
27
|
+
const types_1 = require("@camstack/types");
|
|
28
|
+
/**
|
|
29
|
+
* Pull `deviceId` off a tRPC request input. Device-scope cap methods
|
|
30
|
+
* uniformly take `{deviceId: number, ...}` per the DeviceProxy contract,
|
|
31
|
+
* so a single extractor covers every device-scope call. Returns null when
|
|
32
|
+
* the input doesn't carry a deviceId (system-scope cap, void input, …).
|
|
33
|
+
*/
|
|
34
|
+
function extractDeviceId(input) {
|
|
35
|
+
if (input === null || typeof input !== 'object')
|
|
36
|
+
return null;
|
|
37
|
+
const candidate = input['deviceId'];
|
|
38
|
+
return typeof candidate === 'number' ? candidate : null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the set of deviceIds that count as "this request" for the
|
|
42
|
+
* device-scope match: the deviceId itself plus every ancestor (so a
|
|
43
|
+
* scope on the parent camera covers accessory children).
|
|
44
|
+
*/
|
|
45
|
+
function effectiveDeviceIds(deviceId, getAncestors) {
|
|
46
|
+
if (!getAncestors)
|
|
47
|
+
return [String(deviceId)];
|
|
48
|
+
const out = new Set([String(deviceId)]);
|
|
49
|
+
for (const ancestor of getAncestors(deviceId))
|
|
50
|
+
out.add(String(ancestor));
|
|
51
|
+
return [...out];
|
|
52
|
+
}
|
|
53
|
+
function checkScopeAccess(scopes, path, input, getDeviceAncestors) {
|
|
54
|
+
const meta = types_1.METHOD_ACCESS_MAP[path];
|
|
55
|
+
if (!meta) {
|
|
56
|
+
return { ok: false, reason: `Unknown method '${path}' — codegen drift` };
|
|
57
|
+
}
|
|
58
|
+
const deviceId = meta.capScope === 'device' ? extractDeviceId(input) : null;
|
|
59
|
+
const deviceChain = deviceId !== null ? effectiveDeviceIds(deviceId, getDeviceAncestors) : [];
|
|
60
|
+
for (const s of scopes) {
|
|
61
|
+
let targetMatches = false;
|
|
62
|
+
switch (s.type) {
|
|
63
|
+
case 'category':
|
|
64
|
+
targetMatches = s.target === meta.capScope;
|
|
65
|
+
break;
|
|
66
|
+
case 'capability':
|
|
67
|
+
targetMatches = s.target === meta.capName;
|
|
68
|
+
break;
|
|
69
|
+
case 'addon':
|
|
70
|
+
targetMatches = meta.addonId !== null && s.target === meta.addonId;
|
|
71
|
+
break;
|
|
72
|
+
case 'device':
|
|
73
|
+
// Match if the request's device — or any of its ancestors — is
|
|
74
|
+
// in the grant's target list. Accessory children inherit the
|
|
75
|
+
// parent's scope without re-enumeration.
|
|
76
|
+
targetMatches = deviceChain.some((id) => s.targets.includes(id));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (!targetMatches)
|
|
80
|
+
continue;
|
|
81
|
+
if (s.access.includes(meta.access))
|
|
82
|
+
return { ok: true, access: meta.access };
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: `No scope grants ${meta.access} on '${meta.capName}' (${meta.capScope}-scope cap${deviceId !== null ? `, device=${deviceId}` : ''}). Have: ${scopes
|
|
87
|
+
.map((s) => {
|
|
88
|
+
const target = s.type === 'device' ? `[${s.targets.join(',')}]` : s.target;
|
|
89
|
+
return `${s.type}:${target}[${s.access.join(',')}]`;
|
|
90
|
+
})
|
|
91
|
+
.join(', ') || '(none)'}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMeshTrpcContext = createMeshTrpcContext;
|
|
4
|
+
exports.createTrpcContext = createTrpcContext;
|
|
5
|
+
exports.createWsTrpcContext = createWsTrpcContext;
|
|
6
|
+
/** Read `req.query` if present (Fastify-only) without losing type safety. */
|
|
7
|
+
function readQuery(req) {
|
|
8
|
+
if (!('query' in req))
|
|
9
|
+
return null;
|
|
10
|
+
const q = Reflect.get(req, 'query');
|
|
11
|
+
if (q === null || typeof q !== 'object' || Array.isArray(q))
|
|
12
|
+
return null;
|
|
13
|
+
return { ...q };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Extract a JWT token from an HTTP request.
|
|
17
|
+
* Priority: Authorization header → Fastify req.query → URL query string
|
|
18
|
+
*/
|
|
19
|
+
function extractTokenFromRequest(req) {
|
|
20
|
+
const authHeader = req.headers.authorization;
|
|
21
|
+
if (authHeader && typeof authHeader === 'string') {
|
|
22
|
+
const [scheme, token] = authHeader.split(' ');
|
|
23
|
+
if (scheme === 'Bearer' && token)
|
|
24
|
+
return token;
|
|
25
|
+
}
|
|
26
|
+
// Fastify-parsed query object (HTTP tRPC requests)
|
|
27
|
+
const q = readQuery(req);
|
|
28
|
+
if (q && typeof q.token === 'string') {
|
|
29
|
+
return q.token;
|
|
30
|
+
}
|
|
31
|
+
// Raw URL query string fallback (IncomingMessage or Fastify w/o parsed query)
|
|
32
|
+
const url = req.url;
|
|
33
|
+
if (url) {
|
|
34
|
+
const qIdx = url.indexOf('?');
|
|
35
|
+
if (qIdx !== -1) {
|
|
36
|
+
const t = new URLSearchParams(url.slice(qIdx + 1)).get('token');
|
|
37
|
+
if (t)
|
|
38
|
+
return t;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve an AuthenticatedAgent from a raw token. Handles both JWT
|
|
45
|
+
* (sync, via authService) and `cst_*` scoped tokens (async, via the
|
|
46
|
+
* `user-management` cap singleton — same path addon-upload uses for
|
|
47
|
+
* its REST auth chain).
|
|
48
|
+
*
|
|
49
|
+
* Returns `null` for: missing token, malformed JWT, unknown scoped
|
|
50
|
+
* token. Caller (protectedProcedure) decides the failure response
|
|
51
|
+
* (typically UNAUTHORIZED).
|
|
52
|
+
*/
|
|
53
|
+
async function resolveUser(token, authService, addonRegistry) {
|
|
54
|
+
if (!token)
|
|
55
|
+
return null;
|
|
56
|
+
// Scoped-token path: hit the user-management cap. Synthetic user
|
|
57
|
+
// with `isAdmin: false` so admin-gated procedures bounce while
|
|
58
|
+
// protectedProcedure can still gate by scope match.
|
|
59
|
+
if (token.startsWith('cst_')) {
|
|
60
|
+
try {
|
|
61
|
+
const userMgmt = addonRegistry.getCapabilityRegistry().getSingleton('user-management');
|
|
62
|
+
if (!userMgmt)
|
|
63
|
+
return null;
|
|
64
|
+
const record = await userMgmt.validateScopedToken({ token });
|
|
65
|
+
if (!record)
|
|
66
|
+
return null;
|
|
67
|
+
return {
|
|
68
|
+
id: record.userId,
|
|
69
|
+
// Display label — `scoped:<prefix>` makes audit logs read
|
|
70
|
+
// naturally without exposing the token hash.
|
|
71
|
+
username: `scoped:${record.tokenPrefix}`,
|
|
72
|
+
isAdmin: false,
|
|
73
|
+
permissions: {
|
|
74
|
+
isAdmin: false,
|
|
75
|
+
allowedProviders: '*',
|
|
76
|
+
allowedDevices: {},
|
|
77
|
+
},
|
|
78
|
+
isApiKey: true,
|
|
79
|
+
isScoped: true,
|
|
80
|
+
scopes: record.scopes,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// JWT path.
|
|
88
|
+
try {
|
|
89
|
+
const payload = authService.verifyToken(token);
|
|
90
|
+
// Reject pre-v2 JWTs at the boundary. Tokens issued before the
|
|
91
|
+
// role → isAdmin migration don't carry the `isAdmin` field, so
|
|
92
|
+
// letting them through would degrade silently into "non-admin
|
|
93
|
+
// with no scopes" → locked out of every cap. Returning null forces
|
|
94
|
+
// the client to land on 401 → re-login, where it picks up a fresh
|
|
95
|
+
// v2 token. No back-compat shim — the role enum is gone.
|
|
96
|
+
if (typeof payload.isAdmin !== 'boolean') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
id: payload.userId ?? payload.keyId ?? 'unknown',
|
|
101
|
+
username: payload.username ?? 'unknown',
|
|
102
|
+
isAdmin: payload.isAdmin,
|
|
103
|
+
permissions: {
|
|
104
|
+
isAdmin: payload.isAdmin,
|
|
105
|
+
allowedProviders: payload.allowedProviders,
|
|
106
|
+
allowedDevices: payload.allowedDevices,
|
|
107
|
+
},
|
|
108
|
+
isApiKey: payload.type === 'api_key',
|
|
109
|
+
agentId: payload.agentId,
|
|
110
|
+
// Scopes are baked into the JWT at login; the middleware uses
|
|
111
|
+
// them to gate every call until the user re-logs.
|
|
112
|
+
...(payload.scopes !== undefined ? { scopes: payload.scopes } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build the parent-chain walker for the scope-access matcher. Returns
|
|
121
|
+
* every ancestor deviceId of `deviceId` (parent, grandparent, …) so a
|
|
122
|
+
* grant on a Reolink camera covers its accessory children without
|
|
123
|
+
* re-enumerating them.
|
|
124
|
+
*
|
|
125
|
+
* Bounded by hop count (defence-in-depth — the device tree should
|
|
126
|
+
* never exceed 2-3 levels but a corrupt registry shouldn't loop forever).
|
|
127
|
+
*/
|
|
128
|
+
function makeAncestorLookup(addonRegistry) {
|
|
129
|
+
return (deviceId) => {
|
|
130
|
+
const out = [];
|
|
131
|
+
const registry = addonRegistry.getDeviceRegistry();
|
|
132
|
+
let current = registry.getById(deviceId);
|
|
133
|
+
for (let hop = 0; hop < 8 && current?.parentDeviceId != null; hop++) {
|
|
134
|
+
out.push(current.parentDeviceId);
|
|
135
|
+
current = registry.getById(current.parentDeviceId);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Context factory for hub-internal calls originating from the trusted
|
|
142
|
+
* Moleculer mesh (the `$core-caps` bridge service in `core-cap-bridge.ts`).
|
|
143
|
+
*
|
|
144
|
+
* Cluster membership is gated by `CAMSTACK_CLUSTER_SECRET`, so a
|
|
145
|
+
* mesh-originated call is treated as a fully-trusted admin: it carries
|
|
146
|
+
* a synthetic `isAdmin` user, which makes `protectedProcedure` /
|
|
147
|
+
* `adminProcedure` pass without a JWT and skips the scope-access
|
|
148
|
+
* matcher entirely. There is no HTTP request behind the call.
|
|
149
|
+
*/
|
|
150
|
+
function createMeshTrpcContext() {
|
|
151
|
+
const user = {
|
|
152
|
+
id: 'mesh',
|
|
153
|
+
username: 'mesh',
|
|
154
|
+
isAdmin: true,
|
|
155
|
+
permissions: { isAdmin: true, allowedProviders: '*', allowedDevices: {} },
|
|
156
|
+
isApiKey: true,
|
|
157
|
+
};
|
|
158
|
+
return { user };
|
|
159
|
+
}
|
|
160
|
+
/** Context factory for HTTP tRPC requests (Fastify adapter). */
|
|
161
|
+
async function createTrpcContext(req, authService, addonRegistry) {
|
|
162
|
+
const token = extractTokenFromRequest(req);
|
|
163
|
+
return {
|
|
164
|
+
user: await resolveUser(token, authService, addonRegistry),
|
|
165
|
+
req,
|
|
166
|
+
getDeviceAncestors: makeAncestorLookup(addonRegistry),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Context factory for WebSocket tRPC connections (applyWSSHandler).
|
|
171
|
+
* Token is sent via tRPC connectionParams (a JSON message sent right after
|
|
172
|
+
* the WS handshake), which is more reliable than query params through proxies.
|
|
173
|
+
*/
|
|
174
|
+
async function createWsTrpcContext(opts, authService, addonRegistry) {
|
|
175
|
+
// 1. connectionParams.token (sent by BackendClient's createWSClient)
|
|
176
|
+
const paramToken = opts.info.connectionParams?.['token'];
|
|
177
|
+
const token = (typeof paramToken === 'string' ? paramToken : null) ?? extractTokenFromRequest(opts.req);
|
|
178
|
+
const user = await resolveUser(token, authService, addonRegistry);
|
|
179
|
+
return {
|
|
180
|
+
user,
|
|
181
|
+
req: opts.req,
|
|
182
|
+
getDeviceAncestors: makeAncestorLookup(addonRegistry),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.agentProcedure = exports.adminProcedure = exports.protectedProcedure = exports.createCallerFactory = exports.publicProcedure = exports.trpcRouter = void 0;
|
|
7
|
+
exports.iterableSubscription = iterableSubscription;
|
|
8
|
+
exports.iterableInterval = iterableInterval;
|
|
9
|
+
const server_1 = require("@trpc/server");
|
|
10
|
+
const superjson_1 = __importDefault(require("superjson"));
|
|
11
|
+
const types_1 = require("@camstack/types");
|
|
12
|
+
const scope_access_js_1 = require("./scope-access.js");
|
|
13
|
+
const cap_route_error_formatter_js_1 = require("./cap-route-error-formatter.js");
|
|
14
|
+
const t = server_1.initTRPC.context().create({
|
|
15
|
+
transformer: superjson_1.default,
|
|
16
|
+
errorFormatter: cap_route_error_formatter_js_1.formatTrpcError,
|
|
17
|
+
});
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Async-generator subscription helpers (tRPC v11 — replaces deprecated observable)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Convert a push-based subscription (callback → unsubscribe) into an async generator
|
|
23
|
+
* suitable for tRPC v11 `.subscription()`.
|
|
24
|
+
*
|
|
25
|
+
* @param subscribe — called once; receives a `push` callback and must return an unsubscribe fn.
|
|
26
|
+
*/
|
|
27
|
+
async function* iterableSubscription(subscribe) {
|
|
28
|
+
const queue = [];
|
|
29
|
+
let resolve = null;
|
|
30
|
+
const unsub = subscribe((value) => {
|
|
31
|
+
queue.push(value);
|
|
32
|
+
resolve?.();
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
while (true) {
|
|
36
|
+
while (queue.length > 0) {
|
|
37
|
+
yield queue.shift();
|
|
38
|
+
}
|
|
39
|
+
await new Promise((r) => {
|
|
40
|
+
resolve = r;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
unsub();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create an interval-based async generator that yields a value on each tick.
|
|
50
|
+
* Useful for polling subscriptions.
|
|
51
|
+
*/
|
|
52
|
+
async function* iterableInterval(intervalMs, getValue) {
|
|
53
|
+
try {
|
|
54
|
+
while (true) {
|
|
55
|
+
yield getValue();
|
|
56
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
// cleanup handled by generator return
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.trpcRouter = t.router;
|
|
64
|
+
exports.publicProcedure = t.procedure;
|
|
65
|
+
/**
|
|
66
|
+
* Server-side caller factory — turns the hub appRouter into a directly
|
|
67
|
+
* invokable record under a supplied `TrpcContext`. The core-cap bridge
|
|
68
|
+
* (`core-cap-bridge.ts`) uses it to expose core routers over the
|
|
69
|
+
* Moleculer mesh without an HTTP round-trip.
|
|
70
|
+
*/
|
|
71
|
+
exports.createCallerFactory = t.createCallerFactory;
|
|
72
|
+
/**
|
|
73
|
+
* Caps-only authenticated procedure (v2).
|
|
74
|
+
*
|
|
75
|
+
* - `isAdmin: true` → pass-through. Admin's `scopes` field is ignored.
|
|
76
|
+
* - `isAdmin: false` → `METHOD_ACCESS_MAP[path]` lookup + scope match.
|
|
77
|
+
* The caller's scope set must grant the required (capName, access)
|
|
78
|
+
* pair via one of the three forms (`category`/`capability`/`addon`).
|
|
79
|
+
*
|
|
80
|
+
* Hand-written core routers (`auth.*`, `system.info`, etc.) are not
|
|
81
|
+
* codegen'd from cap definitions and therefore not in
|
|
82
|
+
* `METHOD_ACCESS_MAP`. Those routes carry their own gating via
|
|
83
|
+
* `adminProcedure` when destructive; the bare `protectedProcedure`
|
|
84
|
+
* authentication check is the only gate. The middleware skips the
|
|
85
|
+
* scope-check for unknown paths so the SDK boot probe (`auth.me`) and
|
|
86
|
+
* `system.info` reach non-admin / scoped-token callers — without
|
|
87
|
+
* pulling every core route into the codegen map.
|
|
88
|
+
*
|
|
89
|
+
* Single source of authority for caps: `isAdmin`. The legacy role enum
|
|
90
|
+
* collapsed onto this boolean in v2.
|
|
91
|
+
*/
|
|
92
|
+
exports.protectedProcedure = t.procedure.use(async ({ ctx, next, path, getRawInput }) => {
|
|
93
|
+
if (!ctx.user) {
|
|
94
|
+
throw new server_1.TRPCError({ code: 'UNAUTHORIZED' });
|
|
95
|
+
}
|
|
96
|
+
// Spread+reassign of `user` narrows downstream ctx from `User | null`
|
|
97
|
+
// to `User` so `adminProcedure` / `agentProcedure` can read fields
|
|
98
|
+
// without re-checking.
|
|
99
|
+
if (ctx.user.isAdmin) {
|
|
100
|
+
return next({ ctx: { ...ctx, user: ctx.user } });
|
|
101
|
+
}
|
|
102
|
+
// Hand-written core route — no cap entry. Authentication has already
|
|
103
|
+
// passed; defer further gating to any explicit `adminProcedure`
|
|
104
|
+
// chained on top of this one.
|
|
105
|
+
if (!(path in types_1.METHOD_ACCESS_MAP)) {
|
|
106
|
+
return next({ ctx: { ...ctx, user: ctx.user } });
|
|
107
|
+
}
|
|
108
|
+
// Device-scope caps may be gated by a `device:N` scope. Resolve the
|
|
109
|
+
// raw input once so the matcher can read `input.deviceId` without
|
|
110
|
+
// re-doing the Zod parse (tRPC caches the parsed input downstream).
|
|
111
|
+
// The `getDeviceAncestors` hook lets the matcher walk parent → child
|
|
112
|
+
// accessory inheritance (grant on Reolink also covers its siren / PIR).
|
|
113
|
+
const rawInput = await getRawInput();
|
|
114
|
+
const result = (0, scope_access_js_1.checkScopeAccess)(ctx.user.scopes ?? [], path, rawInput, ctx.getDeviceAncestors);
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
throw new server_1.TRPCError({ code: 'FORBIDDEN', message: result.reason });
|
|
117
|
+
}
|
|
118
|
+
return next({ ctx: { ...ctx, user: ctx.user } });
|
|
119
|
+
});
|
|
120
|
+
/**
|
|
121
|
+
* Destructive-ops gate. Adds an explicit admin check on top of
|
|
122
|
+
* `protectedProcedure`. Useful on hand-written routes whose admin-only
|
|
123
|
+
* nature should be obvious to a code reader.
|
|
124
|
+
*/
|
|
125
|
+
exports.adminProcedure = exports.protectedProcedure.use(({ ctx, next }) => {
|
|
126
|
+
if (!ctx.user.isAdmin) {
|
|
127
|
+
throw new server_1.TRPCError({ code: 'FORBIDDEN', message: 'Admin required' });
|
|
128
|
+
}
|
|
129
|
+
return next({ ctx });
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* Procedure for agent service accounts. After the v2 collapse, agents
|
|
133
|
+
* are admin sessions issued via `createServiceToken` — they all get
|
|
134
|
+
* `isAdmin: true`. This procedure is identical to `adminProcedure`;
|
|
135
|
+
* kept for naming clarity on agent-specific routes.
|
|
136
|
+
*/
|
|
137
|
+
exports.agentProcedure = exports.adminProcedure.use(({ ctx, next }) => {
|
|
138
|
+
return next({ ctx: { ...ctx, agentId: ctx.user.agentId ?? ctx.user.id } });
|
|
139
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enrichInputWithUserAgent = enrichInputWithUserAgent;
|
|
4
|
+
exports.wrapWebrtcSessionProviderWithRelay = wrapWebrtcSessionProviderWithRelay;
|
|
5
|
+
exports.buildAppRouter = buildAppRouter;
|
|
6
|
+
const trpc_middleware_1 = require("./trpc.middleware");
|
|
7
|
+
const generated_cap_routers_1 = require("./generated-cap-routers");
|
|
8
|
+
const generated_cap_mounts_js_1 = require("./generated-cap-mounts.js");
|
|
9
|
+
const cap_providers_js_1 = require("../core/cap-providers.js");
|
|
10
|
+
const auth_router_js_1 = require("../core/auth.router.js");
|
|
11
|
+
const addon_settings_router_js_1 = require("../core/addon-settings.router.js");
|
|
12
|
+
const settings_backend_router_js_1 = require("../core/settings-backend.router.js");
|
|
13
|
+
const event_bus_proxy_router_js_1 = require("../core/event-bus-proxy.router.js");
|
|
14
|
+
const repl_router_js_1 = require("../core/repl.router.js");
|
|
15
|
+
const notifications_router_js_1 = require("../core/notifications.router.js");
|
|
16
|
+
const logs_router_js_1 = require("../core/logs.router.js");
|
|
17
|
+
const system_events_router_js_1 = require("../core/system-events.router.js");
|
|
18
|
+
const live_events_router_js_1 = require("../core/live-events.router.js");
|
|
19
|
+
const capabilities_router_js_1 = require("../core/capabilities.router.js");
|
|
20
|
+
const stream_probe_router_js_1 = require("../core/stream-probe.router.js");
|
|
21
|
+
const hwaccel_router_js_1 = require("../core/hwaccel.router.js");
|
|
22
|
+
const cap_mount_helpers_js_1 = require("./cap-mount-helpers.js");
|
|
23
|
+
const client_ip_js_1 = require("./client-ip.js");
|
|
24
|
+
/**
|
|
25
|
+
* Merge the server-read User-Agent into a signaling call's
|
|
26
|
+
* `consumerAttribution`, building a NEW input object (immutable — never
|
|
27
|
+
* mutates the caller's input). When `userAgent` is null (mesh-originated
|
|
28
|
+
* call, or a client that omits the header) the input passes through
|
|
29
|
+
* unchanged. Any client-supplied `userAgent` is OVERWRITTEN — the hub
|
|
30
|
+
* trusts only the request context, never the client.
|
|
31
|
+
*/
|
|
32
|
+
function enrichInputWithUserAgent(input, userAgent) {
|
|
33
|
+
if (userAgent === null)
|
|
34
|
+
return input;
|
|
35
|
+
const base = input.consumerAttribution ?? { kind: 'webrtc-browser' };
|
|
36
|
+
return {
|
|
37
|
+
...input,
|
|
38
|
+
consumerAttribution: { ...base, userAgent },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Relay-only forcing for remote viewers is DISABLED (2026-05-26).
|
|
43
|
+
*
|
|
44
|
+
* It was meant to give CGNAT/4G viewers a clean relay↔relay path, but werift's
|
|
45
|
+
* TURN media-forward is unreliable between two real TURN servers (relay↔relay
|
|
46
|
+
* connects yet media never arrives → connected-but-black), and forcing relay
|
|
47
|
+
* ALSO kills the direct LAN/Tailscale host pair — which carries full native
|
|
48
|
+
* quality with no relay. We now offer ALL candidates (host incl. the hub's
|
|
49
|
+
* advertised Tailscale address, srflx, relay) and let ICE nominate the best
|
|
50
|
+
* reachable pair: direct when possible, relay only as a fallback. The
|
|
51
|
+
* `relayOnly` cap field + broker support remain for when relay media-forward
|
|
52
|
+
* is fixed.
|
|
53
|
+
*
|
|
54
|
+
* The wrapper additionally enriches the `createSession` / `handleOffer`
|
|
55
|
+
* subscriber attribution with the originating client's User-Agent, read
|
|
56
|
+
* from the tRPC request context (browser sessions). All OTHER methods
|
|
57
|
+
* delegate straight through — auth, the remote-proxy factory and every
|
|
58
|
+
* signaling behaviour are untouched.
|
|
59
|
+
*/
|
|
60
|
+
function wrapWebrtcSessionProviderWithRelay(provider, ctx) {
|
|
61
|
+
const userAgent = (0, client_ip_js_1.extractUserAgent)(ctx.req);
|
|
62
|
+
return {
|
|
63
|
+
...provider,
|
|
64
|
+
createSession: (input) => provider.createSession(enrichInputWithUserAgent(input, userAgent)),
|
|
65
|
+
handleOffer: (input) => provider.handleOffer(enrichInputWithUserAgent(input, userAgent)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the AppRouter. Mounts every codegen'd cap router via the auto-
|
|
70
|
+
* mount entrypoint and overrides the handful that need service-backed
|
|
71
|
+
* providers or custom collection dispatch. Non-cap (core) routers ride
|
|
72
|
+
* alongside in the same root object.
|
|
73
|
+
*
|
|
74
|
+
* Override-by-spread: spread `mountAllCaps(services)` first, then the
|
|
75
|
+
* overrides AFTER — the later property wins. The drift guard in
|
|
76
|
+
* `scripts/codegen.ts` ensures every codegen'd `createCapRouter_X` is
|
|
77
|
+
* present in the auto-mount inventory (or explicitly in the legacy
|
|
78
|
+
* skip-list), so the override list below NEVER needs to add a new entry
|
|
79
|
+
* just to mount a new cap — only to swap in a custom provider.
|
|
80
|
+
*/
|
|
81
|
+
function buildCapabilityRouters(services) {
|
|
82
|
+
return {
|
|
83
|
+
// ── Auto-mount: every codegen'd cap router with a canonical
|
|
84
|
+
// provider shape. Everything below this line OVERRIDES the
|
|
85
|
+
// auto-mount entry for caps with service-backed providers,
|
|
86
|
+
// custom collection routing, or a hub-only `null` remote proxy.
|
|
87
|
+
...(0, generated_cap_mounts_js_1.mountAllCaps)(services),
|
|
88
|
+
// ── Non-cap (core) routers — hand-written, single-impl ──────────
|
|
89
|
+
notifications: (0, notifications_router_js_1.createNotificationsRouter)(services.notificationService),
|
|
90
|
+
// Raw DB proxy for forked workers to read/write addon store.
|
|
91
|
+
// Workers use ctx.api.addonSettingsRaw.getGlobal.query({...}).
|
|
92
|
+
// NOT the three-level settings gateway — that's the codegen'd
|
|
93
|
+
// `addonSettings` cap router (mounted via auto-mount above).
|
|
94
|
+
addonSettingsRaw: (0, addon_settings_router_js_1.createAddonSettingsRouter)(services.configService),
|
|
95
|
+
settingsBackend: (0, settings_backend_router_js_1.createSettingsBackendRouter)(() => services.addonRegistry.getSettingsBackend()),
|
|
96
|
+
eventBusProxy: (0, event_bus_proxy_router_js_1.createEventBusProxyRouter)(services.eventBus),
|
|
97
|
+
repl: (0, repl_router_js_1.createReplRouter)(services.replEngine),
|
|
98
|
+
systemEvents: (0, system_events_router_js_1.createSystemEventsRouter)(services.eventBus),
|
|
99
|
+
capabilities: (0, capabilities_router_js_1.createCapabilitiesRouter)(services.capabilityRegistry, services.configService),
|
|
100
|
+
logs: (0, logs_router_js_1.createLogsRouter)(services.loggingService),
|
|
101
|
+
live: (0, live_events_router_js_1.createLiveEventsRouter)(services.eventBus, services.addonRegistry),
|
|
102
|
+
// stream-probe — fixed core API (ffprobe wrapper), not a cap.
|
|
103
|
+
streamProbe: (0, stream_probe_router_js_1.createStreamProbeRouter)(services.streamProbe),
|
|
104
|
+
// hwaccel — fixed core API, wraps the per-node `$hwaccel` Moleculer
|
|
105
|
+
// service. UI pipeline / NodeDetail pages query per-node to show
|
|
106
|
+
// which hardware backend each agent will use.
|
|
107
|
+
hwaccel: (0, hwaccel_router_js_1.createHwAccelRouter)(services.moleculer?.broker ?? null),
|
|
108
|
+
auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry),
|
|
109
|
+
// ── Cap overrides: service-backed providers ─────────────────────
|
|
110
|
+
// These caps don't have an addon registering a provider in the
|
|
111
|
+
// CapabilityRegistry — the provider is built on-demand from
|
|
112
|
+
// backend services. `mountAllCaps` would return `null` for them
|
|
113
|
+
// (registry lookup miss), so we re-mount with `buildXProvider`.
|
|
114
|
+
networkQuality: (0, generated_cap_routers_1.createCapRouter_networkQuality)((_ctx) => (0, cap_providers_js_1.buildNetworkQualityProvider)(services.networkQualityService)),
|
|
115
|
+
system: (0, generated_cap_routers_1.createCapRouter_system)((_ctx) => (0, cap_providers_js_1.buildSystemProvider)(services.featureService, services.capabilityRegistry)),
|
|
116
|
+
toast: (0, generated_cap_routers_1.createCapRouter_toast)((ctx) => (0, cap_providers_js_1.buildToastProvider)(services.toastService, ctx)),
|
|
117
|
+
integrations: (0, generated_cap_routers_1.createCapRouter_integrations)((_ctx) => (0, cap_providers_js_1.buildIntegrationsProvider)(services.addonRegistry, services.eventBus, services.loggingService, services.capabilityRegistry)),
|
|
118
|
+
nodes: (0, generated_cap_routers_1.createCapRouter_nodes)((_ctx) => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry)),
|
|
119
|
+
addons: (0, generated_cap_routers_1.createCapRouter_addons)((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx, services.eventBus)),
|
|
120
|
+
// ── Cap overrides: cross-node remote-proxy cast ─────────────────
|
|
121
|
+
// These caps' providers have manual interface types that pre-date
|
|
122
|
+
// `InferProvider<typeof xCap>` — structurally identical, nominally
|
|
123
|
+
// distinct. Casting at the override site is cheaper than reworking
|
|
124
|
+
// the provider declarations. Auto-mount can't infer the cast.
|
|
125
|
+
pipelineExecutor: (0, generated_cap_routers_1.createCapRouter_pipelineExecutor)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'pipeline-executor'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
|
|
126
|
+
pipelineRunner: (0, generated_cap_routers_1.createCapRouter_pipelineRunner)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'pipeline-runner'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
|
|
127
|
+
pipelineOrchestrator: (0, generated_cap_routers_1.createCapRouter_pipelineOrchestrator)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'pipeline-orchestrator'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
|
|
128
|
+
audioAnalyzer: (0, generated_cap_routers_1.createCapRouter_audioAnalyzer)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'audio-analyzer'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
|
|
129
|
+
audioCodec: (0, generated_cap_routers_1.createCapRouter_audioCodec)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'audio-codec'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
|
|
130
|
+
decoder: (0, generated_cap_routers_1.createCapRouter_decoder)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'decoder'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
|
|
131
|
+
platformProbe: (0, generated_cap_routers_1.createCapRouter_platformProbe)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'platform-probe'), (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
|
|
132
|
+
// ── Cap overrides: hub-only, no remote fallback ─────────────────
|
|
133
|
+
// The cap is intentionally single-node; agents are not directly
|
|
134
|
+
// addressable. Auto-mount would still set up a proxy factory; we
|
|
135
|
+
// explicitly return `null` to short-circuit any cross-node attempt.
|
|
136
|
+
localNetwork: (0, generated_cap_routers_1.createCapRouter_localNetwork)((_ctx) => (0, cap_mount_helpers_js_1.requireSingleton)(services.capabilityRegistry, 'local-network'), (_capName, _nodeId) => null),
|
|
137
|
+
// ── Cap overrides: collection dispatch (contribution / probe) ──
|
|
138
|
+
// `turn-provider.getTurnServers` is now handled generically by the
|
|
139
|
+
// auto-mount: it's an array-output method on a `collection` cap, so
|
|
140
|
+
// `mountAllCaps` fans it across every enabled provider via
|
|
141
|
+
// `concatCollection` when no `addonId` is supplied. No hand-written
|
|
142
|
+
// override needed.
|
|
143
|
+
//
|
|
144
|
+
// `snapshot-provider.supportsDevice` is an OR across providers;
|
|
145
|
+
// `getSnapshot` picks the first one that claims the device. The
|
|
146
|
+
// generic first-provider resolver from the auto-mount can't model
|
|
147
|
+
// this — we hand-write the probe + fan-out logic.
|
|
148
|
+
snapshotProvider: (0, generated_cap_routers_1.createCapRouter_snapshotProvider)((_ctx) => {
|
|
149
|
+
const reg = services.capabilityRegistry;
|
|
150
|
+
if (!reg)
|
|
151
|
+
return null;
|
|
152
|
+
const providers = reg.getCollection('snapshot-provider');
|
|
153
|
+
if (!providers || providers.length === 0)
|
|
154
|
+
return null;
|
|
155
|
+
const supportsDevice = (0, cap_mount_helpers_js_1.anySupports)(providers, 'supportsDevice');
|
|
156
|
+
const getSnapshot = (0, cap_mount_helpers_js_1.firstSupported)(providers, 'supportsDevice', 'getSnapshot');
|
|
157
|
+
return { supportsDevice, getSnapshot };
|
|
158
|
+
}),
|
|
159
|
+
// ── Cap override: server-detected remote → relay-only ────────────
|
|
160
|
+
// The broker (a forked addon) can't see the HTTP request, so it
|
|
161
|
+
// can't tell a LAN viewer from a remote one. We override only the
|
|
162
|
+
// `getProvider` accessor to return a per-request provider whose
|
|
163
|
+
// `createSession` carries a server-computed `relayOnly` flag derived
|
|
164
|
+
// from the client IP in `ctx.req`. Remote (CGNAT/4G) viewers force
|
|
165
|
+
// TURN-relay-only ICE; LAN viewers keep the direct host/srflx path.
|
|
166
|
+
// All other methods delegate straight through, and the cross-node
|
|
167
|
+
// remote-proxy routing is preserved (forked/agent-hosted brokers).
|
|
168
|
+
webrtcSession: (0, generated_cap_routers_1.createCapRouter_webrtcSession)((ctx) => {
|
|
169
|
+
const provider = services.capabilityRegistry?.getSingleton('webrtc-session') ?? null;
|
|
170
|
+
return provider ? wrapWebrtcSessionProviderWithRelay(provider, ctx) : null;
|
|
171
|
+
}, (capName, nodeId) => services.moleculer.createCapabilityProxy(capName, nodeId)),
|
|
172
|
+
// NOT MOUNTED — legacy provider shapes (positional args / sync
|
|
173
|
+
// returns) that don't match the codegen routers' {input}-object +
|
|
174
|
+
// Promise<T> contract. Tracked by `LEGACY_SHAPE_SKIP` in
|
|
175
|
+
// `generated-cap-mounts.ts` until the provider refactor (task #195):
|
|
176
|
+
// - addon-routes (IAddonRouteProvider: getRoutes sync)
|
|
177
|
+
// - auth-provider (IAuthProvider: positional credentials)
|
|
178
|
+
// - log-destination (ILogDestination: positional + extra lifecycle)
|
|
179
|
+
// - restreamer (IRestreamer: registerDevice positional)
|
|
180
|
+
// - streaming-engine (IStreamingEngine: registerStream positional)
|
|
181
|
+
// - webrtc (IWebRtcProvider: missing hasAdaptiveBitrate)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function buildAppRouter(services) {
|
|
185
|
+
return (0, trpc_middleware_1.trpcRouter)({
|
|
186
|
+
...buildCapabilityRouters(services),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SESSION_COOKIE = void 0;
|
|
4
|
+
exports.buildSessionCookie = buildSessionCookie;
|
|
5
|
+
exports.clearSessionCookie = clearSessionCookie;
|
|
6
|
+
exports.shouldRedirectToLogin = shouldRedirectToLogin;
|
|
7
|
+
exports.loginRedirectUrl = loginRedirectUrl;
|
|
8
|
+
exports.isEmbedRedirectTarget = isEmbedRedirectTarget;
|
|
9
|
+
/** Browser session cookie carrying the hub JWT. Set by POST /api/auth/session
|
|
10
|
+
* after a tRPC login; read by the addon-route catch-all for `authenticated`
|
|
11
|
+
* routes hit by a plain browser navigation. */
|
|
12
|
+
exports.SESSION_COOKIE = 'camstack_session';
|
|
13
|
+
function buildSessionCookie(token, ttlSec) {
|
|
14
|
+
return {
|
|
15
|
+
name: exports.SESSION_COOKIE,
|
|
16
|
+
value: token,
|
|
17
|
+
options: { httpOnly: true, sameSite: 'lax', secure: true, path: '/', maxAge: ttlSec },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function clearSessionCookie() {
|
|
21
|
+
return {
|
|
22
|
+
name: exports.SESSION_COOKIE,
|
|
23
|
+
value: '',
|
|
24
|
+
options: { httpOnly: true, sameSite: 'lax', secure: true, path: '/', maxAge: 0 },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** A browser navigation we can bounce to the login page: a top-level GET
|
|
28
|
+
* that wants HTML. Anything else (API call, POST, non-HTML) keeps the
|
|
29
|
+
* 401 behavior so programmatic clients get a clean error. */
|
|
30
|
+
function shouldRedirectToLogin(method, accept) {
|
|
31
|
+
return method === 'GET' && typeof accept === 'string' && accept.includes('text/html');
|
|
32
|
+
}
|
|
33
|
+
/** Build the `/login?next=…` URL for an unauthenticated browser request. */
|
|
34
|
+
function loginRedirectUrl(originalUrl) {
|
|
35
|
+
return `/login?next=${encodeURIComponent(originalUrl)}`;
|
|
36
|
+
}
|
|
37
|
+
/** Allowed redirect target for `GET /api/embed-auth`: a same-origin RELATIVE
|
|
38
|
+
* path to a stream-broker embed page. Defeats open-redirects — the endpoint
|
|
39
|
+
* sets the session cookie from a Bearer token, so the `next` must be safe to
|
|
40
|
+
* bounce to. Rejects absolute/protocol-relative URLs, backslashes, and `..`. */
|
|
41
|
+
function isEmbedRedirectTarget(next) {
|
|
42
|
+
if (!next.startsWith('/addon/stream-broker/embed/'))
|
|
43
|
+
return false;
|
|
44
|
+
if (next.includes('\\') || next.includes('://') || next.includes('..'))
|
|
45
|
+
return false;
|
|
46
|
+
return true;
|
|
47
|
+
}
|