@camstack/server 1.2.376 → 1.2.378
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.
|
@@ -56,6 +56,7 @@ exports.isCompressibleAsset = isCompressibleAsset;
|
|
|
56
56
|
exports.resolvePrecompressedAsset = resolvePrecompressedAsset;
|
|
57
57
|
exports.warmPrecompressedAssets = warmPrecompressedAssets;
|
|
58
58
|
exports.resetPrecompressedWarning = resetPrecompressedWarning;
|
|
59
|
+
exports.precompressedTempPath = precompressedTempPath;
|
|
59
60
|
exports.precompressedSiblingExists = precompressedSiblingExists;
|
|
60
61
|
const fs = __importStar(require("node:fs"));
|
|
61
62
|
const fsp = __importStar(require("node:fs/promises"));
|
|
@@ -113,7 +114,7 @@ async function compressToDisk(filePath, brPath, log) {
|
|
|
113
114
|
});
|
|
114
115
|
// Write via a temp file + rename so a concurrent reader never sees a
|
|
115
116
|
// half-written body.
|
|
116
|
-
const tmp =
|
|
117
|
+
const tmp = precompressedTempPath(brPath);
|
|
117
118
|
await fsp.writeFile(tmp, out);
|
|
118
119
|
await fsp.rename(tmp, brPath);
|
|
119
120
|
log?.('precompressed a static asset', {
|
|
@@ -215,6 +216,15 @@ async function listCompressible(dir, out = []) {
|
|
|
215
216
|
function resetPrecompressedWarning() {
|
|
216
217
|
warnedUnwritable = false;
|
|
217
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Where a compression is staged before the rename. Exported so the test can
|
|
221
|
+
* make THIS write fail for any uid: a `chmod 555` on the dist does nothing to
|
|
222
|
+
* root, which is what CI runs as, and the test that relied on it produced a
|
|
223
|
+
* `.br` in a "read-only" directory (2026-09-23).
|
|
224
|
+
*/
|
|
225
|
+
function precompressedTempPath(brPath) {
|
|
226
|
+
return `${brPath}.${String(process.pid)}.tmp`;
|
|
227
|
+
}
|
|
218
228
|
/** Synchronous existence probe used by the callers' own guards. */
|
|
219
229
|
function precompressedSiblingExists(filePath) {
|
|
220
230
|
return fs.existsSync(`${filePath}.br`);
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
64
|
exports.SOURCE_LISTING_METHOD = void 0;
|
|
65
65
|
exports.requireDeviceCollection = requireDeviceCollection;
|
|
66
|
+
const system_1 = require("@camstack/system");
|
|
66
67
|
const types_1 = require("@camstack/types");
|
|
67
68
|
const server_1 = require("@trpc/server");
|
|
68
69
|
/**
|
|
@@ -232,10 +233,32 @@ async function callSource(source, method, input, capName, deviceId) {
|
|
|
232
233
|
if (typeof fn !== 'function') {
|
|
233
234
|
throw new server_1.TRPCError({
|
|
234
235
|
code: 'NOT_IMPLEMENTED',
|
|
235
|
-
message:
|
|
236
|
+
message: notImplementedMessage(capName, source.addonId, deviceId, method),
|
|
236
237
|
});
|
|
237
238
|
}
|
|
238
|
-
|
|
239
|
+
// …and that check is VACUOUS for a forked provider: the UDS proxy fabricates
|
|
240
|
+
// a callable for every property, so the absence is only observable on the
|
|
241
|
+
// child. `child-cap-dispatch` answers a method declared `optionalMethod`
|
|
242
|
+
// with a prefixed message, which becomes the SAME refusal here — otherwise
|
|
243
|
+
// an honest "this source has no file behind it" arrives as
|
|
244
|
+
// INTERNAL_SERVER_ERROR and reads as a method somebody forgot.
|
|
245
|
+
try {
|
|
246
|
+
return await fn.call(source.provider, input);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
if (errMessage(err).includes(system_1.CAP_METHOD_NOT_IMPLEMENTED)) {
|
|
250
|
+
throw new server_1.TRPCError({
|
|
251
|
+
code: 'NOT_IMPLEMENTED',
|
|
252
|
+
message: notImplementedMessage(capName, source.addonId, deviceId, method),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** One sentence for both arms, so the two paths cannot describe the same fact
|
|
259
|
+
* differently. */
|
|
260
|
+
function notImplementedMessage(capName, addonId, deviceId, method) {
|
|
261
|
+
return `Capability "${capName}" source "${addonId}" for device ${deviceId} does not implement "${method}"`;
|
|
239
262
|
}
|
|
240
263
|
/**
|
|
241
264
|
* The `startMs` a row is ordered by, or null when the row does not declare a
|
|
@@ -323,7 +346,7 @@ async function fanOut(sources, method, input, capName, deviceId, logger) {
|
|
|
323
346
|
});
|
|
324
347
|
continue;
|
|
325
348
|
}
|
|
326
|
-
rows.push(...finishRows(source, result.value, isSourceListing));
|
|
349
|
+
rows.push(...finishRows(source, result.value, isSourceListing, capName, deviceId, logger));
|
|
327
350
|
}
|
|
328
351
|
return mergeRows(rows);
|
|
329
352
|
}
|
|
@@ -345,7 +368,7 @@ async function fanOut(sources, method, input, capName, deviceId, logger) {
|
|
|
345
368
|
* filters any more; the filtering happened when {@link sourcesForDevice} read
|
|
346
369
|
* the bindings.
|
|
347
370
|
*/
|
|
348
|
-
function finishRows(source, rows, isSourceListing) {
|
|
371
|
+
function finishRows(source, rows, isSourceListing, capName, deviceId, logger) {
|
|
349
372
|
if (!isSourceListing)
|
|
350
373
|
return rows;
|
|
351
374
|
const out = [];
|
|
@@ -354,6 +377,25 @@ function finishRows(source, rows, isSourceListing) {
|
|
|
354
377
|
out.push(row);
|
|
355
378
|
continue;
|
|
356
379
|
}
|
|
380
|
+
const id = Reflect.get(row, SOURCE_ID_FIELD);
|
|
381
|
+
if (typeof id === 'string') {
|
|
382
|
+
const owner = (0, types_1.reservedSourceOwner)(capName, id);
|
|
383
|
+
if (owner !== null && owner !== source.addonId) {
|
|
384
|
+
// A RESERVED id (D625) is the DEFAULT of a picker, expressed as a
|
|
385
|
+
// value rather than as a sort order. A second row spelling it would
|
|
386
|
+
// put that default back behind the fan-out order — the
|
|
387
|
+
// `?? sources[0]` accident the reservation exists to remove. The row
|
|
388
|
+
// goes and the addon's own rows stay: it may well have real sources,
|
|
389
|
+
// and emptying a camera's picker over one bad row is worse than the
|
|
390
|
+
// bad row. The guard in `scripts/` refuses the same spelling at build
|
|
391
|
+
// time; this is what happens when an old build reaches a new hub.
|
|
392
|
+
logger?.warn(`${capName}.${exports.SOURCE_LISTING_METHOD}: source id "${id}" is RESERVED for "${owner}" — this addon's row is dropped`, {
|
|
393
|
+
tags: { deviceId, addonId: source.addonId },
|
|
394
|
+
meta: { capability: capName, sourceId: id, reservedFor: owner },
|
|
395
|
+
});
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
357
399
|
out.push({ ...row, [SOURCE_ADDON_FIELD]: source.addonId });
|
|
358
400
|
}
|
|
359
401
|
return out;
|
|
@@ -508,7 +550,7 @@ function requireDeviceCollection(registry, capName, logger) {
|
|
|
508
550
|
const answered = await callSource(first, prop, input, capName, deviceId);
|
|
509
551
|
if (!Array.isArray(answered))
|
|
510
552
|
return answered;
|
|
511
|
-
return finishRows(first, answered, prop === exports.SOURCE_LISTING_METHOD);
|
|
553
|
+
return finishRows(first, answered, prop === exports.SOURCE_LISTING_METHOD, capName, deviceId, logger);
|
|
512
554
|
}
|
|
513
555
|
return await fanOut(sources, prop, input, capName, deviceId, logger);
|
|
514
556
|
}
|
|
@@ -218,7 +218,6 @@ function mountAllCaps(services) {
|
|
|
218
218
|
}, remoteCapProxy),
|
|
219
219
|
enumSensor: (0, generated_cap_routers_js_1.createCapRouter_enumSensor)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'enum-sensor'), remoteCapProxy),
|
|
220
220
|
eventEmitter: (0, generated_cap_routers_js_1.createCapRouter_eventEmitter)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'event-emitter'), remoteCapProxy),
|
|
221
|
-
events: (0, generated_cap_routers_js_1.createCapRouter_events)((_ctx) => reg?.getSingleton('events') ?? null, remoteCapProxy),
|
|
222
221
|
faceGallery: (0, generated_cap_routers_js_1.createCapRouter_faceGallery)((_ctx) => reg?.getSingleton('face-gallery') ?? null, remoteCapProxy),
|
|
223
222
|
failureContribution: (0, generated_cap_routers_js_1.createCapRouter_failureContribution)((_ctx, addonId) => {
|
|
224
223
|
if (!reg)
|
|
@@ -428,7 +427,9 @@ function mountAllCaps(services) {
|
|
|
428
427
|
ptz: (0, generated_cap_routers_js_1.createCapRouter_ptz)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'ptz'), remoteCapProxy),
|
|
429
428
|
ptzAutotrack: (0, generated_cap_routers_js_1.createCapRouter_ptzAutotrack)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'ptz-autotrack'), remoteCapProxy),
|
|
430
429
|
reboot: (0, generated_cap_routers_js_1.createCapRouter_reboot)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'reboot'), remoteCapProxy),
|
|
431
|
-
recording: (0, generated_cap_routers_js_1.createCapRouter_recording)((_ctx) => reg
|
|
430
|
+
recording: (0, generated_cap_routers_js_1.createCapRouter_recording)((_ctx) => (0, device_collection_dispatch_js_1.requireDeviceCollection)(reg, 'recording'), remoteCapProxy),
|
|
431
|
+
recordingArchive: (0, generated_cap_routers_js_1.createCapRouter_recordingArchive)((_ctx) => reg?.getSingleton('recording-archive') ??
|
|
432
|
+
null, remoteCapProxy),
|
|
432
433
|
recordingExport: (0, generated_cap_routers_js_1.createCapRouter_recordingExport)((_ctx) => reg?.getSingleton('recording-export') ??
|
|
433
434
|
null, remoteCapProxy),
|
|
434
435
|
recordingOnboard: (0, generated_cap_routers_js_1.createCapRouter_recordingOnboard)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'recording-onboard'), remoteCapProxy),
|