@camstack/server 1.2.354 → 1.2.355
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/dist/api/capabilities.router.js +10 -20
- package/dist/api/core/capabilities.router.js +10 -20
- package/dist/api/trpc/cap-router-runtime.js +42 -3
- package/dist/api/trpc/device-collection-dispatch.js +484 -0
- package/dist/api/trpc/generated-cap-mounts.js +8 -9
- package/dist/api/trpc/generated-cap-routers.js +11 -2
- package/package.json +8 -8
|
@@ -205,25 +205,15 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
|
|
|
205
205
|
const overrides = registry.getDeviceOverrides(input.deviceId);
|
|
206
206
|
return Object.fromEntries(overrides);
|
|
207
207
|
}),
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
clearDeviceCollectionFilter: trpc_middleware_1.adminProcedure
|
|
219
|
-
.input(zod_1.z.object({ deviceId: zod_1.z.string(), capability: zod_1.z.string() }))
|
|
220
|
-
.mutation(({ input }) => {
|
|
221
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
222
|
-
registry.clearDeviceCollectionFilter(input.deviceId, input.capability);
|
|
223
|
-
logger.info('Device collection filter cleared', {
|
|
224
|
-
tags: { deviceId: Number(input.deviceId) },
|
|
225
|
-
meta: { capability: input.capability },
|
|
226
|
-
});
|
|
227
|
-
}),
|
|
208
|
+
/*
|
|
209
|
+
* `setDeviceCollectionFilter` / `clearDeviceCollectionFilter` were REMOVED
|
|
210
|
+
* here (D556). They wrote an in-memory per-device allow list that nothing
|
|
211
|
+
* persisted, so an operator's pick survived until the next hub restart and
|
|
212
|
+
* then silently reverted — a control that looks like it works, which is the
|
|
213
|
+
* defect D62 exists for. Which sources a camera has is now asked of the ONE
|
|
214
|
+
* authority that already owned a per-camera function switch:
|
|
215
|
+
* `deviceManager.setWrapperActive`. The registry keeps only its projection,
|
|
216
|
+
* written by `device-source-activation.ts` and by nothing else.
|
|
217
|
+
*/
|
|
228
218
|
});
|
|
229
219
|
}
|
|
@@ -114,25 +114,15 @@ function createCapabilitiesRouter(registry, config) {
|
|
|
114
114
|
}
|
|
115
115
|
return result;
|
|
116
116
|
}),
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
registry.setDeviceCollectionFilter(input.deviceId, input.capability, input.addonIds);
|
|
128
|
-
}),
|
|
129
|
-
clearDeviceCollectionFilter: trpc_middleware_js_1.adminProcedure
|
|
130
|
-
.input(zod_1.z.object({ deviceId: zod_1.z.string(), capability: zod_1.z.string() }))
|
|
131
|
-
.output(zod_1.z.void())
|
|
132
|
-
.mutation(({ input }) => {
|
|
133
|
-
if (!registry)
|
|
134
|
-
throw new Error('Capability registry unavailable');
|
|
135
|
-
registry.clearDeviceCollectionFilter(input.deviceId, input.capability);
|
|
136
|
-
}),
|
|
117
|
+
/*
|
|
118
|
+
* `setDeviceCollectionFilter` / `clearDeviceCollectionFilter` were REMOVED
|
|
119
|
+
* here (D556). They wrote an in-memory per-device allow list that nothing
|
|
120
|
+
* persisted, so an operator's pick survived until the next hub restart and
|
|
121
|
+
* then silently reverted — a control that looks like it works, which is the
|
|
122
|
+
* defect D62 exists for. Which sources a camera has is now asked of the ONE
|
|
123
|
+
* authority that already owned a per-camera function switch:
|
|
124
|
+
* `deviceManager.setWrapperActive`. The registry keeps only its projection,
|
|
125
|
+
* written by `device-source-activation.ts` and by nothing else.
|
|
126
|
+
*/
|
|
137
127
|
});
|
|
138
128
|
}
|
|
@@ -28,8 +28,10 @@ exports.createCapRouterPrimitives = createCapRouterPrimitives;
|
|
|
28
28
|
exports.createCapRouterServices = createCapRouterServices;
|
|
29
29
|
exports.buildRuntimeCapRouters = buildRuntimeCapRouters;
|
|
30
30
|
const system_1 = require("@camstack/system");
|
|
31
|
+
const types_1 = require("@camstack/types");
|
|
31
32
|
const server_1 = require("@trpc/server");
|
|
32
33
|
const cap_mount_helpers_js_1 = require("./cap-mount-helpers.js");
|
|
34
|
+
const device_collection_dispatch_js_1 = require("./device-collection-dispatch.js");
|
|
33
35
|
const trpc_middleware_js_1 = require("./trpc.middleware.js");
|
|
34
36
|
/**
|
|
35
37
|
* Pre-computed set of capName → array-output method names for collection
|
|
@@ -41,7 +43,10 @@ exports.COLLECTION_ARRAY_METHODS = computeArrayMethods();
|
|
|
41
43
|
function computeArrayMethods() {
|
|
42
44
|
const out = new Map();
|
|
43
45
|
for (const def of system_1.ALL_CAPABILITY_DEFINITIONS) {
|
|
44
|
-
|
|
46
|
+
// The effective MOUNT, never `mode`: a device-scoped wrapper collection
|
|
47
|
+
// (`videoclips`, D554) fans over ONE DEVICE's sources through the
|
|
48
|
+
// per-device dispatcher, and must never be fanned globally here.
|
|
49
|
+
if ((0, types_1.resolveCapMount)(def).kind !== 'collection')
|
|
45
50
|
continue;
|
|
46
51
|
const names = [];
|
|
47
52
|
for (const [name, schema] of Object.entries(def.methods)) {
|
|
@@ -56,6 +61,22 @@ function computeArrayMethods() {
|
|
|
56
61
|
}
|
|
57
62
|
return out;
|
|
58
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Is this cap served by the GLOBAL collection mount — every registered
|
|
66
|
+
* provider, for every caller?
|
|
67
|
+
*
|
|
68
|
+
* `mode: 'collection'` alone does not answer it any more: a device-scoped
|
|
69
|
+
* wrapper collection keeps the per-device mount (D554,
|
|
70
|
+
* `device-collection-dispatch.ts`), and fanning it globally here would answer
|
|
71
|
+
* one camera with another camera's sources on the `ctx.api` path while the
|
|
72
|
+
* tRPC path resolved per device — two authorities disagreeing about the same
|
|
73
|
+
* call, which is exactly what {@link aggregateCollectionCall} exists to
|
|
74
|
+
* prevent.
|
|
75
|
+
*/
|
|
76
|
+
function isGlobalCollection(reg, capName) {
|
|
77
|
+
const def = reg.getDefinition(capName);
|
|
78
|
+
return def !== undefined && (0, types_1.resolveCapMount)(def).kind === 'collection';
|
|
79
|
+
}
|
|
59
80
|
/**
|
|
60
81
|
* Resolve a collection cap's provider exactly as the codegen-emitted
|
|
61
82
|
* mount did:
|
|
@@ -108,7 +129,7 @@ function resolveCollectionProvider(reg, capName, addonId) {
|
|
|
108
129
|
async function aggregateCollectionCall(reg, capName, method, args) {
|
|
109
130
|
if (!reg)
|
|
110
131
|
return null;
|
|
111
|
-
if (reg
|
|
132
|
+
if (!isGlobalCollection(reg, capName))
|
|
112
133
|
return null;
|
|
113
134
|
if (!exports.COLLECTION_ARRAY_METHODS.get(capName)?.includes(method))
|
|
114
135
|
return null;
|
|
@@ -141,7 +162,7 @@ async function aggregateCollectionCall(reg, capName, method, args) {
|
|
|
141
162
|
function addonPinOf(reg, capName, method, args) {
|
|
142
163
|
if (!reg)
|
|
143
164
|
return null;
|
|
144
|
-
if (reg
|
|
165
|
+
if (!isGlobalCollection(reg, capName))
|
|
145
166
|
return null;
|
|
146
167
|
if (exports.COLLECTION_ARRAY_METHODS.get(capName)?.includes(method) === true)
|
|
147
168
|
return null;
|
|
@@ -258,8 +279,26 @@ function createCapRouterServices(deps) {
|
|
|
258
279
|
const factory = deps.serverProviders[capName];
|
|
259
280
|
return factory ? factory(ctx) : null;
|
|
260
281
|
}
|
|
282
|
+
// Both per-device mounts go through the SAME dispatcher. It resolves
|
|
283
|
+
// `getNativeProvider` first (the provider IS the device) and falls
|
|
284
|
+
// through to the D13 resolver `getProviderForDevice` — which is what a
|
|
285
|
+
// `device-scoped` WRAPPER needs: the per-device binding
|
|
286
|
+
// (`setWrapperActive` → `setDeviceOverride`) when there is one, the
|
|
287
|
+
// global active provider when there is not. No second resolver.
|
|
261
288
|
case 'device-native':
|
|
289
|
+
case 'device-scoped': {
|
|
290
|
+
// A device-scoped cap whose mode is `collection` has SEVERAL providers
|
|
291
|
+
// per device — sources listed beside each other (D549 decision 2,
|
|
292
|
+
// D554). `requireDeviceScoped` resolves exactly one of them, and on a
|
|
293
|
+
// collection cap `getProviderForDevice` answers with whichever addon
|
|
294
|
+
// registered FIRST: every camera served by one source, silently. The
|
|
295
|
+
// collection twin fans the array methods over the device's sources and
|
|
296
|
+
// routes the scalar ones by the source inside the id.
|
|
297
|
+
if (reg?.getMode(capName) === 'collection') {
|
|
298
|
+
return (0, device_collection_dispatch_js_1.requireDeviceCollection)(reg, capName, deps.logger);
|
|
299
|
+
}
|
|
262
300
|
return (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, capName);
|
|
301
|
+
}
|
|
263
302
|
case 'collection':
|
|
264
303
|
return resolveCollectionProvider(reg, capName, addonId);
|
|
265
304
|
// singleton / per-node / hub-only resolve the active singleton.
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The per-device COLLECTION dispatcher — a capability whose providers are
|
|
4
|
+
* SOURCES of one device, listed beside each other.
|
|
5
|
+
*
|
|
6
|
+
* `requireDeviceScoped` (`cap-mount-helpers.ts`) resolves ONE provider for a
|
|
7
|
+
* device: the native, else the D13 `getProviderForDevice`. That is right for a
|
|
8
|
+
* cap where a device has one answer. `videoclips` is not that cap any more
|
|
9
|
+
* (D549 decision 2, D554): a camera's clips live in several places at once —
|
|
10
|
+
* the analytics visit log, the camera's own SD card, a hub's storage, HomeKit
|
|
11
|
+
* — and the operator asked for all of them, side by side, never one hidden
|
|
12
|
+
* behind another. `getProviderForDevice` on a collection cap returns the FIRST
|
|
13
|
+
* registered provider, so leaving `videoclips` on it would have answered every
|
|
14
|
+
* camera from whichever addon booted first: the same silent single-answer
|
|
15
|
+
* defect D552 closed, wearing a different hat.
|
|
16
|
+
*
|
|
17
|
+
* The three rules this module implements, and nothing else:
|
|
18
|
+
*
|
|
19
|
+
* 1. **Fan out, per device.** An ARRAY-returning method is called on every
|
|
20
|
+
* source the DEVICE has (`CapabilityRegistry.resolveCollectionForDevice` —
|
|
21
|
+
* the enabled providers, minus the ones the operator switched off for this
|
|
22
|
+
* camera through `deviceManager.setWrapperActive`, D556), and the results
|
|
23
|
+
* are concatenated. No dedup, no best-source
|
|
24
|
+
* pick: a clip held in two storages appears twice, once per source, and
|
|
25
|
+
* each row says which (D549 decision 11 — stated there as a consequence
|
|
26
|
+
* precisely so it is not "fixed" later).
|
|
27
|
+
* 2. **One failing source never empties the others.** Every source is called
|
|
28
|
+
* independently and a rejection removes only its own rows — and says so,
|
|
29
|
+
* per source, with `tags: { deviceId }`. A fan-out where one throwing
|
|
30
|
+
* provider empties the list is worse than no fan-out. For the SOURCE
|
|
31
|
+
* listing itself the failure becomes a row (`unreachable` + the reason):
|
|
32
|
+
* an empty list from a camera nobody could reach reads as "this camera has
|
|
33
|
+
* no recordings", which is the defect this whole line of work is about.
|
|
34
|
+
* 3. **A scalar method routes by the SOURCE inside the id it carries.** Clip
|
|
35
|
+
* ids are self-contained and source-prefixed (`analytics:…`,
|
|
36
|
+
* `native:reolink:onboard:…`, `hksv:…`), so the dispatcher asks each
|
|
37
|
+
* source which ids it owns ({@link SOURCE_LISTING_METHOD}) and hands the
|
|
38
|
+
* call to the one that claims the longest matching prefix. An id nobody
|
|
39
|
+
* claims is REFUSED by name — never handed to the first provider, which is
|
|
40
|
+
* how a fabricated `addonId` once came back with another addon's answer
|
|
41
|
+
* (D552 consequences, `rejectUnresolvedAddonPin`).
|
|
42
|
+
*
|
|
43
|
+
* A device with exactly ONE source skips 1's merge and 3's probe entirely: its
|
|
44
|
+
* rows are returned verbatim in the provider's own order and its scalar calls
|
|
45
|
+
* go straight through. That is not an optimisation — it is the promise that a
|
|
46
|
+
* camera with one provider behaves exactly as it did before this mount
|
|
47
|
+
* existed, and it is pinned by spec.
|
|
48
|
+
*/
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.SOURCE_LISTING_METHOD = void 0;
|
|
51
|
+
exports.requireDeviceCollection = requireDeviceCollection;
|
|
52
|
+
const system_1 = require("@camstack/system");
|
|
53
|
+
const types_1 = require("@camstack/types");
|
|
54
|
+
const server_1 = require("@trpc/server");
|
|
55
|
+
/**
|
|
56
|
+
* The device-collection CONVENTION: a cap whose providers are sources declares
|
|
57
|
+
* this method, taking `{ deviceId }` and returning one row per source with a
|
|
58
|
+
* `source` id and an `availability` block. It is what makes rule 3 possible
|
|
59
|
+
* without a second registry of id prefixes — the provider that mints the ids
|
|
60
|
+
* is the one that names them.
|
|
61
|
+
*/
|
|
62
|
+
exports.SOURCE_LISTING_METHOD = 'listSources';
|
|
63
|
+
/** The field on a source row that carries its id / clip-id namespace. */
|
|
64
|
+
const SOURCE_ID_FIELD = 'source';
|
|
65
|
+
/** The field on a source row that names the addon serving it (D555/D557). */
|
|
66
|
+
const SOURCE_ADDON_FIELD = 'addonId';
|
|
67
|
+
/** The field a source row carries the operator's off switch on (D556 amended). */
|
|
68
|
+
const SOURCE_SWITCHED_OFF_FIELD = 'switchedOff';
|
|
69
|
+
/** The availability field a switched-off row must not carry a value in. */
|
|
70
|
+
const CATALOG_AGE_FIELD = 'catalogAsOf';
|
|
71
|
+
/**
|
|
72
|
+
* EVERY source a DEVICE has, in registration order with the device's native
|
|
73
|
+
* provider first when it has one — each one marked with whether the operator
|
|
74
|
+
* switched it off.
|
|
75
|
+
*
|
|
76
|
+
* `resolveCollectionForDevice` decides which of the registered providers this
|
|
77
|
+
* device may use: it drops disabled ones and ones inactive for the system. The
|
|
78
|
+
* ones the operator switched OFF for this camera are NOT dropped here — they
|
|
79
|
+
* are marked, because a switched-off source must still produce a ROW to be
|
|
80
|
+
* switched back on from, and only its own provider can name it (D554 decision
|
|
81
|
+
* 4; D556 as amended). What that mark costs the source is everything else:
|
|
82
|
+
* {@link requireDeviceCollection} calls it for the source listing and for
|
|
83
|
+
* nothing else. The deny set is the registry's PROJECTION of the persisted
|
|
84
|
+
* wrapper activation — the operator's verb is `deviceManager.setWrapperActive`
|
|
85
|
+
* and the authority is its store, never a second one here (D556).
|
|
86
|
+
*
|
|
87
|
+
* `resolveCollectionForDevice` returns provider objects only, so the addon ids
|
|
88
|
+
* — needed to NAME a source that failed, to stamp the rows, and to apply the
|
|
89
|
+
* addon-grained half of the deny set — come from `getCollectionEntries` by
|
|
90
|
+
* object identity: both come from the same registry map in the same call, so
|
|
91
|
+
* the reference IS the key.
|
|
92
|
+
*/
|
|
93
|
+
function sourcesForDevice(registry, capName, deviceId) {
|
|
94
|
+
const admitted = new Set(registry.resolveCollectionForDevice(capName, String(deviceId), {
|
|
95
|
+
includeSwitchedOff: true,
|
|
96
|
+
}));
|
|
97
|
+
const sources = [];
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
const native = registry.getNativeProvider(capName, deviceId);
|
|
100
|
+
if (native !== null) {
|
|
101
|
+
// The native is NOT in `state.providers`, so `resolveCollectionForDevice`
|
|
102
|
+
// never sees it — it is asked about separately, against the same authority
|
|
103
|
+
// (D556 decision 5). Without this the picker would carry one row the
|
|
104
|
+
// operator cannot switch off, and it is the camera's OWN recordings:
|
|
105
|
+
// exactly the source the external-videoclips line is about.
|
|
106
|
+
const nativeAddonId = registry.getNativeAddonId(capName, deviceId) ?? 'native';
|
|
107
|
+
sources.push({
|
|
108
|
+
addonId: nativeAddonId,
|
|
109
|
+
provider: native,
|
|
110
|
+
switchedOff: registry.isDeviceSourceDisabled(String(deviceId), capName, nativeAddonId),
|
|
111
|
+
});
|
|
112
|
+
seen.add(native);
|
|
113
|
+
}
|
|
114
|
+
for (const [addonId, provider] of registry.getCollectionEntries(capName)) {
|
|
115
|
+
if (!admitted.has(provider) || seen.has(provider))
|
|
116
|
+
continue;
|
|
117
|
+
seen.add(provider);
|
|
118
|
+
sources.push({
|
|
119
|
+
addonId,
|
|
120
|
+
provider,
|
|
121
|
+
switchedOff: registry.isDeviceSourceDisabled(String(deviceId), capName, addonId),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return sources;
|
|
125
|
+
}
|
|
126
|
+
/** Is `method` one of the cap's array-returning (fan-out) methods? */
|
|
127
|
+
function isFanOutMethod(registry, capName, method) {
|
|
128
|
+
const def = registry.getDefinition(capName);
|
|
129
|
+
if (def === undefined)
|
|
130
|
+
return false;
|
|
131
|
+
const schema = (0, types_1.expandCapMethods)(def)[method];
|
|
132
|
+
if (schema === undefined || schema.kind === 'subscription')
|
|
133
|
+
return false;
|
|
134
|
+
return (0, types_1.isArrayOutputSchema)(schema.output);
|
|
135
|
+
}
|
|
136
|
+
function errMessage(err) {
|
|
137
|
+
return err instanceof Error ? err.message : String(err);
|
|
138
|
+
}
|
|
139
|
+
/** Call one source, naming the cap, the source and the method on any refusal. */
|
|
140
|
+
async function callSource(source, method, input, capName, deviceId) {
|
|
141
|
+
const fn = source.provider[method];
|
|
142
|
+
if (typeof fn !== 'function') {
|
|
143
|
+
throw new server_1.TRPCError({
|
|
144
|
+
code: 'NOT_IMPLEMENTED',
|
|
145
|
+
message: `Capability "${capName}" source "${source.addonId}" for device ${deviceId} does not implement "${method}"`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return await fn.call(source.provider, input);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The `startMs` a row is ordered by, or null when the row does not declare a
|
|
152
|
+
* time range. A merge across sources is newest-first (D549 decision 2's
|
|
153
|
+
* `(startMs, endMs, id)`); rows that carry no window — a source listing, say —
|
|
154
|
+
* keep the order their providers produced them in.
|
|
155
|
+
*/
|
|
156
|
+
function startMsOf(row) {
|
|
157
|
+
if (row === null || typeof row !== 'object')
|
|
158
|
+
return null;
|
|
159
|
+
const range = Reflect.get(row, 'timeRange');
|
|
160
|
+
if (range === null || typeof range !== 'object')
|
|
161
|
+
return null;
|
|
162
|
+
const startMs = Reflect.get(range, 'startMs');
|
|
163
|
+
return typeof startMs === 'number' ? startMs : null;
|
|
164
|
+
}
|
|
165
|
+
function endMsOf(row) {
|
|
166
|
+
if (row === null || typeof row !== 'object')
|
|
167
|
+
return 0;
|
|
168
|
+
const range = Reflect.get(row, 'timeRange');
|
|
169
|
+
if (range === null || typeof range !== 'object')
|
|
170
|
+
return 0;
|
|
171
|
+
const endMs = Reflect.get(range, 'endMs');
|
|
172
|
+
return typeof endMs === 'number' ? endMs : 0;
|
|
173
|
+
}
|
|
174
|
+
function idOf(row) {
|
|
175
|
+
if (row === null || typeof row !== 'object')
|
|
176
|
+
return '';
|
|
177
|
+
const id = Reflect.get(row, 'id');
|
|
178
|
+
return typeof id === 'string' ? id : '';
|
|
179
|
+
}
|
|
180
|
+
/** Newest-first by `(startMs, endMs, id)`; applied only when EVERY row has a window. */
|
|
181
|
+
function mergeRows(rows) {
|
|
182
|
+
if (rows.some((row) => startMsOf(row) === null))
|
|
183
|
+
return rows;
|
|
184
|
+
return [...rows].sort((a, b) => {
|
|
185
|
+
const byStart = (startMsOf(b) ?? 0) - (startMsOf(a) ?? 0);
|
|
186
|
+
if (byStart !== 0)
|
|
187
|
+
return byStart;
|
|
188
|
+
const byEnd = endMsOf(b) - endMsOf(a);
|
|
189
|
+
if (byEnd !== 0)
|
|
190
|
+
return byEnd;
|
|
191
|
+
return idOf(a).localeCompare(idOf(b));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The row a source listing gets when the source could not be reached AT ALL.
|
|
196
|
+
*
|
|
197
|
+
* The source id is the addon's, because a provider that threw could not name
|
|
198
|
+
* its own sources — saying "this provider, as a whole, did not answer" is the
|
|
199
|
+
* honest version, and the reason carries the refusal verbatim. Never composed
|
|
200
|
+
* with a provider's output: this row replaces rows that do not exist.
|
|
201
|
+
*/
|
|
202
|
+
function unreachableSourceRow(addonId, reason) {
|
|
203
|
+
return {
|
|
204
|
+
[SOURCE_ID_FIELD]: addonId,
|
|
205
|
+
label: addonId,
|
|
206
|
+
availability: { state: 'unreachable', reason },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async function fanOut(registry, sources, method, input, capName, deviceId, logger) {
|
|
210
|
+
const isSourceListing = method === exports.SOURCE_LISTING_METHOD;
|
|
211
|
+
const settled = await Promise.allSettled(sources.map(async (source) => await callSource(source, method, input, capName, deviceId)));
|
|
212
|
+
const rows = [];
|
|
213
|
+
for (const [index, result] of settled.entries()) {
|
|
214
|
+
const source = sources[index];
|
|
215
|
+
if (source === undefined)
|
|
216
|
+
continue;
|
|
217
|
+
if (result.status === 'rejected') {
|
|
218
|
+
const reason = errMessage(result.reason);
|
|
219
|
+
// A branch that drops work says so — a fan-out that silently loses a
|
|
220
|
+
// source is indistinguishable from a camera that has none.
|
|
221
|
+
logger?.warn(`${capName}.${method}: source failed — its rows are missing from this answer`, {
|
|
222
|
+
tags: { deviceId, addonId: source.addonId },
|
|
223
|
+
meta: { capability: capName, method, reason },
|
|
224
|
+
});
|
|
225
|
+
if (isSourceListing)
|
|
226
|
+
rows.push(unreachableSourceRow(source.addonId, reason));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (!Array.isArray(result.value)) {
|
|
230
|
+
logger?.warn(`${capName}.${method}: source answered a non-array — contributing nothing`, {
|
|
231
|
+
tags: { deviceId, addonId: source.addonId },
|
|
232
|
+
meta: { capability: capName, method },
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
rows.push(...finishRows(registry, capName, deviceId, source, result.value, isSourceListing));
|
|
237
|
+
}
|
|
238
|
+
return mergeRows(rows);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The availability block without its catalog age.
|
|
242
|
+
*
|
|
243
|
+
* `catalogAsOf` means "when this source's catalog was last CONFIRMED". A source
|
|
244
|
+
* the operator switched off is never asked for a catalog, so it has no
|
|
245
|
+
* confirmation to report and will never get a fresher one — and an age that can
|
|
246
|
+
* only grow draws a STALLING source rather than an off switch (D62: disabled
|
|
247
|
+
* must never look like broken). The rest of the block is left exactly as the
|
|
248
|
+
* provider said it: what the source CAN do is a different question from whether
|
|
249
|
+
* it may.
|
|
250
|
+
*/
|
|
251
|
+
function withoutCatalogAge(availability) {
|
|
252
|
+
if (availability === null || typeof availability !== 'object')
|
|
253
|
+
return availability;
|
|
254
|
+
return Object.fromEntries(Object.entries(availability).filter(([key]) => key !== CATALOG_AGE_FIELD));
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Stamp each row with the addon that really served it, mark a source listing
|
|
258
|
+
* row the operator switched off, and drop the CLIP rows of such a source.
|
|
259
|
+
*
|
|
260
|
+
* The two halves are deliberately asymmetric, and that asymmetry is the whole
|
|
261
|
+
* of D556's amendment: a switched-off source contributes no clips (the point of
|
|
262
|
+
* the switch) and keeps its row (the control the switch is undone from). A
|
|
263
|
+
* `listSources` that dropped the row left an unticked source with nothing to
|
|
264
|
+
* un-tick — on a Reolink camera, which has exactly one source since D555's
|
|
265
|
+
* collapse, that is the entire picker.
|
|
266
|
+
*
|
|
267
|
+
* **The stamp** is authoritative: a surface cannot otherwise resolve a source
|
|
268
|
+
* row to who answers it, and the alternative — a `native:reolink:* →
|
|
269
|
+
* provider-reolink` prefix table inside the widget — is a second authority on
|
|
270
|
+
* provider identity living in the one package with no business knowing it
|
|
271
|
+
* (D557). The registry's id is a fact; a provider's own value is a claim, so
|
|
272
|
+
* the fact wins.
|
|
273
|
+
*
|
|
274
|
+
* **The drop** happens here rather than in `sourcesForDevice` because one
|
|
275
|
+
* addon serves SEVERAL of a device's sources (`addon-provider-reolink` answers
|
|
276
|
+
* a hub child with `onboard` AND `hub`), so dropping the PROVIDER would turn
|
|
277
|
+
* off every source it has while the operator ticked one. Rows are where
|
|
278
|
+
* sources are actually named, so this is the grain the per-source authority
|
|
279
|
+
* applies at — same registry projection, same migration rule, one step finer.
|
|
280
|
+
*
|
|
281
|
+
* A camera with NO deny entry is untouched: the rows come back verbatim, in
|
|
282
|
+
* the provider's own order, which is D554 decision 6's guarantee and today's
|
|
283
|
+
* behaviour for every camera on the fleet.
|
|
284
|
+
*/
|
|
285
|
+
function finishRows(registry, capName, deviceId, source, rows, isSourceListing) {
|
|
286
|
+
const denied = registry.getDeviceDisabledSources(String(deviceId), capName);
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const row of rows) {
|
|
289
|
+
if (row === null || typeof row !== 'object') {
|
|
290
|
+
out.push(row);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const rowSourceId = Reflect.get(row, SOURCE_ID_FIELD);
|
|
294
|
+
// ONE predicate for the whole deny set, migration rule included: an entry
|
|
295
|
+
// is a source id, or (pre-re-key, D555) the id of the addon that serves it.
|
|
296
|
+
const off = denied.length > 0 &&
|
|
297
|
+
(0, system_1.isSourceDenied)(denied, {
|
|
298
|
+
addonId: source.addonId,
|
|
299
|
+
...(typeof rowSourceId === 'string' ? { sourceId: rowSourceId } : {}),
|
|
300
|
+
});
|
|
301
|
+
if (isSourceListing) {
|
|
302
|
+
const stamped = {
|
|
303
|
+
...row,
|
|
304
|
+
[SOURCE_ADDON_FIELD]: source.addonId,
|
|
305
|
+
};
|
|
306
|
+
out.push(off
|
|
307
|
+
? {
|
|
308
|
+
...stamped,
|
|
309
|
+
availability: withoutCatalogAge(Reflect.get(row, 'availability')),
|
|
310
|
+
[SOURCE_SWITCHED_OFF_FIELD]: true,
|
|
311
|
+
}
|
|
312
|
+
: stamped);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (off)
|
|
316
|
+
continue;
|
|
317
|
+
out.push(row);
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
async function claimsOf(sources, capName, deviceId, logger) {
|
|
322
|
+
const claimed = new Map();
|
|
323
|
+
const unknownClaims = [];
|
|
324
|
+
const settled = await Promise.allSettled(sources.map(async (source) => await callSource(source, exports.SOURCE_LISTING_METHOD, { deviceId }, capName, deviceId)));
|
|
325
|
+
for (const [index, result] of settled.entries()) {
|
|
326
|
+
const source = sources[index];
|
|
327
|
+
if (source === undefined)
|
|
328
|
+
continue;
|
|
329
|
+
if (result.status === 'rejected' || !Array.isArray(result.value)) {
|
|
330
|
+
// It could not name its sources, so it cannot be excluded on that basis
|
|
331
|
+
// either — a read that FAILS must not destroy work (D49).
|
|
332
|
+
logger?.warn(`${capName}.${exports.SOURCE_LISTING_METHOD}: source could not name its ids — routing falls back to it only if it is the only such source`, {
|
|
333
|
+
tags: { deviceId, addonId: source.addonId },
|
|
334
|
+
meta: {
|
|
335
|
+
capability: capName,
|
|
336
|
+
reason: result.status === 'rejected' ? errMessage(result.reason) : 'non-array answer',
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
unknownClaims.push(source);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
for (const row of result.value) {
|
|
343
|
+
if (row === null || typeof row !== 'object')
|
|
344
|
+
continue;
|
|
345
|
+
const id = Reflect.get(row, SOURCE_ID_FIELD);
|
|
346
|
+
if (typeof id === 'string' && id.length > 0 && !claimed.has(id))
|
|
347
|
+
claimed.set(id, source);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { claimed, unknownClaims };
|
|
351
|
+
}
|
|
352
|
+
/** Every top-level string value of an object input — the candidate routing keys. */
|
|
353
|
+
function stringFieldsOf(input) {
|
|
354
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
355
|
+
return [];
|
|
356
|
+
const out = [];
|
|
357
|
+
for (const value of Object.values(input)) {
|
|
358
|
+
if (typeof value === 'string' && value.length > 0)
|
|
359
|
+
out.push(value);
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The source that owns a source-prefixed id: `<sourceId>` itself, or anything
|
|
365
|
+
* under `<sourceId>:`. The LONGEST matching source wins, so
|
|
366
|
+
* `native:reolink:onboard` beats a hypothetical `native:reolink`.
|
|
367
|
+
*/
|
|
368
|
+
function ownerOf(claims, input) {
|
|
369
|
+
let best = null;
|
|
370
|
+
for (const value of stringFieldsOf(input)) {
|
|
371
|
+
for (const [sourceId, source] of claims.claimed) {
|
|
372
|
+
if (value !== sourceId && !value.startsWith(`${sourceId}:`))
|
|
373
|
+
continue;
|
|
374
|
+
if (best === null || sourceId.length > best.sourceId.length)
|
|
375
|
+
best = { source, sourceId };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return best;
|
|
379
|
+
}
|
|
380
|
+
async function dispatchScalar(sources, method, input, capName, deviceId, logger) {
|
|
381
|
+
const claims = await claimsOf(sources, capName, deviceId, logger);
|
|
382
|
+
const owner = ownerOf(claims, input);
|
|
383
|
+
if (owner !== null)
|
|
384
|
+
return await callSource(owner.source, method, input, capName, deviceId);
|
|
385
|
+
const fallback = claims.unknownClaims.length === 1 ? claims.unknownClaims[0] : undefined;
|
|
386
|
+
if (fallback !== undefined) {
|
|
387
|
+
logger?.warn(`${capName}.${method}: no source claimed this id — using the one source that could not name its ids`, {
|
|
388
|
+
tags: { deviceId, addonId: fallback.addonId },
|
|
389
|
+
meta: { capability: capName, method },
|
|
390
|
+
});
|
|
391
|
+
return await callSource(fallback, method, input, capName, deviceId);
|
|
392
|
+
}
|
|
393
|
+
const known = [...claims.claimed.keys()];
|
|
394
|
+
logger?.warn(`${capName}.${method}: refused — no source of this device claims the id`, {
|
|
395
|
+
tags: { deviceId },
|
|
396
|
+
meta: { capability: capName, method, knownSources: known, input },
|
|
397
|
+
});
|
|
398
|
+
throw new server_1.TRPCError({
|
|
399
|
+
code: 'BAD_REQUEST',
|
|
400
|
+
message: `Capability "${capName}.${method}": no source of device ${deviceId} claims ` +
|
|
401
|
+
`${JSON.stringify(input)}. Sources: ${known.length > 0 ? known.join(', ') : '(none named)'}. ` +
|
|
402
|
+
`An unclaimed id is refused rather than answered by another source.`,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Build the per-device COLLECTION dispatcher for `capName` — the collection
|
|
407
|
+
* twin of `requireDeviceScoped`, and the provider object the cap router's
|
|
408
|
+
* `device-scoped` mount resolves when the cap's mode is `collection`.
|
|
409
|
+
*
|
|
410
|
+
* Returns null without a registry (the router then surfaces
|
|
411
|
+
* PRECONDITION_FAILED), exactly as the singleton dispatcher does.
|
|
412
|
+
*/
|
|
413
|
+
function requireDeviceCollection(registry, capName, logger) {
|
|
414
|
+
if (!registry)
|
|
415
|
+
return null;
|
|
416
|
+
const dispatcher = new Proxy({}, {
|
|
417
|
+
get(_target, prop) {
|
|
418
|
+
if (typeof prop !== 'string')
|
|
419
|
+
return undefined;
|
|
420
|
+
return async (input) => {
|
|
421
|
+
const deviceId = input !== null && typeof input === 'object' ? Reflect.get(input, 'deviceId') : undefined;
|
|
422
|
+
if (typeof deviceId !== 'number') {
|
|
423
|
+
throw new server_1.TRPCError({
|
|
424
|
+
code: 'BAD_REQUEST',
|
|
425
|
+
message: `${capName}.${prop}: input must carry numeric "deviceId"`,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
const all = sourcesForDevice(registry, capName, deviceId);
|
|
429
|
+
if (all.length === 0) {
|
|
430
|
+
throw new server_1.TRPCError({
|
|
431
|
+
code: 'PRECONDITION_FAILED',
|
|
432
|
+
message: `Capability "${capName}" not registered for device ${deviceId}`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
// The LISTING sees every source the device has, switched off or not
|
|
436
|
+
// — that row is the only control the operator has to switch one back
|
|
437
|
+
// on. Everything else sees the SERVING set.
|
|
438
|
+
const listing = prop === exports.SOURCE_LISTING_METHOD;
|
|
439
|
+
const sources = listing ? all : all.filter((source) => !source.switchedOff);
|
|
440
|
+
if (sources.length === 0) {
|
|
441
|
+
// Every source is switched off. That is an operator's decision and
|
|
442
|
+
// not a missing provider, so it must not wear PRECONDITION_FAILED's
|
|
443
|
+
// "not registered" — the rows say which it is, and the answer to
|
|
444
|
+
// "what clips does it have" is honestly none (D556 left this open).
|
|
445
|
+
logger?.info(`${capName}.${prop}: every source of this device is switched off`, {
|
|
446
|
+
tags: { deviceId },
|
|
447
|
+
meta: { capability: capName, method: prop, sources: all.map((s) => s.addonId) },
|
|
448
|
+
});
|
|
449
|
+
if (isFanOutMethod(registry, capName, prop))
|
|
450
|
+
return [];
|
|
451
|
+
throw new server_1.TRPCError({
|
|
452
|
+
code: 'BAD_REQUEST',
|
|
453
|
+
message: `Capability "${capName}.${prop}": every source of device ${deviceId} is ` +
|
|
454
|
+
`switched off for this camera. Switch one back on to serve it.`,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const first = sources[0];
|
|
458
|
+
if (isFanOutMethod(registry, capName, prop)) {
|
|
459
|
+
// One source is not a merge: its rows travel verbatim, in its own
|
|
460
|
+
// order, so a camera with one provider answers exactly as it did
|
|
461
|
+
// before this dispatcher existed.
|
|
462
|
+
if (sources.length === 1 && first !== undefined) {
|
|
463
|
+
// One PROVIDER is not a merge — but it may still serve several
|
|
464
|
+
// SOURCES, and the operator may have switched one of them off.
|
|
465
|
+
// So the rows are finished (stamped, and filtered by source)
|
|
466
|
+
// even here; with no deny entry that is the identity function,
|
|
467
|
+
// so an untouched camera answers exactly as it did before
|
|
468
|
+
// (D554 decision 6).
|
|
469
|
+
const answered = await callSource(first, prop, input, capName, deviceId);
|
|
470
|
+
if (!Array.isArray(answered))
|
|
471
|
+
return answered;
|
|
472
|
+
return finishRows(registry, capName, deviceId, first, answered, prop === exports.SOURCE_LISTING_METHOD);
|
|
473
|
+
}
|
|
474
|
+
return await fanOut(registry, sources, prop, input, capName, deviceId, logger);
|
|
475
|
+
}
|
|
476
|
+
if (sources.length === 1 && first !== undefined) {
|
|
477
|
+
return await callSource(first, prop, input, capName, deviceId);
|
|
478
|
+
}
|
|
479
|
+
return await dispatchScalar(sources, prop, input, capName, deviceId, logger);
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
return dispatcher;
|
|
484
|
+
}
|
|
@@ -78,7 +78,7 @@ function mountAllCaps(services) {
|
|
|
78
78
|
alarmPanel: (0, generated_cap_routers_js_1.createCapRouter_alarmPanel)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'alarm-panel'), remoteCapProxy),
|
|
79
79
|
alerts: (0, generated_cap_routers_js_1.createCapRouter_alerts)((_ctx) => reg?.getSingleton('alerts') ?? null, remoteCapProxy),
|
|
80
80
|
ambientLightSensor: (0, generated_cap_routers_js_1.createCapRouter_ambientLightSensor)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'ambient-light-sensor'), remoteCapProxy),
|
|
81
|
-
audioAnalysis: (0, generated_cap_routers_js_1.createCapRouter_audioAnalysis)((_ctx) => reg
|
|
81
|
+
audioAnalysis: (0, generated_cap_routers_js_1.createCapRouter_audioAnalysis)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'audio-analysis'), remoteCapProxy),
|
|
82
82
|
audioAnalyzer: (0, generated_cap_routers_js_1.createCapRouter_audioAnalyzer)((_ctx) => reg?.getSingleton('audio-analyzer') ?? null, remoteCapProxy),
|
|
83
83
|
audioCodec: (0, generated_cap_routers_js_1.createCapRouter_audioCodec)((_ctx) => reg?.getSingleton('audio-codec') ?? null, remoteCapProxy),
|
|
84
84
|
audioMetrics: (0, generated_cap_routers_js_1.createCapRouter_audioMetrics)((_ctx) => reg?.getSingleton('audio-metrics') ?? null, remoteCapProxy),
|
|
@@ -107,8 +107,8 @@ function mountAllCaps(services) {
|
|
|
107
107
|
button: (0, generated_cap_routers_js_1.createCapRouter_button)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'button'), remoteCapProxy),
|
|
108
108
|
cameraCredentials: (0, generated_cap_routers_js_1.createCapRouter_cameraCredentials)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'camera-credentials'), remoteCapProxy),
|
|
109
109
|
cameraGridLayout: (0, generated_cap_routers_js_1.createCapRouter_cameraGridLayout)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'camera-grid-layout'), remoteCapProxy),
|
|
110
|
-
cameraPipelineConfig: (0, generated_cap_routers_js_1.createCapRouter_cameraPipelineConfig)((_ctx) => reg
|
|
111
|
-
cameraStreams: (0, generated_cap_routers_js_1.createCapRouter_cameraStreams)((_ctx) => reg
|
|
110
|
+
cameraPipelineConfig: (0, generated_cap_routers_js_1.createCapRouter_cameraPipelineConfig)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'camera-pipeline-config'), remoteCapProxy),
|
|
111
|
+
cameraStreams: (0, generated_cap_routers_js_1.createCapRouter_cameraStreams)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'camera-streams'), remoteCapProxy),
|
|
112
112
|
carbonMonoxide: (0, generated_cap_routers_js_1.createCapRouter_carbonMonoxide)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'carbon-monoxide'), remoteCapProxy),
|
|
113
113
|
climateControl: (0, generated_cap_routers_js_1.createCapRouter_climateControl)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'climate-control'), remoteCapProxy),
|
|
114
114
|
color: (0, generated_cap_routers_js_1.createCapRouter_color)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'color'), remoteCapProxy),
|
|
@@ -162,7 +162,7 @@ function mountAllCaps(services) {
|
|
|
162
162
|
}, remoteCapProxy),
|
|
163
163
|
dayNight: (0, generated_cap_routers_js_1.createCapRouter_dayNight)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'day-night'), remoteCapProxy),
|
|
164
164
|
decoder: (0, generated_cap_routers_js_1.createCapRouter_decoder)((_ctx) => reg?.getSingleton('decoder') ?? null, remoteCapProxy),
|
|
165
|
-
detectionPipeline: (0, generated_cap_routers_js_1.createCapRouter_detectionPipeline)((_ctx) => reg
|
|
165
|
+
detectionPipeline: (0, generated_cap_routers_js_1.createCapRouter_detectionPipeline)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'detection-pipeline'), remoteCapProxy),
|
|
166
166
|
deviceAdoption: (0, generated_cap_routers_js_1.createCapRouter_deviceAdoption)((_ctx) => reg?.getSingleton('device-adoption') ??
|
|
167
167
|
null, remoteCapProxy),
|
|
168
168
|
deviceDiscovery: (0, generated_cap_routers_js_1.createCapRouter_deviceDiscovery)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'device-discovery'), remoteCapProxy),
|
|
@@ -337,8 +337,7 @@ function mountAllCaps(services) {
|
|
|
337
337
|
modelDistributor: (0, generated_cap_routers_js_1.createCapRouter_modelDistributor)((_ctx) => reg?.getSingleton('model-distributor') ??
|
|
338
338
|
null, remoteCapProxy),
|
|
339
339
|
motion: (0, generated_cap_routers_js_1.createCapRouter_motion)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'motion'), remoteCapProxy),
|
|
340
|
-
motionDetection: (0, generated_cap_routers_js_1.createCapRouter_motionDetection)((_ctx) => reg
|
|
341
|
-
null, remoteCapProxy),
|
|
340
|
+
motionDetection: (0, generated_cap_routers_js_1.createCapRouter_motionDetection)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'motion-detection'), remoteCapProxy),
|
|
342
341
|
motionTrigger: (0, generated_cap_routers_js_1.createCapRouter_motionTrigger)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'motion-trigger'), remoteCapProxy),
|
|
343
342
|
motionZones: (0, generated_cap_routers_js_1.createCapRouter_motionZones)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'motion-zones'), remoteCapProxy),
|
|
344
343
|
mqttBroker: (0, generated_cap_routers_js_1.createCapRouter_mqttBroker)((_ctx, addonId) => {
|
|
@@ -412,7 +411,7 @@ function mountAllCaps(services) {
|
|
|
412
411
|
osd: (0, generated_cap_routers_js_1.createCapRouter_osd)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'osd'), remoteCapProxy),
|
|
413
412
|
osdManager: (0, generated_cap_routers_js_1.createCapRouter_osdManager)((_ctx) => reg?.getSingleton('osd-manager') ?? null, remoteCapProxy),
|
|
414
413
|
petFeeder: (0, generated_cap_routers_js_1.createCapRouter_petFeeder)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pet-feeder'), remoteCapProxy),
|
|
415
|
-
pipelineAnalytics: (0, generated_cap_routers_js_1.createCapRouter_pipelineAnalytics)((_ctx) => reg
|
|
414
|
+
pipelineAnalytics: (0, generated_cap_routers_js_1.createCapRouter_pipelineAnalytics)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pipeline-analytics'), remoteCapProxy),
|
|
416
415
|
pipelineExecutor: (0, generated_cap_routers_js_1.createCapRouter_pipelineExecutor)((_ctx) => reg?.getSingleton('pipeline-executor') ??
|
|
417
416
|
null, remoteCapProxy),
|
|
418
417
|
pipelineOrchestrator: (0, generated_cap_routers_js_1.createCapRouter_pipelineOrchestrator)((_ctx) => reg?.getSingleton('pipeline-orchestrator') ?? null, remoteCapProxy),
|
|
@@ -431,7 +430,7 @@ function mountAllCaps(services) {
|
|
|
431
430
|
recordingExport: (0, generated_cap_routers_js_1.createCapRouter_recordingExport)((_ctx) => reg?.getSingleton('recording-export') ??
|
|
432
431
|
null, remoteCapProxy),
|
|
433
432
|
recordingSignal: (0, generated_cap_routers_js_1.createCapRouter_recordingSignal)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'recording-signal'), remoteCapProxy),
|
|
434
|
-
sceneMonitor: (0, generated_cap_routers_js_1.createCapRouter_sceneMonitor)((_ctx) => reg
|
|
433
|
+
sceneMonitor: (0, generated_cap_routers_js_1.createCapRouter_sceneMonitor)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'scene-monitor'), remoteCapProxy),
|
|
435
434
|
scriptRunner: (0, generated_cap_routers_js_1.createCapRouter_scriptRunner)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'script-runner'), remoteCapProxy),
|
|
436
435
|
serverManagement: (0, generated_cap_routers_js_1.createCapRouter_serverManagement)((_ctx) => reg?.getSingleton('server-management') ??
|
|
437
436
|
null, remoteCapProxy),
|
|
@@ -542,7 +541,7 @@ function mountAllCaps(services) {
|
|
|
542
541
|
valve: (0, generated_cap_routers_js_1.createCapRouter_valve)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'valve'), remoteCapProxy),
|
|
543
542
|
vectorStore: (0, generated_cap_routers_js_1.createCapRouter_vectorStore)((_ctx) => reg?.getSingleton('vector-store') ?? null, remoteCapProxy),
|
|
544
543
|
vibration: (0, generated_cap_routers_js_1.createCapRouter_vibration)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'vibration'), remoteCapProxy),
|
|
545
|
-
videoclips: (0, generated_cap_routers_js_1.createCapRouter_videoclips)((_ctx) => reg
|
|
544
|
+
videoclips: (0, generated_cap_routers_js_1.createCapRouter_videoclips)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'videoclips'), remoteCapProxy),
|
|
546
545
|
viewerUi: (0, generated_cap_routers_js_1.createCapRouter_viewerUi)((_ctx) => reg?.getSingleton('viewer-ui') ?? null, remoteCapProxy),
|
|
547
546
|
waterHeater: (0, generated_cap_routers_js_1.createCapRouter_waterHeater)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'water-heater'), remoteCapProxy),
|
|
548
547
|
weather: (0, generated_cap_routers_js_1.createCapRouter_weather)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'weather'), remoteCapProxy),
|
|
@@ -10733,7 +10733,7 @@ function createCapRouter_vibration(getProvider, createRemoteProxy) {
|
|
|
10733
10733
|
}
|
|
10734
10734
|
function createCapRouter_videoclips(getProvider, createRemoteProxy) {
|
|
10735
10735
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
10736
|
-
listClips: trpc_middleware_js_1.
|
|
10736
|
+
listClips: trpc_middleware_js_1.protectedProcedure
|
|
10737
10737
|
.input(types_126.videoclipsCapability.methods.listClips.input.loose())
|
|
10738
10738
|
.output(types_126.videoclipsCapability.methods.listClips.output)
|
|
10739
10739
|
.query(async ({ input, ctx }) => {
|
|
@@ -10742,7 +10742,16 @@ function createCapRouter_videoclips(getProvider, createRemoteProxy) {
|
|
|
10742
10742
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
10743
10743
|
return p.listClips(methodInput);
|
|
10744
10744
|
}),
|
|
10745
|
-
|
|
10745
|
+
listSources: trpc_middleware_js_1.protectedProcedure
|
|
10746
|
+
.input(types_126.videoclipsCapability.methods.listSources.input.loose())
|
|
10747
|
+
.output(types_126.videoclipsCapability.methods.listSources.output)
|
|
10748
|
+
.query(async ({ input, ctx }) => {
|
|
10749
|
+
const { nodeId, ...methodInput } = input;
|
|
10750
|
+
const p = resolveProvider('videoclips', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
10751
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
10752
|
+
return p.listSources(methodInput);
|
|
10753
|
+
}),
|
|
10754
|
+
getClipPlayback: trpc_middleware_js_1.protectedProcedure
|
|
10746
10755
|
.input(types_126.videoclipsCapability.methods.getClipPlayback.input.loose())
|
|
10747
10756
|
.output(types_126.videoclipsCapability.methods.getClipPlayback.output)
|
|
10748
10757
|
.query(async ({ input, ctx }) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.355",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
39
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
38
|
+
"@camstack/addon-admin-ui": "1.2.244",
|
|
39
|
+
"@camstack/addon-agent-ui": "1.2.132",
|
|
40
40
|
"@camstack/addon-auth": "1.2.123",
|
|
41
41
|
"@camstack/addon-decoder-nodeav": "1.2.115",
|
|
42
42
|
"@camstack/addon-notifiers": "1.2.119",
|
|
43
|
-
"@camstack/addon-pipeline": "1.2.
|
|
43
|
+
"@camstack/addon-pipeline": "1.2.273",
|
|
44
44
|
"@camstack/addon-pipeline-orchestrator": "1.2.231",
|
|
45
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
45
|
+
"@camstack/addon-post-analysis": "1.2.272",
|
|
46
46
|
"@camstack/sdk": "1.2.118",
|
|
47
|
-
"@camstack/system": "1.2.
|
|
48
|
-
"@camstack/types": "1.2.
|
|
49
|
-
"@camstack/ui-library": "1.2.
|
|
47
|
+
"@camstack/system": "1.2.276",
|
|
48
|
+
"@camstack/types": "1.2.234",
|
|
49
|
+
"@camstack/ui-library": "1.2.190",
|
|
50
50
|
"@fastify/compress": "^9.0.0",
|
|
51
51
|
"@fastify/cookie": "^11.0.2",
|
|
52
52
|
"@fastify/cors": "^11.2.0",
|