@camstack/system 1.2.438 → 1.2.440

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