@camstack/system 1.2.275 → 1.2.276

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 (64) 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 +12 -2
  20. package/dist/builtins/device-manager/device-manager.addon.d.ts +4 -0
  21. package/dist/builtins/device-manager/device-manager.addon.js +177 -58
  22. package/dist/builtins/device-manager/device-manager.addon.mjs +174 -59
  23. package/dist/builtins/device-manager/device-meta-types.d.ts +36 -3
  24. package/dist/builtins/device-manager/device-source-activation.d.ts +70 -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-DWSJVcZe.mjs → dist-AHdiIdJH.mjs} +331 -12
  56. package/dist/{dist-CYXhwfDf.js → dist-D_OlSf31.js} +336 -11
  57. package/dist/index.d.ts +7 -0
  58. package/dist/index.js +103 -37
  59. package/dist/index.mjs +101 -39
  60. package/dist/kernel/capability-registry.d.ts +59 -9
  61. package/dist/kernel/index.d.ts +1 -0
  62. package/dist/{retired-settings-keys-BHGnCLW0.mjs → retired-settings-keys-Cp3oG_aH.mjs} +1 -1
  63. package/dist/{retired-settings-keys-DEFtUH8o.js → retired-settings-keys-LwFyTKun.js} +1 -1
  64. 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-D_OlSf31.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-LwFyTKun.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
@@ -762,6 +762,7 @@ async function resolveBindingsForDevice(deps, deviceId, row, pass) {
762
762
  const seenCaps = /* @__PURE__ */ new Set();
763
763
  const resolveRemote = (capName) => deps.remoteNativeCaps.get(deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, deviceId);
764
764
  for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
765
+ if (wrapperAddonId === void 0) continue;
765
766
  const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, deviceId) ?? null;
766
767
  const remoteNative = resolveRemote(capName);
767
768
  const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
@@ -2526,28 +2527,6 @@ function splitDeviceStoreKeys(patch) {
2526
2527
  //#endregion
2527
2528
  //#region src/builtins/device-manager/device-aggregation.ts
2528
2529
  /**
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
2530
  * One process-wide cache behind every device-details fan-out.
2552
2531
  *
2553
2532
  * Module-level on purpose: the page re-polls every 2.5 s and a per-call cache
@@ -3250,39 +3229,6 @@ async function listBindableCapsForDeviceType(deps, input) {
3250
3229
  wrappers: [...registry.getWrappersForCap(capName)]
3251
3230
  }));
3252
3231
  }
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
3232
  /**
3287
3233
  * Build the wireable-cap entry for one capability definition: the status
3288
3234
  * schema's scalar leaf fields plus — for an item-array cap
@@ -3356,6 +3302,162 @@ async function getWireableFields(deps, input) {
3356
3302
  return { caps };
3357
3303
  }
3358
3304
  //#endregion
3305
+ //#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
+ /** Is `capName` a COLLECTION cap — a cap whose device has several SOURCES? */
3327
+ function isCollectionCap(registry, capName) {
3328
+ return registry?.getDefinition(capName)?.mode === "collection";
3329
+ }
3330
+ /**
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
+ * Activate or deactivate a wrapper for one device.
3386
+ *
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.
3394
+ */
3395
+ async function setWrapperActive(deps, input) {
3396
+ const storeKey = String(input.deviceId);
3397
+ const collection = isCollectionCap(deps.capabilityRegistry, input.capName);
3398
+ let mirrored = [];
3399
+ await deps.withAddonStoreWriteLock(async () => {
3400
+ const store = await readBindingsStore(deps);
3401
+ const perDevice = { ...store.deviceBindings[storeKey] };
3402
+ 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] = {
3414
+ ...existing,
3415
+ wrapperAddonId: input.wrapperAddonId
3416
+ };
3417
+ else perDevice[input.capName] = {
3418
+ ...existing,
3419
+ wrapperAddonId: null
3420
+ };
3421
+ await writeBindingsStore(deps, { deviceBindings: Object.keys(perDevice).length > 0 ? {
3422
+ ...store.deviceBindings,
3423
+ [storeKey]: perDevice
3424
+ } : (() => {
3425
+ const { [storeKey]: _drop, ...rest } = store.deviceBindings;
3426
+ return rest;
3427
+ })() });
3428
+ });
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
+ deps.ctx.eventBus.emit({
3444
+ id: (0, node_crypto.randomUUID)(),
3445
+ timestamp: /* @__PURE__ */ new Date(),
3446
+ source: {
3447
+ type: "addon",
3448
+ id: deps.ctx.id
3449
+ },
3450
+ category: require_dist.EventCategory.DeviceBindingsChanged,
3451
+ data: {
3452
+ deviceId: input.deviceId,
3453
+ capName: input.capName,
3454
+ reason: input.active ? "wrapper-activated" : "wrapper-deactivated",
3455
+ addonId: input.wrapperAddonId,
3456
+ nodeId: resolveWrapperNodeId(input.wrapperAddonId)
3457
+ }
3458
+ });
3459
+ }
3460
+ //#endregion
3359
3461
  //#region src/builtins/device-manager/device-event-propagator.ts
3360
3462
  /**
3361
3463
  * Walks the parent chain for every device-sourced event and re-emits a
@@ -7327,7 +7429,15 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
7327
7429
  return listBindableCapsForDeviceType(this.aggregationDeps, input);
7328
7430
  }
7329
7431
  async setWrapperActive(input) {
7330
- return setWrapperActive(this.aggregationDeps, input);
7432
+ return setWrapperActive(this.wrapperActivationDeps, input);
7433
+ }
7434
+ /** The narrow dependency bag the activation verb needs (D556). */
7435
+ get wrapperActivationDeps() {
7436
+ return {
7437
+ ctx: this.ctx,
7438
+ capabilityRegistry: this.capabilityRegistry,
7439
+ withAddonStoreWriteLock: this.addonStoreWriteLock
7440
+ };
7331
7441
  }
7332
7442
  async onInitialize() {
7333
7443
  const settings = this.ctx.settings;
@@ -7348,6 +7458,11 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
7348
7458
  const deviceRows = new DeviceRowStore(deviceRowBackendOf(settingsStoreApi), this.ctx.logger.child("rows"));
7349
7459
  await deviceRows.declare();
7350
7460
  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
+ }
7351
7466
  try {
7352
7467
  await require_retired_settings_keys.purgeRetiredSettingsRows(deviceRows.retiredRowStore(), this.ctx.logger.child("RetiredRows"));
7353
7468
  } catch (err) {
@@ -7602,4 +7717,8 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
7602
7717
  //#endregion
7603
7718
  exports.DeviceManagerAddon = DeviceManagerAddon;
7604
7719
  exports.default = DeviceManagerAddon;
7720
+ exports.isSourceDenied = isSourceDenied;
7605
7721
  exports.mergeAggregates = mergeAggregates;
7722
+ exports.nextDisabledSources = nextDisabledSources;
7723
+ exports.restoreDeviceSourceActivations = restoreDeviceSourceActivations;
7724
+ 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-AHdiIdJH.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-Cp3oG_aH.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
@@ -757,6 +757,7 @@ async function resolveBindingsForDevice(deps, deviceId, row, pass) {
757
757
  const seenCaps = /* @__PURE__ */ new Set();
758
758
  const resolveRemote = (capName) => deps.remoteNativeCaps.get(deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, deviceId);
759
759
  for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
760
+ if (wrapperAddonId === void 0) continue;
760
761
  const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, deviceId) ?? null;
761
762
  const remoteNative = resolveRemote(capName);
762
763
  const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
@@ -2521,28 +2522,6 @@ function splitDeviceStoreKeys(patch) {
2521
2522
  //#endregion
2522
2523
  //#region src/builtins/device-manager/device-aggregation.ts
2523
2524
  /**
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
2525
  * One process-wide cache behind every device-details fan-out.
2547
2526
  *
2548
2527
  * Module-level on purpose: the page re-polls every 2.5 s and a per-call cache
@@ -3245,39 +3224,6 @@ async function listBindableCapsForDeviceType(deps, input) {
3245
3224
  wrappers: [...registry.getWrappersForCap(capName)]
3246
3225
  }));
3247
3226
  }
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
3227
  /**
3282
3228
  * Build the wireable-cap entry for one capability definition: the status
3283
3229
  * schema's scalar leaf fields plus — for an item-array cap
@@ -3351,6 +3297,162 @@ async function getWireableFields(deps, input) {
3351
3297
  return { caps };
3352
3298
  }
3353
3299
  //#endregion
3300
+ //#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
+ /** Is `capName` a COLLECTION cap — a cap whose device has several SOURCES? */
3322
+ function isCollectionCap(registry, capName) {
3323
+ return registry?.getDefinition(capName)?.mode === "collection";
3324
+ }
3325
+ /**
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
+ * Activate or deactivate a wrapper for one device.
3381
+ *
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.
3389
+ */
3390
+ async function setWrapperActive(deps, input) {
3391
+ const storeKey = String(input.deviceId);
3392
+ const collection = isCollectionCap(deps.capabilityRegistry, input.capName);
3393
+ let mirrored = [];
3394
+ await deps.withAddonStoreWriteLock(async () => {
3395
+ const store = await readBindingsStore(deps);
3396
+ const perDevice = { ...store.deviceBindings[storeKey] };
3397
+ 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] = {
3409
+ ...existing,
3410
+ wrapperAddonId: input.wrapperAddonId
3411
+ };
3412
+ else perDevice[input.capName] = {
3413
+ ...existing,
3414
+ wrapperAddonId: null
3415
+ };
3416
+ await writeBindingsStore(deps, { deviceBindings: Object.keys(perDevice).length > 0 ? {
3417
+ ...store.deviceBindings,
3418
+ [storeKey]: perDevice
3419
+ } : (() => {
3420
+ const { [storeKey]: _drop, ...rest } = store.deviceBindings;
3421
+ return rest;
3422
+ })() });
3423
+ });
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
+ deps.ctx.eventBus.emit({
3439
+ id: randomUUID(),
3440
+ timestamp: /* @__PURE__ */ new Date(),
3441
+ source: {
3442
+ type: "addon",
3443
+ id: deps.ctx.id
3444
+ },
3445
+ category: EventCategory.DeviceBindingsChanged,
3446
+ data: {
3447
+ deviceId: input.deviceId,
3448
+ capName: input.capName,
3449
+ reason: input.active ? "wrapper-activated" : "wrapper-deactivated",
3450
+ addonId: input.wrapperAddonId,
3451
+ nodeId: resolveWrapperNodeId(input.wrapperAddonId)
3452
+ }
3453
+ });
3454
+ }
3455
+ //#endregion
3354
3456
  //#region src/builtins/device-manager/device-event-propagator.ts
3355
3457
  /**
3356
3458
  * Walks the parent chain for every device-sourced event and re-emits a
@@ -7322,7 +7424,15 @@ var DeviceManagerAddon = class extends BaseAddon {
7322
7424
  return listBindableCapsForDeviceType(this.aggregationDeps, input);
7323
7425
  }
7324
7426
  async setWrapperActive(input) {
7325
- return setWrapperActive(this.aggregationDeps, input);
7427
+ return setWrapperActive(this.wrapperActivationDeps, input);
7428
+ }
7429
+ /** The narrow dependency bag the activation verb needs (D556). */
7430
+ get wrapperActivationDeps() {
7431
+ return {
7432
+ ctx: this.ctx,
7433
+ capabilityRegistry: this.capabilityRegistry,
7434
+ withAddonStoreWriteLock: this.addonStoreWriteLock
7435
+ };
7326
7436
  }
7327
7437
  async onInitialize() {
7328
7438
  const settings = this.ctx.settings;
@@ -7343,6 +7453,11 @@ var DeviceManagerAddon = class extends BaseAddon {
7343
7453
  const deviceRows = new DeviceRowStore(deviceRowBackendOf(settingsStoreApi), this.ctx.logger.child("rows"));
7344
7454
  await deviceRows.declare();
7345
7455
  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
+ }
7346
7461
  try {
7347
7462
  await purgeRetiredSettingsRows(deviceRows.retiredRowStore(), this.ctx.logger.child("RetiredRows"));
7348
7463
  } catch (err) {
@@ -7595,4 +7710,4 @@ var DeviceManagerAddon = class extends BaseAddon {
7595
7710
  }
7596
7711
  };
7597
7712
  //#endregion
7598
- export { DeviceManagerAddon, DeviceManagerAddon as default, mergeAggregates };
7713
+ export { DeviceManagerAddon, DeviceManagerAddon as default, setWrapperActive as i, mergeAggregates, nextDisabledSources as n, restoreDeviceSourceActivations as r, isSourceDenied as t };
@@ -43,10 +43,43 @@ 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) and, since D556,
49
+ * for which SOURCES a camera's collection cap has.
50
+ *
51
+ * The two fields answer two different cardinalities and are never both the
52
+ * answer for one cap:
53
+ *
54
+ * - `wrapperAddonId` — a SINGLETON cap's bound wrapper, or the explicit
55
+ * `null` "operator disabled" marker that stops `getBindings` step 2
56
+ * re-binding a `defaultActive` wrapper. Absent means the operator never
57
+ * touched this cap on this device.
58
+ * - `disabledSourceIds` — a COLLECTION cap's switched-OFF sources. A DENY
59
+ * list on purpose: absence means every source the camera has, which is both
60
+ * today's behaviour and the only shape under which a provider registered
61
+ * later is seen without a second write (D556).
62
+ *
63
+ * `disabledSourceIds` holds SOURCE ids (`native:reolink:hub`), not addon ids.
64
+ * One addon can serve several of a device's sources, so an addon-keyed entry
65
+ * could only ever mean "all of them" — which is exactly how a LEGACY entry is
66
+ * still read: {@link DeviceCapBinding.disabledSourceAddonIds} is never written
67
+ * any more and every id in it denies every source that addon serves for this
68
+ * device. One predicate, stated once, so an operator's existing decision does
69
+ * not silently stop applying (D555).
70
+ */
71
+ export interface DeviceCapBinding {
72
+ readonly wrapperAddonId?: string | null;
73
+ /** SOURCE ids the operator switched off for this device. */
74
+ readonly disabledSourceIds?: readonly string[];
75
+ /**
76
+ * @deprecated Pre-re-key entries, keyed by ADDON id. Read, never written:
77
+ * an id here denies every source that addon serves for this device.
78
+ */
79
+ readonly disabledSourceAddonIds?: readonly string[];
80
+ }
46
81
  export interface DeviceBindingsStore {
47
- readonly deviceBindings: Record<string, Record<string, {
48
- wrapperAddonId: string | null;
49
- }>>;
82
+ readonly deviceBindings: Record<string, Record<string, DeviceCapBinding>>;
50
83
  }
51
84
  export interface PersistedDeviceMeta {
52
85
  type: string;
@@ -0,0 +1,70 @@
1
+ import { AddonContext, ICapabilityRegistry, IScopedLogger } from '@camstack/types';
2
+ import { DeviceBindingsStore } from './device-meta-types.js';
3
+ import { WriteLock } from './write-lock.js';
4
+ /**
5
+ * What a wrapper-activation write needs, and nothing more.
6
+ *
7
+ * Deliberately narrower than `AggregationDeps`: the verb touches the settings
8
+ * door, the event bus, the registry and the addon-store write lock. Handing it
9
+ * the device ledger and the remote-native cache it never reads would make this
10
+ * loop untestable without standing up the whole addon.
11
+ */
12
+ export interface WrapperActivationDeps {
13
+ readonly ctx: AddonContext;
14
+ readonly capabilityRegistry: ICapabilityRegistry | undefined;
15
+ /** Ordering for every read-modify-write of the fleet-wide `deviceBindings` blob (D62). */
16
+ readonly withAddonStoreWriteLock: WriteLock;
17
+ }
18
+ /** The operator-facing activation, as it arrives from the cap router. */
19
+ export interface WrapperActivationInput {
20
+ readonly deviceId: number;
21
+ readonly capName: string;
22
+ readonly wrapperAddonId: string;
23
+ /**
24
+ * The SOURCE to toggle, for a collection cap. Absent keeps the cap-wide
25
+ * meaning D556 shipped — every source this addon serves for this device —
26
+ * which is also how a pre-re-key entry is read.
27
+ */
28
+ readonly sourceId?: string;
29
+ readonly active: boolean;
30
+ }
31
+ /**
32
+ * Is `source` denied for a device, given the persisted deny set?
33
+ *
34
+ * The ONE place the migration rule lives: an entry matches either the source's
35
+ * own id or the id of the addon that serves it. New writes are always source
36
+ * ids; an addon id can only be a pre-re-key entry, and it still means "every
37
+ * source this addon serves here".
38
+ */
39
+ export declare function isSourceDenied(denied: readonly string[], source: {
40
+ readonly sourceId?: string;
41
+ readonly addonId: string;
42
+ }): boolean;
43
+ /**
44
+ * The deny list after one toggle. Pure, order-preserving, idempotent — a
45
+ * second "off" on an addon already off changes nothing.
46
+ */
47
+ export declare function nextDisabledSources(current: readonly string[], key: string, active: boolean): readonly string[];
48
+ /**
49
+ * Replay the whole persisted store onto a fresh registry.
50
+ *
51
+ * The projection is in-memory, so a hub restart starts it empty — and an empty
52
+ * deny list means "every source", which would silently switch every source the
53
+ * operator turned off back ON. That is precisely the D62 failure (an off switch
54
+ * that reports on), so the replay is not an optimisation: it is what makes the
55
+ * authority durable. Called from the device-manager's `onInitialize`, before
56
+ * any camera can be asked for its clips, and safe to call again.
57
+ */
58
+ export declare function restoreDeviceSourceActivations(registry: ICapabilityRegistry, store: Partial<DeviceBindingsStore>, logger: IScopedLogger): number;
59
+ /**
60
+ * Activate or deactivate a wrapper for one device.
61
+ *
62
+ * Singleton caps keep the shape D62 fixed: one `wrapperAddonId`, or the
63
+ * explicit `null` marker that stops `getBindings` step 2 re-binding a
64
+ * `defaultActive` wrapper. Collection caps write `disabledSourceAddonIds`
65
+ * instead — their `wrapperAddonId` is left exactly as it was, because a cap
66
+ * with several sources has no "which one" for `getBindings` to answer (D554
67
+ * context 3) and inventing one would turn a source toggle into a cap-wide off
68
+ * switch.
69
+ */
70
+ export declare function setWrapperActive(deps: WrapperActivationDeps, input: WrapperActivationInput): Promise<void>;