@camstack/system 1.2.195 → 1.2.197

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 (54) 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/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +132 -16
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +132 -16
  15. package/dist/builtins/device-manager/device-online-fallback.d.ts +35 -0
  16. package/dist/builtins/device-manager/device-provider-context.d.ts +6 -0
  17. package/dist/builtins/device-manager/removal-fence.d.ts +58 -0
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  20. package/dist/builtins/hub-forwarder/index.js +1 -1
  21. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  26. package/dist/builtins/local-network/local-network.addon.js +1 -1
  27. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  28. package/dist/builtins/loki-logging/index.js +1 -1
  29. package/dist/builtins/loki-logging/index.mjs +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  32. package/dist/builtins/platform-probe/index.js +1 -1
  33. package/dist/builtins/platform-probe/index.mjs +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  36. package/dist/builtins/snapshot/index.js +1 -1
  37. package/dist/builtins/snapshot/index.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/system-config/system-config.addon.js +1 -1
  45. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  46. package/dist/builtins/winston-logging/index.js +1 -1
  47. package/dist/builtins/winston-logging/index.mjs +1 -1
  48. package/dist/{dist-Qnc3kuba.js → dist-BNaW68aZ.js} +124 -2
  49. package/dist/{dist-Cr3ChLtE.mjs → dist-DWnHJI2T.mjs} +124 -2
  50. package/dist/index.js +1 -1
  51. package/dist/index.mjs +1 -1
  52. package/dist/{retired-settings-keys-C_h6HXZi.js → retired-settings-keys-FwgzdSHJ.js} +1 -1
  53. package/dist/{retired-settings-keys-BG6g74f1.mjs → retired-settings-keys-cjLVvYV0.mjs} +1 -1
  54. 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-Qnc3kuba.js");
6
+ const require_dist = require("../../dist-BNaW68aZ.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 { D as addonPagesCapability, Ft as errMsg, It as BaseAddon, cn as EventCategory } from "../../dist-Cr3ChLtE.mjs";
1
+ import { D as addonPagesCapability, Ft as errMsg, It as BaseAddon, cn as EventCategory } from "../../dist-DWnHJI2T.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-Qnc3kuba.js");
6
+ const require_dist = require("../../dist-BNaW68aZ.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 { Ft as errMsg, It as BaseAddon, cn as EventCategory, k as addonWidgetsCapability } from "../../dist-Cr3ChLtE.mjs";
1
+ import { Ft as errMsg, It as BaseAddon, cn as EventCategory, k as addonWidgetsCapability } from "../../dist-DWnHJI2T.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-Qnc3kuba.js");
6
+ const require_dist = require("../../dist-BNaW68aZ.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 { A as alertsCapability, Ft as errMsg, It as BaseAddon, Yt as createEvent, cn as EventCategory, n as AlertSchema } from "../../dist-Cr3ChLtE.mjs";
1
+ import { A as alertsCapability, Ft as errMsg, It as BaseAddon, Yt as createEvent, cn as EventCategory, n as AlertSchema } from "../../dist-DWnHJI2T.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
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-Qnc3kuba.js");
6
+ const require_dist = require("../../dist-BNaW68aZ.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 { It as BaseAddon, M as backupCapability, cn as EventCategory } from "../../dist-Cr3ChLtE.mjs";
1
+ import { It as BaseAddon, M as backupCapability, cn as EventCategory } from "../../dist-DWnHJI2T.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-Qnc3kuba.js");
6
+ const require_dist = require("../../dist-BNaW68aZ.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 { It as BaseAddon, lt as logDestinationCapability } from "../../dist-Cr3ChLtE.mjs";
1
+ import { It as BaseAddon, lt as logDestinationCapability } from "../../dist-DWnHJI2T.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-Qnc3kuba.js");
3
+ const require_dist = require("../../dist-BNaW68aZ.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 { Ft as errMsg, It as BaseAddon, L as coreBlockAddonId, R as coreBlockIdFromAddonId, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID, z as coreBlocksCapability } from "../../dist-Cr3ChLtE.mjs";
1
+ import { Ft as errMsg, It as BaseAddon, L as coreBlockAddonId, R as coreBlockIdFromAddonId, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID, z as coreBlocksCapability } from "../../dist-DWnHJI2T.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";
@@ -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-Qnc3kuba.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-C_h6HXZi.js");
6
+ const require_dist = require("../../dist-BNaW68aZ.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-FwgzdSHJ.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
@@ -1367,6 +1367,17 @@ function resolveDeviceById(registry, deviceId) {
1367
1367
  };
1368
1368
  }
1369
1369
  //#endregion
1370
+ //#region src/builtins/device-manager/device-online-fallback.ts
1371
+ /**
1372
+ * The value to hand `resolveDeviceOnline` as its fallback. The mirrored
1373
+ * `device-status` slice, when one exists, still wins over this.
1374
+ */
1375
+ function persistedOnlineFallback(input) {
1376
+ if (!input.registryPresent) return false;
1377
+ if (input.parentDeviceId !== null) return true;
1378
+ return Object.keys(input.config ?? {}).length > 0;
1379
+ }
1380
+ //#endregion
1370
1381
  //#region src/builtins/device-manager/device-queries.ts
1371
1382
  /**
1372
1383
  * The ORIGINATING addon id off a cap-channel call's `args`, or `undefined`.
@@ -1384,15 +1395,35 @@ function extractCallerAddonId(input) {
1384
1395
  }
1385
1396
  async function listPersistedByAddon(pctx, input) {
1386
1397
  const { addonId } = input;
1387
- return (await pctx.metaStore.rows.listByAddon(addonId)).map(({ meta: m }) => ({
1388
- id: m.id,
1389
- stableId: m.stableId,
1390
- type: m.type,
1391
- name: m.name,
1392
- location: m.location ?? null,
1393
- disabled: m.disabled ?? false,
1394
- parentDeviceId: m.parentDeviceId
1395
- }));
1398
+ const rows = await pctx.metaStore.rows.listByAddon(addonId);
1399
+ const saved = [];
1400
+ for (const row of rows) {
1401
+ const m = row.meta;
1402
+ if (!row.registered) {
1403
+ const reserved = await pctx.settings.readDeviceStore(m.id);
1404
+ if (Object.keys(reserved ?? {}).length === 0) {
1405
+ pctx.host.ctx.logger.warn("Skipping an unregistered device reservation with no configuration — not a device, not restored", {
1406
+ tags: { deviceId: m.id },
1407
+ meta: {
1408
+ addonId,
1409
+ stableId: m.stableId,
1410
+ type: m.type
1411
+ }
1412
+ });
1413
+ continue;
1414
+ }
1415
+ }
1416
+ saved.push({
1417
+ id: m.id,
1418
+ stableId: m.stableId,
1419
+ type: m.type,
1420
+ name: m.name,
1421
+ location: m.location ?? null,
1422
+ disabled: m.disabled ?? false,
1423
+ parentDeviceId: m.parentDeviceId
1424
+ });
1425
+ }
1426
+ return saved;
1396
1427
  }
1397
1428
  /**
1398
1429
  * Every requested device's persisted config, in as few store round trips as the
@@ -1476,7 +1507,11 @@ async function listAll(pctx, input) {
1476
1507
  disabled: m.disabled ?? false,
1477
1508
  parentDeviceId: m.parentDeviceId ?? null,
1478
1509
  role: toDeviceRole(m.role),
1479
- online: pctx.host.resolveDeviceOnline(m.id, pctx.registry !== null),
1510
+ online: pctx.host.resolveDeviceOnline(m.id, persistedOnlineFallback({
1511
+ registryPresent: pctx.registry !== null,
1512
+ parentDeviceId: m.parentDeviceId ?? null,
1513
+ config: persistedConfig
1514
+ })),
1480
1515
  probed: pctx.host.resolveDeviceProbed(m.id),
1481
1516
  features: persistedFeatures(m.features),
1482
1517
  isCamera: persistedIsCamera(persistedType),
@@ -1521,7 +1556,11 @@ async function getDevice(pctx, input) {
1521
1556
  disabled: m.disabled ?? false,
1522
1557
  parentDeviceId: m.parentDeviceId,
1523
1558
  role: toDeviceRole(m.role),
1524
- online: pctx.host.resolveDeviceOnline(deviceId, true),
1559
+ online: pctx.host.resolveDeviceOnline(deviceId, persistedOnlineFallback({
1560
+ registryPresent: true,
1561
+ parentDeviceId: m.parentDeviceId ?? null,
1562
+ config: persistedConfig
1563
+ })),
1525
1564
  probed: pctx.host.resolveDeviceProbed(deviceId),
1526
1565
  features: persistedFeatures(m.features),
1527
1566
  isCamera: persistedIsCamera(m.type),
@@ -1905,7 +1944,7 @@ async function discoverDevices(pctx, input) {
1905
1944
  async function adoptDevice(pctx, input) {
1906
1945
  const dp = await pctx.host.requireDeviceProvider(input.addonId);
1907
1946
  if (!await dp.supportsDiscovery({})) throw new Error(`Addon "${input.addonId}" does not support device adoption`);
1908
- const summary = await dp.adoptDiscoveredDevice({ candidate: input.candidate });
1947
+ const summary = await pctx.removalFence.runUnfenced(() => dp.adoptDiscoveredDevice({ candidate: input.candidate }));
1909
1948
  if (input.integrationId !== void 0) try {
1910
1949
  await pctx.stampIntegrationId(summary.id, input.integrationId);
1911
1950
  } catch (err) {
@@ -1927,10 +1966,10 @@ async function getCreationSchema(pctx, input) {
1927
1966
  async function createDevice(pctx, input) {
1928
1967
  const dp = await pctx.host.requireDeviceProvider(input.addonId);
1929
1968
  if (!await dp.supportsManualCreation({})) throw new Error(`Addon "${input.addonId}" does not support manual device creation`);
1930
- const summary = await dp.createDevice({
1969
+ const summary = await pctx.removalFence.runUnfenced(() => dp.createDevice({
1931
1970
  type: input.type,
1932
1971
  config: input.config
1933
- });
1972
+ }));
1934
1973
  if (input.integrationId !== void 0) try {
1935
1974
  await pctx.stampIntegrationId(summary.id, input.integrationId);
1936
1975
  } catch (err) {
@@ -3792,6 +3831,17 @@ async function allocateDeviceId(pctx, input) {
3792
3831
  return await pctx.metaStore.withMetaWriteLock(async () => {
3793
3832
  const existing = await pctx.metaStore.rows.findByStableId(addonId, stableId);
3794
3833
  if (existing) return { id: existing.meta.id };
3834
+ const fenced = pctx.removalFence.isPairFenced(addonId, stableId);
3835
+ if (fenced) {
3836
+ pctx.host.ctx.logger.warn("allocateDeviceId refused — this device was just removed, NOT re-created", {
3837
+ tags: { deviceId: fenced.deviceId },
3838
+ meta: {
3839
+ addonId,
3840
+ stableId
3841
+ }
3842
+ });
3843
+ throw new Error(`[device-manager] allocateDeviceId: device ${fenced.deviceId} (${addonId}/${stableId}) was removed — refusing to re-create it`);
3844
+ }
3795
3845
  const id = await pctx.metaStore.allocateNextDeviceId();
3796
3846
  await pctx.metaStore.rows.upsert({
3797
3847
  addonId,
@@ -3939,6 +3989,16 @@ async function migrateDevice(pctx, input) {
3939
3989
  async function registerDevice(pctx, input) {
3940
3990
  const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
3941
3991
  const featuresArr = Array.isArray(features) ? [...features] : [];
3992
+ if (pctx.removalFence.isIdFenced(id) ?? pctx.removalFence.isPairFenced(addonId, stableId)) {
3993
+ pctx.host.ctx.logger.warn("registerDevice refused — this device was removed, NOT resurrected", {
3994
+ tags: { deviceId: id },
3995
+ meta: {
3996
+ addonId,
3997
+ stableId
3998
+ }
3999
+ });
4000
+ throw new Error(`[device-manager] registerDevice: device ${id} (${addonId}/${stableId}) was removed — refusing to resurrect it`);
4001
+ }
3942
4002
  const { isFirstRegistration, exportFingerprint, fingerprintChanged, inheritedLocation, inheritedDescendantIds, inheritFailed } = await pctx.metaStore.withMetaWriteLock(async () => {
3943
4003
  const existingRow = await pctx.metaStore.rows.get(id);
3944
4004
  const existingMeta = existingRow?.meta;
@@ -4079,6 +4139,11 @@ async function removeDevice(pctx, input) {
4079
4139
  const deviceName = persistedMeta.name;
4080
4140
  await pctx.metaStore.withMetaWriteLock(async () => {
4081
4141
  await pctx.metaStore.rows.remove(deviceId);
4142
+ pctx.removalFence.record({
4143
+ deviceId,
4144
+ addonId,
4145
+ stableId
4146
+ });
4082
4147
  });
4083
4148
  await pctx.settings.clearDeviceStore(deviceId);
4084
4149
  await pctx.settings.clearDeviceRuntimeState(deviceId);
@@ -6444,6 +6509,56 @@ function createMigrationGuard(now = Date.now) {
6444
6509
  }
6445
6510
  };
6446
6511
  }
6512
+ function pairKey(addonId, stableId) {
6513
+ return `${addonId}${stableId}`;
6514
+ }
6515
+ function createRemovalFence(options = {}) {
6516
+ const ttlMs = options.ttlMs ?? 3e5;
6517
+ const now = options.now ?? (() => Date.now());
6518
+ const byId = /* @__PURE__ */ new Map();
6519
+ const byPair = /* @__PURE__ */ new Map();
6520
+ let suspensions = 0;
6521
+ const fresh = (entry) => {
6522
+ if (entry === void 0) return null;
6523
+ if (suspensions > 0) return null;
6524
+ if (now() - entry.removedAt >= ttlMs) {
6525
+ byId.delete(entry.deviceId);
6526
+ byPair.delete(pairKey(entry.addonId, entry.stableId));
6527
+ return null;
6528
+ }
6529
+ return entry;
6530
+ };
6531
+ return {
6532
+ record(entry) {
6533
+ const full = {
6534
+ ...entry,
6535
+ removedAt: now()
6536
+ };
6537
+ byId.set(full.deviceId, full);
6538
+ byPair.set(pairKey(full.addonId, full.stableId), full);
6539
+ },
6540
+ isIdFenced(deviceId) {
6541
+ return fresh(byId.get(deviceId));
6542
+ },
6543
+ isPairFenced(addonId, stableId) {
6544
+ return fresh(byPair.get(pairKey(addonId, stableId)));
6545
+ },
6546
+ lift(addonId, stableId) {
6547
+ const entry = byPair.get(pairKey(addonId, stableId));
6548
+ if (entry === void 0) return;
6549
+ byPair.delete(pairKey(addonId, stableId));
6550
+ byId.delete(entry.deviceId);
6551
+ },
6552
+ async runUnfenced(fn) {
6553
+ suspensions += 1;
6554
+ try {
6555
+ return await fn();
6556
+ } finally {
6557
+ suspensions -= 1;
6558
+ }
6559
+ }
6560
+ };
6561
+ }
6447
6562
  //#endregion
6448
6563
  //#region src/builtins/device-manager/device-manager.addon.ts
6449
6564
  var DeviceManagerAddon = class extends require_dist.BaseAddon {
@@ -6686,6 +6801,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
6686
6801
  registry,
6687
6802
  bindingsDeps: this.bindingsDeps,
6688
6803
  migrationGuard: createMigrationGuard(),
6804
+ removalFence: createRemovalFence(),
6689
6805
  requireDeviceOps,
6690
6806
  stampIntegrationId: stampIntegrationId$1,
6691
6807
  get provider() {
@@ -1,5 +1,5 @@
1
- import { Bt as DeviceFeature, F as buildStreamParamsConfigSchema, Ft as errMsg, G as deviceStatusCapability, Gt as WELL_KNOWN_TAB_MAP, Ht as DeviceType, It as BaseAddon, J as enumerateItemArrayFields, St as runtimeStatePolicyFor, U as deviceManagerCapability, Vt as DeviceRole, W as deviceStateCapability, Y as enumerateSchemaFields, a as CAMERA_SWITCH_ORDER, bt as resolveContainerPrimaryChild, cn as EventCategory, en as isDeviceConfigCap, f as DeviceStatusSchema, ht as parseStreamParamsFormPatch, o as CAP_NAMES_WITH_STATUS, pt as normalizeUnit, sn as sleep, t as ALL_CAPABILITY_DEFINITIONS, x as STREAM_PROFILE_META } from "../../dist-Cr3ChLtE.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-BG6g74f1.mjs";
1
+ import { Bt as DeviceFeature, F as buildStreamParamsConfigSchema, Ft as errMsg, G as deviceStatusCapability, Gt as WELL_KNOWN_TAB_MAP, Ht as DeviceType, It as BaseAddon, J as enumerateItemArrayFields, St as runtimeStatePolicyFor, U as deviceManagerCapability, Vt as DeviceRole, W as deviceStateCapability, Y as enumerateSchemaFields, a as CAMERA_SWITCH_ORDER, bt as resolveContainerPrimaryChild, cn as EventCategory, en as isDeviceConfigCap, f as DeviceStatusSchema, ht as parseStreamParamsFormPatch, o as CAP_NAMES_WITH_STATUS, pt as normalizeUnit, sn as sleep, t as ALL_CAPABILITY_DEFINITIONS, x as STREAM_PROFILE_META } from "../../dist-DWnHJI2T.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-cjLVvYV0.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
@@ -1362,6 +1362,17 @@ function resolveDeviceById(registry, deviceId) {
1362
1362
  };
1363
1363
  }
1364
1364
  //#endregion
1365
+ //#region src/builtins/device-manager/device-online-fallback.ts
1366
+ /**
1367
+ * The value to hand `resolveDeviceOnline` as its fallback. The mirrored
1368
+ * `device-status` slice, when one exists, still wins over this.
1369
+ */
1370
+ function persistedOnlineFallback(input) {
1371
+ if (!input.registryPresent) return false;
1372
+ if (input.parentDeviceId !== null) return true;
1373
+ return Object.keys(input.config ?? {}).length > 0;
1374
+ }
1375
+ //#endregion
1365
1376
  //#region src/builtins/device-manager/device-queries.ts
1366
1377
  /**
1367
1378
  * The ORIGINATING addon id off a cap-channel call's `args`, or `undefined`.
@@ -1379,15 +1390,35 @@ function extractCallerAddonId(input) {
1379
1390
  }
1380
1391
  async function listPersistedByAddon(pctx, input) {
1381
1392
  const { addonId } = input;
1382
- return (await pctx.metaStore.rows.listByAddon(addonId)).map(({ meta: m }) => ({
1383
- id: m.id,
1384
- stableId: m.stableId,
1385
- type: m.type,
1386
- name: m.name,
1387
- location: m.location ?? null,
1388
- disabled: m.disabled ?? false,
1389
- parentDeviceId: m.parentDeviceId
1390
- }));
1393
+ const rows = await pctx.metaStore.rows.listByAddon(addonId);
1394
+ const saved = [];
1395
+ for (const row of rows) {
1396
+ const m = row.meta;
1397
+ if (!row.registered) {
1398
+ const reserved = await pctx.settings.readDeviceStore(m.id);
1399
+ if (Object.keys(reserved ?? {}).length === 0) {
1400
+ pctx.host.ctx.logger.warn("Skipping an unregistered device reservation with no configuration — not a device, not restored", {
1401
+ tags: { deviceId: m.id },
1402
+ meta: {
1403
+ addonId,
1404
+ stableId: m.stableId,
1405
+ type: m.type
1406
+ }
1407
+ });
1408
+ continue;
1409
+ }
1410
+ }
1411
+ saved.push({
1412
+ id: m.id,
1413
+ stableId: m.stableId,
1414
+ type: m.type,
1415
+ name: m.name,
1416
+ location: m.location ?? null,
1417
+ disabled: m.disabled ?? false,
1418
+ parentDeviceId: m.parentDeviceId
1419
+ });
1420
+ }
1421
+ return saved;
1391
1422
  }
1392
1423
  /**
1393
1424
  * Every requested device's persisted config, in as few store round trips as the
@@ -1471,7 +1502,11 @@ async function listAll(pctx, input) {
1471
1502
  disabled: m.disabled ?? false,
1472
1503
  parentDeviceId: m.parentDeviceId ?? null,
1473
1504
  role: toDeviceRole(m.role),
1474
- online: pctx.host.resolveDeviceOnline(m.id, pctx.registry !== null),
1505
+ online: pctx.host.resolveDeviceOnline(m.id, persistedOnlineFallback({
1506
+ registryPresent: pctx.registry !== null,
1507
+ parentDeviceId: m.parentDeviceId ?? null,
1508
+ config: persistedConfig
1509
+ })),
1475
1510
  probed: pctx.host.resolveDeviceProbed(m.id),
1476
1511
  features: persistedFeatures(m.features),
1477
1512
  isCamera: persistedIsCamera(persistedType),
@@ -1516,7 +1551,11 @@ async function getDevice(pctx, input) {
1516
1551
  disabled: m.disabled ?? false,
1517
1552
  parentDeviceId: m.parentDeviceId,
1518
1553
  role: toDeviceRole(m.role),
1519
- online: pctx.host.resolveDeviceOnline(deviceId, true),
1554
+ online: pctx.host.resolveDeviceOnline(deviceId, persistedOnlineFallback({
1555
+ registryPresent: true,
1556
+ parentDeviceId: m.parentDeviceId ?? null,
1557
+ config: persistedConfig
1558
+ })),
1520
1559
  probed: pctx.host.resolveDeviceProbed(deviceId),
1521
1560
  features: persistedFeatures(m.features),
1522
1561
  isCamera: persistedIsCamera(m.type),
@@ -1900,7 +1939,7 @@ async function discoverDevices(pctx, input) {
1900
1939
  async function adoptDevice(pctx, input) {
1901
1940
  const dp = await pctx.host.requireDeviceProvider(input.addonId);
1902
1941
  if (!await dp.supportsDiscovery({})) throw new Error(`Addon "${input.addonId}" does not support device adoption`);
1903
- const summary = await dp.adoptDiscoveredDevice({ candidate: input.candidate });
1942
+ const summary = await pctx.removalFence.runUnfenced(() => dp.adoptDiscoveredDevice({ candidate: input.candidate }));
1904
1943
  if (input.integrationId !== void 0) try {
1905
1944
  await pctx.stampIntegrationId(summary.id, input.integrationId);
1906
1945
  } catch (err) {
@@ -1922,10 +1961,10 @@ async function getCreationSchema(pctx, input) {
1922
1961
  async function createDevice(pctx, input) {
1923
1962
  const dp = await pctx.host.requireDeviceProvider(input.addonId);
1924
1963
  if (!await dp.supportsManualCreation({})) throw new Error(`Addon "${input.addonId}" does not support manual device creation`);
1925
- const summary = await dp.createDevice({
1964
+ const summary = await pctx.removalFence.runUnfenced(() => dp.createDevice({
1926
1965
  type: input.type,
1927
1966
  config: input.config
1928
- });
1967
+ }));
1929
1968
  if (input.integrationId !== void 0) try {
1930
1969
  await pctx.stampIntegrationId(summary.id, input.integrationId);
1931
1970
  } catch (err) {
@@ -3787,6 +3826,17 @@ async function allocateDeviceId(pctx, input) {
3787
3826
  return await pctx.metaStore.withMetaWriteLock(async () => {
3788
3827
  const existing = await pctx.metaStore.rows.findByStableId(addonId, stableId);
3789
3828
  if (existing) return { id: existing.meta.id };
3829
+ const fenced = pctx.removalFence.isPairFenced(addonId, stableId);
3830
+ if (fenced) {
3831
+ pctx.host.ctx.logger.warn("allocateDeviceId refused — this device was just removed, NOT re-created", {
3832
+ tags: { deviceId: fenced.deviceId },
3833
+ meta: {
3834
+ addonId,
3835
+ stableId
3836
+ }
3837
+ });
3838
+ throw new Error(`[device-manager] allocateDeviceId: device ${fenced.deviceId} (${addonId}/${stableId}) was removed — refusing to re-create it`);
3839
+ }
3790
3840
  const id = await pctx.metaStore.allocateNextDeviceId();
3791
3841
  await pctx.metaStore.rows.upsert({
3792
3842
  addonId,
@@ -3934,6 +3984,16 @@ async function migrateDevice(pctx, input) {
3934
3984
  async function registerDevice(pctx, input) {
3935
3985
  const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
3936
3986
  const featuresArr = Array.isArray(features) ? [...features] : [];
3987
+ if (pctx.removalFence.isIdFenced(id) ?? pctx.removalFence.isPairFenced(addonId, stableId)) {
3988
+ pctx.host.ctx.logger.warn("registerDevice refused — this device was removed, NOT resurrected", {
3989
+ tags: { deviceId: id },
3990
+ meta: {
3991
+ addonId,
3992
+ stableId
3993
+ }
3994
+ });
3995
+ throw new Error(`[device-manager] registerDevice: device ${id} (${addonId}/${stableId}) was removed — refusing to resurrect it`);
3996
+ }
3937
3997
  const { isFirstRegistration, exportFingerprint, fingerprintChanged, inheritedLocation, inheritedDescendantIds, inheritFailed } = await pctx.metaStore.withMetaWriteLock(async () => {
3938
3998
  const existingRow = await pctx.metaStore.rows.get(id);
3939
3999
  const existingMeta = existingRow?.meta;
@@ -4074,6 +4134,11 @@ async function removeDevice(pctx, input) {
4074
4134
  const deviceName = persistedMeta.name;
4075
4135
  await pctx.metaStore.withMetaWriteLock(async () => {
4076
4136
  await pctx.metaStore.rows.remove(deviceId);
4137
+ pctx.removalFence.record({
4138
+ deviceId,
4139
+ addonId,
4140
+ stableId
4141
+ });
4077
4142
  });
4078
4143
  await pctx.settings.clearDeviceStore(deviceId);
4079
4144
  await pctx.settings.clearDeviceRuntimeState(deviceId);
@@ -6439,6 +6504,56 @@ function createMigrationGuard(now = Date.now) {
6439
6504
  }
6440
6505
  };
6441
6506
  }
6507
+ function pairKey(addonId, stableId) {
6508
+ return `${addonId}${stableId}`;
6509
+ }
6510
+ function createRemovalFence(options = {}) {
6511
+ const ttlMs = options.ttlMs ?? 3e5;
6512
+ const now = options.now ?? (() => Date.now());
6513
+ const byId = /* @__PURE__ */ new Map();
6514
+ const byPair = /* @__PURE__ */ new Map();
6515
+ let suspensions = 0;
6516
+ const fresh = (entry) => {
6517
+ if (entry === void 0) return null;
6518
+ if (suspensions > 0) return null;
6519
+ if (now() - entry.removedAt >= ttlMs) {
6520
+ byId.delete(entry.deviceId);
6521
+ byPair.delete(pairKey(entry.addonId, entry.stableId));
6522
+ return null;
6523
+ }
6524
+ return entry;
6525
+ };
6526
+ return {
6527
+ record(entry) {
6528
+ const full = {
6529
+ ...entry,
6530
+ removedAt: now()
6531
+ };
6532
+ byId.set(full.deviceId, full);
6533
+ byPair.set(pairKey(full.addonId, full.stableId), full);
6534
+ },
6535
+ isIdFenced(deviceId) {
6536
+ return fresh(byId.get(deviceId));
6537
+ },
6538
+ isPairFenced(addonId, stableId) {
6539
+ return fresh(byPair.get(pairKey(addonId, stableId)));
6540
+ },
6541
+ lift(addonId, stableId) {
6542
+ const entry = byPair.get(pairKey(addonId, stableId));
6543
+ if (entry === void 0) return;
6544
+ byPair.delete(pairKey(addonId, stableId));
6545
+ byId.delete(entry.deviceId);
6546
+ },
6547
+ async runUnfenced(fn) {
6548
+ suspensions += 1;
6549
+ try {
6550
+ return await fn();
6551
+ } finally {
6552
+ suspensions -= 1;
6553
+ }
6554
+ }
6555
+ };
6556
+ }
6442
6557
  //#endregion
6443
6558
  //#region src/builtins/device-manager/device-manager.addon.ts
6444
6559
  var DeviceManagerAddon = class extends BaseAddon {
@@ -6681,6 +6796,7 @@ var DeviceManagerAddon = class extends BaseAddon {
6681
6796
  registry,
6682
6797
  bindingsDeps: this.bindingsDeps,
6683
6798
  migrationGuard: createMigrationGuard(),
6799
+ removalFence: createRemovalFence(),
6684
6800
  requireDeviceOps,
6685
6801
  stampIntegrationId: stampIntegrationId$1,
6686
6802
  get provider() {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * What "online" means for a device the hub can only see on disk.
3
+ *
4
+ * The authority for a device's liveness is the addon that owns it (D224): it
5
+ * publishes `device-status` through `markOnline`, the hub mirrors that slice,
6
+ * and every persisted-row projection reads the mirror. The FALLBACK — what the
7
+ * hub answers before any slice has arrived — used to be "a persisted row
8
+ * implies a successful registration, so: online". That assumption is false for
9
+ * exactly one shape of row, and it is the shape a bad removal leaves behind: a
10
+ * top-level device with NO configuration, which its provider can never restore
11
+ * (its config schema rejects an empty blob), so no owner will ever publish a
12
+ * slice contradicting the guess.
13
+ *
14
+ * Device 614 was reported `online: true` for a full day that way, while its
15
+ * provider logged `Failed to restore device` eight times. A device without
16
+ * configuration cannot be online — the row is a leftover, not a camera. See
17
+ * D376.
18
+ *
19
+ * Children are left on the legacy fallback deliberately: an accessory child
20
+ * (siren, floodlight, PIR) legitimately carries an empty config blob and gets
21
+ * its liveness from the parent's worker.
22
+ */
23
+ export interface PersistedOnlineFallbackInput {
24
+ /** Is a hub-side device registry present at all? `false` is the "offline view". */
25
+ readonly registryPresent: boolean;
26
+ /** `null` for a top-level device. */
27
+ readonly parentDeviceId: number | null;
28
+ /** The device's persisted config blob, as read from its device store. */
29
+ readonly config: Record<string, unknown> | undefined;
30
+ }
31
+ /**
32
+ * The value to hand `resolveDeviceOnline` as its fallback. The mirrored
33
+ * `device-status` slice, when one exists, still wins over this.
34
+ */
35
+ export declare function persistedOnlineFallback(input: PersistedOnlineFallbackInput): boolean;
@@ -3,6 +3,7 @@ import { AdoptionJobEngine } from './adoption-job-engine.js';
3
3
  import { BindingsDeps } from './device-bindings-store.js';
4
4
  import { DeviceManagerSettings, DeviceMetaStore } from './device-meta-store.js';
5
5
  import { MigrationGuard } from './migrate-guard.js';
6
+ import { RemovalFence } from './removal-fence.js';
6
7
  type IDeviceProviderCap = InferProvider<typeof deviceProviderCapability>;
7
8
  /**
8
9
  * The addon-instance surface the extracted provider functions need. Exposed by
@@ -51,6 +52,11 @@ export interface ProviderContext {
51
52
  /** In-flight refusal + completed-swap replay for `migrateDevice` — the
52
53
  * reason a retried migration can no longer undo the one that ran. */
53
54
  readonly migrationGuard: MigrationGuard;
55
+ /** Keeps a removed device removed: refuses the `registerDevice` /
56
+ * `allocateDeviceId` that a still-live device object issues as its runner
57
+ * tears down, which is how a deleted camera came back as a config-less
58
+ * shell (D376). */
59
+ readonly removalFence: RemovalFence;
54
60
  /** Resolve the per-device `device-ops` native provider (throws when absent). */
55
61
  requireDeviceOps(deviceId: number): IDeviceOpsProvider;
56
62
  /** Stamp an integration id onto a device's meta row + emit DeviceMetaChanged. */