@camstack/system 1.2.439 → 1.2.441

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 (103) hide show
  1. package/dist/addon-runner.js +743 -79
  2. package/dist/addon-runner.mjs +743 -79
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/autotrack/index.js +1 -1
  12. package/dist/builtins/autotrack/index.mjs +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  15. package/dist/builtins/calendars/calendars.addon.js +1 -1
  16. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  17. package/dist/builtins/camera-grid/index.js +1 -1
  18. package/dist/builtins/camera-grid/index.mjs +1 -1
  19. package/dist/builtins/composer/composer.addon.js +2 -2
  20. package/dist/builtins/composer/composer.addon.mjs +2 -2
  21. package/dist/builtins/console-logging/index.js +1 -1
  22. package/dist/builtins/console-logging/index.mjs +1 -1
  23. package/dist/builtins/core-blocks/block-settings-store.d.ts +1 -1
  24. package/dist/builtins/core-blocks/core-blocks.addon.js +77 -11
  25. package/dist/builtins/core-blocks/core-blocks.addon.mjs +77 -12
  26. package/dist/builtins/core-blocks/index.mjs +1 -1
  27. package/dist/builtins/device-manager/device-manager.addon.js +181 -59
  28. package/dist/builtins/device-manager/device-manager.addon.mjs +180 -60
  29. package/dist/builtins/device-manager/device-removal-hooks.d.ts +0 -11
  30. package/dist/builtins/hub-forwarder/index.js +1 -1
  31. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  32. package/dist/builtins/keychain/index.js +29 -11
  33. package/dist/builtins/keychain/index.mjs +29 -11
  34. package/dist/builtins/keychain/keychain-service.d.ts +8 -0
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  37. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  38. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  39. package/dist/builtins/local-network/local-network.addon.js +2 -2
  40. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  41. package/dist/builtins/loki-logging/index.js +1 -1
  42. package/dist/builtins/loki-logging/index.mjs +1 -1
  43. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  44. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  45. package/dist/builtins/platform-probe/index.js +1 -1
  46. package/dist/builtins/platform-probe/index.mjs +1 -1
  47. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  48. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  49. package/dist/builtins/snapshot/index.js +1 -1
  50. package/dist/builtins/snapshot/index.mjs +1 -1
  51. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  52. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  53. package/dist/builtins/sqlite-storage/index.js +1 -1
  54. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  55. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +38 -7
  56. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +69 -3
  57. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +69 -3
  58. package/dist/builtins/sqlite-storage/storage-admin-provider.d.ts +31 -0
  59. package/dist/builtins/storage-orchestrator/hub-settings-forward.d.ts +3 -3
  60. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +4 -4
  61. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +4 -4
  62. package/dist/builtins/system-config/system-config.addon.js +2 -2
  63. package/dist/builtins/system-config/system-config.addon.mjs +2 -2
  64. package/dist/builtins/winston-logging/index.js +1 -1
  65. package/dist/builtins/winston-logging/index.mjs +1 -1
  66. package/dist/{composition-sources-B4jC2FC7.js → composition-sources-BG-W2U0d.js} +1 -1
  67. package/dist/{composition-sources-BGOSCGWa.mjs → composition-sources-ByNISOnW.mjs} +1 -1
  68. package/dist/{constants-B92BjObl.mjs → constants-DJILnUSb.mjs} +1 -1
  69. package/dist/{constants-BRsjQwi-.js → constants-UojhAJcm.js} +1 -1
  70. package/dist/{device-ca-DHG9OW2m.js → device-ca-196DAEZT.js} +1 -1
  71. package/dist/{device-ca-46BUsWD1.mjs → device-ca-D8Ri9TVz.mjs} +1 -1
  72. package/dist/{dist-yI6kF6r6.mjs → dist-BWvkuc31.mjs} +248 -27
  73. package/dist/{dist-BOWdLjoK.js → dist-BiTCcaf-.js} +265 -26
  74. package/dist/file-host-worker.js +1 -1
  75. package/dist/file-host-worker.mjs +1 -1
  76. package/dist/{hub-mdns-ciao-DGwVc2uh.mjs → hub-mdns-ciao-Cnd0bTB9.mjs} +1 -1
  77. package/dist/{hub-mdns-ciao-D7S5AJRU.js → hub-mdns-ciao-DOBTq8qr.js} +1 -1
  78. package/dist/index.js +336 -336
  79. package/dist/index.mjs +337 -337
  80. package/dist/kernel/addon-settings-store.d.ts +43 -0
  81. package/dist/kernel/config-manager.d.ts +0 -19
  82. package/dist/kernel/device-settings-store.d.ts +74 -0
  83. package/dist/kernel/legacy-blob-guard.d.ts +58 -0
  84. package/dist/kernel/moleculer/runner-addon-settings.d.ts +34 -0
  85. package/dist/kernel/moleculer/runner-device-settings.d.ts +39 -0
  86. package/dist/kernel/prefix-walk.d.ts +39 -0
  87. package/dist/kernel/sections-store.d.ts +20 -0
  88. package/dist/kernel/settings-door-view.d.ts +2 -2
  89. package/dist/kernel/settings-view-types.d.ts +5 -30
  90. package/dist/kernel/store-settings-view.d.ts +0 -3
  91. package/dist/prefix-range-D_KueXbQ.js +27 -0
  92. package/dist/prefix-range-Dl0znowN.mjs +22 -0
  93. package/dist/prefix-walk-DAOBSyH1.mjs +88 -0
  94. package/dist/prefix-walk-Dv6MLZlM.js +99 -0
  95. package/dist/{restore-archive-CpdUO-uS.mjs → restore-archive-4ziluTU3.mjs} +1 -1
  96. package/dist/{restore-archive-DiGiI6Jp.js → restore-archive-CfSSjKnA.js} +1 -1
  97. package/dist/{retired-settings-keys-UsVtLOT0.mjs → retired-settings-keys-Bhwe4ymb.mjs} +2 -2
  98. package/dist/{retired-settings-keys-CyQ7uvta.js → retired-settings-keys-D_VwZiDt.js} +10 -4
  99. package/dist/{wal-maintenance-xsvdT5Go.mjs → wal-maintenance-B57SAp4a.mjs} +0 -0
  100. package/dist/{wal-maintenance-DEL_jeG0.js → wal-maintenance-CgZTNWAz.js} +0 -0
  101. package/package.json +5 -2
  102. package/dist/{custom-action-registry-Bf_YnagS.js → custom-action-registry-BKmoaqG0.js} +1 -1
  103. package/dist/{custom-action-registry-BQnbN0DP.mjs → custom-action-registry-CyUuhATc.mjs} +1 -1
@@ -45,7 +45,7 @@ let _camstack_types_node = require("@camstack/types/node");
45
45
  let node_module = require("node:module");
46
46
  let node_url = require("node:url");
47
47
  //#region src/kernel/addon-class-resolver.ts
48
- function isRecord$3(value) {
48
+ function isRecord$5(value) {
49
49
  return typeof value === "object" && value !== null && !Array.isArray(value);
50
50
  }
51
51
  /**
@@ -69,14 +69,14 @@ function isRecord$3(value) {
69
69
  function isAddonConstructor(fn) {
70
70
  if (typeof fn !== "function") return false;
71
71
  const proto = fn.prototype;
72
- if (!isRecord$3(proto)) return false;
72
+ if (!isRecord$5(proto)) return false;
73
73
  return typeof proto["initialize"] === "function";
74
74
  }
75
75
  function resolveAddonClass(mod) {
76
76
  let candidate = mod["default"];
77
- if (isRecord$3(candidate) && "default" in candidate) candidate = candidate["default"];
77
+ if (isRecord$5(candidate) && "default" in candidate) candidate = candidate["default"];
78
78
  if (isAddonConstructor(candidate)) return candidate;
79
- if (isRecord$3(candidate)) {
79
+ if (isRecord$5(candidate)) {
80
80
  const fn = Object.values(candidate).find(isAddonConstructor);
81
81
  if (fn) return fn;
82
82
  }
@@ -5182,27 +5182,6 @@ function createUdsLoggerWithControl(client, addonId, nodeId, scope, tags = {}) {
5182
5182
  };
5183
5183
  }
5184
5184
  //#endregion
5185
- //#region src/kernel/moleculer/settings-read-guard.ts
5186
- /**
5187
- * Merge `patch` over the current value and write it — unless the read failed,
5188
- * in which case write NOTHING.
5189
- *
5190
- * Returns whether the write was attempted, so a caller can surface the refusal.
5191
- */
5192
- async function readModifyWrite(input) {
5193
- const current = await input.read();
5194
- if (current.kind === "failed") {
5195
- input.onRefused(current.error);
5196
- return false;
5197
- }
5198
- const base = current.kind === "value" ? current.value : {};
5199
- await input.write({
5200
- ...base,
5201
- ...input.patch
5202
- });
5203
- return true;
5204
- }
5205
- //#endregion
5206
5185
  //#region src/kernel/capability-handle.ts
5207
5186
  var CapabilityUnavailableError = class extends Error {
5208
5187
  capName;
@@ -5415,7 +5394,7 @@ var RuntimeStateLoadError = class extends Error {
5415
5394
  this.name = "RuntimeStateLoadError";
5416
5395
  }
5417
5396
  };
5418
- function isRecord$2(value) {
5397
+ function isRecord$4(value) {
5419
5398
  return typeof value === "object" && value !== null && !Array.isArray(value);
5420
5399
  }
5421
5400
  function errorMessage(err) {
@@ -5446,7 +5425,7 @@ async function loadRuntimeStateOrRefuse(load, logger, target) {
5446
5425
  } catch (err) {
5447
5426
  return refuse(errorMessage(err), err);
5448
5427
  }
5449
- if (!isRecord$2(answer)) return refuse(`answered ${answer === null ? "null" : typeof answer}`);
5428
+ if (!isRecord$4(answer)) return refuse(`answered ${answer === null ? "null" : typeof answer}`);
5450
5429
  return answer;
5451
5430
  }
5452
5431
  //#endregion
@@ -9363,6 +9342,727 @@ function createRemoteProviderProxy(broker, serviceName, capName, nodeId, methods
9363
9342
  return dispatcher;
9364
9343
  }
9365
9344
  //#endregion
9345
+ //#region src/builtins/sqlite-storage/prefix-range.ts
9346
+ var MAX_ASCII = 127;
9347
+ /**
9348
+ * @returns the range for `prefix`, or `null` when the caller must fall back to
9349
+ * a `LIKE` scan (empty prefix — which is the whole table and has no upper
9350
+ * bound — or any byte outside ASCII).
9351
+ */
9352
+ function prefixRange(prefix) {
9353
+ if (prefix.length === 0) return null;
9354
+ for (let i = 0; i < prefix.length; i++) {
9355
+ const code = prefix.charCodeAt(i);
9356
+ if (code === 0 || code > MAX_ASCII) return null;
9357
+ }
9358
+ const last = prefix.charCodeAt(prefix.length - 1);
9359
+ if (last >= MAX_ASCII) return null;
9360
+ return {
9361
+ lo: prefix,
9362
+ hi: prefix.slice(0, -1) + String.fromCharCode(last + 1)
9363
+ };
9364
+ }
9365
+ //#endregion
9366
+ //#region src/kernel/prefix-walk.ts
9367
+ function isRecord$3(value) {
9368
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9369
+ }
9370
+ function unwrapStoredValue(parsed) {
9371
+ if (!isRecord$3(parsed) || !("value" in parsed)) return parsed;
9372
+ return parsed.value;
9373
+ }
9374
+ /**
9375
+ * Rows one page of a prefix read asks for.
9376
+ *
9377
+ * Below `SETTINGS_QUERY_DEFAULT_ROW_CAP` (2 000, `query-bounds.ts`) on purpose:
9378
+ * this read now names its own bound, so the engine's defensive cap is never the
9379
+ * thing that decides how much of the answer the caller gets.
9380
+ */
9381
+ var PREFIX_PAGE_ROWS = 1e3;
9382
+ /**
9383
+ * Pages one prefix read may take before it refuses.
9384
+ *
9385
+ * 512 pages is 512 000 rows — three orders of magnitude past the largest live
9386
+ * scope (`addon-device-settings` for one addon over a ~1 019-device fleet) and
9387
+ * still a bound, because a walk with no bound is the defect this file was
9388
+ * fixing, not one to reintroduce.
9389
+ */
9390
+ var PREFIX_PAGE_BUDGET = 512;
9391
+ /**
9392
+ * Every row under `prefix`, or a throw — never the first 2 000 of them.
9393
+ *
9394
+ * **The defect.** This issued ONE `query` with no `limit`, and
9395
+ * `queryDeclared` resolves an absent limit to `SETTINGS_QUERY_DEFAULT_ROW_CAP`
9396
+ * and cuts the result there. On the live hub that fired 138 times in two hours
9397
+ * (`settings-store query truncated by a defensive row cap`, collection
9398
+ * `addon-device-settings`, `rowCap: 2000`, `requestedLimit: null`): a
9399
+ * ~1 019-device fleet with more than two stored keys per device puts one
9400
+ * addon's `<addonId>:` range past the cap. The engine warned; the door did not
9401
+ * notice, and handed its caller a short map that is **shape-identical to a
9402
+ * complete one**. Every device whose rows sort past the cap reads as "has no
9403
+ * stored settings", which is exactly what a device with none looks like.
9404
+ *
9405
+ * **The fix is a keyset walk, not a bigger limit.** The cursor is the last id
9406
+ * of the previous page, INCLUSIVE — so the page boundary re-reads exactly one
9407
+ * row (ids are unique, so a full page always advances) and no row can fall
9408
+ * between two pages the way an `OFFSET` walk drops rows under a concurrent
9409
+ * write (D295 rule 1). Re-reading is free here: the accumulator is keyed by
9410
+ * suffix, so the duplicate overwrites itself with its own value.
9411
+ *
9412
+ * `ORDER BY id` is not decoration — the cursor is only meaningful if the
9413
+ * engine returns the range in the order the cursor advances through. It is the
9414
+ * primary key, so the ordering is the range scan's own.
9415
+ *
9416
+ * @throws when the prefix cannot be expressed as a range and the collection is
9417
+ * larger than one page, or when the walk exceeds {@link PREFIX_PAGE_BUDGET}.
9418
+ * A throw is the point: a caller that cannot be answered completely must not
9419
+ * be handed something that looks like a complete answer.
9420
+ */
9421
+ async function loadPrefixed(door, collection, prefix, decode = unwrapStoredValue) {
9422
+ const range = prefixRange(prefix);
9423
+ const result = {};
9424
+ let cursor = range?.lo ?? null;
9425
+ for (let page = 0;; page++) {
9426
+ if (page >= PREFIX_PAGE_BUDGET) throw new Error(`settings door: reading "${prefix}" from "${collection}" exceeded ${PREFIX_PAGE_BUDGET} pages of ${PREFIX_PAGE_ROWS} rows. Refusing to return the part that was read — a partial scope read is indistinguishable from an empty one.`);
9427
+ const rows = await door.query({
9428
+ collection,
9429
+ filter: {
9430
+ ...range !== null && cursor !== null ? { whereBetween: { id: [cursor, range.hi] } } : {},
9431
+ orderBy: {
9432
+ field: "id",
9433
+ direction: "asc"
9434
+ },
9435
+ limit: PREFIX_PAGE_ROWS
9436
+ }
9437
+ });
9438
+ for (const row of rows) {
9439
+ if (range !== null && (row.id < range.lo || row.id >= range.hi)) continue;
9440
+ if (!row.id.startsWith(prefix)) continue;
9441
+ result[row.id.slice(prefix.length)] = decode(row.data);
9442
+ }
9443
+ if (rows.length < PREFIX_PAGE_ROWS) return result;
9444
+ const last = rows[rows.length - 1];
9445
+ if (last === void 0) return result;
9446
+ if (range === null) throw new Error(`settings door: prefix "${prefix}" on "${collection}" cannot be expressed as an id range, and the unfiltered read filled a page — the answer would be partial. Use an ASCII, non-empty prefix.`);
9447
+ if (last.id === cursor) throw new Error(`settings door: reading "${prefix}" from "${collection}" made no progress at id "${last.id}" — ids under a prefix must be unique.`);
9448
+ cursor = last.id;
9449
+ }
9450
+ }
9451
+ //#endregion
9452
+ //#region src/kernel/addon-settings-store.ts
9453
+ /** The collection every addon's settings share. Named nowhere else (with sections' own module). */
9454
+ var ADDON_SETTINGS_COLLECTION = "addon-settings";
9455
+ function createAddonSettingsStore(door, addonId) {
9456
+ const prefix = `${addonId}.`;
9457
+ const message = (err) => err instanceof Error ? err.message : String(err);
9458
+ return {
9459
+ async read() {
9460
+ try {
9461
+ const value = await loadPrefixed(door, ADDON_SETTINGS_COLLECTION, prefix);
9462
+ return Object.keys(value).length === 0 ? { kind: "absent" } : {
9463
+ kind: "present",
9464
+ value
9465
+ };
9466
+ } catch (err) {
9467
+ return {
9468
+ kind: "unreadable",
9469
+ error: message(err)
9470
+ };
9471
+ }
9472
+ },
9473
+ async readKey(key) {
9474
+ try {
9475
+ const row = await door.get({
9476
+ collection: ADDON_SETTINGS_COLLECTION,
9477
+ key: `${prefix}${key}`
9478
+ });
9479
+ return row === void 0 || row === null ? { kind: "absent" } : {
9480
+ kind: "present",
9481
+ value: unwrapStoredValue(row)
9482
+ };
9483
+ } catch (err) {
9484
+ return {
9485
+ kind: "unreadable",
9486
+ error: message(err)
9487
+ };
9488
+ }
9489
+ },
9490
+ async merge(patch) {
9491
+ const records = [];
9492
+ const removed = [];
9493
+ for (const [key, value] of Object.entries(patch)) if (value === void 0) removed.push(`${prefix}${key}`);
9494
+ else records.push({
9495
+ id: `${prefix}${key}`,
9496
+ data: {
9497
+ addonId,
9498
+ key,
9499
+ value
9500
+ }
9501
+ });
9502
+ if (records.length === 0 && removed.length === 0) return;
9503
+ if (prefixRange(prefix) === null) throw new Error(`addon settings: "${addonId}" cannot be expressed as an id range`);
9504
+ await door.transact({ ops: [...removed.length > 0 ? [{
9505
+ op: "deleteWhere",
9506
+ collection: ADDON_SETTINGS_COLLECTION,
9507
+ filter: { whereIn: { id: removed } }
9508
+ }] : [], ...records.length > 0 ? [{
9509
+ op: "upsertMany",
9510
+ collection: ADDON_SETTINGS_COLLECTION,
9511
+ conflict: ["id"],
9512
+ records
9513
+ }] : []] });
9514
+ }
9515
+ };
9516
+ }
9517
+ //#endregion
9518
+ //#region src/kernel/system-settings-policy.ts
9519
+ /**
9520
+ * Which `system-settings` keys the STORE may answer, and which the kernel
9521
+ * loads from the door into memory at boot — D675.
9522
+ *
9523
+ * ## The store never answers for a bootstrap section
9524
+ *
9525
+ * `system-settings` may still hold `auth.jwtSecret`, `auth.adminUsername` and
9526
+ * `auth.adminPassword` — written there by `local-auth` (and by older builds)
9527
+ * next to `auth.tokenExpiry`. Since D797 none of the three is a setting at
9528
+ * all (the signing key is the keychain's, the first-boot admin is fixed), and
9529
+ * the stale rows must still never answer: merging them back in would let a
9530
+ * stale DB copy stand in for a credential. The one-off ops script deletes them.
9531
+ *
9532
+ * So a bootstrap-owned section is answered from env + config.yaml ONLY, and
9533
+ * the store may add exactly the keys in {@link STORE_READABLE_BOOTSTRAP_KEYS}:
9534
+ * the values the operator edits on System → Authentication (`auth.tokenExpiry`,
9535
+ * `auth.accessTokenExpiry`, `auth.trustedProxies`). Widening this list is a security decision, not a
9536
+ * convenience.
9537
+ */
9538
+ /** Top-level sections whose authority is env + config.yaml (`bootstrapSchema`). */
9539
+ var BOOTSTRAP_OWNED_SECTIONS = new Set([
9540
+ "mode",
9541
+ "server",
9542
+ "auth",
9543
+ "hub",
9544
+ "agent",
9545
+ "tls",
9546
+ "bootstrap",
9547
+ "keychain"
9548
+ ]);
9549
+ /** The only keys under a bootstrap-owned section the store may answer. */
9550
+ var STORE_READABLE_BOOTSTRAP_KEYS = new Set([
9551
+ "auth.tokenExpiry",
9552
+ "auth.accessTokenExpiry",
9553
+ "auth.trustedProxies",
9554
+ "auth.trustedNetworks",
9555
+ "server.name"
9556
+ ]);
9557
+ /** May the settings store answer `key`, or is it env + config.yaml only? */
9558
+ function storeMayAnswer(key) {
9559
+ const dot = key.indexOf(".");
9560
+ const section = dot === -1 ? key : key.slice(0, dot);
9561
+ if (!BOOTSTRAP_OWNED_SECTIONS.has(section)) return true;
9562
+ return STORE_READABLE_BOOTSTRAP_KEYS.has(key);
9563
+ }
9564
+ //#endregion
9565
+ //#region src/kernel/device-settings-store.ts
9566
+ /** The collection every addon's per-device settings share. Named nowhere else. */
9567
+ var DEVICE_SETTINGS_COLLECTION = "addon-device-settings";
9568
+ function scopeOf(addonId, deviceId) {
9569
+ return `${addonId}:${String(deviceId)}.`;
9570
+ }
9571
+ function assertDeviceId(deviceId) {
9572
+ if (!Number.isInteger(deviceId) || deviceId <= 0) throw new Error(`device settings: "${String(deviceId)}" is not a device id`);
9573
+ }
9574
+ function deviceIdOfSuffix(suffix) {
9575
+ const dot = suffix.indexOf(".");
9576
+ if (dot <= 0) return null;
9577
+ const id = Number(suffix.slice(0, dot));
9578
+ return Number.isInteger(id) && id > 0 ? id : null;
9579
+ }
9580
+ /** Every field row of one device scope, or a throw. */
9581
+ async function readScope(door, addonId, deviceId) {
9582
+ return loadPrefixed(door, DEVICE_SETTINGS_COLLECTION, scopeOf(addonId, deviceId));
9583
+ }
9584
+ function createDeviceSettingsStore(door, addonId) {
9585
+ return {
9586
+ async read(deviceId) {
9587
+ try {
9588
+ assertDeviceId(deviceId);
9589
+ const value = await readScope(door, addonId, deviceId);
9590
+ return Object.keys(value).length === 0 ? { kind: "absent" } : {
9591
+ kind: "present",
9592
+ value
9593
+ };
9594
+ } catch (err) {
9595
+ return {
9596
+ kind: "unreadable",
9597
+ error: err instanceof Error ? err.message : String(err)
9598
+ };
9599
+ }
9600
+ },
9601
+ async readBatch(deviceIds) {
9602
+ const out = /* @__PURE__ */ new Map();
9603
+ if (deviceIds.length === 0) return out;
9604
+ for (const id of deviceIds) out.set(id, {});
9605
+ const wanted = new Set(deviceIds);
9606
+ const all = await loadPrefixed(door, DEVICE_SETTINGS_COLLECTION, `${addonId}:`);
9607
+ for (const [suffix, value] of Object.entries(all)) {
9608
+ const id = deviceIdOfSuffix(suffix);
9609
+ if (id === null || !wanted.has(id)) continue;
9610
+ const blob = out.get(id);
9611
+ if (blob !== void 0) blob[suffix.slice(suffix.indexOf(".") + 1)] = value;
9612
+ }
9613
+ return out;
9614
+ },
9615
+ async listIds() {
9616
+ const all = await loadPrefixed(door, DEVICE_SETTINGS_COLLECTION, `${addonId}:`, () => null);
9617
+ const ids = /* @__PURE__ */ new Set();
9618
+ for (const suffix of Object.keys(all)) {
9619
+ const id = deviceIdOfSuffix(suffix);
9620
+ if (id !== null) ids.add(id);
9621
+ }
9622
+ return [...ids].toSorted((a, b) => a - b);
9623
+ },
9624
+ async merge(deviceId, patch) {
9625
+ assertDeviceId(deviceId);
9626
+ const scope = scopeOf(addonId, deviceId);
9627
+ const records = [];
9628
+ const removed = [];
9629
+ for (const [key, value] of Object.entries(patch)) if (value === void 0) removed.push(`${scope}${key}`);
9630
+ else records.push({
9631
+ id: `${scope}${key}`,
9632
+ data: {
9633
+ addonId,
9634
+ deviceId: String(deviceId),
9635
+ key,
9636
+ value
9637
+ }
9638
+ });
9639
+ if (records.length === 0 && removed.length === 0) return;
9640
+ await door.transact({ ops: [...removed.length > 0 ? [{
9641
+ op: "deleteWhere",
9642
+ collection: DEVICE_SETTINGS_COLLECTION,
9643
+ filter: { whereIn: { id: removed } }
9644
+ }] : [], ...records.length > 0 ? [{
9645
+ op: "upsertMany",
9646
+ collection: DEVICE_SETTINGS_COLLECTION,
9647
+ conflict: ["id"],
9648
+ records
9649
+ }] : []] });
9650
+ },
9651
+ async clear(deviceId) {
9652
+ assertDeviceId(deviceId);
9653
+ const range = prefixRange(scopeOf(addonId, deviceId));
9654
+ if (range === null) throw new Error(`device settings: no id range for addon "${addonId}"`);
9655
+ await door.deleteWhere({
9656
+ collection: DEVICE_SETTINGS_COLLECTION,
9657
+ filter: { whereBetween: { id: [range.lo, range.hi] } }
9658
+ });
9659
+ }
9660
+ };
9661
+ }
9662
+ /** The `Record` form of a read: `{}` for a store that answered with no row, a THROW for one that did not. */
9663
+ async function readDeviceOrThrow(store, deviceId) {
9664
+ const read = await store.read(deviceId);
9665
+ switch (read.kind) {
9666
+ case "present": return { ...read.value };
9667
+ case "absent": return {};
9668
+ case "unreadable": throw new Error(`device settings of ${String(deviceId)} could not be read: ${read.error}`);
9669
+ }
9670
+ }
9671
+ //#endregion
9672
+ //#region src/kernel/settings-door-view.ts
9673
+ /**
9674
+ * Every `system-settings` row under `prefix`, keyed by the suffix — or a throw.
9675
+ *
9676
+ * `system-settings` stores the value ITSELF (`setSystem` / `door.set` write
9677
+ * `JSON.stringify(value)`), mostly scalars. The engine's `query` hands a KV row
9678
+ * back parsed only when its text is an object or an array; a scalar comes back
9679
+ * as `{ value: <its JSON text> }` — `"12h"` arrives as `{ value: '"12h"' }`.
9680
+ * That is not a value anyone stored, and guessing it back would misread a
9681
+ * stored object that happens to be `{ value: … }`. So the range read only
9682
+ * ENUMERATES the keys (same keyset walk and refusal as every prefix read
9683
+ * here), and each value is taken from `get`, which parses the row exactly
9684
+ * (D675). These reads are small and rare: a section's handful of keys when a
9685
+ * form opens, and the capability preferences once at boot.
9686
+ */
9687
+ async function loadSystemSettingsPrefix(door, prefix, mayRead = () => true) {
9688
+ const listed = await loadPrefixed(door, "system-settings", prefix, () => null);
9689
+ const result = {};
9690
+ for (const suffix of Object.keys(listed).filter(mayRead)) {
9691
+ const value = await door.get({
9692
+ collection: "system-settings",
9693
+ key: `${prefix}${suffix}`
9694
+ });
9695
+ if (value !== void 0) result[suffix] = value;
9696
+ }
9697
+ return result;
9698
+ }
9699
+ //#endregion
9700
+ //#region src/kernel/sections-store.ts
9701
+ function createSectionsStore(door) {
9702
+ return {
9703
+ read: (section) => loadSystemSettingsPrefix(door, `${section}.`, (key) => storeMayAnswer(`${section}.${key}`)),
9704
+ write: async (section, patch) => {
9705
+ for (const [key, value] of Object.entries(patch)) await door.set({
9706
+ collection: "system-settings",
9707
+ key: `${section}.${key}`,
9708
+ value
9709
+ });
9710
+ }
9711
+ };
9712
+ }
9713
+ //#endregion
9714
+ //#region src/kernel/moleculer/runner-addon-settings.ts
9715
+ /** @param client the hub's `settingsStore` client; `undefined` makes every call THROW. */
9716
+ function createRunnerAddonSettings(client, addonId, before = async () => {}) {
9717
+ if (client === void 0) {
9718
+ const refuse = async () => {
9719
+ throw new Error(`settings of "${addonId}": settings store not wired`);
9720
+ };
9721
+ return {
9722
+ readAddonStore: refuse,
9723
+ readAddonStoreResult: async () => ({
9724
+ kind: "failed",
9725
+ error: "settings store not wired"
9726
+ }),
9727
+ readAddonStoreKeyResult: async () => ({
9728
+ kind: "unreadable",
9729
+ reason: "store-read-failed",
9730
+ error: "settings store not wired"
9731
+ }),
9732
+ writeAddonStore: refuse,
9733
+ getSection: refuse,
9734
+ setSection: refuse
9735
+ };
9736
+ }
9737
+ const door = {
9738
+ query: async (input) => {
9739
+ await before();
9740
+ return client.query.query(input);
9741
+ },
9742
+ get: async (input) => {
9743
+ await before();
9744
+ return client.get.query(input);
9745
+ },
9746
+ set: async (input) => {
9747
+ await before();
9748
+ return client.set.mutate(input);
9749
+ },
9750
+ transact: async (input) => {
9751
+ await before();
9752
+ return client.transact.mutate(input);
9753
+ }
9754
+ };
9755
+ const store = createAddonSettingsStore(door, addonId);
9756
+ const sections = createSectionsStore(door);
9757
+ return {
9758
+ readAddonStore: async () => {
9759
+ const read = await store.read();
9760
+ switch (read.kind) {
9761
+ case "present": return { ...read.value };
9762
+ case "absent": return {};
9763
+ case "unreadable": throw new Error(`settings of "${addonId}" could not be read: ${read.error}`);
9764
+ }
9765
+ },
9766
+ readAddonStoreResult: async () => {
9767
+ const read = await store.read();
9768
+ switch (read.kind) {
9769
+ case "present": return {
9770
+ kind: "value",
9771
+ value: { ...read.value }
9772
+ };
9773
+ case "absent": return { kind: "missing" };
9774
+ case "unreadable": return {
9775
+ kind: "failed",
9776
+ error: read.error
9777
+ };
9778
+ }
9779
+ },
9780
+ readAddonStoreKeyResult: async (key) => {
9781
+ const read = await store.readKey(key);
9782
+ return read.kind === "unreadable" ? {
9783
+ kind: "unreadable",
9784
+ reason: "store-read-failed",
9785
+ error: read.error
9786
+ } : read;
9787
+ },
9788
+ writeAddonStore: (patch) => store.merge(patch),
9789
+ getSection: (section) => sections.read(section),
9790
+ setSection: (section, patch) => sections.write(section, patch)
9791
+ };
9792
+ }
9793
+ //#endregion
9794
+ //#region src/builtins/sqlite-storage/retired-settings-keys.ts
9795
+ /**
9796
+ * The dead keys, per row.
9797
+ *
9798
+ * ## Addressing these rows
9799
+ *
9800
+ * Every namespace below is a FORKED addon, whose `ctx.settings` comes from
9801
+ * `addon-context-factory` and writes `<addonId>:addon-settings` row `root`. The
9802
+ * scoped table is therefore the right target — unlike the hub builtin whose
9803
+ * mis-addressed spec is dissected at {@link RetiredRowSpec}. Confirmed per
9804
+ * namespace against `packages/<pkg>/package.json`'s manifest `id`.
9805
+ *
9806
+ * ## `keys` vs `perNodeKeys` is a data-loss decision, not a style choice
9807
+ *
9808
+ * `perNodeKeys` matches `<key>` AND `<key>@<nodeId>`. That is right for a key
9809
+ * whose per-node copies are all dead (the engine cascade, the decoder
9810
+ * `backend`). It is WRONG for `probedBestHwaccel` / `probedBestAudioBackend`,
9811
+ * where the bare key is unreachable residue but every `@<nodeId>` copy is the
9812
+ * node's live probe result — those go under `keys`, which is exact-match.
9813
+ *
9814
+ * `detection-pipeline` / `addon-settings` / `root` — every key in this row is
9815
+ * dead; the row itself is kept because the addon writes a live key into it
9816
+ * (`videoPipelineSteps`). `pipelineTemplates` is no longer read or written
9817
+ * (templates were removed, D759) and is deliberately NOT listed here: stored
9818
+ * template rows are left exactly as they are.
9819
+ *
9820
+ * - `pipelineSteps` / `pipelineEngine`: the persisted GLOBAL step + engine
9821
+ * seed. Removed with `getGlobalSteps` becoming pure — dispatch builds steps
9822
+ * from the orchestrator's `agentSettings` plus per-camera overrides, and
9823
+ * never consulted the seed. See `addon-pipeline/src/detection-pipeline/provider.ts`.
9824
+ * - `engineRuntime` / `engineBackend` / `engineDevice` / `probedBestEngine`:
9825
+ * the per-node engine CASCADE, removed with the operator-facing engine
9826
+ * election. Each node now runs a device-array of inference pools balanced
9827
+ * per session, and the node-default pool derives live from the node's own
9828
+ * hardware (`resolveAutoEngine`). See
9829
+ * `addon-pipeline/src/detection-pipeline/engine-store-keys.ts`.
9830
+ */
9831
+ var RETIRED_SETTINGS_KEYS = [
9832
+ {
9833
+ namespace: "detection-pipeline",
9834
+ collection: "addon-settings",
9835
+ row: "root",
9836
+ keys: ["pipelineSteps", "pipelineEngine"],
9837
+ perNodeKeys: [
9838
+ "engineRuntime",
9839
+ "engineBackend",
9840
+ "engineDevice",
9841
+ "probedBestEngine"
9842
+ ],
9843
+ reason: "global step/engine seed and the per-node engine cascade — both removed; the live per-camera dispatch path never read either"
9844
+ },
9845
+ {
9846
+ namespace: "pipeline-orchestrator",
9847
+ collection: "addon-settings",
9848
+ row: "root",
9849
+ keys: [
9850
+ "enabledNodes",
9851
+ "enabledDecoderNodes",
9852
+ "enabledAudioNodes",
9853
+ "remoteSourcingNodes",
9854
+ "nativeLeaseTtlMs"
9855
+ ],
9856
+ perNodeKeys: ["backend"],
9857
+ reason: "the pre-singleton \"enabled nodes\" role model (node capability now lives per node in agent-settings and is derived by NodeTopology.refresh, never read from this row), `nativeLeaseTtlMs` superseded by `nativeLeaseActivityMs`, and the decoder `backend` selector whose resolver returns DEFAULT_DECODER_BACKEND unconditionally (decoder-backend.ts)"
9858
+ },
9859
+ {
9860
+ namespace: "stream-broker",
9861
+ collection: "addon-settings",
9862
+ row: "root",
9863
+ keys: [
9864
+ "maxDecodeFps",
9865
+ "initialReconnectDelayMs",
9866
+ "maxReconnectDelayMs"
9867
+ ],
9868
+ perNodeKeys: [],
9869
+ reason: "three form knobs deleted 2026-08-28 (D272): nothing capped decode fps, and the reconnect ladder has always run on the INITIAL/MAX_RECONNECT_DELAY_MS constants"
9870
+ },
9871
+ {
9872
+ namespace: "embedding-encoder",
9873
+ collection: "addon-settings",
9874
+ row: "root",
9875
+ keys: ["runtime", "backend"],
9876
+ perNodeKeys: [],
9877
+ reason: "two selects deleted 2026-08-28 (D272); their only reference was a withTags log field, and the CUDA/CoreML options named acceleration that has never existed — embedding inference is always ONNX in the embedded Python"
9878
+ },
9879
+ {
9880
+ namespace: "audio-analyzer",
9881
+ collection: "addon-settings",
9882
+ row: "root",
9883
+ keys: ["selectedAudioModel", "probedBestAudioBackend"],
9884
+ perNodeKeys: [],
9885
+ reason: "`selectedAudioModel` deleted 2026-08-28 (D272) — createAudioPipeline takes a backend and no model id, so it never reached inference; the BARE `probedBestAudioBackend` is pre-perNode residue that projectStore already drops (no bare fallback), so no node can ever read it"
9886
+ },
9887
+ {
9888
+ namespace: "decoder-nodeav",
9889
+ collection: "addon-settings",
9890
+ row: "root",
9891
+ keys: ["probedBestHwaccel"],
9892
+ perNodeKeys: [],
9893
+ reason: "bare pre-perNode residue — the live probe result is `probedBestHwaccel@<nodeId>` and `projectStore` deliberately drops the bare key, so nothing can read it"
9894
+ },
9895
+ {
9896
+ namespace: "pipeline-analytics",
9897
+ collection: "addon-settings",
9898
+ row: "root",
9899
+ keys: ["mediaAttachPolicy"],
9900
+ perNodeKeys: [],
9901
+ reason: "deleted 2026-08-28 (D272) — absent from MediaSettingsSchema, so the value was never even resolved, let alone read"
9902
+ }
9903
+ ];
9904
+ //#endregion
9905
+ //#region src/kernel/legacy-blob-guard.ts
9906
+ /** Root keys retired long before D870: they never count as "settings still to migrate". */
9907
+ var OWN_RETIRED_ROOT_KEYS = new Set([
9908
+ "recordingConfigs",
9909
+ "recordingExports",
9910
+ "recordingOpsLog",
9911
+ "probeSnapshots",
9912
+ "rtspTokens",
9913
+ "profileMap"
9914
+ ]);
9915
+ /** Is `key` of `addonId` a root key that was retired (and must not be migrated)? */
9916
+ function isRetiredRootKey(addonId, key) {
9917
+ if (OWN_RETIRED_ROOT_KEYS.has(key)) return true;
9918
+ return RETIRED_SETTINGS_KEYS.some((spec) => spec.namespace === addonId && (spec.keys.includes(key) || spec.perNodeKeys.some((k) => key === k || key.startsWith(`${k}@`))));
9919
+ }
9920
+ var UnmigratedSettingsError = class extends Error {
9921
+ constructor(addonId, what) {
9922
+ super(`addon "${addonId}" refuses to start: ${what} - the settings it had are still in the per-addon tables and the standard store holds none. Run scripts/hub/migrate-device-store.ts --copy --apply (D870) before starting it.`);
9923
+ this.name = "UnmigratedSettingsError";
9924
+ }
9925
+ };
9926
+ async function countOf(client, input) {
9927
+ const n = await client.count.query(input);
9928
+ if (typeof n !== "number") throw new Error("settings store answered a non-number to a count");
9929
+ return n;
9930
+ }
9931
+ var LEGACY_PAGE = 500;
9932
+ var LEGACY_PAGE_BUDGET = 200;
9933
+ var ID_TAIL = "";
9934
+ function isRecord$2(value) {
9935
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9936
+ }
9937
+ /**
9938
+ * Does the addon's per-device blob table hold a row the copy would actually
9939
+ * migrate - a positive numeric id with a non-empty record? An addon whose rows
9940
+ * the copy skips anyway must not be refused for them.
9941
+ */
9942
+ async function hasMigratableDeviceRow(client, addonId) {
9943
+ let cursor = "";
9944
+ for (let page = 0; page < LEGACY_PAGE_BUDGET; page++) {
9945
+ const rows = await client.query.query({
9946
+ namespace: addonId,
9947
+ collection: "addon-devices",
9948
+ filter: {
9949
+ whereBetween: { id: [cursor, ID_TAIL] },
9950
+ orderBy: {
9951
+ field: "id",
9952
+ direction: "asc"
9953
+ },
9954
+ limit: LEGACY_PAGE
9955
+ }
9956
+ });
9957
+ if (!Array.isArray(rows)) throw new Error("settings store answered a non-list to a query");
9958
+ let lastId = cursor;
9959
+ for (const row of rows) {
9960
+ if (!isRecord$2(row) || typeof row["id"] !== "string") continue;
9961
+ lastId = row["id"];
9962
+ const id = Number(row["id"]);
9963
+ const data = row["data"];
9964
+ if (Number.isInteger(id) && id > 0 && isRecord$2(data) && Object.keys(data).length > 0) return true;
9965
+ }
9966
+ if (rows.length < LEGACY_PAGE || lastId === cursor) return false;
9967
+ cursor = lastId;
9968
+ }
9969
+ throw new Error(`addon "${addonId}": more legacy blob pages than the guard will read`);
9970
+ }
9971
+ /**
9972
+ * THROWS when the addon has blob settings and the standard store has none of
9973
+ * its own. A store that cannot be asked also throws: it is not "no blobs".
9974
+ */
9975
+ async function assertLegacyBlobsMigrated(client, addonId, logger) {
9976
+ const legacyDevices = await hasMigratableDeviceRow(client, addonId);
9977
+ const root = await client.get.query({
9978
+ namespace: addonId,
9979
+ collection: "addon-settings",
9980
+ key: "root"
9981
+ });
9982
+ const legacySettings = isRecord$2(root) && Object.keys(root).some((key) => !isRetiredRootKey(addonId, key));
9983
+ if (!legacyDevices && !legacySettings) return;
9984
+ const rangeOf = (prefix) => {
9985
+ const range = prefixRange(prefix);
9986
+ if (range === null) throw new Error(`no id range for addon "${addonId}"`);
9987
+ return { whereBetween: { id: [range.lo, range.hi] } };
9988
+ };
9989
+ const newDevices = await countOf(client, {
9990
+ collection: "addon-device-settings",
9991
+ filter: rangeOf(`${addonId}:`)
9992
+ });
9993
+ const newSettings = await countOf(client, {
9994
+ collection: "addon-settings",
9995
+ filter: rangeOf(`${addonId}.`)
9996
+ });
9997
+ const problems = [];
9998
+ if (legacyDevices && newDevices === 0) problems.push("per-device blobs");
9999
+ if (legacySettings && newSettings === 0) problems.push("an addon settings blob");
10000
+ if (problems.length === 0) return;
10001
+ const error = new UnmigratedSettingsError(addonId, problems.join(" and "));
10002
+ logger.error(error.message, { meta: {
10003
+ addonId,
10004
+ newDevices,
10005
+ newSettings
10006
+ } });
10007
+ throw error;
10008
+ }
10009
+ //#endregion
10010
+ //#region src/kernel/moleculer/runner-device-settings.ts
10011
+ /**
10012
+ * The forked runner's `ctx.settings` device methods (D870).
10013
+ *
10014
+ * A runner does not hold the settings door; it holds a tRPC client of the hub's
10015
+ * `settings-store` cap. This file adapts that client to the door slice the
10016
+ * per-device store needs and hands back the five view methods — the SAME module
10017
+ * hub-main's view uses, so a device's settings are one set of rows whichever
10018
+ * process asks. Before D870 the runner kept its own `<addonId>:addon-devices`
10019
+ * blob per device; a hub-side write to the rows was invisible to it.
10020
+ */
10021
+ /** The door slice the per-device store needs, over a tRPC client. */
10022
+ function doorOverClient(client, before = async () => {}) {
10023
+ return {
10024
+ query: async (input) => {
10025
+ await before();
10026
+ return client.query.query(input);
10027
+ },
10028
+ transact: async (input) => {
10029
+ await before();
10030
+ return client.transact.mutate(input);
10031
+ },
10032
+ deleteWhere: async (input) => {
10033
+ await before();
10034
+ return client.deleteWhere.mutate(input);
10035
+ }
10036
+ };
10037
+ }
10038
+ /**
10039
+ * @param client the hub's `settingsStore` client, or `undefined` when the
10040
+ * runner has none — every call then THROWS: a store that could not be asked
10041
+ * is not an empty one.
10042
+ */
10043
+ function createRunnerDeviceSettings(client, addonId, before = async () => {}) {
10044
+ if (client === void 0) {
10045
+ const refuse = () => {
10046
+ throw new Error(`device settings of "${addonId}": settings store not wired`);
10047
+ };
10048
+ return {
10049
+ readDeviceStore: async () => refuse(),
10050
+ readDeviceStoreBatch: async () => refuse(),
10051
+ listDeviceStoreIds: async () => refuse(),
10052
+ writeDeviceStore: async () => refuse(),
10053
+ clearDeviceStore: async () => refuse()
10054
+ };
10055
+ }
10056
+ const store = createDeviceSettingsStore(doorOverClient(client, before), addonId);
10057
+ return {
10058
+ readDeviceStore: (deviceId) => readDeviceOrThrow(store, deviceId),
10059
+ readDeviceStoreBatch: (ids) => store.readBatch(ids),
10060
+ listDeviceStoreIds: () => store.listIds(),
10061
+ writeDeviceStore: (deviceId, patch) => store.merge(deviceId, patch),
10062
+ clearDeviceStore: (deviceId) => store.clear(deviceId)
10063
+ };
10064
+ }
10065
+ //#endregion
9366
10066
  //#region src/kernel/moleculer/addon-context-factory.ts
9367
10067
  /** This runtime's node id, regardless of mode (broker.nodeID or the UDS child nodeId). */
9368
10068
  function runtimeNodeId(runtime) {
@@ -9607,20 +10307,6 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
9607
10307
  }
9608
10308
  return r.kind === "value" ? r.value : {};
9609
10309
  };
9610
- /** Read-modify-write that REFUSES to write when the read failed. */
9611
- const mergeBlob = async (coll, key, patch, namespace) => {
9612
- await readModifyWrite({
9613
- read: () => readBlobResult(coll, key, namespace),
9614
- write: (next) => writeBlob(coll, key, next, namespace),
9615
- patch,
9616
- onRefused: (error) => {
9617
- scopedLogger.error(`settings merge REFUSED (${coll}/${key}) — read failed, refusing to overwrite with a partial blob`, { meta: {
9618
- error,
9619
- patchKeys: Object.keys(patch)
9620
- } });
9621
- }
9622
- });
9623
- };
9624
10310
  const writeBlob = async (coll, key, value, namespace) => {
9625
10311
  if (!settingsApi) return;
9626
10312
  try {
@@ -9643,38 +10329,24 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
9643
10329
  } });
9644
10330
  }
9645
10331
  };
9646
- const rmwChains = /* @__PURE__ */ new Map();
9647
- const serializeRmw = (coll, key, op) => {
9648
- const chainKey = `${coll}/${key}`;
9649
- const next = (rmwChains.get(chainKey) ?? Promise.resolve()).catch(() => void 0).then(op);
9650
- rmwChains.set(chainKey, next);
9651
- next.finally(() => {
9652
- if (rmwChains.get(chainKey) === next) rmwChains.delete(chainKey);
9653
- }).catch(() => void 0);
9654
- return next;
10332
+ const deviceSettingsClient = Reflect.get(api, "settingsStore");
10333
+ let blobsChecked = null;
10334
+ const bootGuard = () => {
10335
+ const client = deviceSettingsClient;
10336
+ if (client === void 0) return Promise.resolve();
10337
+ blobsChecked ??= assertLegacyBlobsMigrated({
10338
+ get: { query: (input) => client.get.query(input) },
10339
+ query: { query: (input) => client.query.query(input) },
10340
+ count: { query: (input) => client.count.query(input) }
10341
+ }, addonId, scopedLogger).catch((err) => {
10342
+ blobsChecked = null;
10343
+ throw err;
10344
+ });
10345
+ return blobsChecked;
9655
10346
  };
9656
10347
  const settingsView = {
9657
- async readAddonStore() {
9658
- return readBlob("addon-settings", "root", addonId);
9659
- },
9660
- readAddonStoreResult: () => readBlobResult("addon-settings", "root", addonId),
9661
- async writeAddonStore(patch) {
9662
- return serializeRmw("addon-settings", "root", async () => {
9663
- await mergeBlob("addon-settings", "root", patch, addonId);
9664
- });
9665
- },
9666
- async readDeviceStore(deviceId) {
9667
- return readBlob("addon-devices", String(deviceId), addonId);
9668
- },
9669
- async writeDeviceStore(deviceId, patch) {
9670
- const key = String(deviceId);
9671
- return serializeRmw("addon-devices", key, async () => {
9672
- await mergeBlob("addon-devices", key, patch, addonId);
9673
- });
9674
- },
9675
- async clearDeviceStore(deviceId) {
9676
- await writeBlob("addon-devices", String(deviceId), {}, addonId);
9677
- },
10348
+ ...createRunnerAddonSettings(deviceSettingsClient, addonId, bootGuard),
10349
+ ...createRunnerDeviceSettings(deviceSettingsClient, addonId, bootGuard),
9678
10350
  async readDeviceRuntimeState(deviceId) {
9679
10351
  return readBlob("device-runtime-state", String(deviceId));
9680
10352
  },
@@ -9683,14 +10355,6 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
9683
10355
  },
9684
10356
  async clearDeviceRuntimeState(deviceId) {
9685
10357
  await writeBlob("device-runtime-state", String(deviceId), {});
9686
- },
9687
- async getSection(section) {
9688
- return readBlob("sections", section);
9689
- },
9690
- async setSection(section, patch) {
9691
- return serializeRmw("sections", section, async () => {
9692
- await mergeBlob("sections", section, patch);
9693
- });
9694
10358
  }
9695
10359
  };
9696
10360
  const readinessRegistry = () => runtime.mode === "broker" ? getOrInitReadinessRegistry(runtime.broker, scopedEventBus, scopedLogger) : getOrInitReadinessRegistryForClient(runtime.client, scopedEventBus, scopedLogger, nodeId);