@camstack/system 1.2.276 → 1.2.278

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.
Files changed (62) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/camera-grid/index.js +1 -1
  12. package/dist/builtins/camera-grid/index.mjs +1 -1
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  17. package/dist/builtins/device-manager/device-bindings-store.d.ts +10 -0
  18. package/dist/builtins/device-manager/device-manager.addon.d.ts +0 -2
  19. package/dist/builtins/device-manager/device-manager.addon.js +63 -118
  20. package/dist/builtins/device-manager/device-manager.addon.mjs +64 -116
  21. package/dist/builtins/device-manager/device-meta-types.d.ts +15 -27
  22. package/dist/builtins/device-manager/device-source-activation.d.ts +9 -43
  23. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  24. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  25. package/dist/builtins/hub-forwarder/index.js +1 -1
  26. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  27. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  28. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  29. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  30. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  31. package/dist/builtins/local-network/local-network.addon.js +1 -1
  32. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  33. package/dist/builtins/loki-logging/index.js +1 -1
  34. package/dist/builtins/loki-logging/index.mjs +1 -1
  35. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  36. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  37. package/dist/builtins/platform-probe/index.js +1 -1
  38. package/dist/builtins/platform-probe/index.mjs +1 -1
  39. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  40. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  41. package/dist/builtins/snapshot/index.js +1 -1
  42. package/dist/builtins/snapshot/index.mjs +1 -1
  43. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  44. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  45. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  46. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  47. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  48. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  49. package/dist/builtins/system-config/system-config.addon.js +1 -1
  50. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  51. package/dist/builtins/winston-logging/index.js +1 -1
  52. package/dist/builtins/winston-logging/index.mjs +1 -1
  53. package/dist/{dist-D_OlSf31.js → dist-BWz_gZ5l.js} +145 -85
  54. package/dist/{dist-AHdiIdJH.mjs → dist-D1atncyi.mjs} +145 -85
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.js +14 -67
  57. package/dist/index.mjs +16 -66
  58. package/dist/kernel/capability-registry.d.ts +12 -42
  59. package/dist/kernel/index.d.ts +0 -1
  60. package/dist/{retired-settings-keys-LwFyTKun.js → retired-settings-keys-Ek9MPzCi.js} +1 -1
  61. package/dist/{retired-settings-keys-Cp3oG_aH.mjs → retired-settings-keys-rG_QVMT3.mjs} +1 -1
  62. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D_OlSf31.js");
6
+ const require_dist = require("../../dist-BWz_gZ5l.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Fn as EventCategory, P as addonPagesCapability, cn as BaseAddon, on as errMsg } from "../../dist-AHdiIdJH.mjs";
1
+ import { Fn as EventCategory, P as addonPagesCapability, cn as BaseAddon, on as errMsg } from "../../dist-D1atncyi.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D_OlSf31.js");
6
+ const require_dist = require("../../dist-BWz_gZ5l.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Fn as EventCategory, I as addonWidgetsCapability, cn as BaseAddon, on as errMsg } from "../../dist-AHdiIdJH.mjs";
1
+ import { Fn as EventCategory, I as addonWidgetsCapability, cn as BaseAddon, on as errMsg } from "../../dist-D1atncyi.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D_OlSf31.js");
6
+ const require_dist = require("../../dist-BWz_gZ5l.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Fn as EventCategory, L as alertsCapability, cn as BaseAddon, n as AlertSchema, on as errMsg, xn as createEvent } from "../../dist-AHdiIdJH.mjs";
1
+ import { Fn as EventCategory, L as alertsCapability, cn as BaseAddon, n as AlertSchema, on as errMsg, xn as createEvent } from "../../dist-D1atncyi.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D_OlSf31.js");
6
+ const require_dist = require("../../dist-BWz_gZ5l.js");
7
7
  //#region src/builtins/autotrack/autotrack-config.ts
8
8
  var DEFAULT_AUTOTRACK_LOOP_CONFIG = {
9
9
  deadZoneFraction: .5,
@@ -1,4 +1,4 @@
1
- import { B as bareAddonId, Dn as isEvent, Fn as EventCategory, Lt as ptzAutotrackCapability, cn as BaseAddon, h as FIRST_LEVEL_MACRO_CLASSES, on as errMsg } from "../../dist-AHdiIdJH.mjs";
1
+ import { B as bareAddonId, Dn as isEvent, Fn as EventCategory, Lt as ptzAutotrackCapability, cn as BaseAddon, h as FIRST_LEVEL_MACRO_CLASSES, on as errMsg } from "../../dist-D1atncyi.mjs";
2
2
  //#region src/builtins/autotrack/autotrack-config.ts
3
3
  var DEFAULT_AUTOTRACK_LOOP_CONFIG = {
4
4
  deadZoneFraction: .5,
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D_OlSf31.js");
6
+ const require_dist = require("../../dist-BWz_gZ5l.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let buffer = require("buffer");
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { Dt as mayWriteToLocation, Fn as EventCategory, Qt as storageOccupancyCapability, cn as BaseAddon, z as backupCapability } from "../../dist-AHdiIdJH.mjs";
1
+ import { Dt as mayWriteToLocation, Fn as EventCategory, Qt as storageOccupancyCapability, cn as BaseAddon, z as backupCapability } from "../../dist-D1atncyi.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { Buffer as Buffer$1 } from "buffer";
4
4
  import * as fsp from "node:fs/promises";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D_OlSf31.js");
6
+ const require_dist = require("../../dist-BWz_gZ5l.js");
7
7
  let zod = require("zod");
8
8
  let _camstack_types_node = require("@camstack/types/node");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { At as motionCapability, Fn as EventCategory, Tn as hydrateSchema, U as cameraGridLayoutCapability, Y as declarationOwnerNodeId, Yt as snapshotCapability, b as MOTION_CLOSE_AFTER_MS, cn as BaseAddon, en as streamCatalogCapability, f as DERIVED_CAMERA_SILENCED_CAP_NAMES, fn as DeviceFeature, i as BaseDevice, mn as DeviceType, on as errMsg, p as DeclaredDevices, sn as buildFfmpegArgs, xn as createEvent } from "../../dist-AHdiIdJH.mjs";
1
+ import { At as motionCapability, Fn as EventCategory, Tn as hydrateSchema, U as cameraGridLayoutCapability, Y as declarationOwnerNodeId, Yt as snapshotCapability, b as MOTION_CLOSE_AFTER_MS, cn as BaseAddon, en as streamCatalogCapability, f as DERIVED_CAMERA_SILENCED_CAP_NAMES, fn as DeviceFeature, i as BaseDevice, mn as DeviceType, on as errMsg, p as DeclaredDevices, sn as buildFfmpegArgs, xn as createEvent } from "../../dist-D1atncyi.mjs";
2
2
  import { z } from "zod";
3
3
  import { FfmpegProcess } from "@camstack/types/node";
4
4
  import { join } from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D_OlSf31.js");
6
+ const require_dist = require("../../dist-BWz_gZ5l.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { Ct as logDestinationCapability, cn as BaseAddon } from "../../dist-AHdiIdJH.mjs";
1
+ import { Ct as logDestinationCapability, cn as BaseAddon } from "../../dist-D1atncyi.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-D_OlSf31.js");
3
+ const require_dist = require("../../dist-BWz_gZ5l.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { G as coreBlockAddonId, K as coreBlockIdFromAddonId, c as CORE_BLOCKS_ADDON_ID, cn as BaseAddon, l as CORE_BLOCK_ADDON_PREFIX, on as errMsg, p as DeclaredDevices, q as coreBlocksCapability, u as CoreBlockSchema } from "../../dist-AHdiIdJH.mjs";
1
+ import { G as coreBlockAddonId, K as coreBlockIdFromAddonId, c as CORE_BLOCKS_ADDON_ID, cn as BaseAddon, l as CORE_BLOCK_ADDON_PREFIX, on as errMsg, p as DeclaredDevices, q as coreBlocksCapability, u as CoreBlockSchema } from "../../dist-D1atncyi.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -161,6 +161,16 @@ export type LedgerNonEmpty = () => Promise<boolean>;
161
161
  * changes nothing (D49: a read that fails must not destroy work).
162
162
  */
163
163
  export declare function capAppliesToDeviceType(def: CapabilityDefinition | undefined, deviceType: string | undefined): boolean;
164
+ /**
165
+ * An addon's identity, with the node it happens to run on stripped.
166
+ *
167
+ * A capability registered on an agent arrives under `<addonId>@<nodeId>` —
168
+ * `pipeline-analytics@little-unraid`. That is the same ADDON as the hub-local
169
+ * `pipeline-analytics`, and treating the two as different providers is what
170
+ * put `analytics` in device 592's clip picker three times (D554 amended).
171
+ * Where a provider RUNS is a routing fact; it is not part of who it is.
172
+ */
173
+ export declare function addonIdentityOf(addonId: string): string;
164
174
  export declare function getBindings(deps: BindingsDeps, input: {
165
175
  deviceId: number;
166
176
  }): Promise<{
@@ -166,8 +166,6 @@ export declare class DeviceManagerAddon extends BaseAddon {
166
166
  deviceId: number;
167
167
  capName: string;
168
168
  wrapperAddonId: string;
169
- /** The source to toggle on a collection cap. Absent = all of the addon's. */
170
- sourceId?: string;
171
169
  active: boolean;
172
170
  }): Promise<void>;
173
171
  /** The narrow dependency bag the activation verb needs (D556). */
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D_OlSf31.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-LwFyTKun.js");
6
+ const require_dist = require("../../dist-BWz_gZ5l.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-Ek9MPzCi.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
10
10
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -722,6 +722,52 @@ function capAppliesToDeviceType(def, deviceType) {
722
722
  if (!declared || declared.length === 0) return true;
723
723
  return declared.some((t) => t === deviceType);
724
724
  }
725
+ /**
726
+ * An addon's identity, with the node it happens to run on stripped.
727
+ *
728
+ * A capability registered on an agent arrives under `<addonId>@<nodeId>` —
729
+ * `pipeline-analytics@little-unraid`. That is the same ADDON as the hub-local
730
+ * `pipeline-analytics`, and treating the two as different providers is what
731
+ * put `analytics` in device 592's clip picker three times (D554 amended).
732
+ * Where a provider RUNS is a routing fact; it is not part of who it is.
733
+ */
734
+ function addonIdentityOf(addonId) {
735
+ const at = addonId.indexOf("@");
736
+ return at === -1 ? addonId : addonId.slice(0, at);
737
+ }
738
+ /**
739
+ * Every provider addon of every COLLECTION capability that applies to this
740
+ * device, one entry each.
741
+ *
742
+ * Deduped by {@link addonIdentityOf}, so the hub-local registration, the
743
+ * agent's `<addon>@<node>` one and the device's native registration of the same
744
+ * addon are ONE provider — the defect this exists to make unrepresentable.
745
+ * Gated only by the cap's declared `deviceTypes` (D4), like step 2's auto-bind.
746
+ */
747
+ function collectionBindings(deps, deviceId, deviceType) {
748
+ const registry = deps.capabilityRegistry;
749
+ if (!registry) return [];
750
+ const out = [];
751
+ for (const declared of registry.listCapabilities()) {
752
+ if (declared.mode !== "collection") continue;
753
+ if (!capAppliesToDeviceType(registry.getDefinition(declared.name), deviceType)) continue;
754
+ const nativeAddonId = registry.getNativeAddonId(declared.name, deviceId);
755
+ const seen = /* @__PURE__ */ new Set();
756
+ for (const addonId of [...declared.providers, ...nativeAddonId ? [nativeAddonId] : []]) {
757
+ const identity = addonIdentityOf(addonId);
758
+ if (identity.length === 0 || seen.has(identity)) continue;
759
+ seen.add(identity);
760
+ out.push({
761
+ capName: declared.name,
762
+ kind: "wrapped",
763
+ providerAddonId: identity,
764
+ providerNodeId: resolveWrapperNodeId(identity),
765
+ nativeAddonId: nativeAddonId === null ? "" : addonIdentityOf(nativeAddonId)
766
+ });
767
+ }
768
+ }
769
+ return out;
770
+ }
725
771
  async function getBindings(deps, input) {
726
772
  const [row, store] = await Promise.all([deps.rows.get(input.deviceId), readBindingsStore(deps)]);
727
773
  return resolveBindingsForDevice(deps, input.deviceId, row, {
@@ -760,6 +806,11 @@ async function resolveBindingsForDevice(deps, deviceId, row, pass) {
760
806
  const presence = await resolveDevicePresence(deps, row, deviceId, pass.ledgerNonEmpty);
761
807
  const entries = [];
762
808
  const seenCaps = /* @__PURE__ */ new Set();
809
+ const collectionEntries = collectionBindings(deps, deviceId, deviceType);
810
+ for (const entry of collectionEntries) {
811
+ entries.push(entry);
812
+ seenCaps.add(entry.capName);
813
+ }
763
814
  const resolveRemote = (capName) => deps.remoteNativeCaps.get(deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, deviceId);
764
815
  for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
765
816
  if (wrapperAddonId === void 0) continue;
@@ -3303,114 +3354,30 @@ async function getWireableFields(deps, input) {
3303
3354
  }
3304
3355
  //#endregion
3305
3356
  //#region src/builtins/device-manager/device-source-activation.ts
3306
- /**
3307
- * Is `source` denied for a device, given the persisted deny set?
3308
- *
3309
- * The ONE place the migration rule lives: an entry matches either the source's
3310
- * own id or the id of the addon that serves it. New writes are always source
3311
- * ids; an addon id can only be a pre-re-key entry, and it still means "every
3312
- * source this addon serves here".
3313
- */
3314
- function isSourceDenied(denied, source) {
3315
- if (denied.includes(source.addonId)) return true;
3316
- return source.sourceId !== void 0 && denied.includes(source.sourceId);
3317
- }
3318
- /**
3319
- * The deny list after one toggle. Pure, order-preserving, idempotent — a
3320
- * second "off" on an addon already off changes nothing.
3321
- */
3322
- function nextDisabledSources(current, key, active) {
3323
- if (active) return current.filter((id) => id !== key);
3324
- return current.includes(key) ? current : [...current, key];
3325
- }
3326
3357
  /** Is `capName` a COLLECTION cap — a cap whose device has several SOURCES? */
3327
3358
  function isCollectionCap(registry, capName) {
3328
3359
  return registry?.getDefinition(capName)?.mode === "collection";
3329
3360
  }
3330
3361
  /**
3331
- * The persisted deny list for one `(device, cap)`, empty when never written.
3332
- *
3333
- * Reads the current field AND the pre-re-key one, unioned: a legacy addon id
3334
- * is still an entry and still denies every source that addon serves here. The
3335
- * union is what makes the migration a read rule rather than a data rewrite —
3336
- * nothing has to walk a thousand cameras to keep an operator's decision.
3337
- */
3338
- function disabledSourcesOf(record) {
3339
- const current = record?.disabledSourceIds ?? [];
3340
- const legacy = record?.disabledSourceAddonIds ?? [];
3341
- const out = [...current];
3342
- for (const id of legacy) if (!out.includes(id)) out.push(id);
3343
- return out;
3344
- }
3345
- /**
3346
- * Push ONE device's persisted source deactivations onto the resolution path.
3347
- * The only writer of the registry's per-device source set.
3348
- */
3349
- function mirrorDeviceSources(registry, deviceId, capName, disabledAddonIds) {
3350
- registry?.setDeviceDisabledSources(String(deviceId), capName, disabledAddonIds);
3351
- }
3352
- /**
3353
- * Replay the whole persisted store onto a fresh registry.
3354
- *
3355
- * The projection is in-memory, so a hub restart starts it empty — and an empty
3356
- * deny list means "every source", which would silently switch every source the
3357
- * operator turned off back ON. That is precisely the D62 failure (an off switch
3358
- * that reports on), so the replay is not an optimisation: it is what makes the
3359
- * authority durable. Called from the device-manager's `onInitialize`, before
3360
- * any camera can be asked for its clips, and safe to call again.
3361
- */
3362
- function restoreDeviceSourceActivations(registry, store, logger) {
3363
- const deviceBindings = store.deviceBindings;
3364
- if (deviceBindings === void 0) return 0;
3365
- let restored = 0;
3366
- for (const [deviceKey, perDevice] of Object.entries(deviceBindings)) {
3367
- const deviceId = Number(deviceKey);
3368
- for (const [capName, record] of Object.entries(perDevice)) {
3369
- const disabled = disabledSourcesOf(record);
3370
- if (disabled.length === 0) continue;
3371
- registry.setDeviceDisabledSources(deviceKey, capName, disabled);
3372
- restored += 1;
3373
- logger.info("restored per-device source deactivations", {
3374
- tags: { deviceId },
3375
- meta: {
3376
- capability: capName,
3377
- disabledSourceIds: disabled
3378
- }
3379
- });
3380
- }
3381
- }
3382
- return restored;
3383
- }
3384
- /**
3385
3362
  * Activate or deactivate a wrapper for one device.
3386
3363
  *
3387
- * Singleton caps keep the shape D62 fixed: one `wrapperAddonId`, or the
3388
- * explicit `null` marker that stops `getBindings` step 2 re-binding a
3389
- * `defaultActive` wrapper. Collection caps write `disabledSourceAddonIds`
3390
- * instead — their `wrapperAddonId` is left exactly as it was, because a cap
3391
- * with several sources has no "which one" for `getBindings` to answer (D554
3392
- * context 3) and inventing one would turn a source toggle into a cap-wide off
3393
- * switch.
3364
+ * SINGLETON caps only, and that is now enforced rather than assumed: one
3365
+ * `wrapperAddonId`, or the explicit `null` "operator disabled" marker that
3366
+ * stops `getBindings` step 2 re-binding a `defaultActive` wrapper.
3367
+ *
3368
+ * A COLLECTION cap is REFUSED by name. Its providers are a derived SET
3369
+ * (`device-bindings-store.ts` step 0) — there is nothing to activate, and a
3370
+ * verb that accepted the call and wrote a store nobody reads is exactly what
3371
+ * this module shipped until 2026-09-20.
3394
3372
  */
3395
3373
  async function setWrapperActive(deps, input) {
3396
3374
  const storeKey = String(input.deviceId);
3397
- const collection = isCollectionCap(deps.capabilityRegistry, input.capName);
3398
- let mirrored = [];
3375
+ if (isCollectionCap(deps.capabilityRegistry, input.capName)) throw new Error(`setWrapperActive: "${input.capName}" is a COLLECTION capability — its providers are a derived set, not an operator choice, so there is nothing to activate. Every provider that serves this device is already one of its sources.`);
3399
3376
  await deps.withAddonStoreWriteLock(async () => {
3400
3377
  const store = await readBindingsStore(deps);
3401
3378
  const perDevice = { ...store.deviceBindings[storeKey] };
3402
3379
  const existing = perDevice[input.capName];
3403
- if (collection) {
3404
- const key = input.sourceId ?? input.wrapperAddonId;
3405
- const disabled = nextDisabledSources(disabledSourcesOf(existing), key, input.active);
3406
- mirrored = disabled;
3407
- const wrapperAddonId = existing?.wrapperAddonId;
3408
- perDevice[input.capName] = {
3409
- ...wrapperAddonId === void 0 ? {} : { wrapperAddonId },
3410
- ...disabled.length === 0 ? {} : { disabledSourceIds: disabled }
3411
- };
3412
- if (Object.keys(perDevice[input.capName] ?? {}).length === 0) delete perDevice[input.capName];
3413
- } else if (input.active) perDevice[input.capName] = {
3380
+ if (input.active) perDevice[input.capName] = {
3414
3381
  ...existing,
3415
3382
  wrapperAddonId: input.wrapperAddonId
3416
3383
  };
@@ -3426,20 +3393,6 @@ async function setWrapperActive(deps, input) {
3426
3393
  return rest;
3427
3394
  })() });
3428
3395
  });
3429
- if (collection) {
3430
- mirrorDeviceSources(deps.capabilityRegistry, input.deviceId, input.capName, mirrored);
3431
- deps.ctx.logger.info(input.active ? "source activated for device" : "source deactivated for device", {
3432
- tags: {
3433
- deviceId: input.deviceId,
3434
- addonId: input.wrapperAddonId
3435
- },
3436
- meta: {
3437
- capability: input.capName,
3438
- sourceId: input.sourceId ?? null,
3439
- disabledSourceIds: [...mirrored]
3440
- }
3441
- });
3442
- }
3443
3396
  deps.ctx.eventBus.emit({
3444
3397
  id: (0, node_crypto.randomUUID)(),
3445
3398
  timestamp: /* @__PURE__ */ new Date(),
@@ -7458,11 +7411,6 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
7458
7411
  const deviceRows = new DeviceRowStore(deviceRowBackendOf(settingsStoreApi), this.ctx.logger.child("rows"));
7459
7412
  await deviceRows.declare();
7460
7413
  this.deviceRows = deviceRows;
7461
- const capRegistryForRestore = this.capabilityRegistry;
7462
- if (capRegistryForRestore) {
7463
- const restored = restoreDeviceSourceActivations(capRegistryForRestore, await readBindingsStore({ ctx: this.ctx }), this.ctx.logger);
7464
- if (restored > 0) this.ctx.logger.info("per-device source deactivations restored", { meta: { rows: restored } });
7465
- }
7466
7414
  try {
7467
7415
  await require_retired_settings_keys.purgeRetiredSettingsRows(deviceRows.retiredRowStore(), this.ctx.logger.child("RetiredRows"));
7468
7416
  } catch (err) {
@@ -7717,8 +7665,5 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
7717
7665
  //#endregion
7718
7666
  exports.DeviceManagerAddon = DeviceManagerAddon;
7719
7667
  exports.default = DeviceManagerAddon;
7720
- exports.isSourceDenied = isSourceDenied;
7721
7668
  exports.mergeAggregates = mergeAggregates;
7722
- exports.nextDisabledSources = nextDisabledSources;
7723
- exports.restoreDeviceSourceActivations = restoreDeviceSourceActivations;
7724
7669
  exports.setWrapperActive = setWrapperActive;
@@ -1,5 +1,5 @@
1
- import { $ as deviceStateCapability, Bt as resolveContainerPrimaryChild, D as STREAM_PROFILE_META, En as isDeviceConfigCap, Fn as EventCategory, H as buildStreamParamsConfigSchema, Jt as siteZoneKey, Pn as sleep$1, Pt as parseStreamParamsFormPatch, Q as deviceManagerCapability, St as locationIconKey, Ut as runtimeStatePolicyFor, _n as WELL_KNOWN_TAB_MAP, cn as BaseAddon, et as deviceStatusCapability, fn as DeviceFeature, it as enumerateSchemaFields, jt as normalizeUnit, m as DeviceStatusSchema, mn as DeviceType, o as CAMERA_SWITCH_ORDER, on as errMsg, pn as DeviceRole, rt as enumerateItemArrayFields, s as CAP_NAMES_WITH_STATUS, t as ALL_CAPABILITY_DEFINITIONS, v as LocationIconIdSchema } from "../../dist-AHdiIdJH.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-Cp3oG_aH.mjs";
1
+ import { $ as deviceStateCapability, Bt as resolveContainerPrimaryChild, D as STREAM_PROFILE_META, En as isDeviceConfigCap, Fn as EventCategory, H as buildStreamParamsConfigSchema, Jt as siteZoneKey, Pn as sleep$1, Pt as parseStreamParamsFormPatch, Q as deviceManagerCapability, St as locationIconKey, Ut as runtimeStatePolicyFor, _n as WELL_KNOWN_TAB_MAP, cn as BaseAddon, et as deviceStatusCapability, fn as DeviceFeature, it as enumerateSchemaFields, jt as normalizeUnit, m as DeviceStatusSchema, mn as DeviceType, o as CAMERA_SWITCH_ORDER, on as errMsg, pn as DeviceRole, rt as enumerateItemArrayFields, s as CAP_NAMES_WITH_STATUS, t as ALL_CAPABILITY_DEFINITIONS, v as LocationIconIdSchema } from "../../dist-D1atncyi.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-rG_QVMT3.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -717,6 +717,52 @@ function capAppliesToDeviceType(def, deviceType) {
717
717
  if (!declared || declared.length === 0) return true;
718
718
  return declared.some((t) => t === deviceType);
719
719
  }
720
+ /**
721
+ * An addon's identity, with the node it happens to run on stripped.
722
+ *
723
+ * A capability registered on an agent arrives under `<addonId>@<nodeId>` —
724
+ * `pipeline-analytics@little-unraid`. That is the same ADDON as the hub-local
725
+ * `pipeline-analytics`, and treating the two as different providers is what
726
+ * put `analytics` in device 592's clip picker three times (D554 amended).
727
+ * Where a provider RUNS is a routing fact; it is not part of who it is.
728
+ */
729
+ function addonIdentityOf(addonId) {
730
+ const at = addonId.indexOf("@");
731
+ return at === -1 ? addonId : addonId.slice(0, at);
732
+ }
733
+ /**
734
+ * Every provider addon of every COLLECTION capability that applies to this
735
+ * device, one entry each.
736
+ *
737
+ * Deduped by {@link addonIdentityOf}, so the hub-local registration, the
738
+ * agent's `<addon>@<node>` one and the device's native registration of the same
739
+ * addon are ONE provider — the defect this exists to make unrepresentable.
740
+ * Gated only by the cap's declared `deviceTypes` (D4), like step 2's auto-bind.
741
+ */
742
+ function collectionBindings(deps, deviceId, deviceType) {
743
+ const registry = deps.capabilityRegistry;
744
+ if (!registry) return [];
745
+ const out = [];
746
+ for (const declared of registry.listCapabilities()) {
747
+ if (declared.mode !== "collection") continue;
748
+ if (!capAppliesToDeviceType(registry.getDefinition(declared.name), deviceType)) continue;
749
+ const nativeAddonId = registry.getNativeAddonId(declared.name, deviceId);
750
+ const seen = /* @__PURE__ */ new Set();
751
+ for (const addonId of [...declared.providers, ...nativeAddonId ? [nativeAddonId] : []]) {
752
+ const identity = addonIdentityOf(addonId);
753
+ if (identity.length === 0 || seen.has(identity)) continue;
754
+ seen.add(identity);
755
+ out.push({
756
+ capName: declared.name,
757
+ kind: "wrapped",
758
+ providerAddonId: identity,
759
+ providerNodeId: resolveWrapperNodeId(identity),
760
+ nativeAddonId: nativeAddonId === null ? "" : addonIdentityOf(nativeAddonId)
761
+ });
762
+ }
763
+ }
764
+ return out;
765
+ }
720
766
  async function getBindings(deps, input) {
721
767
  const [row, store] = await Promise.all([deps.rows.get(input.deviceId), readBindingsStore(deps)]);
722
768
  return resolveBindingsForDevice(deps, input.deviceId, row, {
@@ -755,6 +801,11 @@ async function resolveBindingsForDevice(deps, deviceId, row, pass) {
755
801
  const presence = await resolveDevicePresence(deps, row, deviceId, pass.ledgerNonEmpty);
756
802
  const entries = [];
757
803
  const seenCaps = /* @__PURE__ */ new Set();
804
+ const collectionEntries = collectionBindings(deps, deviceId, deviceType);
805
+ for (const entry of collectionEntries) {
806
+ entries.push(entry);
807
+ seenCaps.add(entry.capName);
808
+ }
758
809
  const resolveRemote = (capName) => deps.remoteNativeCaps.get(deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, deviceId);
759
810
  for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
760
811
  if (wrapperAddonId === void 0) continue;
@@ -3298,114 +3349,30 @@ async function getWireableFields(deps, input) {
3298
3349
  }
3299
3350
  //#endregion
3300
3351
  //#region src/builtins/device-manager/device-source-activation.ts
3301
- /**
3302
- * Is `source` denied for a device, given the persisted deny set?
3303
- *
3304
- * The ONE place the migration rule lives: an entry matches either the source's
3305
- * own id or the id of the addon that serves it. New writes are always source
3306
- * ids; an addon id can only be a pre-re-key entry, and it still means "every
3307
- * source this addon serves here".
3308
- */
3309
- function isSourceDenied(denied, source) {
3310
- if (denied.includes(source.addonId)) return true;
3311
- return source.sourceId !== void 0 && denied.includes(source.sourceId);
3312
- }
3313
- /**
3314
- * The deny list after one toggle. Pure, order-preserving, idempotent — a
3315
- * second "off" on an addon already off changes nothing.
3316
- */
3317
- function nextDisabledSources(current, key, active) {
3318
- if (active) return current.filter((id) => id !== key);
3319
- return current.includes(key) ? current : [...current, key];
3320
- }
3321
3352
  /** Is `capName` a COLLECTION cap — a cap whose device has several SOURCES? */
3322
3353
  function isCollectionCap(registry, capName) {
3323
3354
  return registry?.getDefinition(capName)?.mode === "collection";
3324
3355
  }
3325
3356
  /**
3326
- * The persisted deny list for one `(device, cap)`, empty when never written.
3327
- *
3328
- * Reads the current field AND the pre-re-key one, unioned: a legacy addon id
3329
- * is still an entry and still denies every source that addon serves here. The
3330
- * union is what makes the migration a read rule rather than a data rewrite —
3331
- * nothing has to walk a thousand cameras to keep an operator's decision.
3332
- */
3333
- function disabledSourcesOf(record) {
3334
- const current = record?.disabledSourceIds ?? [];
3335
- const legacy = record?.disabledSourceAddonIds ?? [];
3336
- const out = [...current];
3337
- for (const id of legacy) if (!out.includes(id)) out.push(id);
3338
- return out;
3339
- }
3340
- /**
3341
- * Push ONE device's persisted source deactivations onto the resolution path.
3342
- * The only writer of the registry's per-device source set.
3343
- */
3344
- function mirrorDeviceSources(registry, deviceId, capName, disabledAddonIds) {
3345
- registry?.setDeviceDisabledSources(String(deviceId), capName, disabledAddonIds);
3346
- }
3347
- /**
3348
- * Replay the whole persisted store onto a fresh registry.
3349
- *
3350
- * The projection is in-memory, so a hub restart starts it empty — and an empty
3351
- * deny list means "every source", which would silently switch every source the
3352
- * operator turned off back ON. That is precisely the D62 failure (an off switch
3353
- * that reports on), so the replay is not an optimisation: it is what makes the
3354
- * authority durable. Called from the device-manager's `onInitialize`, before
3355
- * any camera can be asked for its clips, and safe to call again.
3356
- */
3357
- function restoreDeviceSourceActivations(registry, store, logger) {
3358
- const deviceBindings = store.deviceBindings;
3359
- if (deviceBindings === void 0) return 0;
3360
- let restored = 0;
3361
- for (const [deviceKey, perDevice] of Object.entries(deviceBindings)) {
3362
- const deviceId = Number(deviceKey);
3363
- for (const [capName, record] of Object.entries(perDevice)) {
3364
- const disabled = disabledSourcesOf(record);
3365
- if (disabled.length === 0) continue;
3366
- registry.setDeviceDisabledSources(deviceKey, capName, disabled);
3367
- restored += 1;
3368
- logger.info("restored per-device source deactivations", {
3369
- tags: { deviceId },
3370
- meta: {
3371
- capability: capName,
3372
- disabledSourceIds: disabled
3373
- }
3374
- });
3375
- }
3376
- }
3377
- return restored;
3378
- }
3379
- /**
3380
3357
  * Activate or deactivate a wrapper for one device.
3381
3358
  *
3382
- * Singleton caps keep the shape D62 fixed: one `wrapperAddonId`, or the
3383
- * explicit `null` marker that stops `getBindings` step 2 re-binding a
3384
- * `defaultActive` wrapper. Collection caps write `disabledSourceAddonIds`
3385
- * instead — their `wrapperAddonId` is left exactly as it was, because a cap
3386
- * with several sources has no "which one" for `getBindings` to answer (D554
3387
- * context 3) and inventing one would turn a source toggle into a cap-wide off
3388
- * switch.
3359
+ * SINGLETON caps only, and that is now enforced rather than assumed: one
3360
+ * `wrapperAddonId`, or the explicit `null` "operator disabled" marker that
3361
+ * stops `getBindings` step 2 re-binding a `defaultActive` wrapper.
3362
+ *
3363
+ * A COLLECTION cap is REFUSED by name. Its providers are a derived SET
3364
+ * (`device-bindings-store.ts` step 0) — there is nothing to activate, and a
3365
+ * verb that accepted the call and wrote a store nobody reads is exactly what
3366
+ * this module shipped until 2026-09-20.
3389
3367
  */
3390
3368
  async function setWrapperActive(deps, input) {
3391
3369
  const storeKey = String(input.deviceId);
3392
- const collection = isCollectionCap(deps.capabilityRegistry, input.capName);
3393
- let mirrored = [];
3370
+ if (isCollectionCap(deps.capabilityRegistry, input.capName)) throw new Error(`setWrapperActive: "${input.capName}" is a COLLECTION capability — its providers are a derived set, not an operator choice, so there is nothing to activate. Every provider that serves this device is already one of its sources.`);
3394
3371
  await deps.withAddonStoreWriteLock(async () => {
3395
3372
  const store = await readBindingsStore(deps);
3396
3373
  const perDevice = { ...store.deviceBindings[storeKey] };
3397
3374
  const existing = perDevice[input.capName];
3398
- if (collection) {
3399
- const key = input.sourceId ?? input.wrapperAddonId;
3400
- const disabled = nextDisabledSources(disabledSourcesOf(existing), key, input.active);
3401
- mirrored = disabled;
3402
- const wrapperAddonId = existing?.wrapperAddonId;
3403
- perDevice[input.capName] = {
3404
- ...wrapperAddonId === void 0 ? {} : { wrapperAddonId },
3405
- ...disabled.length === 0 ? {} : { disabledSourceIds: disabled }
3406
- };
3407
- if (Object.keys(perDevice[input.capName] ?? {}).length === 0) delete perDevice[input.capName];
3408
- } else if (input.active) perDevice[input.capName] = {
3375
+ if (input.active) perDevice[input.capName] = {
3409
3376
  ...existing,
3410
3377
  wrapperAddonId: input.wrapperAddonId
3411
3378
  };
@@ -3421,20 +3388,6 @@ async function setWrapperActive(deps, input) {
3421
3388
  return rest;
3422
3389
  })() });
3423
3390
  });
3424
- if (collection) {
3425
- mirrorDeviceSources(deps.capabilityRegistry, input.deviceId, input.capName, mirrored);
3426
- deps.ctx.logger.info(input.active ? "source activated for device" : "source deactivated for device", {
3427
- tags: {
3428
- deviceId: input.deviceId,
3429
- addonId: input.wrapperAddonId
3430
- },
3431
- meta: {
3432
- capability: input.capName,
3433
- sourceId: input.sourceId ?? null,
3434
- disabledSourceIds: [...mirrored]
3435
- }
3436
- });
3437
- }
3438
3391
  deps.ctx.eventBus.emit({
3439
3392
  id: randomUUID(),
3440
3393
  timestamp: /* @__PURE__ */ new Date(),
@@ -7453,11 +7406,6 @@ var DeviceManagerAddon = class extends BaseAddon {
7453
7406
  const deviceRows = new DeviceRowStore(deviceRowBackendOf(settingsStoreApi), this.ctx.logger.child("rows"));
7454
7407
  await deviceRows.declare();
7455
7408
  this.deviceRows = deviceRows;
7456
- const capRegistryForRestore = this.capabilityRegistry;
7457
- if (capRegistryForRestore) {
7458
- const restored = restoreDeviceSourceActivations(capRegistryForRestore, await readBindingsStore({ ctx: this.ctx }), this.ctx.logger);
7459
- if (restored > 0) this.ctx.logger.info("per-device source deactivations restored", { meta: { rows: restored } });
7460
- }
7461
7409
  try {
7462
7410
  await purgeRetiredSettingsRows(deviceRows.retiredRowStore(), this.ctx.logger.child("RetiredRows"));
7463
7411
  } catch (err) {
@@ -7710,4 +7658,4 @@ var DeviceManagerAddon = class extends BaseAddon {
7710
7658
  }
7711
7659
  };
7712
7660
  //#endregion
7713
- export { DeviceManagerAddon, DeviceManagerAddon as default, setWrapperActive as i, mergeAggregates, nextDisabledSources as n, restoreDeviceSourceActivations as r, isSourceDenied as t };
7661
+ export { DeviceManagerAddon, DeviceManagerAddon as default, mergeAggregates, setWrapperActive as t };