@camstack/system 1.2.316 → 1.2.317

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 (87) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/auth/auth-manager.d.ts +11 -0
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +1 -1
  11. package/dist/builtins/autotrack/index.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  14. package/dist/builtins/camera-grid/index.js +1 -1
  15. package/dist/builtins/camera-grid/index.mjs +1 -1
  16. package/dist/builtins/composer/composer.addon.js +347 -181
  17. package/dist/builtins/composer/composer.addon.mjs +347 -181
  18. package/dist/builtins/composer/composition-evaluation.d.ts +37 -0
  19. package/dist/builtins/composer/composition-runtime.d.ts +11 -52
  20. package/dist/builtins/composer/composition-target.d.ts +50 -0
  21. package/dist/builtins/composer/existing-target.d.ts +19 -1
  22. package/dist/builtins/composer/own-field-reads.d.ts +19 -0
  23. package/dist/builtins/console-logging/index.js +1 -1
  24. package/dist/builtins/console-logging/index.mjs +1 -1
  25. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  26. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  27. package/dist/builtins/device-manager/device-manager.addon.js +265 -29
  28. package/dist/builtins/device-manager/device-manager.addon.mjs +265 -29
  29. package/dist/builtins/device-manager/device-provider-context.d.ts +6 -0
  30. package/dist/builtins/device-manager/device-source-activation.d.ts +26 -1
  31. package/dist/builtins/device-manager/device-state-mirror.d.ts +41 -9
  32. package/dist/builtins/device-manager/mirror-row-writer.d.ts +59 -1
  33. package/dist/builtins/doorbell/binding-mirror.d.ts +45 -81
  34. package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +4 -3
  35. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +53 -11
  36. package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -82
  37. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -82
  38. package/dist/builtins/hub-forwarder/index.js +1 -1
  39. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  40. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  41. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  42. package/dist/builtins/local-auth/local-auth.addon.js +47 -3
  43. package/dist/builtins/local-auth/local-auth.addon.mjs +46 -4
  44. package/dist/builtins/local-network/local-network.addon.js +1 -1
  45. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  46. package/dist/builtins/loki-logging/index.js +1 -1
  47. package/dist/builtins/loki-logging/index.mjs +1 -1
  48. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  49. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  50. package/dist/builtins/platform-probe/index.js +1 -1
  51. package/dist/builtins/platform-probe/index.mjs +1 -1
  52. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/snapshot/index.js +1 -1
  55. package/dist/builtins/snapshot/index.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  57. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  60. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  61. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  62. package/dist/builtins/system-config/system-config.addon.js +4 -4
  63. package/dist/builtins/system-config/system-config.addon.mjs +4 -4
  64. package/dist/builtins/winston-logging/index.js +1 -1
  65. package/dist/builtins/winston-logging/index.mjs +1 -1
  66. package/dist/{child-cap-dispatch-AAltfBUH.mjs → child-cap-dispatch-C2VINHu1.mjs} +70 -18
  67. package/dist/{child-cap-dispatch-DzcUd_ct.js → child-cap-dispatch-C8uts1P4.js} +70 -18
  68. package/dist/{composition-sources-fUyF4z39.mjs → composition-sources-DVClu006.mjs} +1 -1
  69. package/dist/{composition-sources-BwE1POnF.js → composition-sources-Dep7nBwu.js} +1 -1
  70. package/dist/{dist-BPvku43i.mjs → dist-Dori4pf3.mjs} +85 -6
  71. package/dist/{dist-RAl10EfG.js → dist-a6p0V-IL.js} +85 -6
  72. package/dist/index.js +739 -350
  73. package/dist/index.mjs +737 -352
  74. package/dist/kernel/bootstrap-config.d.ts +8 -0
  75. package/dist/kernel/capability-registry.d.ts +30 -0
  76. package/dist/kernel/config-manager.d.ts +75 -129
  77. package/dist/kernel/index.d.ts +5 -2
  78. package/dist/kernel/moleculer/runtime-state-load.d.ts +19 -0
  79. package/dist/kernel/settings-door-view.d.ts +24 -1
  80. package/dist/kernel/settings-read-error.d.ts +37 -0
  81. package/dist/kernel/settings-view-types.d.ts +105 -0
  82. package/dist/kernel/store-settings-view.d.ts +17 -0
  83. package/dist/kernel/system-settings-mirror.d.ts +34 -0
  84. package/dist/kernel/system-settings-policy.d.ts +47 -0
  85. package/dist/{retired-settings-keys-D3rGTGq5.js → retired-settings-keys-DC1uHWPZ.js} +1 -1
  86. package/dist/{retired-settings-keys-DqVWSrVw.mjs → retired-settings-keys-DVcKRgzm.mjs} +1 -1
  87. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { At as isSameAddonId, Dn as SOURCE_CAP_ACTIVE_FIELD, En as SOURCE_CAPS, Gn as BaseAddon, On as SOURCE_CAP_CHANGED_AT_FIELD, Rn as evaluateSensorEdge, Un as DeviceType, W as bareAddonId, Wn as errMsg, ct as defineExtensionSlotProvider, hr as EventCategory, it as createDeviceExtensionProvider, mt as doorbellCapability, tr as createEvent, ut as deviceExtensionCapability, zn as isSourceCap } from "../../dist-BPvku43i.mjs";
1
+ import { At as isSameAddonId, Dn as SOURCE_CAP_ACTIVE_FIELD, En as SOURCE_CAPS, Gn as BaseAddon, On as SOURCE_CAP_CHANGED_AT_FIELD, Rn as evaluateSensorEdge, Un as DeviceType, W as bareAddonId, Wn as errMsg, ct as defineExtensionSlotProvider, hr as EventCategory, it as createDeviceExtensionProvider, mt as doorbellCapability, tr as createEvent, ut as deviceExtensionCapability, zn as isSourceCap } from "../../dist-Dori4pf3.mjs";
2
2
  import { n as parseDeviceStateChanged } from "../../device-state-changed-DoBbkyEI.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/builtins/doorbell/trigger-engine.ts
@@ -196,11 +196,18 @@ var DoorbellTriggerEngine = class {
196
196
  * 2. The DERIVED view (`deviceManager.getBindings`) is no longer the sole
197
197
  * input. Before a negative is believed — and again on the ring path
198
198
  * before a press is destroyed — the DURABLE authority is consulted
199
- * directly ({@link readDurableWrapperBinding} over the `device-manager`
200
- * addon store's `deviceBindings` map). That read carries a discriminator
201
- * the derived view genuinely cannot offer: a store answering from an
202
- * empty snapshot (D44) has no `deviceBindings` key at all, while a
203
- * genuine unbind leaves the key present and merely omits the row.
199
+ * directly ({@link durableVerdictOf} over
200
+ * `deviceManager.getWrapperActivation`). That read carries a discriminator
201
+ * the derived view genuinely cannot offer: it is three-way, so a store
202
+ * that could not answer (`failed`) is never mistaken for a store that
203
+ * answered "nothing here" (`missing`).
204
+ *
205
+ * 2026-09-29 (D675). Until then the durable read was
206
+ * `addonSettingsRaw.getGlobal({ addonId: 'device-manager' })`, whose backend
207
+ * (`ConfigManager.getAddonConfig`) has had no store on the hub since D231
208
+ * isolated the engine: it answered `{}` on every call, so every durable read
209
+ * was `unreadable`, an operator's explicit "off" (camera 615,
210
+ * `wrapperAddonId: null`) was never honoured, and a press rang anyway.
204
211
  */
205
212
  /**
206
213
  * Fold one SUCCESSFUL bindings read into the mirror. A read that failed must
@@ -211,9 +218,12 @@ function nextBindingState(previous, boundNow) {
211
218
  if (boundNow) return "bound";
212
219
  return previous === "unbind-pending" || previous === "unbound" ? "unbound" : "unbind-pending";
213
220
  }
214
- /** May a ring proceed? Only a corroborated unbind stops one. */
221
+ /**
222
+ * May a ring proceed on the mirror STATE alone? Only a corroborated unbind
223
+ * stops one. {@link needsDurableRead} adds the served-elsewhere case on top.
224
+ */
215
225
  function allowsRing(state) {
216
- return !needsDurableRead(state);
226
+ return state !== "unbound";
217
227
  }
218
228
  /** True only when a read has positively confirmed the binding. */
219
229
  function hasConfirmedBinding(state) {
@@ -238,46 +248,62 @@ function entriesBindWrapper(entries, capName, ownAddonId) {
238
248
  function summariseEntries(entries) {
239
249
  return entries.map((entry) => `${entry.capName}:${entry.kind}:${entry.providerAddonId}`);
240
250
  }
241
- function isRecord$2(value) {
242
- return typeof value === "object" && value !== null && !Array.isArray(value);
243
- }
244
251
  /**
245
- * Read the DURABLE authority — the `device-manager` addon store's
246
- * `deviceBindings` map — for "is `ownAddonId` the active wrapper of
247
- * `capName` on `deviceId`".
252
+ * The durable verdict for `ownAddonId`, from device-manager's STORED
253
+ * activation of this cap on this device (`deviceManager.getWrapperActivation`).
248
254
  *
249
- * This is the input the derived `getBindings` view is not: it is the row the
250
- * operator's toggle actually writes, with no registry resolution, no
251
- * `defaultActive` auto-binding and no cross-node cap folding in between. It
252
- * also grades its own trustworthiness, which `getBindings` cannot: the
253
- * absence of the `deviceBindings` KEY is the signature of a store answering
254
- * from an empty snapshot, whereas a genuine unbind leaves the key present.
255
+ * - `value` naming this wrapper → `bound`.
256
+ * - `value` naming another wrapper, or `null` — the operator's explicit "off"
257
+ * marker (D62) — → `unbound`.
258
+ * - `missing` → `unbound`: the store ANSWERED and holds no activation, and the
259
+ * doorbell cap is `defaultActive: false`, so a wrapper nobody activated is
260
+ * not bound.
261
+ * - `failed` → `unreadable`: it carries no information and destroys nothing.
262
+ */
263
+ function durableVerdictOf(read, ownAddonId) {
264
+ switch (read.kind) {
265
+ case "failed": return "unreadable";
266
+ case "missing": return "unbound";
267
+ case "value": return read.wrapperAddonId !== null && isSameAddonId(read.wrapperAddonId, ownAddonId) ? "bound" : "unbound";
268
+ }
269
+ }
270
+ /**
271
+ * Who, other than this wrapper, the derived view says serves `capName` —
272
+ * `composer` on a camera whose doorbell was migrated to a composition (D671).
273
+ * Non-empty means a press this wrapper does not ring is still somebody's ring.
255
274
  */
256
- function readDurableWrapperBinding(record, deviceId, capName, ownAddonId) {
257
- if (!isRecord$2(record)) return "unreadable";
258
- const deviceBindings = record["deviceBindings"];
259
- if (!isRecord$2(deviceBindings)) return "unreadable";
260
- const perDevice = deviceBindings[String(deviceId)];
261
- if (!isRecord$2(perDevice)) return "unbound";
262
- const activation = perDevice[capName];
263
- if (!isRecord$2(activation)) return "unbound";
264
- const wrapperAddonId = activation["wrapperAddonId"];
265
- if (typeof wrapperAddonId !== "string") return "unbound";
266
- return isSameAddonId(wrapperAddonId, ownAddonId) ? "bound" : "unbound";
275
+ function otherProvidersOf(entries, capName, ownAddonId) {
276
+ return entries.filter((entry) => entry.capName === capName && !isSameAddonId(entry.providerAddonId, ownAddonId)).map((entry) => entry.providerAddonId);
267
277
  }
268
- /** Does this mirror state need the durable authority before a verdict? Only `unbound` does. */
269
- function needsDurableRead(state) {
270
- return state === "unbound";
278
+ /**
279
+ * Does this camera need the durable authority before a verdict?
280
+ *
281
+ * An `unbound` mirror does (D72). So does any mirror short of `bound` on a
282
+ * camera whose doorbell another provider serves — the D671 cutover, where
283
+ * erring open means ringing ON TOP of the composer: after every restart the
284
+ * mirror starts `undefined` and spends a reload cycle at `unbind-pending`, and
285
+ * each press in that window was a duplicate ring. Two independent reads
286
+ * agreeing — the derived view naming another provider, the store naming no
287
+ * wrapper — is the D49 "second read agrees"; an unreadable store still rings.
288
+ */
289
+ function needsDurableRead(mirror) {
290
+ if (!allowsRing(mirror.state)) return true;
291
+ return mirror.servedBy.length > 0 && mirror.state !== "bound";
271
292
  }
272
293
  /**
273
- * THE decision — the ring gate (`fireDoorbell` / `ringsDespiteUnboundMirror`)
294
+ * THE decision — the ring gate (`fireDoorbell` / `ringsAfterDurableRead`)
274
295
  * and the Extensions card's activation both call it, so what the card says
275
296
  * and what a press does cannot disagree. `durable` is `null` when it was not
276
- * read, which {@link needsDurableRead} allows for every state but `unbound`.
297
+ * read, which {@link needsDurableRead} allows only where it says so.
277
298
  * Pure: it reads nothing and writes nothing.
278
299
  */
279
- function ringVerdict(state, durable) {
300
+ function ringVerdict(mirror, durable) {
301
+ const state = mirror.state;
280
302
  if (state === "bound") return "rings";
303
+ if (mirror.servedBy.length > 0) {
304
+ if (durable === "unbound") return "silent";
305
+ if (durable === "bound") return "rings";
306
+ }
281
307
  if (state === "unbind-pending") return "rings-pending";
282
308
  if (state === void 0) return "rings-unconfirmed";
283
309
  if (durable === "bound") return "rings";
@@ -848,13 +874,6 @@ function errorText(err) {
848
874
  }
849
875
  //#endregion
850
876
  //#region src/builtins/doorbell/virtual-doorbell.addon.ts
851
- /**
852
- * The addon whose store IS the durable authority on wrapper activations.
853
- * `deviceBindings[<deviceId>][<capName>].wrapperAddonId` is the row the
854
- * operator's bindings toggle writes; `deviceManager.getBindings` is a VIEW
855
- * derived from it plus the capability registry.
856
- */
857
- var DEVICE_MANAGER_ADDON_ID = "device-manager";
858
877
  /** Narrow a raw `doorbell` runtime-state slice to counters (defaults on absence). */
859
878
  function parseDoorbellSlice(slice) {
860
879
  const lastPressedAtRaw = slice?.["lastPressedAt"];
@@ -907,6 +926,24 @@ var VirtualDoorbellAddon = class extends BaseAddon {
907
926
  * `binding-mirror.ts` for why a single negative read is not believed.
908
927
  */
909
928
  bindingMirror = /* @__PURE__ */ new Map();
929
+ /**
930
+ * Per camera, the last NEGATIVE refresh this addon reported — its state,
931
+ * the authority's verdict and who else serves the doorbell. A refresh runs
932
+ * every reload cycle (about once a minute); the same answer repeated is not
933
+ * news, so it is reported once per change and forgotten on a positive read.
934
+ * Camera 615 logged the same WARN 607 times in 14 h after D671.
935
+ */
936
+ reportedNegative = /* @__PURE__ */ new Map();
937
+ /** Cameras whose last durable-authority read THREW — reported once per outage. */
938
+ durableReadFailing = /* @__PURE__ */ new Set();
939
+ /**
940
+ * Per camera, the OTHER providers of `doorbell` the last successful derived
941
+ * read named (the composer after D671). Written off the ring path with the
942
+ * mirror; read on it, to decide whether erring open would double a ring.
943
+ */
944
+ servedElsewhere = /* @__PURE__ */ new Map();
945
+ /** Cameras whose store-over-derived disagreement has been reported, until a derived read agrees. */
946
+ reportedStoreOverride = /* @__PURE__ */ new Set();
910
947
  constructor() {
911
948
  super({
912
949
  debounceMs: 2e3,
@@ -987,7 +1024,7 @@ var VirtualDoorbellAddon = class extends BaseAddon {
987
1024
  projection: "slim",
988
1025
  deviceIds: [...deviceIds]
989
1026
  })).map(toSlotDevice),
990
- readBindingMirror: (deviceId) => this.bindingMirror.get(deviceId),
1027
+ readBindingMirror: (deviceId) => this.mirrorRead(deviceId),
991
1028
  readDurableBinding: (deviceId) => this.readDurableAuthority(deviceId),
992
1029
  sourceCapableDeviceIds: () => this.ctx.api.deviceManager.listDeviceIdsByCaps.query({ capNames: [...SOURCE_CAPS] }),
993
1030
  listFleet: async () => (await this.ctx.api.deviceManager.listAll.query({ projection: "slim" })).map(toSlotDevice)
@@ -1036,9 +1073,9 @@ var VirtualDoorbellAddon = class extends BaseAddon {
1036
1073
  * (UI toast, notifier rules, exporters) is agnostic to the origin.
1037
1074
  *
1038
1075
  * The binding gate here is a pure in-memory read on the path that RINGS:
1039
- * an open mirror performs NO I/O and cannot fail. The mirror can only
1040
- * DESTROY a press through the one branch below, and that branch is not
1041
- * allowed to be the last word — see `ringsDespiteUnboundMirror`.
1076
+ * a `bound` mirror, or an open one on a camera nobody else serves, performs
1077
+ * NO I/O and cannot fail. Every other case asks the durable authority once
1078
+ * before deciding — see `needsDurableRead` and `ringsAfterDurableRead`.
1042
1079
  *
1043
1080
  * `sourceDeviceId`/`capName` are the source that produced this rise —
1044
1081
  * carried through purely so the terminal `pressed` line can name it. Every
@@ -1048,8 +1085,9 @@ var VirtualDoorbellAddon = class extends BaseAddon {
1048
1085
  * timestamp against provider logs (proven live 2026-08-25, camera 615).
1049
1086
  */
1050
1087
  async fireDoorbell(cameraId, timestamp, sourceDeviceId, capName) {
1051
- const binding = this.bindingMirror.get(cameraId);
1052
- if (!allowsRing(binding) && !await this.ringsDespiteUnboundMirror(cameraId, binding)) return;
1088
+ const mirror = this.mirrorRead(cameraId);
1089
+ const binding = mirror.state;
1090
+ if (needsDurableRead(mirror) && !await this.ringsAfterDurableRead(cameraId, mirror)) return;
1053
1091
  if (!hasConfirmedBinding(binding)) this.ctx.logger.warn("virtual-doorbell: ringing on an unconfirmed binding — the bindings store has not corroborated an unbind, and a one-shot press is never dropped on an unproven read", {
1054
1092
  tags: { deviceId: cameraId },
1055
1093
  meta: { bindingState: binding ?? "never-read" }
@@ -1126,8 +1164,10 @@ var VirtualDoorbellAddon = class extends BaseAddon {
1126
1164
  }
1127
1165
  }
1128
1166
  /**
1129
- * One read of the DURABLE authority: the `device-manager` addon store's
1130
- * `deviceBindings` map, straight from the settings DB.
1167
+ * One read of the DURABLE authority: the wrapper activation device-manager
1168
+ * STORED for this camera (`deviceManager.getWrapperActivation`, D675) — the
1169
+ * row the operator's bindings toggle writes. `deviceManager.getBindings` is
1170
+ * a VIEW derived from it plus the capability registry.
1131
1171
  *
1132
1172
  * This is deliberately NOT `getBindings`. The derived view folds the store
1133
1173
  * together with the capability registry, `defaultActive` auto-binding and
@@ -1136,17 +1176,35 @@ var VirtualDoorbellAddon = class extends BaseAddon {
1136
1176
  * spelling mismatch) made it disagree *systematically* while looking
1137
1177
  * exactly like a genuine unbind. The raw row cannot: it is what the
1138
1178
  * bindings toggle wrote, and it grades its own trustworthiness (D44).
1179
+ *
1180
+ * A read that throws is reported when the outage STARTS and when it ends —
1181
+ * never per read: this runs on every reload cycle.
1139
1182
  */
1140
1183
  async readDurableAuthority(cameraId) {
1184
+ let read;
1141
1185
  try {
1142
- return readDurableWrapperBinding(await this.ctx.api.addonSettingsRaw.getGlobal.query({ addonId: DEVICE_MANAGER_ADDON_ID }), cameraId, doorbellCapability.name, this.ownAddonId);
1143
- } catch (err) {
1144
- this.ctx.logger.warn("virtual-doorbell: the durable bindings store could not be read — it carries no information and revokes nothing", {
1145
- tags: { deviceId: cameraId },
1146
- meta: { error: errMsg(err) }
1186
+ read = await this.ctx.api.deviceManager.getWrapperActivation.query({
1187
+ deviceId: cameraId,
1188
+ capName: doorbellCapability.name
1147
1189
  });
1190
+ } catch (err) {
1191
+ read = {
1192
+ kind: "failed",
1193
+ error: errMsg(err)
1194
+ };
1195
+ }
1196
+ if (read.kind === "failed") {
1197
+ if (!this.durableReadFailing.has(cameraId)) {
1198
+ this.durableReadFailing.add(cameraId);
1199
+ this.ctx.logger.warn("virtual-doorbell: the durable bindings store could not be read — it carries no information and revokes nothing (reported once until it answers again)", {
1200
+ tags: { deviceId: cameraId },
1201
+ meta: { error: read.error }
1202
+ });
1203
+ }
1148
1204
  return "unreadable";
1149
1205
  }
1206
+ if (this.durableReadFailing.delete(cameraId)) this.ctx.logger.info("virtual-doorbell: the durable bindings store answers again", { tags: { deviceId: cameraId } });
1207
+ return durableVerdictOf(read, this.ownAddonId);
1150
1208
  }
1151
1209
  /**
1152
1210
  * Reconcile `cameraId`'s mirror against the authority. Runs on the
@@ -1164,60 +1222,114 @@ var VirtualDoorbellAddon = class extends BaseAddon {
1164
1222
  async refreshBindingMirror(cameraId) {
1165
1223
  const derived = await this.readDerivedBinding(cameraId);
1166
1224
  if (derived === null) return;
1225
+ const servedBy = otherProvidersOf(derived.entries, doorbellCapability.name, this.ownAddonId);
1226
+ this.servedElsewhere.set(cameraId, servedBy);
1167
1227
  if (derived.bound) {
1168
1228
  this.bindingMirror.set(cameraId, nextBindingState(this.bindingMirror.get(cameraId), true));
1229
+ this.reportedNegative.delete(cameraId);
1230
+ this.reportedStoreOverride.delete(cameraId);
1169
1231
  return;
1170
1232
  }
1171
1233
  const authority = await this.readDurableAuthority(cameraId);
1172
1234
  const entries = summariseEntries(derived.entries);
1173
1235
  if (authority === "bound") {
1174
- this.ctx.logger.warn("virtual-doorbell: the derived binding list omits this wrapper but the durable bindings store still names this wrapper — trusting the store", {
1175
- tags: { deviceId: cameraId },
1176
- meta: {
1177
- ownAddonId: this.ownAddonId,
1178
- entries
1179
- }
1180
- });
1236
+ this.reportedNegative.delete(cameraId);
1237
+ if (!this.reportedStoreOverride.has(cameraId)) {
1238
+ this.reportedStoreOverride.add(cameraId);
1239
+ this.ctx.logger.warn("virtual-doorbell: the derived binding list omits this wrapper but the durable bindings store still names this wrapper — trusting the store (reported once until they agree)", {
1240
+ tags: { deviceId: cameraId },
1241
+ meta: {
1242
+ ownAddonId: this.ownAddonId,
1243
+ entries
1244
+ }
1245
+ });
1246
+ }
1181
1247
  this.bindingMirror.set(cameraId, nextBindingState(this.bindingMirror.get(cameraId), true));
1182
1248
  return;
1183
1249
  }
1250
+ this.reportedStoreOverride.delete(cameraId);
1184
1251
  const previous = this.bindingMirror.get(cameraId);
1185
1252
  const next = nextBindingState(previous, false);
1186
1253
  this.bindingMirror.set(cameraId, next);
1187
- this.ctx.logger.warn("virtual-doorbell: bindings read did not see this wrapper", {
1254
+ this.reportNegativeRefresh(cameraId, {
1255
+ from: previous ?? "never-read",
1256
+ to: next,
1257
+ authority,
1258
+ servedBy,
1259
+ entries
1260
+ });
1261
+ }
1262
+ /** The ring gate's in-memory input for one camera — no I/O. */
1263
+ mirrorRead(cameraId) {
1264
+ return {
1265
+ state: this.bindingMirror.get(cameraId),
1266
+ servedBy: this.servedElsewhere.get(cameraId) ?? []
1267
+ };
1268
+ }
1269
+ /**
1270
+ * Report a negative refresh ONCE per change of what it says (D391: a
1271
+ * repeated identical line is noise that buries the next real one).
1272
+ *
1273
+ * The level follows what the answer MEANS:
1274
+ * - the durable store could not grade it (`unreadable`) — WARN: the
1275
+ * camera's ring now depends on erring open, and nobody can say whether
1276
+ * that is right;
1277
+ * - the store CONFIRMS the wrapper is not bound — an operator's unbind or
1278
+ * the D671 cutover to a composition. That is a state, not an anomaly:
1279
+ * INFO when it is believed (`unbound`), DEBUG for the first,
1280
+ * not-yet-believed read.
1281
+ */
1282
+ reportNegativeRefresh(cameraId, report) {
1283
+ const key = `${report.to}|${report.authority}|${report.servedBy.join(",")}`;
1284
+ if (this.reportedNegative.get(cameraId) === key) return;
1285
+ this.reportedNegative.set(cameraId, key);
1286
+ const options = {
1188
1287
  tags: { deviceId: cameraId },
1189
1288
  meta: {
1190
- from: previous ?? "never-read",
1191
- to: next,
1289
+ from: report.from,
1290
+ to: report.to,
1192
1291
  ownAddonId: this.ownAddonId,
1193
- durableAuthority: authority,
1194
- entries
1292
+ durableAuthority: report.authority,
1293
+ doorbellServedBy: report.servedBy,
1294
+ entries: report.entries
1195
1295
  }
1196
- });
1296
+ };
1297
+ if (report.authority === "unreadable") {
1298
+ this.ctx.logger.warn("virtual-doorbell: bindings read did not see this wrapper and the durable store cannot confirm it — a press still rings", options);
1299
+ return;
1300
+ }
1301
+ const message = report.servedBy.length > 0 ? `virtual-doorbell: wrapper deactivated on this camera — its doorbell is served by ${report.servedBy.join(", ")}` : "virtual-doorbell: wrapper deactivated on this camera — the bindings store confirms it";
1302
+ if (report.to === "unbound") this.ctx.logger.info(message, options);
1303
+ else this.ctx.logger.debug(message, options);
1197
1304
  }
1198
1305
  /**
1199
- * The mirror says `unbound` and a real press just arrived. Decide, with
1200
- * ONE read of the durable authority, whether to destroy it.
1306
+ * A real press arrived on a camera the in-memory gate cannot clear on its
1307
+ * own: the mirror is `unbound`, or it is short of `bound` while another
1308
+ * provider serves the doorbell. Decide, with ONE read of the durable
1309
+ * authority, whether this wrapper rings.
1201
1310
  *
1202
1311
  * D49 forbade I/O on the ring path because the read it would have made was
1203
1312
  * the same fallible derived read that caused the problem. This read is
1204
1313
  * different in both directions: it is the authority rather than a view of
1205
- * it, and it is paid ONLY on the branch that would otherwise drop a press —
1206
- * a ring is rare, one query is cheap, and a dropped ring has no retry, no
1207
- * reconcile and nothing downstream that notices.
1314
+ * it, and it is paid ONLY where erring open could be wrong — a dropped ring
1315
+ * (an `unbound` mirror) or a DOUBLED one (another provider rings this
1316
+ * camera). A ring is rare and one query is cheap. An unreadable store still
1317
+ * rings: it carries no information.
1208
1318
  *
1209
1319
  * Returns `true` when the ring must proceed.
1210
1320
  */
1211
- async ringsDespiteUnboundMirror(cameraId, binding) {
1321
+ async ringsAfterDurableRead(cameraId, mirror) {
1322
+ const binding = mirror.state;
1212
1323
  const authority = await this.readDurableAuthority(cameraId);
1213
- const verdict = ringVerdict(binding, authority);
1324
+ const verdict = ringVerdict(mirror, authority);
1214
1325
  switch (verdict) {
1215
1326
  case "rings":
1216
1327
  this.bindingMirror.set(cameraId, "bound");
1217
- this.ctx.logger.warn("virtual-doorbell: ring RESCUED — the mirror said unbound but the durable bindings store still names this wrapper; the mirror has been repaired", {
1328
+ this.ctx.logger.warn("virtual-doorbell: ring RESCUED — the mirror did not confirm this wrapper but the durable bindings store still names it; the mirror has been repaired", {
1218
1329
  tags: { deviceId: cameraId },
1219
1330
  meta: {
1220
- bindingState: binding,
1331
+ bindingState: binding ?? "never-read",
1332
+ doorbellServedBy: mirror.servedBy,
1221
1333
  ownAddonId: this.ownAddonId
1222
1334
  }
1223
1335
  });
@@ -1233,19 +1345,32 @@ var VirtualDoorbellAddon = class extends BaseAddon {
1233
1345
  return verdictRings(verdict);
1234
1346
  case "silent": {
1235
1347
  const derived = await this.readDerivedBinding(cameraId);
1236
- this.ctx.logger.warn("virtual-doorbell: ring DROPPED — the wrapper is not bound to this camera, but a trigger source is still configured for it", {
1348
+ const servedBy = derived === null ? mirror.servedBy : otherProvidersOf(derived.entries, doorbellCapability.name, this.ownAddonId);
1349
+ const options = {
1237
1350
  tags: { deviceId: cameraId },
1238
1351
  meta: {
1239
1352
  bindingState: binding,
1240
1353
  ownAddonId: this.ownAddonId,
1241
1354
  durableAuthority: authority,
1355
+ doorbellServedBy: servedBy,
1242
1356
  entries: derived === null ? "unreadable" : summariseEntries(derived.entries)
1243
1357
  }
1244
- });
1358
+ };
1359
+ if (servedBy.length > 0) this.ctx.logger.info(`virtual-doorbell: press left to ${servedBy.join(", ")} — this wrapper is deactivated on this camera`, options);
1360
+ else this.ctx.logger.warn("virtual-doorbell: ring DROPPED — the wrapper is not bound to this camera, but a trigger source is still configured for it", options);
1245
1361
  return verdictRings(verdict);
1246
1362
  }
1247
1363
  case "rings-pending":
1248
- case "rings-unconfirmed": return verdictRings(verdict);
1364
+ case "rings-unconfirmed":
1365
+ this.ctx.logger.warn(`virtual-doorbell: ringing although ${mirror.servedBy.join(", ")} serves this doorbell — the durable bindings store could not confirm this wrapper was deactivated`, {
1366
+ tags: { deviceId: cameraId },
1367
+ meta: {
1368
+ bindingState: binding ?? "never-read",
1369
+ durableAuthority: authority,
1370
+ ownAddonId: this.ownAddonId
1371
+ }
1372
+ });
1373
+ return verdictRings(verdict);
1249
1374
  }
1250
1375
  }
1251
1376
  /**
@@ -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-RAl10EfG.js");
6
+ const require_dist = require("../../dist-a6p0V-IL.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 { Gn as BaseAddon, Rt as logDestinationCapability } from "../../dist-BPvku43i.mjs";
1
+ import { Gn as BaseAddon, Rt as logDestinationCapability } from "../../dist-Dori4pf3.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-RAl10EfG.js");
3
+ const require_dist = require("../../dist-a6p0V-IL.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 { E as RECORDING_SOURCE_CAMSTACK_ADDON, Gn as BaseAddon, Wn as errMsg, hr as EventCategory, k as RecordingStatusSchema, tr as createEvent } from "../../dist-BPvku43i.mjs";
1
+ import { E as RECORDING_SOURCE_CAMSTACK_ADDON, Gn as BaseAddon, Wn as errMsg, hr as EventCategory, k as RecordingStatusSchema, tr as createEvent } from "../../dist-Dori4pf3.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-RAl10EfG.js");
6
+ const require_dist = require("../../dist-a6p0V-IL.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");
@@ -6017,8 +6017,24 @@ var require_jsonwebtoken = /* @__PURE__ */ require_chunk.__commonJSMin(((exports
6017
6017
  };
6018
6018
  }));
6019
6019
  //#endregion
6020
- //#region src/auth/token-expiry.ts
6020
+ //#region src/kernel/settings-read-error.ts
6021
6021
  var import_jsonwebtoken = /* @__PURE__ */ require_chunk.__toESM(require_jsonwebtoken());
6022
+ var SettingsReadUnavailableError = class extends Error {
6023
+ reader;
6024
+ reason;
6025
+ name = "SettingsReadUnavailableError";
6026
+ constructor(reader, reason, detail) {
6027
+ super(`[${reader}] settings read refused (${reason}): ${detail}`);
6028
+ this.reader = reader;
6029
+ this.reason = reason;
6030
+ }
6031
+ };
6032
+ /** Narrow an unknown throw to {@link SettingsReadUnavailableError}. */
6033
+ function isSettingsReadUnavailable(err) {
6034
+ return err instanceof SettingsReadUnavailableError;
6035
+ }
6036
+ //#endregion
6037
+ //#region src/auth/token-expiry.ts
6022
6038
  /**
6023
6039
  * Validation for the operator-configured session-JWT lifetime
6024
6040
  * (`auth.tokenExpiry`, System → Settings → Authentication → Token Expiry).
@@ -6209,7 +6225,7 @@ var AuthManager = class {
6209
6225
  }
6210
6226
  }
6211
6227
  signToken(payload) {
6212
- const configured = this.config.get("auth.tokenExpiry");
6228
+ const configured = this.readConfiguredExpiry();
6213
6229
  const parsed = parseTokenExpiry(configured);
6214
6230
  if (!parsed.ok) {
6215
6231
  const seen = typeof configured === "string" ? configured : JSON.stringify(configured);
@@ -6220,6 +6236,32 @@ var AuthManager = class {
6220
6236
  }
6221
6237
  return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn: parsed.seconds });
6222
6238
  }
6239
+ /**
6240
+ * The operator's `auth.tokenExpiry`, or `undefined` → DEFAULT_TOKEN_EXPIRY.
6241
+ *
6242
+ * On the hub this reader is `ConfigService`, which answers the key from its
6243
+ * boot-time mirror of the settings store and REFUSES while that mirror is
6244
+ * unknown (pending or failed, D675). A login must not fail over it, so the
6245
+ * refusal signs with the default — and says so once per reason, because a
6246
+ * mint that silently ignored a saved lifetime is the defect this setting
6247
+ * already had once.
6248
+ */
6249
+ readConfiguredExpiry() {
6250
+ try {
6251
+ return this.config.get("auth.tokenExpiry");
6252
+ } catch (err) {
6253
+ if (!isSettingsReadUnavailable(err)) throw err;
6254
+ const key = `unreadable:${err.reason}`;
6255
+ if (!this.reportedInvalidExpiries.has(key)) {
6256
+ this.reportedInvalidExpiries.add(key);
6257
+ this.logger.warn("auth.tokenExpiry could not be read from the settings store — signing with the default lifetime", { meta: {
6258
+ reason: err.reason,
6259
+ error: err.message
6260
+ } });
6261
+ }
6262
+ return;
6263
+ }
6264
+ }
6223
6265
  verifyToken(token) {
6224
6266
  return import_jsonwebtoken.verify(token, this.jwtSecret);
6225
6267
  }
@@ -9947,7 +9989,9 @@ exports.AuthManager = AuthManager;
9947
9989
  exports.LocalAuthAddon = LocalAuthAddon;
9948
9990
  exports.default = LocalAuthAddon;
9949
9991
  exports.ScopedTokenManager = ScopedTokenManager;
9992
+ exports.SettingsReadUnavailableError = SettingsReadUnavailableError;
9950
9993
  exports.UserManager = UserManager;
9994
+ exports.isSettingsReadUnavailable = isSettingsReadUnavailable;
9951
9995
  Object.defineProperty(exports, "require_ms", {
9952
9996
  enumerable: true,
9953
9997
  get: function() {
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Gn as BaseAddon, H as authProviderCapability, L as UserRecordSchema, M as ScopedTokenSchema, r as ApiKeyRecordSchema, vn as userManagementCapability } from "../../dist-BPvku43i.mjs";
2
+ import { Gn as BaseAddon, H as authProviderCapability, L as UserRecordSchema, M as ScopedTokenSchema, r as ApiKeyRecordSchema, vn as userManagementCapability } from "../../dist-Dori4pf3.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";
@@ -6000,7 +6000,7 @@ var require_sign = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6000
6000
  };
6001
6001
  }));
6002
6002
  //#endregion
6003
- //#region src/auth/token-expiry.ts
6003
+ //#region src/kernel/settings-read-error.ts
6004
6004
  var import_jsonwebtoken = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
6005
6005
  module.exports = {
6006
6006
  decode: require_decode(),
@@ -6011,6 +6011,22 @@ var import_jsonwebtoken = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin
6011
6011
  TokenExpiredError: require_TokenExpiredError()
6012
6012
  };
6013
6013
  })))());
6014
+ var SettingsReadUnavailableError = class extends Error {
6015
+ reader;
6016
+ reason;
6017
+ name = "SettingsReadUnavailableError";
6018
+ constructor(reader, reason, detail) {
6019
+ super(`[${reader}] settings read refused (${reason}): ${detail}`);
6020
+ this.reader = reader;
6021
+ this.reason = reason;
6022
+ }
6023
+ };
6024
+ /** Narrow an unknown throw to {@link SettingsReadUnavailableError}. */
6025
+ function isSettingsReadUnavailable(err) {
6026
+ return err instanceof SettingsReadUnavailableError;
6027
+ }
6028
+ //#endregion
6029
+ //#region src/auth/token-expiry.ts
6014
6030
  /**
6015
6031
  * Validation for the operator-configured session-JWT lifetime
6016
6032
  * (`auth.tokenExpiry`, System → Settings → Authentication → Token Expiry).
@@ -6201,7 +6217,7 @@ var AuthManager = class {
6201
6217
  }
6202
6218
  }
6203
6219
  signToken(payload) {
6204
- const configured = this.config.get("auth.tokenExpiry");
6220
+ const configured = this.readConfiguredExpiry();
6205
6221
  const parsed = parseTokenExpiry(configured);
6206
6222
  if (!parsed.ok) {
6207
6223
  const seen = typeof configured === "string" ? configured : JSON.stringify(configured);
@@ -6212,6 +6228,32 @@ var AuthManager = class {
6212
6228
  }
6213
6229
  return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn: parsed.seconds });
6214
6230
  }
6231
+ /**
6232
+ * The operator's `auth.tokenExpiry`, or `undefined` → DEFAULT_TOKEN_EXPIRY.
6233
+ *
6234
+ * On the hub this reader is `ConfigService`, which answers the key from its
6235
+ * boot-time mirror of the settings store and REFUSES while that mirror is
6236
+ * unknown (pending or failed, D675). A login must not fail over it, so the
6237
+ * refusal signs with the default — and says so once per reason, because a
6238
+ * mint that silently ignored a saved lifetime is the defect this setting
6239
+ * already had once.
6240
+ */
6241
+ readConfiguredExpiry() {
6242
+ try {
6243
+ return this.config.get("auth.tokenExpiry");
6244
+ } catch (err) {
6245
+ if (!isSettingsReadUnavailable(err)) throw err;
6246
+ const key = `unreadable:${err.reason}`;
6247
+ if (!this.reportedInvalidExpiries.has(key)) {
6248
+ this.reportedInvalidExpiries.add(key);
6249
+ this.logger.warn("auth.tokenExpiry could not be read from the settings store — signing with the default lifetime", { meta: {
6250
+ reason: err.reason,
6251
+ error: err.message
6252
+ } });
6253
+ }
6254
+ return;
6255
+ }
6256
+ }
6215
6257
  verifyToken(token) {
6216
6258
  return import_jsonwebtoken.verify(token, this.jwtSecret);
6217
6259
  }
@@ -9934,4 +9976,4 @@ var LocalAuthAddon = class extends BaseAddon {
9934
9976
  }
9935
9977
  };
9936
9978
  //#endregion
9937
- export { LocalAuthAddon, LocalAuthAddon as default, require_safe_buffer as a, require_ms as i, ScopedTokenManager as n, ApiKeyManager as o, AuthManager as r, UserManager as t };
9979
+ export { LocalAuthAddon, LocalAuthAddon as default, isSettingsReadUnavailable as a, ApiKeyManager as c, SettingsReadUnavailableError as i, ScopedTokenManager as n, require_ms as o, AuthManager as r, require_safe_buffer as s, UserManager as t };
@@ -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-RAl10EfG.js");
3
+ const require_dist = require("../../dist-a6p0V-IL.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");