@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 = `${brPath}.${String(process.pid)}.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: `Capability "${capName}" source "${source.addonId}" for device ${deviceId} does not implement "${method}"`,
236
+ message: notImplementedMessage(capName, source.addonId, deviceId, method),
236
237
  });
237
238
  }
238
- return await fn.call(source.provider, input);
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?.getSingleton('recording') ?? null, remoteCapProxy),
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),