@camstack/system 1.2.275 → 1.2.277

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 (63) 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/grid-last-frame-store.d.ts +39 -0
  12. package/dist/builtins/camera-grid/index.js +67 -12
  13. package/dist/builtins/camera-grid/index.mjs +67 -12
  14. package/dist/builtins/console-logging/index.js +1 -1
  15. package/dist/builtins/console-logging/index.mjs +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  17. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  18. package/dist/builtins/device-manager/device-aggregation.d.ts +0 -6
  19. package/dist/builtins/device-manager/device-bindings-store.d.ts +22 -2
  20. package/dist/builtins/device-manager/device-manager.addon.d.ts +4 -0
  21. package/dist/builtins/device-manager/device-manager.addon.js +122 -58
  22. package/dist/builtins/device-manager/device-manager.addon.mjs +122 -59
  23. package/dist/builtins/device-manager/device-meta-types.d.ts +24 -3
  24. package/dist/builtins/device-manager/device-source-activation.d.ts +36 -0
  25. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  26. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  27. package/dist/builtins/hub-forwarder/index.js +1 -1
  28. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  29. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  30. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  31. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  32. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  33. package/dist/builtins/local-network/local-network.addon.js +1 -1
  34. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  35. package/dist/builtins/loki-logging/index.js +1 -1
  36. package/dist/builtins/loki-logging/index.mjs +1 -1
  37. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  38. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  39. package/dist/builtins/platform-probe/index.js +1 -1
  40. package/dist/builtins/platform-probe/index.mjs +1 -1
  41. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  43. package/dist/builtins/snapshot/index.js +1 -1
  44. package/dist/builtins/snapshot/index.mjs +1 -1
  45. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  47. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  51. package/dist/builtins/system-config/system-config.addon.js +1 -1
  52. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  53. package/dist/builtins/winston-logging/index.js +1 -1
  54. package/dist/builtins/winston-logging/index.mjs +1 -1
  55. package/dist/{dist-CYXhwfDf.js → dist-Bu8HN-KS.js} +354 -11
  56. package/dist/{dist-DWSJVcZe.mjs → dist-DOF9wnat.mjs} +349 -12
  57. package/dist/index.d.ts +7 -0
  58. package/dist/index.js +61 -48
  59. package/dist/index.mjs +62 -50
  60. package/dist/kernel/capability-registry.d.ts +28 -8
  61. package/dist/{retired-settings-keys-DEFtUH8o.js → retired-settings-keys-BwE6S2eQ.js} +1 -1
  62. package/dist/{retired-settings-keys-BHGnCLW0.mjs → retired-settings-keys-DXMfts8c.mjs} +1 -1
  63. package/package.json +2 -2
@@ -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-CYXhwfDf.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-DEFtUH8o.js");
6
+ const require_dist = require("../../dist-Bu8HN-KS.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-BwE6S2eQ.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,8 +806,14 @@ 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)) {
816
+ if (wrapperAddonId === void 0) continue;
765
817
  const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, deviceId) ?? null;
766
818
  const remoteNative = resolveRemote(capName);
767
819
  const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
@@ -2526,28 +2578,6 @@ function splitDeviceStoreKeys(patch) {
2526
2578
  //#endregion
2527
2579
  //#region src/builtins/device-manager/device-aggregation.ts
2528
2580
  /**
2529
- * Device-details aggregation + field-update routing for the device-manager
2530
- * addon. Extracted from `device-manager.addon.ts` as free functions that
2531
- * receive an explicit `AggregationDeps` context (DI) rather than closing over
2532
- * the addon instance.
2533
- *
2534
- * This module owns the binding-driven settings/live/status aggregators
2535
- * (`getDeviceAggregate`, `collectSystemDeviceContributions`,
2536
- * `deriveDeviceConfigContribution`, `buildBaseDeviceSection`,
2537
- * `getDeviceStatusAggregate`, `resolveDriverConfigSchema`) and the
2538
- * field-update routing (`updateDeviceField`, `updateDeviceFieldsBatch`,
2539
- * `applyGroupPatch`, `resolveContributionProvider`, `dispatchDeviceAction`)
2540
- * plus the wrapper-activation mutators (`setWrapperActive`,
2541
- * `listWrappersForCap`, `listBindableCapsForDeviceType`).
2542
- *
2543
- * Sibling aggregator functions call each other directly (passing `deps`);
2544
- * cross-domain leaf calls (`getBindings`, `lookupPersistedStableId`,
2545
- * `resolveRemoteNativeCapFromRegistry`, read/writeBindingsStore) route through
2546
- * the bindings-store module via `deps.bindingsDeps`. EXACT semantics, ordering,
2547
- * and side effects of the original class methods are preserved; the addon keeps
2548
- * thin delegator methods over these functions.
2549
- */
2550
- /**
2551
2581
  * One process-wide cache behind every device-details fan-out.
2552
2582
  *
2553
2583
  * Module-level on purpose: the page re-polls every 2.5 s and a per-call cache
@@ -3250,39 +3280,6 @@ async function listBindableCapsForDeviceType(deps, input) {
3250
3280
  wrappers: [...registry.getWrappersForCap(capName)]
3251
3281
  }));
3252
3282
  }
3253
- async function setWrapperActive(deps, input) {
3254
- const storeKey = String(input.deviceId);
3255
- await deps.bindingsDeps.withAddonStoreWriteLock(async () => {
3256
- const store = await readBindingsStore(deps.bindingsDeps);
3257
- const perDevice = { ...store.deviceBindings[storeKey] };
3258
- if (input.active) perDevice[input.capName] = { wrapperAddonId: input.wrapperAddonId };
3259
- else perDevice[input.capName] = { wrapperAddonId: null };
3260
- const nextDeviceBindings = Object.keys(perDevice).length > 0 ? {
3261
- ...store.deviceBindings,
3262
- [storeKey]: perDevice
3263
- } : (() => {
3264
- const { [storeKey]: _drop, ...rest } = store.deviceBindings;
3265
- return rest;
3266
- })();
3267
- await writeBindingsStore(deps.bindingsDeps, { deviceBindings: nextDeviceBindings });
3268
- });
3269
- deps.ctx.eventBus.emit({
3270
- id: (0, node_crypto.randomUUID)(),
3271
- timestamp: /* @__PURE__ */ new Date(),
3272
- source: {
3273
- type: "addon",
3274
- id: deps.ctx.id
3275
- },
3276
- category: require_dist.EventCategory.DeviceBindingsChanged,
3277
- data: {
3278
- deviceId: input.deviceId,
3279
- capName: input.capName,
3280
- reason: input.active ? "wrapper-activated" : "wrapper-deactivated",
3281
- addonId: input.wrapperAddonId,
3282
- nodeId: resolveWrapperNodeId(input.wrapperAddonId)
3283
- }
3284
- });
3285
- }
3286
3283
  /**
3287
3284
  * Build the wireable-cap entry for one capability definition: the status
3288
3285
  * schema's scalar leaf fields plus — for an item-array cap
@@ -3356,6 +3353,64 @@ async function getWireableFields(deps, input) {
3356
3353
  return { caps };
3357
3354
  }
3358
3355
  //#endregion
3356
+ //#region src/builtins/device-manager/device-source-activation.ts
3357
+ /** Is `capName` a COLLECTION cap — a cap whose device has several SOURCES? */
3358
+ function isCollectionCap(registry, capName) {
3359
+ return registry?.getDefinition(capName)?.mode === "collection";
3360
+ }
3361
+ /**
3362
+ * Activate or deactivate a wrapper for one device.
3363
+ *
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.
3372
+ */
3373
+ async function setWrapperActive(deps, input) {
3374
+ const storeKey = String(input.deviceId);
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.`);
3376
+ await deps.withAddonStoreWriteLock(async () => {
3377
+ const store = await readBindingsStore(deps);
3378
+ const perDevice = { ...store.deviceBindings[storeKey] };
3379
+ const existing = perDevice[input.capName];
3380
+ if (input.active) perDevice[input.capName] = {
3381
+ ...existing,
3382
+ wrapperAddonId: input.wrapperAddonId
3383
+ };
3384
+ else perDevice[input.capName] = {
3385
+ ...existing,
3386
+ wrapperAddonId: null
3387
+ };
3388
+ await writeBindingsStore(deps, { deviceBindings: Object.keys(perDevice).length > 0 ? {
3389
+ ...store.deviceBindings,
3390
+ [storeKey]: perDevice
3391
+ } : (() => {
3392
+ const { [storeKey]: _drop, ...rest } = store.deviceBindings;
3393
+ return rest;
3394
+ })() });
3395
+ });
3396
+ deps.ctx.eventBus.emit({
3397
+ id: (0, node_crypto.randomUUID)(),
3398
+ timestamp: /* @__PURE__ */ new Date(),
3399
+ source: {
3400
+ type: "addon",
3401
+ id: deps.ctx.id
3402
+ },
3403
+ category: require_dist.EventCategory.DeviceBindingsChanged,
3404
+ data: {
3405
+ deviceId: input.deviceId,
3406
+ capName: input.capName,
3407
+ reason: input.active ? "wrapper-activated" : "wrapper-deactivated",
3408
+ addonId: input.wrapperAddonId,
3409
+ nodeId: resolveWrapperNodeId(input.wrapperAddonId)
3410
+ }
3411
+ });
3412
+ }
3413
+ //#endregion
3359
3414
  //#region src/builtins/device-manager/device-event-propagator.ts
3360
3415
  /**
3361
3416
  * Walks the parent chain for every device-sourced event and re-emits a
@@ -7327,7 +7382,15 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
7327
7382
  return listBindableCapsForDeviceType(this.aggregationDeps, input);
7328
7383
  }
7329
7384
  async setWrapperActive(input) {
7330
- return setWrapperActive(this.aggregationDeps, input);
7385
+ return setWrapperActive(this.wrapperActivationDeps, input);
7386
+ }
7387
+ /** The narrow dependency bag the activation verb needs (D556). */
7388
+ get wrapperActivationDeps() {
7389
+ return {
7390
+ ctx: this.ctx,
7391
+ capabilityRegistry: this.capabilityRegistry,
7392
+ withAddonStoreWriteLock: this.addonStoreWriteLock
7393
+ };
7331
7394
  }
7332
7395
  async onInitialize() {
7333
7396
  const settings = this.ctx.settings;
@@ -7603,3 +7666,4 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
7603
7666
  exports.DeviceManagerAddon = DeviceManagerAddon;
7604
7667
  exports.default = DeviceManagerAddon;
7605
7668
  exports.mergeAggregates = mergeAggregates;
7669
+ exports.setWrapperActive = setWrapperActive;
@@ -1,5 +1,5 @@
1
- import { $ as deviceStateCapability, D as STREAM_PROFILE_META, H as buildStreamParamsConfigSchema, Ht as runtimeStatePolicyFor, Nn as sleep$1, Nt as parseStreamParamsFormPatch, Pn as EventCategory, Q as deviceManagerCapability, St as locationIconKey, Tn as isDeviceConfigCap, an as errMsg, dn as DeviceFeature, et as deviceStatusCapability, fn as DeviceRole, gn as WELL_KNOWN_TAB_MAP, it as enumerateSchemaFields, jt as normalizeUnit, m as DeviceStatusSchema, o as CAMERA_SWITCH_ORDER, pn as DeviceType, qt as siteZoneKey, rt as enumerateItemArrayFields, s as CAP_NAMES_WITH_STATUS, sn as BaseAddon, t as ALL_CAPABILITY_DEFINITIONS, v as LocationIconIdSchema, zt as resolveContainerPrimaryChild } from "../../dist-DWSJVcZe.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-BHGnCLW0.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-DOF9wnat.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-DXMfts8c.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,8 +801,14 @@ 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)) {
811
+ if (wrapperAddonId === void 0) continue;
760
812
  const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, deviceId) ?? null;
761
813
  const remoteNative = resolveRemote(capName);
762
814
  const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
@@ -2521,28 +2573,6 @@ function splitDeviceStoreKeys(patch) {
2521
2573
  //#endregion
2522
2574
  //#region src/builtins/device-manager/device-aggregation.ts
2523
2575
  /**
2524
- * Device-details aggregation + field-update routing for the device-manager
2525
- * addon. Extracted from `device-manager.addon.ts` as free functions that
2526
- * receive an explicit `AggregationDeps` context (DI) rather than closing over
2527
- * the addon instance.
2528
- *
2529
- * This module owns the binding-driven settings/live/status aggregators
2530
- * (`getDeviceAggregate`, `collectSystemDeviceContributions`,
2531
- * `deriveDeviceConfigContribution`, `buildBaseDeviceSection`,
2532
- * `getDeviceStatusAggregate`, `resolveDriverConfigSchema`) and the
2533
- * field-update routing (`updateDeviceField`, `updateDeviceFieldsBatch`,
2534
- * `applyGroupPatch`, `resolveContributionProvider`, `dispatchDeviceAction`)
2535
- * plus the wrapper-activation mutators (`setWrapperActive`,
2536
- * `listWrappersForCap`, `listBindableCapsForDeviceType`).
2537
- *
2538
- * Sibling aggregator functions call each other directly (passing `deps`);
2539
- * cross-domain leaf calls (`getBindings`, `lookupPersistedStableId`,
2540
- * `resolveRemoteNativeCapFromRegistry`, read/writeBindingsStore) route through
2541
- * the bindings-store module via `deps.bindingsDeps`. EXACT semantics, ordering,
2542
- * and side effects of the original class methods are preserved; the addon keeps
2543
- * thin delegator methods over these functions.
2544
- */
2545
- /**
2546
2576
  * One process-wide cache behind every device-details fan-out.
2547
2577
  *
2548
2578
  * Module-level on purpose: the page re-polls every 2.5 s and a per-call cache
@@ -3245,39 +3275,6 @@ async function listBindableCapsForDeviceType(deps, input) {
3245
3275
  wrappers: [...registry.getWrappersForCap(capName)]
3246
3276
  }));
3247
3277
  }
3248
- async function setWrapperActive(deps, input) {
3249
- const storeKey = String(input.deviceId);
3250
- await deps.bindingsDeps.withAddonStoreWriteLock(async () => {
3251
- const store = await readBindingsStore(deps.bindingsDeps);
3252
- const perDevice = { ...store.deviceBindings[storeKey] };
3253
- if (input.active) perDevice[input.capName] = { wrapperAddonId: input.wrapperAddonId };
3254
- else perDevice[input.capName] = { wrapperAddonId: null };
3255
- const nextDeviceBindings = Object.keys(perDevice).length > 0 ? {
3256
- ...store.deviceBindings,
3257
- [storeKey]: perDevice
3258
- } : (() => {
3259
- const { [storeKey]: _drop, ...rest } = store.deviceBindings;
3260
- return rest;
3261
- })();
3262
- await writeBindingsStore(deps.bindingsDeps, { deviceBindings: nextDeviceBindings });
3263
- });
3264
- deps.ctx.eventBus.emit({
3265
- id: randomUUID(),
3266
- timestamp: /* @__PURE__ */ new Date(),
3267
- source: {
3268
- type: "addon",
3269
- id: deps.ctx.id
3270
- },
3271
- category: EventCategory.DeviceBindingsChanged,
3272
- data: {
3273
- deviceId: input.deviceId,
3274
- capName: input.capName,
3275
- reason: input.active ? "wrapper-activated" : "wrapper-deactivated",
3276
- addonId: input.wrapperAddonId,
3277
- nodeId: resolveWrapperNodeId(input.wrapperAddonId)
3278
- }
3279
- });
3280
- }
3281
3278
  /**
3282
3279
  * Build the wireable-cap entry for one capability definition: the status
3283
3280
  * schema's scalar leaf fields plus — for an item-array cap
@@ -3351,6 +3348,64 @@ async function getWireableFields(deps, input) {
3351
3348
  return { caps };
3352
3349
  }
3353
3350
  //#endregion
3351
+ //#region src/builtins/device-manager/device-source-activation.ts
3352
+ /** Is `capName` a COLLECTION cap — a cap whose device has several SOURCES? */
3353
+ function isCollectionCap(registry, capName) {
3354
+ return registry?.getDefinition(capName)?.mode === "collection";
3355
+ }
3356
+ /**
3357
+ * Activate or deactivate a wrapper for one device.
3358
+ *
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.
3367
+ */
3368
+ async function setWrapperActive(deps, input) {
3369
+ const storeKey = String(input.deviceId);
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.`);
3371
+ await deps.withAddonStoreWriteLock(async () => {
3372
+ const store = await readBindingsStore(deps);
3373
+ const perDevice = { ...store.deviceBindings[storeKey] };
3374
+ const existing = perDevice[input.capName];
3375
+ if (input.active) perDevice[input.capName] = {
3376
+ ...existing,
3377
+ wrapperAddonId: input.wrapperAddonId
3378
+ };
3379
+ else perDevice[input.capName] = {
3380
+ ...existing,
3381
+ wrapperAddonId: null
3382
+ };
3383
+ await writeBindingsStore(deps, { deviceBindings: Object.keys(perDevice).length > 0 ? {
3384
+ ...store.deviceBindings,
3385
+ [storeKey]: perDevice
3386
+ } : (() => {
3387
+ const { [storeKey]: _drop, ...rest } = store.deviceBindings;
3388
+ return rest;
3389
+ })() });
3390
+ });
3391
+ deps.ctx.eventBus.emit({
3392
+ id: randomUUID(),
3393
+ timestamp: /* @__PURE__ */ new Date(),
3394
+ source: {
3395
+ type: "addon",
3396
+ id: deps.ctx.id
3397
+ },
3398
+ category: EventCategory.DeviceBindingsChanged,
3399
+ data: {
3400
+ deviceId: input.deviceId,
3401
+ capName: input.capName,
3402
+ reason: input.active ? "wrapper-activated" : "wrapper-deactivated",
3403
+ addonId: input.wrapperAddonId,
3404
+ nodeId: resolveWrapperNodeId(input.wrapperAddonId)
3405
+ }
3406
+ });
3407
+ }
3408
+ //#endregion
3354
3409
  //#region src/builtins/device-manager/device-event-propagator.ts
3355
3410
  /**
3356
3411
  * Walks the parent chain for every device-sourced event and re-emits a
@@ -7322,7 +7377,15 @@ var DeviceManagerAddon = class extends BaseAddon {
7322
7377
  return listBindableCapsForDeviceType(this.aggregationDeps, input);
7323
7378
  }
7324
7379
  async setWrapperActive(input) {
7325
- return setWrapperActive(this.aggregationDeps, input);
7380
+ return setWrapperActive(this.wrapperActivationDeps, input);
7381
+ }
7382
+ /** The narrow dependency bag the activation verb needs (D556). */
7383
+ get wrapperActivationDeps() {
7384
+ return {
7385
+ ctx: this.ctx,
7386
+ capabilityRegistry: this.capabilityRegistry,
7387
+ withAddonStoreWriteLock: this.addonStoreWriteLock
7388
+ };
7326
7389
  }
7327
7390
  async onInitialize() {
7328
7391
  const settings = this.ctx.settings;
@@ -7595,4 +7658,4 @@ var DeviceManagerAddon = class extends BaseAddon {
7595
7658
  }
7596
7659
  };
7597
7660
  //#endregion
7598
- export { DeviceManagerAddon, DeviceManagerAddon as default, mergeAggregates };
7661
+ export { DeviceManagerAddon, DeviceManagerAddon as default, mergeAggregates, setWrapperActive as t };
@@ -43,10 +43,31 @@ export type DriverConfigSchemaResult = {
43
43
  } | {
44
44
  status: 'unavailable';
45
45
  };
46
+ /**
47
+ * One `(device, capability)` row of the persisted wrapper-activation store —
48
+ * the authority for every per-camera function switch (D62).
49
+ *
50
+ * SINGLETON capabilities only. `wrapperAddonId` is the bound wrapper, or the
51
+ * explicit `null` "operator disabled" marker that stops `getBindings` step 2
52
+ * re-binding a `defaultActive` wrapper; absent means the operator never touched
53
+ * this cap on this device.
54
+ *
55
+ * **A COLLECTION capability has no row here, by design.** Its providers are a
56
+ * SET, and the set is DERIVED from who provides the cap
57
+ * (`device-bindings-store.ts` step 0) — *"tutti i providers funzionano sempre,
58
+ * non serve disattivarli/attivarli … loro vivono lì"* (operator, 2026-09-20).
59
+ * Nothing persists it, so nothing has to be back-filled onto a fleet and
60
+ * nothing goes stale when an addon is deployed to a camera later.
61
+ * `setWrapperActive` REFUSES a collection cap rather than writing a field the
62
+ * enumeration does not read — `disabledSourceIds` / `disabledSourceAddonIds`
63
+ * (D556, D555) were exactly that, and
64
+ * `scripts/check-device-selection-authority.ts` now makes it unrepresentable.
65
+ */
66
+ export interface DeviceCapBinding {
67
+ readonly wrapperAddonId?: string | null;
68
+ }
46
69
  export interface DeviceBindingsStore {
47
- readonly deviceBindings: Record<string, Record<string, {
48
- wrapperAddonId: string | null;
49
- }>>;
70
+ readonly deviceBindings: Record<string, Record<string, DeviceCapBinding>>;
50
71
  }
51
72
  export interface PersistedDeviceMeta {
52
73
  type: string;
@@ -0,0 +1,36 @@
1
+ import { AddonContext, ICapabilityRegistry } from '@camstack/types';
2
+ import { WriteLock } from './write-lock.js';
3
+ /**
4
+ * What a wrapper-activation write needs, and nothing more.
5
+ *
6
+ * Deliberately narrower than `AggregationDeps`: the verb touches the settings
7
+ * door, the event bus, the registry and the addon-store write lock. Handing it
8
+ * the device ledger and the remote-native cache it never reads would make this
9
+ * loop untestable without standing up the whole addon.
10
+ */
11
+ export interface WrapperActivationDeps {
12
+ readonly ctx: AddonContext;
13
+ readonly capabilityRegistry: ICapabilityRegistry | undefined;
14
+ /** Ordering for every read-modify-write of the fleet-wide `deviceBindings` blob (D62). */
15
+ readonly withAddonStoreWriteLock: WriteLock;
16
+ }
17
+ /** The operator-facing activation, as it arrives from the cap router. */
18
+ export interface WrapperActivationInput {
19
+ readonly deviceId: number;
20
+ readonly capName: string;
21
+ readonly wrapperAddonId: string;
22
+ readonly active: boolean;
23
+ }
24
+ /**
25
+ * Activate or deactivate a wrapper for one device.
26
+ *
27
+ * SINGLETON caps only, and that is now enforced rather than assumed: one
28
+ * `wrapperAddonId`, or the explicit `null` "operator disabled" marker that
29
+ * stops `getBindings` step 2 re-binding a `defaultActive` wrapper.
30
+ *
31
+ * A COLLECTION cap is REFUSED by name. Its providers are a derived SET
32
+ * (`device-bindings-store.ts` step 0) — there is nothing to activate, and a
33
+ * verb that accepted the call and wrote a store nobody reads is exactly what
34
+ * this module shipped until 2026-09-20.
35
+ */
36
+ export declare function setWrapperActive(deps: WrapperActivationDeps, input: WrapperActivationInput): Promise<void>;
@@ -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-CYXhwfDf.js");
6
+ const require_dist = require("../../dist-Bu8HN-KS.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { B as bareAddonId, Pn as EventCategory, T as SOURCE_DEVICE_TYPES, an as errMsg, at as evaluateSensorEdge, bn as createEvent, gt as isSourceCap, ht as isSameAddonId, pn as DeviceType, sn as BaseAddon, tt as doorbellCapability, w as SOURCE_CAPS } from "../../dist-DWSJVcZe.mjs";
1
+ import { B as bareAddonId, Fn as EventCategory, T as SOURCE_DEVICE_TYPES, at as evaluateSensorEdge, cn as BaseAddon, gt as isSourceCap, ht as isSameAddonId, mn as DeviceType, on as errMsg, tt as doorbellCapability, w as SOURCE_CAPS, xn as createEvent } from "../../dist-DOF9wnat.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -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-CYXhwfDf.js");
6
+ const require_dist = require("../../dist-Bu8HN-KS.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-DWSJVcZe.mjs";
1
+ import { Ct as logDestinationCapability, cn as BaseAddon } from "../../dist-DOF9wnat.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-CYXhwfDf.js");
3
+ const require_dist = require("../../dist-Bu8HN-KS.js");
4
4
  //#region src/builtins/liveness-monitor/liveness-checks.ts
5
5
  var NO_DEVICES = "liveness:no-devices";
6
6
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -1,4 +1,4 @@
1
- import { C as RecordingStatusSchema, Pn as EventCategory, an as errMsg, bn as createEvent, sn as BaseAddon } from "../../dist-DWSJVcZe.mjs";
1
+ import { C as RecordingStatusSchema, Fn as EventCategory, cn as BaseAddon, on as errMsg, xn as createEvent } from "../../dist-DOF9wnat.mjs";
2
2
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
3
  var NO_DEVICES = "liveness:no-devices";
4
4
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -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-CYXhwfDf.js");
6
+ const require_dist = require("../../dist-Bu8HN-KS.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { N as UserRecordSchema, O as ScopedTokenSchema, R as authProviderCapability, r as ApiKeyRecordSchema, sn as BaseAddon, tn as userManagementCapability } from "../../dist-DWSJVcZe.mjs";
2
+ import { N as UserRecordSchema, O as ScopedTokenSchema, R as authProviderCapability, cn as BaseAddon, nn as userManagementCapability, r as ApiKeyRecordSchema } from "../../dist-DOF9wnat.mjs";
3
3
  import * as crypto$2 from "node:crypto";
4
4
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
@@ -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-CYXhwfDf.js");
3
+ const require_dist = require("../../dist-Bu8HN-KS.js");
4
4
  const require_tls = require("../../tls-D_wUeOTD.js");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Pn as EventCategory, sn as BaseAddon, xt as localNetworkCapability } from "../../dist-DWSJVcZe.mjs";
1
+ import { Fn as EventCategory, cn as BaseAddon, xt as localNetworkCapability } from "../../dist-DOF9wnat.mjs";
2
2
  import { S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, i as writeExtraSans, l as reissueTlsLeaf, n as readTlsAccessStatus, o as validateUploadedTls, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, y as applyLanHttp } from "../../tls-IM-v7JV4.mjs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
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-CYXhwfDf.js");
6
+ const require_dist = require("../../dist-Bu8HN-KS.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-DWSJVcZe.mjs";
1
+ import { Ct as logDestinationCapability, cn as BaseAddon } from "../../dist-DOF9wnat.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -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-CYXhwfDf.js");
6
+ const require_dist = require("../../dist-Bu8HN-KS.js");
7
7
  let node_fs_promises = require("node:fs/promises");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Lt as reducePoints, Ot as metricsProviderCapability, Pn as EventCategory, Rt as resolveBucketMs, an as errMsg, bn as createEvent, lt as foldSnapshotByFunction, sn as BaseAddon } from "../../dist-DWSJVcZe.mjs";
1
+ import { Fn as EventCategory, Ot as metricsProviderCapability, Rt as reducePoints, cn as BaseAddon, lt as foldSnapshotByFunction, on as errMsg, xn as createEvent, zt as resolveBucketMs } from "../../dist-DOF9wnat.mjs";
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import * as fs from "node:fs";
4
4
  import { constants, readFileSync } from "node:fs";