@camstack/system 1.2.304 → 1.2.305

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 (71) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.d.ts +1 -1
  4. package/dist/addon-utils.js +3 -1
  5. package/dist/addon-utils.mjs +2 -2
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/alerts/alerts.addon.js +1 -1
  11. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  12. package/dist/builtins/autotrack/index.js +1 -1
  13. package/dist/builtins/autotrack/index.mjs +1 -1
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  16. package/dist/builtins/camera-grid/index.js +1 -1
  17. package/dist/builtins/camera-grid/index.mjs +1 -1
  18. package/dist/builtins/console-logging/index.js +1 -1
  19. package/dist/builtins/console-logging/index.mjs +1 -1
  20. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  21. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  22. package/dist/builtins/device-manager/device-extension-linked-slot.d.ts +8 -0
  23. package/dist/builtins/device-manager/device-linked-devices.d.ts +34 -0
  24. package/dist/builtins/device-manager/device-manager.addon.js +188 -22
  25. package/dist/builtins/device-manager/device-manager.addon.mjs +188 -22
  26. package/dist/builtins/doorbell/binding-mirror.d.ts +24 -0
  27. package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +66 -0
  28. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +14 -0
  29. package/dist/builtins/doorbell/virtual-doorbell.addon.js +307 -38
  30. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +307 -38
  31. package/dist/builtins/hub-forwarder/index.js +1 -1
  32. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  33. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  34. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  35. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  36. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  37. package/dist/builtins/local-network/local-network.addon.js +1 -1
  38. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  39. package/dist/builtins/loki-logging/index.js +1 -1
  40. package/dist/builtins/loki-logging/index.mjs +1 -1
  41. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  42. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  43. package/dist/builtins/platform-probe/index.js +1 -1
  44. package/dist/builtins/platform-probe/index.mjs +1 -1
  45. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  46. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  47. package/dist/builtins/snapshot/index.js +1 -1
  48. package/dist/builtins/snapshot/index.mjs +1 -1
  49. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  50. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  51. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  52. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  53. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  54. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  55. package/dist/builtins/system-config/system-config.addon.js +1 -1
  56. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  57. package/dist/builtins/winston-logging/index.js +1 -1
  58. package/dist/builtins/winston-logging/index.mjs +1 -1
  59. package/dist/{child-cap-dispatch-C1c3rWaX.mjs → child-cap-dispatch-BCJjUtXV.mjs} +9 -1
  60. package/dist/{child-cap-dispatch-De5_HcvR.js → child-cap-dispatch-Be8e2cBP.js} +9 -1
  61. package/dist/{dist-C571Y95Z.js → dist-BK4iP6ab.js} +1069 -310
  62. package/dist/{dist-Duqf80QM.mjs → dist-Df1NhKxm.mjs} +1046 -311
  63. package/dist/download/model-downloader.d.ts +15 -0
  64. package/dist/{event-loop-stall-monitor-GPrVvx14.mjs → event-loop-stall-monitor-EXjzIxDz.mjs} +25 -3
  65. package/dist/{event-loop-stall-monitor-CAsHdMwP.js → event-loop-stall-monitor-ey4mbofz.js} +36 -2
  66. package/dist/index.js +21 -3
  67. package/dist/index.mjs +21 -3
  68. package/dist/kernel/config-manager.d.ts +3 -1
  69. package/dist/{retired-settings-keys-0j8rNoa8.js → retired-settings-keys-DwWMph6F.js} +1 -1
  70. package/dist/{retired-settings-keys-CeO_dBHU.mjs → retired-settings-keys-dC4ckbXJ.mjs} +1 -1
  71. package/package.json +7 -1
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, D as SOURCE_CAPS, En as createEvent, O as SOURCE_DEVICE_TYPES, U as bareAddonId, bt as isSameAddonId, ct as evaluateSensorEdge, dn as errMsg, it as doorbellCapability, pn as BaseAddon, xt as isSourceCap, yn as DeviceType } from "../../dist-Duqf80QM.mjs";
1
+ import { An as createEvent, Cn as DeviceType, Ct as isSameAddonId, D as SOURCE_CAPS, O as SOURCE_DEVICE_TYPES, U as bareAddonId, Wn as EventCategory, Z as createDeviceExtensionProvider, _n as BaseAddon, dt as evaluateSensorEdge, hn as errMsg, rt as deviceExtensionCapability, st as doorbellCapability, tt as defineExtensionSlotProvider, wt as isSourceCap } from "../../dist-Df1NhKxm.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -205,7 +205,7 @@ function nextBindingState(previous, boundNow) {
205
205
  }
206
206
  /** May a ring proceed? Only a corroborated unbind stops one. */
207
207
  function allowsRing(state) {
208
- return state !== "unbound";
208
+ return !needsDurableRead(state);
209
209
  }
210
210
  /** True only when a read has positively confirmed the binding. */
211
211
  function hasConfirmedBinding(state) {
@@ -257,6 +257,29 @@ function readDurableWrapperBinding(record, deviceId, capName, ownAddonId) {
257
257
  if (typeof wrapperAddonId !== "string") return "unbound";
258
258
  return isSameAddonId(wrapperAddonId, ownAddonId) ? "bound" : "unbound";
259
259
  }
260
+ /** Does this mirror state need the durable authority before a verdict? Only `unbound` does. */
261
+ function needsDurableRead(state) {
262
+ return state === "unbound";
263
+ }
264
+ /**
265
+ * THE decision — the ring gate (`fireDoorbell` / `ringsDespiteUnboundMirror`)
266
+ * and the Extensions card's activation both call it, so what the card says
267
+ * and what a press does cannot disagree. `durable` is `null` when it was not
268
+ * read, which {@link needsDurableRead} allows for every state but `unbound`.
269
+ * Pure: it reads nothing and writes nothing.
270
+ */
271
+ function ringVerdict(state, durable) {
272
+ if (state === "bound") return "rings";
273
+ if (state === "unbind-pending") return "rings-pending";
274
+ if (state === void 0) return "rings-unconfirmed";
275
+ if (durable === "bound") return "rings";
276
+ if (durable === "unbound") return "silent";
277
+ return "unknown";
278
+ }
279
+ /** Whether a press proceeds under this verdict. */
280
+ function verdictRings(verdict) {
281
+ return verdict !== "silent";
282
+ }
260
283
  //#endregion
261
284
  //#region src/builtins/doorbell/doorbell-settings.ts
262
285
  /**
@@ -349,6 +372,203 @@ function applyDoorbellSourcesPatch(current, patchValue) {
349
372
  return next;
350
373
  }
351
374
  //#endregion
375
+ //#region src/builtins/doorbell/doorbell-extension-slot.ts
376
+ /**
377
+ * The virtual doorbell's side of `device-extension` (D648): the
378
+ * `doorbell-source` slot, served over the `doorbellSources` store the
379
+ * "Virtual doorbell" settings section already writes.
380
+ *
381
+ * - `list` reads the store through `parseDoorbellSources` — the parser the
382
+ * trigger engine's assignments are built from — so the slot's value is
383
+ * exactly what rings. It also REPORTS whether the wrapper is bound to the
384
+ * camera (`activation`), named as `setWrapperActive`'s: sources on an
385
+ * unbound camera ring nothing, and the card must say so rather than look
386
+ * configured. Nothing here writes it.
387
+ * - The activation costs NOTHING on the common path. A camera with no
388
+ * sources has no gate worth reporting and makes no read. A camera with
389
+ * sources is answered from the addon's D49 binding mirror — the in-memory
390
+ * verdict the ring path itself uses — through `ringVerdict`, the ring
391
+ * gate's own decision function. The card is READ-ONLY: it never refreshes
392
+ * or writes the mirror, and reads the durable authority only for an
393
+ * `unbound` mirror, exactly where the ring gate reads it.
394
+ * `getBindings` was measured at up to 5.24 s on the live hub (2026-08-30),
395
+ * and the dispatcher already paid one for this page.
396
+ * - The picked sources are labelled with ONE batched `listAll({deviceIds})`,
397
+ * never a `getDevice` per id.
398
+ * - `set` goes through the addon's `persistDoorbellSources`, the SAME
399
+ * normalise-and-store path the settings save uses, which also invalidates
400
+ * the assignment cache so the binding mirror is re-read off the ring path
401
+ * (D49). A `setSlot` call is NOT evidence that the wrapper is bound (the
402
+ * contract reaches this provider through `device-extension`'s own
403
+ * binding), so it never confirms the mirror. A NEW pick must be a device
404
+ * that carries a source cap (`SOURCE_CAPS`, the D636 ids-only read) — which
405
+ * also refuses a container, a device with no source cap and an id no
406
+ * device has — and never the camera itself.
407
+ * - `candidates` are those same source-capable devices, devices in the
408
+ * camera's location suggested.
409
+ */
410
+ /** The routing prefix of this owner's `slotRef`s. */
411
+ var VIRTUAL_DOORBELL_EXTENSION_SOURCE = "virtual-doorbell";
412
+ /** The store the slot reads and writes, named (D62). */
413
+ var DOORBELL_SOURCES_AUTHORITY = `virtual-doorbell:${KEY_SOURCES}`;
414
+ /** The authority that decides whether the wrapper serves a camera at all. */
415
+ var WRAPPER_ACTIVATION_AUTHORITY = "device-manager:setWrapperActive";
416
+ var INACTIVE_DETAIL = "The virtual doorbell is not bound to this camera: these devices ring nothing until it is activated in the camera’s bindings.";
417
+ var PENDING_DETAIL = "One bindings read did not see the wrapper; a press still rings until a second read agrees.";
418
+ /** How the card SAYS a verdict. The verdict itself is `ringVerdict`'s, never the card's. */
419
+ function activationFromVerdict(verdict) {
420
+ const authority = WRAPPER_ACTIVATION_AUTHORITY;
421
+ switch (verdict) {
422
+ case "rings": return {
423
+ state: "active",
424
+ authority
425
+ };
426
+ case "rings-pending": return {
427
+ state: "active",
428
+ authority,
429
+ detail: PENDING_DETAIL
430
+ };
431
+ case "rings-unconfirmed": return {
432
+ state: "active",
433
+ authority,
434
+ detail: "No bindings read has confirmed the wrapper yet; a press rings meanwhile."
435
+ };
436
+ case "unknown": return {
437
+ state: "unknown",
438
+ authority,
439
+ detail: "Could not verify: the last reads did not see the wrapper and the bindings store cannot answer. A press still rings."
440
+ };
441
+ case "silent": return {
442
+ state: "inactive",
443
+ authority,
444
+ detail: INACTIVE_DETAIL
445
+ };
446
+ }
447
+ }
448
+ /**
449
+ * Concurrent card views of the same `unbound` camera share ONE durable read,
450
+ * per camera, for as long as it is in flight — a page with several viewers
451
+ * (or a refetch storm) must not multiply the store round trip. Only the
452
+ * CARD's reads are shared: the ring gate calls the addon's
453
+ * `readDurableAuthority` on its own path and never waits behind a card.
454
+ */
455
+ function shareDurableReads(read) {
456
+ const inFlight = /* @__PURE__ */ new Map();
457
+ return (deviceId) => {
458
+ const pending = inFlight.get(deviceId);
459
+ if (pending !== void 0) return pending;
460
+ const started = read(deviceId).finally(() => {
461
+ inFlight.delete(deviceId);
462
+ });
463
+ inFlight.set(deviceId, started);
464
+ return started;
465
+ };
466
+ }
467
+ async function activationOf(deps, readDurable, deviceId) {
468
+ const mirrored = deps.readBindingMirror(deviceId);
469
+ return activationFromVerdict(ringVerdict(mirrored, needsDurableRead(mirrored) ? await readDurable(deviceId) : null));
470
+ }
471
+ function sourceOf(deviceId, byId) {
472
+ const device = byId.get(deviceId);
473
+ if (device === void 0) return {
474
+ deviceId,
475
+ name: null,
476
+ health: "missing",
477
+ detail: "No device has this id any more — it rings nothing until removed."
478
+ };
479
+ return {
480
+ deviceId,
481
+ name: device.name,
482
+ health: "ok"
483
+ };
484
+ }
485
+ async function candidatesFor(deps, deviceId) {
486
+ const [ids, fleet] = await Promise.all([deps.sourceCapableDeviceIds(), deps.listFleet()]);
487
+ const eligible = new Set(ids);
488
+ const byId = new Map(fleet.map((d) => [d.id, d]));
489
+ const location = byId.get(deviceId)?.location ?? null;
490
+ const out = [];
491
+ for (const device of fleet) {
492
+ if (device.id === deviceId || !eligible.has(device.id)) continue;
493
+ const suggested = location !== null && device.location === location;
494
+ const parent = device.parentDeviceId === null ? void 0 : byId.get(device.parentDeviceId);
495
+ out.push({
496
+ deviceId: device.id,
497
+ name: device.name,
498
+ type: device.type,
499
+ location: device.location,
500
+ parentName: parent?.name ?? null,
501
+ suggested,
502
+ reason: suggested ? "same location" : null
503
+ });
504
+ }
505
+ return out.toSorted((a, b) => Number(b.suggested) - Number(a.suggested) || a.name.localeCompare(b.name));
506
+ }
507
+ /** Why each NEW pick is refused, or [] when every one is a legitimate source. */
508
+ async function refusedPicks(deps, deviceId, picked) {
509
+ const persisted = new Set((await deps.readSources(deviceId)).map((s) => s.deviceId));
510
+ const added = picked.filter((id) => !persisted.has(id));
511
+ if (added.length === 0) return [];
512
+ const eligible = new Set(await deps.sourceCapableDeviceIds());
513
+ const refused = [];
514
+ for (const id of added) if (id === deviceId) refused.push(`${id} is the camera itself`);
515
+ else if (!eligible.has(id)) refused.push(`${id} carries no doorbell source capability (a binary sensor or switch)`);
516
+ return refused;
517
+ }
518
+ /** Build the `device-extension` provider the virtual doorbell registers. */
519
+ function createDoorbellExtensionProvider(deps) {
520
+ const readDurable = shareDurableReads(deps.readDurableBinding);
521
+ return createDeviceExtensionProvider({
522
+ source: VIRTUAL_DOORBELL_EXTENSION_SOURCE,
523
+ label: "Virtual doorbell",
524
+ slots: [defineExtensionSlotProvider("doorbell-source", {
525
+ list: async (deviceId) => {
526
+ const ids = (await deps.readSources(deviceId)).map((s) => s.deviceId);
527
+ if (ids.length === 0) return {
528
+ authority: DOORBELL_SOURCES_AUTHORITY,
529
+ enabled: null,
530
+ activation: null,
531
+ value: {
532
+ slot: "doorbell-source",
533
+ deviceIds: []
534
+ },
535
+ sources: []
536
+ };
537
+ const [devices, activation] = await Promise.all([deps.listDevices(ids), activationOf(deps, readDurable, deviceId)]);
538
+ const byId = new Map(devices.map((d) => [d.id, d]));
539
+ return {
540
+ authority: DOORBELL_SOURCES_AUTHORITY,
541
+ enabled: null,
542
+ activation,
543
+ value: {
544
+ slot: "doorbell-source",
545
+ deviceIds: ids
546
+ },
547
+ sources: ids.map((id) => sourceOf(id, byId))
548
+ };
549
+ },
550
+ set: async (deviceId, value) => {
551
+ const refused = await refusedPicks(deps, deviceId, value.deviceIds);
552
+ if (refused.length > 0) {
553
+ deps.logger.warn("virtual-doorbell: extension write refused — nothing persisted", {
554
+ tags: { deviceId },
555
+ meta: { refused }
556
+ });
557
+ throw new Error(`doorbell-source on ${deviceId}: refused — ${refused.join("; ")}`);
558
+ }
559
+ await deps.persistSources(deviceId, value.deviceIds);
560
+ return {
561
+ applied: true,
562
+ reload: "none"
563
+ };
564
+ },
565
+ candidates: (deviceId) => candidatesFor(deps, deviceId)
566
+ })],
567
+ hostTypeOf: async (deviceId) => (await deps.getDevice(deviceId))?.type ?? null,
568
+ logger: deps.logger
569
+ });
570
+ }
571
+ //#endregion
352
572
  //#region src/builtins/doorbell/virtual-doorbell.addon.ts
353
573
  /**
354
574
  * The addon whose store IS the durable authority on wrapper activations.
@@ -452,8 +672,42 @@ var VirtualDoorbellAddon = class extends BaseAddon {
452
672
  getDeviceLiveContribution: async () => null,
453
673
  applyDeviceSettingsPatch: (input) => this.saveDeviceSettingsPatch(input.deviceId, input.patch)
454
674
  }
675
+ }, {
676
+ capability: deviceExtensionCapability,
677
+ provider: this.buildExtensionProvider()
455
678
  }];
456
679
  }
680
+ /**
681
+ * The `device-extension` provider: the same store and the same save path as
682
+ * the settings section below (see `doorbell-extension-slot.ts`).
683
+ */
684
+ buildExtensionProvider() {
685
+ const toSlotDevice = (d) => ({
686
+ id: d.id,
687
+ name: d.name,
688
+ type: d.type,
689
+ location: d.location,
690
+ parentDeviceId: d.parentDeviceId
691
+ });
692
+ return createDoorbellExtensionProvider({
693
+ ownAddonId: this.ownAddonId,
694
+ logger: this.ctx.logger,
695
+ readSources: (deviceId) => this.readDeviceSources(deviceId),
696
+ persistSources: (deviceId, deviceIds) => this.persistDoorbellSources(deviceId, deviceIds),
697
+ getDevice: async (deviceId) => {
698
+ const found = await this.ctx.api.deviceManager.getDevice.query({ deviceId });
699
+ return found ? toSlotDevice(found) : null;
700
+ },
701
+ listDevices: async (deviceIds) => (await this.ctx.api.deviceManager.listAll.query({
702
+ projection: "slim",
703
+ deviceIds: [...deviceIds]
704
+ })).map(toSlotDevice),
705
+ readBindingMirror: (deviceId) => this.bindingMirror.get(deviceId),
706
+ readDurableBinding: (deviceId) => this.readDurableAuthority(deviceId),
707
+ sourceCapableDeviceIds: () => this.ctx.api.deviceManager.listDeviceIdsByCaps.query({ capNames: [...SOURCE_CAPS] }),
708
+ listFleet: async () => (await this.ctx.api.deviceManager.listAll.query({ projection: "slim" })).map(toSlotDevice)
709
+ });
710
+ }
457
711
  async handleStateChanged(data) {
458
712
  if (!isSourceCap(data.capName)) return;
459
713
  await this.ensureAssignments();
@@ -671,38 +925,43 @@ var VirtualDoorbellAddon = class extends BaseAddon {
671
925
  */
672
926
  async ringsDespiteUnboundMirror(cameraId, binding) {
673
927
  const authority = await this.readDurableAuthority(cameraId);
674
- if (authority === "bound") {
675
- this.bindingMirror.set(cameraId, "bound");
676
- 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", {
677
- tags: { deviceId: cameraId },
678
- meta: {
679
- bindingState: binding,
680
- ownAddonId: this.ownAddonId
681
- }
682
- });
683
- return true;
684
- }
685
- if (authority === "unreadable") {
686
- this.ctx.logger.warn("virtual-doorbell: ringing on an unbound mirror — the durable bindings store could not answer, and an unanswerable store never destroys a one-shot press", {
687
- tags: { deviceId: cameraId },
688
- meta: {
689
- bindingState: binding,
690
- ownAddonId: this.ownAddonId
691
- }
692
- });
693
- return true;
694
- }
695
- const derived = await this.readDerivedBinding(cameraId);
696
- 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", {
697
- tags: { deviceId: cameraId },
698
- meta: {
699
- bindingState: binding,
700
- ownAddonId: this.ownAddonId,
701
- durableAuthority: authority,
702
- entries: derived === null ? "unreadable" : summariseEntries(derived.entries)
928
+ const verdict = ringVerdict(binding, authority);
929
+ switch (verdict) {
930
+ case "rings":
931
+ this.bindingMirror.set(cameraId, "bound");
932
+ 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", {
933
+ tags: { deviceId: cameraId },
934
+ meta: {
935
+ bindingState: binding,
936
+ ownAddonId: this.ownAddonId
937
+ }
938
+ });
939
+ return verdictRings(verdict);
940
+ case "unknown":
941
+ this.ctx.logger.warn("virtual-doorbell: ringing on an unbound mirror — the durable bindings store could not answer, and an unanswerable store never destroys a one-shot press", {
942
+ tags: { deviceId: cameraId },
943
+ meta: {
944
+ bindingState: binding,
945
+ ownAddonId: this.ownAddonId
946
+ }
947
+ });
948
+ return verdictRings(verdict);
949
+ case "silent": {
950
+ const derived = await this.readDerivedBinding(cameraId);
951
+ 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", {
952
+ tags: { deviceId: cameraId },
953
+ meta: {
954
+ bindingState: binding,
955
+ ownAddonId: this.ownAddonId,
956
+ durableAuthority: authority,
957
+ entries: derived === null ? "unreadable" : summariseEntries(derived.entries)
958
+ }
959
+ });
960
+ return verdictRings(verdict);
703
961
  }
704
- });
705
- return false;
962
+ case "rings-pending":
963
+ case "rings-unconfirmed": return verdictRings(verdict);
964
+ }
706
965
  }
707
966
  /**
708
967
  * Record a binding we observed FIRST-HAND. Both callers are cap methods
@@ -848,13 +1107,23 @@ var VirtualDoorbellAddon = class extends BaseAddon {
848
1107
  async saveDeviceSettingsPatch(deviceId, patch) {
849
1108
  if (!this.ctx.settings) throw new Error("[virtual-doorbell] settings store unavailable — cannot persist per-device settings");
850
1109
  this.confirmBindingFromCapCall(deviceId);
851
- if ("doorbellSources" in patch) {
852
- const next = applyDoorbellSourcesPatch(await this.ctx.settings.readDeviceStore(deviceId), patch[KEY_SOURCES]);
853
- await this.ctx.settings.writeDeviceStore(deviceId, next);
854
- this.invalidateAssignments();
855
- }
1110
+ if ("doorbellSources" in patch) await this.persistDoorbellSources(deviceId, patch[KEY_SOURCES]);
856
1111
  return { success: true };
857
1112
  }
1113
+ /**
1114
+ * THE write path for a camera's trigger sources — the settings save above
1115
+ * and the `doorbell-source` extension slot both land here (D648), so the
1116
+ * two surfaces cannot store different shapes: normalise to
1117
+ * `doorbellSources: [{deviceId}]`, strip the legacy keys, and invalidate the
1118
+ * assignment cache so the next rise (and the binding mirror's next
1119
+ * refresh, off the ring path) sees the new wiring.
1120
+ */
1121
+ async persistDoorbellSources(deviceId, value) {
1122
+ if (!this.ctx.settings) throw new Error("[virtual-doorbell] settings store unavailable — cannot persist per-device settings");
1123
+ const current = await this.ctx.settings.readDeviceStore(deviceId);
1124
+ await this.ctx.settings.writeDeviceStore(deviceId, applyDoorbellSourcesPatch(current, value));
1125
+ this.invalidateAssignments();
1126
+ }
858
1127
  };
859
1128
  //#endregion
860
1129
  export { VirtualDoorbellAddon, VirtualDoorbellAddon as default };
@@ -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-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.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 { kt as logDestinationCapability, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { Mt as logDestinationCapability, _n as BaseAddon } from "../../dist-Df1NhKxm.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-C571Y95Z.js");
3
+ const require_dist = require("../../dist-BK4iP6ab.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 { Bn as EventCategory, C as RECORDING_SOURCE_CAMSTACK_ADDON, E as RecordingStatusSchema, En as createEvent, dn as errMsg, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { An as createEvent, C as RECORDING_SOURCE_CAMSTACK_ADDON, E as RecordingStatusSchema, Wn as EventCategory, _n as BaseAddon, hn as errMsg } from "../../dist-Df1NhKxm.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-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { I as UserRecordSchema, V as authProviderCapability, j as ScopedTokenSchema, pn as BaseAddon, r as ApiKeyRecordSchema, sn as userManagementCapability } from "../../dist-Duqf80QM.mjs";
2
+ import { I as UserRecordSchema, V as authProviderCapability, _n as BaseAddon, j as ScopedTokenSchema, r as ApiKeyRecordSchema, un as userManagementCapability } from "../../dist-Df1NhKxm.mjs";
3
3
  import * as crypto$2 from "node:crypto";
4
4
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-C571Y95Z.js");
3
+ const require_dist = require("../../dist-BK4iP6ab.js");
4
4
  const require_tls = require("../../tls-D_wUeOTD.js");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, Dt as localNetworkCapability, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { At as localNetworkCapability, Wn as EventCategory, _n as BaseAddon } from "../../dist-Df1NhKxm.mjs";
2
2
  import { S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, i as writeExtraSans, l as reissueTlsLeaf, n as readTlsAccessStatus, o as validateUploadedTls, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, y as applyLanHttp } from "../../tls-IM-v7JV4.mjs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { kt as logDestinationCapability, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { Mt as logDestinationCapability, _n as BaseAddon } from "../../dist-Df1NhKxm.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  let node_fs_promises = require("node:fs/promises");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, En as createEvent, Gt as resolveBucketMs, Pt as metricsProviderCapability, Wt as reducePoints, dn as errMsg, pn as BaseAddon, pt as foldSnapshotByFunction } from "../../dist-Duqf80QM.mjs";
1
+ import { An as createEvent, Jt as resolveBucketMs, Lt as metricsProviderCapability, Wn as EventCategory, _n as BaseAddon, gt as foldSnapshotByFunction, hn as errMsg, qt as reducePoints } from "../../dist-Df1NhKxm.mjs";
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import * as fs from "node:fs";
4
4
  import { constants, readFileSync } from "node:fs";
@@ -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-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Bn as EventCategory, On as emitReadiness, Vt as platformProbeCapability, Zt as scoreRuntimes, at as enumerateInferenceDevices, dn as errMsg, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
2
+ import { Mn as emitReadiness, Wn as EventCategory, Wt as platformProbeCapability, _n as BaseAddon, ct as enumerateInferenceDevices, en as scoreRuntimes, hn as errMsg } from "../../dist-Df1NhKxm.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { execFile } from "node:child_process";
5
5
  import * as os from "node:os";
@@ -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-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { Wn as EventCategory, _n as BaseAddon } from "../../dist-Df1NhKxm.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -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-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  const require_snapshot_courtesy = require("../../snapshot-courtesy-Cqs7GkOV.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, Nn as nodePin, U as bareAddonId, W as batteryCapability, _n as DeviceFeature, a as BatteryStatusSchema, dn as errMsg, en as snapshotCapability, et as deriveBatteryPresence, on as streamQualityLabel, pn as BaseAddon, yn as DeviceType } from "../../dist-Duqf80QM.mjs";
1
+ import { Cn as DeviceType, Ln as nodePin, U as bareAddonId, W as batteryCapability, Wn as EventCategory, _n as BaseAddon, a as BatteryStatusSchema, hn as errMsg, ln as streamQualityLabel, nt as deriveBatteryPresence, rn as snapshotCapability, xn as DeviceFeature } from "../../dist-Df1NhKxm.mjs";
2
2
  import { i as renderCourtesyOverlayJpeg, n as courtesyCacheKey, r as renderCourtesyJpeg } from "../../snapshot-courtesy-CZPvteq2.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -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-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { ft as filesystemBrowseCapability, in as storageProviderCapability, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { _n as BaseAddon, ht as filesystemBrowseCapability, sn as storageProviderCapability } from "../../dist-Df1NhKxm.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
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-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  const require_hub_cap_forward = require("../../hub-cap-forward-DmHNjbB0.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { Bn as EventCategory, F as StorageMigrationJobSchema, Ft as modeMayWrite, Kt as resolveLocationMode, M as StorageCleanupJobSchema, Mt as mayReadLocation, N as StorageLocationModeSchema, Nt as mayWriteToLocation, P as StorageLocationTypeSchema, Pn as parseJsonObject, Qt as settingsStoreCapability, Tt as legacyModeOf, ht as gbToBytes, lt as evictionPolicyOfLocation, nn as storageMigrationCapability, pn as BaseAddon, q as collectSecretConfigKeys, tn as storageCapability, un as withLocationMode, v as LegacyStorageLocationDefaultSchema, vt as isLocationEnabled, w as REDACTED_SECRET } from "../../dist-Duqf80QM.mjs";
1
+ import { F as StorageMigrationJobSchema, Ft as mayReadLocation, It as mayWriteToLocation, M as StorageCleanupJobSchema, N as StorageLocationModeSchema, Ot as legacyModeOf, P as StorageLocationTypeSchema, Rn as parseJsonObject, Rt as modeMayWrite, Wn as EventCategory, Yt as resolveLocationMode, _n as BaseAddon, an as storageMigrationCapability, ft as evictionPolicyOfLocation, in as storageCapability, mn as withLocationMode, q as collectSecretConfigKeys, tn as settingsStoreCapability, v as LegacyStorageLocationDefaultSchema, vt as gbToBytes, w as REDACTED_SECRET, xt as isLocationEnabled } from "../../dist-Df1NhKxm.mjs";
2
2
  import { t as HUB_CAP_FWD_ACTION } from "../../hub-cap-forward-BdT320dq.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -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-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { An as hydrateSchema, dn as errMsg, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { Pn as hydrateSchema, _n as BaseAddon, hn as errMsg } from "../../dist-Df1NhKxm.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -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-C571Y95Z.js");
6
+ const require_dist = require("../../dist-BK4iP6ab.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { kt as logDestinationCapability, pn as BaseAddon } from "../../dist-Duqf80QM.mjs";
1
+ import { Mt as logDestinationCapability, _n as BaseAddon } from "../../dist-Df1NhKxm.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";