@camstack/system 1.2.157 → 1.2.160

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 (77) hide show
  1. package/dist/addon-runner.js +17 -16
  2. package/dist/addon-runner.mjs +2 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +10 -7
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +50 -12
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +50 -12
  12. package/dist/builtins/backup-orchestrator/write-targets.d.ts +63 -0
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  17. package/dist/builtins/device-manager/device-bindings-store.d.ts +56 -10
  18. package/dist/builtins/device-manager/device-manager.addon.d.ts +11 -0
  19. package/dist/builtins/device-manager/device-manager.addon.js +216 -81
  20. package/dist/builtins/device-manager/device-manager.addon.mjs +216 -81
  21. package/dist/builtins/device-manager/device-meta-actions.d.ts +9 -3
  22. package/dist/builtins/device-manager/device-meta-store.d.ts +32 -5
  23. package/dist/builtins/device-manager/device-queries.d.ts +7 -3
  24. package/dist/builtins/device-manager/write-lock.d.ts +17 -0
  25. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  26. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  27. package/dist/builtins/hub-forwarder/index.js +1 -1
  28. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  29. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  30. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  31. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  32. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  33. package/dist/builtins/local-network/local-network.addon.js +1 -1
  34. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  35. package/dist/builtins/loki-logging/index.js +1 -1
  36. package/dist/builtins/loki-logging/index.mjs +1 -1
  37. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  38. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  39. package/dist/builtins/platform-probe/index.js +1 -1
  40. package/dist/builtins/platform-probe/index.mjs +1 -1
  41. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  43. package/dist/builtins/snapshot/index.js +1 -1
  44. package/dist/builtins/snapshot/index.mjs +1 -1
  45. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  47. package/dist/builtins/sqlite-storage/filter-compiler.d.ts +41 -2
  48. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +15 -7
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  50. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  51. package/dist/builtins/sqlite-storage/vector-index-shared.d.ts +30 -5
  52. package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +28 -6
  53. package/dist/builtins/storage-orchestrator/access-guards.d.ts +32 -0
  54. package/dist/builtins/storage-orchestrator/location-secrets.d.ts +33 -0
  55. package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +74 -3
  56. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +26 -0
  57. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +552 -15
  58. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +552 -15
  59. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +56 -1
  60. package/dist/builtins/system-config/system-config.addon.js +1 -1
  61. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  62. package/dist/builtins/winston-logging/index.js +1 -1
  63. package/dist/builtins/winston-logging/index.mjs +1 -1
  64. package/dist/{dist-CDgIzo82.js → dist-CcvXUhHK.js} +537 -17
  65. package/dist/{dist-LvKus-mT.mjs → dist-DKs---Vx.mjs} +520 -18
  66. package/dist/index.js +141 -139
  67. package/dist/index.mjs +3 -3
  68. package/dist/kernel/deps/manifest-system-deps.d.ts +15 -0
  69. package/dist/kernel/index.d.ts +4 -2
  70. package/dist/kernel/moleculer/cap-usage-registry.d.ts +132 -7
  71. package/dist/kernel/storage-location-registry.d.ts +16 -1
  72. package/dist/kernel/transport/local-child-registry.d.ts +57 -0
  73. package/dist/{manifest-python-deps-CD14Pv0-.mjs → manifest-system-deps-DnoPpLGp.mjs} +466 -83
  74. package/dist/{manifest-python-deps-DVODn-qc.js → manifest-system-deps-oJMkWSX-.js} +477 -82
  75. package/dist/{retired-settings-keys-CV4QVdXj.mjs → retired-settings-keys-C3eONTvV.mjs} +1 -1
  76. package/dist/{retired-settings-keys-BfAzWvPC.js → retired-settings-keys-DXZ2xe7C.js} +1 -1
  77. package/package.json +2 -1
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-DVODn-qc.js");
2
+ const require_manifest_system_deps = require("./manifest-system-deps-oJMkWSX-.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path);
@@ -420,7 +420,7 @@ async function loadAddonModule(addonId, addonDir) {
420
420
  if (!node_fs.existsSync(entryPath)) throw new Error(`Entry point not found: ${entryPath}`);
421
421
  const modUnknown = await import(entryPath);
422
422
  const mod = isRecord(modUnknown) ? modUnknown : {};
423
- const AddonClass = require_manifest_python_deps.resolveAddonClass(mod);
423
+ const AddonClass = require_manifest_system_deps.resolveAddonClass(mod);
424
424
  if (!AddonClass) throw new Error(`No addon class exported from ${entryPath}`);
425
425
  return {
426
426
  declaration,
@@ -554,13 +554,13 @@ async function main() {
554
554
  */
555
555
  const customActionRegistry = new require_custom_action_registry.CustomActionRegistry();
556
556
  let shuttingDown = false;
557
- const udsChildClient = new require_manifest_python_deps.LocalChildClient({
557
+ const udsChildClient = new require_manifest_system_deps.LocalChildClient({
558
558
  nodeId: parentNodeId,
559
559
  childId: runnerId,
560
- caps: buildChildCapDescriptors(runnerManifest, require_manifest_python_deps.getWorkerNativeCapSnapshot()),
560
+ caps: buildChildCapDescriptors(runnerManifest, require_manifest_system_deps.getWorkerNativeCapSnapshot()),
561
561
  dispatch: createChildCapDispatch({
562
562
  getSingletonProvider: (capName, addonId) => resolveSingletonProvider(capName, addonId),
563
- getDeviceProvider: (capName, deviceId) => require_manifest_python_deps.getWorkerNativeCapProvider(capName, deviceId) ?? null,
563
+ getDeviceProvider: (capName, deviceId) => require_manifest_system_deps.getWorkerNativeCapProvider(capName, deviceId) ?? null,
564
564
  isCollectionListMethod: (capName, method) => (0, _camstack_types_addon.isCollectionArrayMethodName)(capName, method)
565
565
  })
566
566
  });
@@ -570,7 +570,7 @@ async function main() {
570
570
  getAddonInstance: (addonId) => loadedAddonInstances.get(addonId) ?? null,
571
571
  getDataPlanes: (addonId) => addonDataPlanes.get(addonId) ?? []
572
572
  }));
573
- const { logger: runnerLog, control: runnerLogControl } = require_manifest_python_deps.createUdsLoggerWithControl(udsChildClient, runnerId, nodeId, "addon-runner");
573
+ const { logger: runnerLog, control: runnerLogControl } = require_manifest_system_deps.createUdsLoggerWithControl(udsChildClient, runnerId, nodeId, "addon-runner");
574
574
  udsChildClient.onSetLogLevel((level) => {
575
575
  runnerLogControl.setLevel(level);
576
576
  runnerLog.info(`Log level changed to "${level}" via UDS`);
@@ -585,13 +585,13 @@ async function main() {
585
585
  if (storageProvider) runnerLog.info(`"${nodeId}" storage provider rooted at ${dataDir}`);
586
586
  else runnerLog.warn(`"${nodeId}" failed to load FilesystemStorageProvider`, { meta: { error: (0, _camstack_types_addon.errMsg)(storageProviderError) } });
587
587
  const refreshChildCaps = (reason) => {
588
- const freshDescriptors = buildChildCapDescriptors(runnerManifest, require_manifest_python_deps.getWorkerNativeCapSnapshot());
588
+ const freshDescriptors = buildChildCapDescriptors(runnerManifest, require_manifest_system_deps.getWorkerNativeCapSnapshot());
589
589
  udsChildClient.updateCaps(freshDescriptors).catch((err) => {
590
590
  runnerLog.warn(`UDS updateCaps failed (${reason})`, { meta: { error: err instanceof Error ? err.message : String(err) } });
591
591
  });
592
592
  };
593
593
  let capRefreshTimer;
594
- require_manifest_python_deps.setWorkerNativeCapsChangeListener(() => {
594
+ require_manifest_system_deps.setWorkerNativeCapsChangeListener(() => {
595
595
  if (capRefreshTimer !== void 0) return;
596
596
  capRefreshTimer = setTimeout(() => {
597
597
  capRefreshTimer = void 0;
@@ -603,7 +603,7 @@ async function main() {
603
603
  const deviceRestorePromises = [];
604
604
  for (const { spec, declaration, AddonClass } of loadedModules) try {
605
605
  const addon = new AddonClass();
606
- const context = await require_manifest_python_deps.createUdsAddonContext(udsChildClient, nodeId, declaration, dataDir, {
606
+ const context = await require_manifest_system_deps.createUdsAddonContext(udsChildClient, nodeId, declaration, dataDir, {
607
607
  ...storageProvider ? { storageProvider } : {},
608
608
  dataPlaneSink: { set: (addonId, endpoints) => {
609
609
  addonDataPlanes.set(addonId, endpoints);
@@ -611,12 +611,13 @@ async function main() {
611
611
  });
612
612
  try {
613
613
  const pkgRaw = JSON.parse(node_fs.readFileSync(node_path.join(spec.addonDir, "package.json"), "utf-8"));
614
- await require_manifest_python_deps.installManifestNativeDeps(spec.addonDir, pkgRaw, context.logger);
614
+ await require_manifest_system_deps.installManifestNativeDeps(spec.addonDir, pkgRaw, context.logger);
615
615
  } catch (nativeErr) {
616
616
  console.error(`[addon-runner] native deps install failed for "${spec.addonId}": ${(0, _camstack_types_addon.errMsg)(nativeErr)}`);
617
617
  process.exit(1);
618
618
  }
619
- await require_manifest_python_deps.installManifestPythonDeps(declaration, spec.addonDir, context.deps, context.logger);
619
+ await require_manifest_system_deps.installManifestPythonDeps(declaration, spec.addonDir, context.deps, context.logger);
620
+ await require_manifest_system_deps.installManifestSystemDeps(declaration, context.logger);
620
621
  const registeredProviders = /* @__PURE__ */ new Map();
621
622
  const initWatchdog = startInitWatchdog({
622
623
  addonId: spec.addonId,
@@ -651,7 +652,7 @@ async function main() {
651
652
  return fn(input, caller);
652
653
  });
653
654
  }
654
- require_manifest_python_deps.validateProviderRegistrations(spec.addonId, declaration, registeredProviders, context.logger);
655
+ require_manifest_system_deps.validateProviderRegistrations(spec.addonId, declaration, registeredProviders, context.logger);
655
656
  const capNames = [...registeredProviders.keys()];
656
657
  context.logger.info(`"${spec.addonId}" caps: ${capNames.length > 0 ? capNames.join(", ") : "(none)"}`);
657
658
  runnerManifest.push({
@@ -688,10 +689,10 @@ async function main() {
688
689
  fireHubReachableOnce();
689
690
  runnerLog.info(`"${nodeId}" started — ${loaded.length} addon(s) ready (UDS connected)`);
690
691
  process.stdout.write(`READY:${nodeId}\n`);
691
- const stopHeapWatch = require_manifest_python_deps.startRunnerHeapWatch({
692
+ const stopHeapWatch = require_manifest_system_deps.startRunnerHeapWatch({
692
693
  heapProfile: process.env["CAMSTACK_RUNNER_HEAP_PROFILE"],
693
694
  reclaimSwitch: process.env["CAMSTACK_RUNNER_HEAP_RECLAIM"],
694
- rssBudgetMb: process.env[require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV],
695
+ rssBudgetMb: process.env[require_manifest_system_deps.RUNNER_RSS_BUDGET_ENV],
695
696
  label: nodeId,
696
697
  sink: {
697
698
  info: (line) => {
@@ -703,7 +704,7 @@ async function main() {
703
704
  }
704
705
  });
705
706
  process.on("SIGUSR2", () => {
706
- require_manifest_python_deps.emitHeapDiagnosticReport({
707
+ require_manifest_system_deps.emitHeapDiagnosticReport({
707
708
  label: nodeId,
708
709
  addons: loaded.map((e) => e.addonId),
709
710
  sink: {
@@ -720,7 +721,7 @@ async function main() {
720
721
  const shutdown = async () => {
721
722
  shuttingDown = true;
722
723
  stopHeapWatch?.();
723
- require_manifest_python_deps.setWorkerNativeCapsChangeListener(null);
724
+ require_manifest_system_deps.setWorkerNativeCapsChangeListener(null);
724
725
  if (capRefreshTimer !== void 0) {
725
726
  clearTimeout(capRefreshTimer);
726
727
  capRefreshTimer = void 0;
@@ -1,4 +1,4 @@
1
- import { F as createUdsLoggerWithControl, I as LocalChildClient, It as RUNNER_RSS_BUDGET_ENV, Yt as startRunnerHeapWatch, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, en as emitHeapDiagnosticReport, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, xt as resolveAddonClass } from "./manifest-python-deps-CD14Pv0-.mjs";
1
+ import { Ct as resolveAddonClass, N as createUdsLoggerWithControl, P as LocalChildClient, Rt as RUNNER_RSS_BUDGET_ENV, St as installManifestNativeDeps, Zt as startRunnerHeapWatch, a as createUdsAddonContext, at as getWorkerNativeCapSnapshot, it as getWorkerNativeCapProvider, lt as validateProviderRegistrations, n as installManifestPythonDeps, nn as emitHeapDiagnosticReport, st as setWorkerNativeCapsChangeListener, t as installManifestSystemDeps } from "./manifest-system-deps-DnoPpLGp.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as path$1 from "node:path";
@@ -614,6 +614,7 @@ async function main() {
614
614
  process.exit(1);
615
615
  }
616
616
  await installManifestPythonDeps(declaration, spec.addonDir, context.deps, context.logger);
617
+ await installManifestSystemDeps(declaration, context.logger);
617
618
  const registeredProviders = /* @__PURE__ */ new Map();
618
619
  const initWatchdog = startInitWatchdog({
619
620
  addonId: spec.addonId,
@@ -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-CDgIzo82.js");
6
+ const require_dist = require("../../dist-CcvXUhHK.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 { Ot as errMsg, S as addonPagesCapability, kt as BaseAddon, tn as EventCategory } from "../../dist-LvKus-mT.mjs";
1
+ import { Mt as BaseAddon, in as EventCategory, jt as errMsg, w as addonPagesCapability } from "../../dist-DKs---Vx.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-CDgIzo82.js");
6
+ const require_dist = require("../../dist-CcvXUhHK.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 { Ot as errMsg, kt as BaseAddon, tn as EventCategory, w as addonWidgetsCapability } from "../../dist-LvKus-mT.mjs";
1
+ import { E as addonWidgetsCapability, Mt as BaseAddon, in as EventCategory, jt as errMsg } from "../../dist-DKs---Vx.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-CDgIzo82.js");
6
+ const require_dist = require("../../dist-CcvXUhHK.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 { Ht as createEvent, Ot as errMsg, T as alertsCapability, kt as BaseAddon, n as AlertSchema, tn as EventCategory } from "../../dist-LvKus-mT.mjs";
1
+ import { D as alertsCapability, Gt as createEvent, Mt as BaseAddon, in as EventCategory, jt as errMsg, n as AlertSchema } from "../../dist-DKs---Vx.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -120,13 +120,16 @@ export declare class BackupOrchestratorAddon extends BaseAddon<Record<string, ne
120
120
  */
121
121
  private scheduleRestore;
122
122
  /**
123
- * Resolve a target destination set: caller-provided list wins;
124
- * otherwise fall back to every destination flagged as enabled in
125
- * its policy row (`triggerSupported !== false` after the policy
126
- * join). The old "global enabledDestinations override" was retired
127
- * along with the orchestrator settings panel — per-destination
128
- * `enabled` toggles in the destinations table are now the only
129
- * routing knob.
123
+ * Resolve a target destination set see {@link selectBackupTargets} for the
124
+ * rule. Caller-provided list wins over the per-destination policy flag;
125
+ * NEITHER wins over `StorageLocation.enabled`, which is the operator's
126
+ * switch on the storage screen and the only one that means "this disk takes
127
+ * no writes" (D62).
128
+ *
129
+ * Reads locations + policies directly rather than going through
130
+ * `listDestinations()`: that method also reads every destination's
131
+ * `manifests.json` to hydrate `lastSuccess`, which target resolution has no
132
+ * use for and which cost one round-trip per destination on every trigger.
130
133
  */
131
134
  private resolveTargets;
132
135
  private startScheduleTimer;
@@ -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-CDgIzo82.js");
6
+ const require_dist = require("../../dist-CcvXUhHK.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");
@@ -4777,6 +4777,28 @@ function mapRowToSchedule(row) {
4777
4777
  };
4778
4778
  }
4779
4779
  //#endregion
4780
+ //#region src/builtins/backup-orchestrator/write-targets.ts
4781
+ /**
4782
+ * Pick the destinations an archive may be fanned out to.
4783
+ *
4784
+ * An explicit `requested` list selects from the candidates by id and bypasses
4785
+ * the policy flag (that is today's behaviour and it is right: naming a
4786
+ * destination IS the opt-in). A requested id that is not a `backups` location
4787
+ * at all is simply absent from the candidates and drops out here, exactly as
4788
+ * before.
4789
+ *
4790
+ * The location flag is applied LAST and to both paths, so there is one place
4791
+ * where "disabled" is honoured rather than one per caller.
4792
+ */
4793
+ function selectBackupTargets(input) {
4794
+ const requested = input.requested;
4795
+ const pool = requested && requested.length > 0 ? input.candidates.filter((c) => requested.includes(c.id)) : input.candidates.filter((c) => c.policyEnabled);
4796
+ return {
4797
+ targets: pool.filter((c) => c.locationEnabled),
4798
+ skippedDisabled: pool.filter((c) => !c.locationEnabled).map((c) => c.id)
4799
+ };
4800
+ }
4801
+ //#endregion
4780
4802
  //#region src/builtins/backup-orchestrator/backup-orchestrator.addon.ts
4781
4803
  /**
4782
4804
  * `backup-orchestrator` builtin — singleton owner of the `backup` cap.
@@ -5002,7 +5024,7 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
5002
5024
  subId,
5003
5025
  displayName,
5004
5026
  kind: kindFromProviderId(loc.providerId),
5005
- triggerSupported: effective.enabled,
5027
+ triggerSupported: effective.enabled && loc.enabled !== false,
5006
5028
  restoreSupported: true,
5007
5029
  enabled: effective.enabled,
5008
5030
  retentionCount: effective.retentionCount,
@@ -5367,18 +5389,34 @@ var BackupOrchestratorAddon = class extends require_dist.BaseAddon {
5367
5389
  });
5368
5390
  }
5369
5391
  /**
5370
- * Resolve a target destination set: caller-provided list wins;
5371
- * otherwise fall back to every destination flagged as enabled in
5372
- * its policy row (`triggerSupported !== false` after the policy
5373
- * join). The old "global enabledDestinations override" was retired
5374
- * along with the orchestrator settings panel — per-destination
5375
- * `enabled` toggles in the destinations table are now the only
5376
- * routing knob.
5392
+ * Resolve a target destination set see {@link selectBackupTargets} for the
5393
+ * rule. Caller-provided list wins over the per-destination policy flag;
5394
+ * NEITHER wins over `StorageLocation.enabled`, which is the operator's
5395
+ * switch on the storage screen and the only one that means "this disk takes
5396
+ * no writes" (D62).
5397
+ *
5398
+ * Reads locations + policies directly rather than going through
5399
+ * `listDestinations()`: that method also reads every destination's
5400
+ * `manifests.json` to hydrate `lastSuccess`, which target resolution has no
5401
+ * use for and which cost one round-trip per destination on every trigger.
5377
5402
  */
5378
5403
  async resolveTargets(requested) {
5379
- const all = await this.listDestinations();
5380
- if (requested && requested.length > 0) return all.filter((d) => requested.includes(d.id));
5381
- return all.filter((d) => d.triggerSupported !== false);
5404
+ const locations = await this.fetchBackupLocations();
5405
+ const policies = await this.loadPoliciesIndexed(locations.map((l) => l.id));
5406
+ const selection = selectBackupTargets({
5407
+ candidates: locations.map((l) => ({
5408
+ id: l.id,
5409
+ locationEnabled: l.enabled !== false,
5410
+ policyEnabled: (policies.get(l.id) ?? this.fallbackPolicy(l.id)).enabled
5411
+ })),
5412
+ ...requested && requested.length > 0 ? { requested: [...requested] } : {}
5413
+ });
5414
+ if (selection.skippedDisabled.length > 0) this.ctx.logger.warn("backup: destination skipped — its storage location is disabled by the operator", { meta: {
5415
+ destinationIds: [...selection.skippedDisabled],
5416
+ requestedExplicitly: requested !== void 0 && requested.length > 0
5417
+ } });
5418
+ if (selection.targets.length === 0 && selection.skippedDisabled.length > 0) throw new Error(`every selected backup destination is disabled (${selection.skippedDisabled.join(", ")}) — enable its storage location on the Storage screen, or choose another destination. Refusing to write an archive to a location the operator turned off.`);
5419
+ return selection.targets.map((t) => ({ id: t.id }));
5382
5420
  }
5383
5421
  startScheduleTimer() {
5384
5422
  if (this.scheduleTimer) return;
@@ -1,4 +1,4 @@
1
- import { D as backupCapability, kt as BaseAddon, tn as EventCategory } from "../../dist-LvKus-mT.mjs";
1
+ import { Mt as BaseAddon, in as EventCategory, k as backupCapability } from "../../dist-DKs---Vx.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";
@@ -4762,6 +4762,28 @@ function mapRowToSchedule(row) {
4762
4762
  };
4763
4763
  }
4764
4764
  //#endregion
4765
+ //#region src/builtins/backup-orchestrator/write-targets.ts
4766
+ /**
4767
+ * Pick the destinations an archive may be fanned out to.
4768
+ *
4769
+ * An explicit `requested` list selects from the candidates by id and bypasses
4770
+ * the policy flag (that is today's behaviour and it is right: naming a
4771
+ * destination IS the opt-in). A requested id that is not a `backups` location
4772
+ * at all is simply absent from the candidates and drops out here, exactly as
4773
+ * before.
4774
+ *
4775
+ * The location flag is applied LAST and to both paths, so there is one place
4776
+ * where "disabled" is honoured rather than one per caller.
4777
+ */
4778
+ function selectBackupTargets(input) {
4779
+ const requested = input.requested;
4780
+ const pool = requested && requested.length > 0 ? input.candidates.filter((c) => requested.includes(c.id)) : input.candidates.filter((c) => c.policyEnabled);
4781
+ return {
4782
+ targets: pool.filter((c) => c.locationEnabled),
4783
+ skippedDisabled: pool.filter((c) => !c.locationEnabled).map((c) => c.id)
4784
+ };
4785
+ }
4786
+ //#endregion
4765
4787
  //#region src/builtins/backup-orchestrator/backup-orchestrator.addon.ts
4766
4788
  /**
4767
4789
  * `backup-orchestrator` builtin — singleton owner of the `backup` cap.
@@ -4987,7 +5009,7 @@ var BackupOrchestratorAddon = class extends BaseAddon {
4987
5009
  subId,
4988
5010
  displayName,
4989
5011
  kind: kindFromProviderId(loc.providerId),
4990
- triggerSupported: effective.enabled,
5012
+ triggerSupported: effective.enabled && loc.enabled !== false,
4991
5013
  restoreSupported: true,
4992
5014
  enabled: effective.enabled,
4993
5015
  retentionCount: effective.retentionCount,
@@ -5352,18 +5374,34 @@ var BackupOrchestratorAddon = class extends BaseAddon {
5352
5374
  });
5353
5375
  }
5354
5376
  /**
5355
- * Resolve a target destination set: caller-provided list wins;
5356
- * otherwise fall back to every destination flagged as enabled in
5357
- * its policy row (`triggerSupported !== false` after the policy
5358
- * join). The old "global enabledDestinations override" was retired
5359
- * along with the orchestrator settings panel — per-destination
5360
- * `enabled` toggles in the destinations table are now the only
5361
- * routing knob.
5377
+ * Resolve a target destination set see {@link selectBackupTargets} for the
5378
+ * rule. Caller-provided list wins over the per-destination policy flag;
5379
+ * NEITHER wins over `StorageLocation.enabled`, which is the operator's
5380
+ * switch on the storage screen and the only one that means "this disk takes
5381
+ * no writes" (D62).
5382
+ *
5383
+ * Reads locations + policies directly rather than going through
5384
+ * `listDestinations()`: that method also reads every destination's
5385
+ * `manifests.json` to hydrate `lastSuccess`, which target resolution has no
5386
+ * use for and which cost one round-trip per destination on every trigger.
5362
5387
  */
5363
5388
  async resolveTargets(requested) {
5364
- const all = await this.listDestinations();
5365
- if (requested && requested.length > 0) return all.filter((d) => requested.includes(d.id));
5366
- return all.filter((d) => d.triggerSupported !== false);
5389
+ const locations = await this.fetchBackupLocations();
5390
+ const policies = await this.loadPoliciesIndexed(locations.map((l) => l.id));
5391
+ const selection = selectBackupTargets({
5392
+ candidates: locations.map((l) => ({
5393
+ id: l.id,
5394
+ locationEnabled: l.enabled !== false,
5395
+ policyEnabled: (policies.get(l.id) ?? this.fallbackPolicy(l.id)).enabled
5396
+ })),
5397
+ ...requested && requested.length > 0 ? { requested: [...requested] } : {}
5398
+ });
5399
+ if (selection.skippedDisabled.length > 0) this.ctx.logger.warn("backup: destination skipped — its storage location is disabled by the operator", { meta: {
5400
+ destinationIds: [...selection.skippedDisabled],
5401
+ requestedExplicitly: requested !== void 0 && requested.length > 0
5402
+ } });
5403
+ if (selection.targets.length === 0 && selection.skippedDisabled.length > 0) throw new Error(`every selected backup destination is disabled (${selection.skippedDisabled.join(", ")}) — enable its storage location on the Storage screen, or choose another destination. Refusing to write an archive to a location the operator turned off.`);
5404
+ return selection.targets.map((t) => ({ id: t.id }));
5367
5405
  }
5368
5406
  startScheduleTimer() {
5369
5407
  if (this.scheduleTimer) return;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Which `backups` locations tonight's archive may land on.
3
+ *
4
+ * ## Why this is its own module
5
+ *
6
+ * The orchestrator had **two** enabled switches for one function: the
7
+ * operator-visible `StorageLocation.enabled` on the storage screen, and
8
+ * `backup_destination_policies.enabled`, seeded `true` for every newly-seen
9
+ * location. Only the second one was consulted, so a location the storage
10
+ * screen showed as disabled still received the archive — the exact shape D62
11
+ * exists to forbid ("a second switch that disagrees with the first is worse
12
+ * than no switch").
13
+ *
14
+ * The location flag is now the OUTER gate for every path, and the policy flag
15
+ * keeps its narrower meaning: *this destination takes part in the unattended
16
+ * fan-out*. An explicit request (an operator pressing Back up now with a
17
+ * destination selected, or a schedule naming `locationIds`) still overrides
18
+ * the policy flag — it never overrides the location flag, because that one is
19
+ * the operator saying "do not write here" about the disk itself.
20
+ *
21
+ * Kept pure and separate so the decision can be read in one screen and the
22
+ * addon is left with fetch → decide → log → upload.
23
+ */
24
+ /** One `backups` location, reduced to the two flags that decide a write. */
25
+ export interface BackupTargetCandidate {
26
+ readonly id: string;
27
+ /**
28
+ * `StorageLocation.enabled`. ABSENT MEANS ACTIVE — the caller maps
29
+ * `enabled !== false`, so a legacy location persisted before the flag
30
+ * existed stays a destination.
31
+ */
32
+ readonly locationEnabled: boolean;
33
+ /** `backup_destination_policies.enabled` — the unattended-fan-out opt-in. */
34
+ readonly policyEnabled: boolean;
35
+ }
36
+ export interface BackupTargetSelection {
37
+ /** Destinations the archive may be written to. */
38
+ readonly targets: readonly BackupTargetCandidate[];
39
+ /**
40
+ * Ids dropped because the OPERATOR disabled the storage location. Returned
41
+ * rather than silently filtered: a fan-out that drops a destination must say
42
+ * so, and the caller decides between "log and continue" and "refuse".
43
+ */
44
+ readonly skippedDisabled: readonly string[];
45
+ }
46
+ export interface BackupTargetSelectionInput {
47
+ readonly candidates: readonly BackupTargetCandidate[];
48
+ /** Explicitly named destinations (cap input, or a schedule's `locationIds`). */
49
+ readonly requested?: readonly string[];
50
+ }
51
+ /**
52
+ * Pick the destinations an archive may be fanned out to.
53
+ *
54
+ * An explicit `requested` list selects from the candidates by id and bypasses
55
+ * the policy flag (that is today's behaviour and it is right: naming a
56
+ * destination IS the opt-in). A requested id that is not a `backups` location
57
+ * at all is simply absent from the candidates and drops out here, exactly as
58
+ * before.
59
+ *
60
+ * The location flag is applied LAST and to both paths, so there is one place
61
+ * where "disabled" is honoured rather than one per caller.
62
+ */
63
+ export declare function selectBackupTargets(input: BackupTargetSelectionInput): BackupTargetSelection;
@@ -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-CDgIzo82.js");
6
+ const require_dist = require("../../dist-CcvXUhHK.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 { kt as BaseAddon, rt as logDestinationCapability } from "../../dist-LvKus-mT.mjs";
1
+ import { Mt as BaseAddon, ot as logDestinationCapability } from "../../dist-DKs---Vx.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-CDgIzo82.js");
3
+ const require_dist = require("../../dist-CcvXUhHK.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 { M as coreBlockIdFromAddonId, N as coreBlocksCapability, Ot as errMsg, c as CoreBlockSchema, j as coreBlockAddonId, kt as BaseAddon, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, u as DeclaredDevices } from "../../dist-LvKus-mT.mjs";
1
+ import { F as coreBlockIdFromAddonId, I as coreBlocksCapability, Mt as BaseAddon, P as coreBlockAddonId, c as CoreBlockSchema, jt as errMsg, o as CORE_BLOCKS_ADDON_ID, s as CORE_BLOCK_ADDON_PREFIX, u as DeclaredDevices } from "../../dist-DKs---Vx.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";
@@ -1,6 +1,7 @@
1
1
  import { AddonContext, CapabilityDefinition, DeviceBindingEntry, ICapabilityRegistry } from '@camstack/types';
2
2
  import { DeviceBindingsStore } from './device-meta-types.js';
3
3
  import { DeviceRow, DeviceRowStore } from './device-row-store.js';
4
+ import { WriteLock } from './write-lock.js';
4
5
  /** Push-fed remote native-cap cache shape (numeric deviceId → capName → owner). */
5
6
  export type RemoteNativeCaps = Map<number, Map<string, {
6
7
  addonId: string;
@@ -18,6 +19,27 @@ export interface BindingsDeps {
18
19
  /** Row access for `device-manager:devices` — the device ledger the type and
19
20
  * presence gates below consult. */
20
21
  readonly rows: DeviceRowStore;
22
+ /**
23
+ * Ordering for every read-modify-write of this addon's `addon-settings` key
24
+ * range — `deviceBindings` above all, but also `locations`, `nextDeviceId`
25
+ * and `roleDisplayDefaults`.
26
+ *
27
+ * `deviceBindings` is ONE fleet-wide blob keyed by device, so a writer that
28
+ * only means to touch camera 615 rewrites every camera's row. Nothing
29
+ * ordered those writers, and the window between the read and the write is a
30
+ * settings-door round trip — an RPC to the isolated `sqlite-settings` child,
31
+ * measured at 2.9 s on the live hub mid-drain (D181). Two toggles on
32
+ * DIFFERENT cameras therefore lost each other, and the value that went
33
+ * missing was `{ wrapperAddonId: null }`: the marker that keeps a
34
+ * `defaultActive` wrapper from re-binding. The operator's OFF came back ON
35
+ * (D62).
36
+ *
37
+ * **Writers only.** No read path takes this lock: `getBindings`,
38
+ * `getBindingsBatch` and `getAllBindings` must never queue behind a write,
39
+ * and putting a read under it would make a 1 019-device sweep wait on
40
+ * whatever toggle happens to be in flight.
41
+ */
42
+ readonly withAddonStoreWriteLock: WriteLock;
21
43
  }
22
44
  /**
23
45
  * Wire the push-fed cross-process native-cap cache (`remoteNativeCaps`).
@@ -107,8 +129,15 @@ export type DevicePresence = 'present' | 'absent' | 'unknown';
107
129
  * same reason `getAllBindings` warns instead of reporting zero devices. That is
108
130
  * the only reason this is async: the `COUNT(*)` runs solely on the miss path,
109
131
  * where the alternative is to call a device gone because the store is empty.
132
+ *
133
+ * `ledgerNonEmpty` is passed IN rather than defaulted, deliberately. A default
134
+ * that issued its own `rows.count()` would be a per-device read hiding behind
135
+ * an optional argument — the exact shape this file just removed from the
136
+ * activations blob — and nothing would fail when a future caller took it.
110
137
  */
111
- export declare function resolveDevicePresence(deps: BindingsDeps, row: DeviceRow | null, deviceId: number): Promise<DevicePresence>;
138
+ export declare function resolveDevicePresence(deps: BindingsDeps, row: DeviceRow | null, deviceId: number, ledgerNonEmpty: LedgerNonEmpty): Promise<DevicePresence>;
139
+ /** "Does the ledger hold any row at all?", asked at most once per pass. */
140
+ export type LedgerNonEmpty = () => Promise<boolean>;
112
141
  /**
113
142
  * Does a capability apply to a device of type `deviceType`?
114
143
  *
@@ -138,15 +167,23 @@ export declare function getBindings(deps: BindingsDeps, input: {
138
167
  * `getAllBindings` (511 ms / 679 KB of mostly-unwanted rows).
139
168
  *
140
169
  * Sequential on purpose, like `getAllBindings`: `resolveBindingsForDevice`'s
141
- * remaining per-device work (the bindings-store read, the routing rules) runs
142
- * on this process's own event loop, so concurrency buys nothing there and a
143
- * fan-out of `Promise.all` over hundreds of ids only makes the queue less fair
144
- * to everything else on it. The one part that DOES amortise across the whole
145
- * set — the device row lookup — is fetched ONCE, ahead of the loop
146
- * ({@link DeviceRowStore.getMany}), rather than once per iteration: measured
147
- * on the live hub 2026-08-25, `get device-manager:devices` ran 1 656
148
- * calls/min steady-state, one row per call, which is exactly what looping
149
- * `getBindings`'s own `rows.get(deviceId)` here produced.
170
+ * remaining per-device work is the routing rules, which run on this process's
171
+ * own event loop, so concurrency buys nothing there and a fan-out of
172
+ * `Promise.all` over hundreds of ids only makes the queue less fair to
173
+ * everything else on it. Both parts that DO amortise across the whole set are
174
+ * fetched ONCE, ahead of the loop, and in parallel with each other:
175
+ *
176
+ * - the device rows ({@link DeviceRowStore.getMany}) rather than once per
177
+ * iteration measured on the live hub 2026-08-25, `get
178
+ * device-manager:devices` ran 1 656 calls/min steady-state, one row per
179
+ * call, which is exactly what looping `getBindings`'s own
180
+ * `rows.get(deviceId)` here produced;
181
+ * - the wrapper-activation blob, which the per-device resolver used to read
182
+ * itself — a second settings-door round trip per id, on the call that
183
+ * exists precisely so a caller needing many devices does not pay per
184
+ * device. Measured on the live hub 2026-08-30, mid-drain:
185
+ * `getBindingsBatch` of 1 id = 5.78 s, of 8 ids = 17.16 s — a slope of
186
+ * 1.63 s PER ADDITIONAL DEVICE, on a call that ships 4.5 KB a row.
150
187
  */
151
188
  export declare function getBindingsBatch(deps: BindingsDeps, input: {
152
189
  deviceIds: readonly number[];
@@ -164,6 +201,15 @@ export declare function getBindingsBatch(deps: BindingsDeps, input: {
164
201
  * clients invalidate via the existing
165
202
  * `capability.binding-changed` event rather than re-fetching this
166
203
  * payload periodically.
204
+ *
205
+ * **"Single round-trip" was a claim, not a fact, until 2026-08-30.** The
206
+ * per-device resolver read the wrapper-activation blob itself, so this call
207
+ * issued one settings-door RPC per device — 1 020 of them on the live
208
+ * 1 019-device hub, at ~2.9 s each while the storage drain ran. It did not
209
+ * return in 240 s, and `SystemMirror.init` gives it 15 s, so the admin UI
210
+ * could not boot at all. Now the whole pass costs exactly TWO reads (the fleet
211
+ * listing and the blob), issued in parallel, and nothing in the loop awaits a
212
+ * store.
167
213
  */
168
214
  export declare function getAllBindings(deps: BindingsDeps): Promise<Array<{
169
215
  deviceId: number;
@@ -83,6 +83,17 @@ export declare class DeviceManagerAddon extends BaseAddon {
83
83
  * it through {@link bindingsDeps} or the `ProviderContext`.
84
84
  */
85
85
  private deviceRows;
86
+ /**
87
+ * The ONE lock ordering every read-modify-write of this addon's
88
+ * `addon-settings` key range (`deviceBindings`, `locations`, `nextDeviceId`,
89
+ * `roleDisplayDefaults`).
90
+ *
91
+ * It lives on the addon rather than on `DeviceMetaStore` because the writers
92
+ * do not all go through that class — `setWrapperActive` and `removeDevice`'s
93
+ * bindings purge reach the store through `BindingsDeps` — and two locks over
94
+ * one key range is the same defect one level up. Handed to both.
95
+ */
96
+ private readonly addonStoreWriteLock;
86
97
  /** Build the dependency context the extracted binding resolvers consume. */
87
98
  private get bindingsDeps();
88
99
  getBindings(input: {