@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.
- package/dist/addon-runner.js +743 -79
- package/dist/addon-runner.mjs +743 -79
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- package/dist/{agent-https-BZIwS4xF.js → agent-https-DlpTMDqA.js} +252 -22
- package/dist/{agent-https-BPfHrI4c.mjs → agent-https-U0d4uvn2.mjs} +211 -23
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/block-settings-store.d.ts +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +77 -11
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +77 -12
- package/dist/builtins/core-blocks/index.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +197 -60
- package/dist/builtins/device-manager/device-manager.addon.mjs +196 -61
- package/dist/builtins/device-manager/device-removal-hooks.d.ts +0 -11
- package/dist/builtins/device-manager/device-tombstone-store.d.ts +12 -2
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +29 -11
- package/dist/builtins/keychain/index.mjs +29 -11
- package/dist/builtins/keychain/keychain-service.d.ts +8 -0
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +2 -2
- package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
- package/dist/builtins/local-network/local-network.addon.js +7 -2
- package/dist/builtins/local-network/local-network.addon.mjs +7 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +38 -7
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +69 -3
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +69 -3
- package/dist/builtins/sqlite-storage/storage-admin-provider.d.ts +31 -0
- package/dist/builtins/storage-orchestrator/hub-settings-forward.d.ts +3 -3
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +4 -4
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +4 -4
- package/dist/builtins/system-config/system-config.addon.js +4 -4
- package/dist/builtins/system-config/system-config.addon.mjs +285 -285
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-DpHGCjdL.mjs → composition-sources-CS0D7imL.mjs} +1 -1
- package/dist/{composition-sources-DI3Z83vG.js → composition-sources-cBG8fBTu.js} +1 -1
- package/dist/{constants-DQK705Sb.mjs → constants-CIzPjw7L.mjs} +1 -1
- package/dist/{constants-wkcn5Ij3.js → constants-CRI3QzT4.js} +1 -1
- package/dist/{device-ca-DvIZjXIl.mjs → device-ca-DLKXYseS.mjs} +2 -2
- package/dist/{device-ca-BSvkXqyI.js → device-ca-DNHjts4_.js} +2 -2
- package/dist/{dist-Dc8GKC5H.mjs → dist-9fcsrFU3.mjs} +271 -32
- package/dist/{dist-GgG-7amw.js → dist-BKhlcYz6.js} +288 -31
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-CBLvq2oj.js → hub-mdns-ciao-3Zc9G9MS.js} +377 -69
- package/dist/{hub-mdns-ciao-DMlQTdPU.mjs → hub-mdns-ciao-Dy2YjRqC.mjs} +344 -72
- package/dist/index.js +372 -339
- package/dist/index.mjs +361 -341
- package/dist/kernel/addon-settings-store.d.ts +43 -0
- package/dist/kernel/config-manager.d.ts +0 -19
- package/dist/kernel/device-settings-store.d.ts +74 -0
- package/dist/kernel/legacy-blob-guard.d.ts +58 -0
- package/dist/kernel/moleculer/runner-addon-settings.d.ts +34 -0
- package/dist/kernel/moleculer/runner-device-settings.d.ts +39 -0
- package/dist/kernel/prefix-walk.d.ts +39 -0
- package/dist/kernel/sections-store.d.ts +20 -0
- package/dist/kernel/settings-door-view.d.ts +2 -2
- package/dist/kernel/settings-view-types.d.ts +5 -30
- package/dist/kernel/store-settings-view.d.ts +0 -3
- package/dist/{lib-NNxHSOIE.js → lib-8oZrELLm.js} +1 -1
- package/dist/{lib-BeGzVQcd.mjs → lib-DK38ompL.mjs} +1 -1
- package/dist/prefix-range-D_KueXbQ.js +27 -0
- package/dist/prefix-range-Dl0znowN.mjs +22 -0
- package/dist/prefix-walk-DAOBSyH1.mjs +88 -0
- package/dist/prefix-walk-Dv6MLZlM.js +99 -0
- package/dist/{restore-archive-CTRz8v9o.js → restore-archive-49aZwkp8.js} +1 -1
- package/dist/{restore-archive-DPg9duVY.mjs → restore-archive-DgvDecXM.mjs} +82 -82
- package/dist/{retired-settings-keys-Is0zO8gX.mjs → retired-settings-keys-Bhwe4ymb.mjs} +2 -2
- package/dist/{retired-settings-keys-Dr_4lfKu.js → retired-settings-keys-D_VwZiDt.js} +10 -4
- package/dist/tls/agent-https.d.ts +2 -2
- package/dist/tls/boot-tls.d.ts +6 -0
- package/dist/tls/cert-evaluation.d.ts +1 -1
- package/dist/tls/cert-issue.d.ts +5 -1
- package/dist/tls/cert-manager.d.ts +18 -0
- package/dist/tls/cert-name-constraints.d.ts +53 -0
- package/dist/tls/index.d.ts +5 -4
- package/dist/tls/tls-actions.d.ts +27 -7
- package/dist/tls/tls-status.d.ts +22 -0
- package/dist/tls/uploaded-cert.d.ts +36 -4
- package/dist/{wal-maintenance-Bx3lcVA7.mjs → wal-maintenance-BZGmF-3U.mjs} +0 -0
- package/dist/{wal-maintenance-mjB2i-VR.js → wal-maintenance-C041zn6V.js} +0 -0
- package/dist/{x509.es-r7Ht1thA.js → x509.es-C4cOo48M.js} +36 -0
- package/dist/{x509.es-CDvj67p1.mjs → x509.es-DK__ETIe.mjs} +1 -1
- package/package.json +7 -2
- package/dist/{custom-action-registry-Bf_YnagS.js → custom-action-registry-BKmoaqG0.js} +1 -1
- package/dist/{custom-action-registry-BQnbN0DP.mjs → custom-action-registry-CyUuhATc.mjs} +1 -1
package/dist/addon-runner.mjs
CHANGED
|
@@ -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$
|
|
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$
|
|
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$
|
|
53
|
+
if (isRecord$5(candidate) && "default" in candidate) candidate = candidate["default"];
|
|
54
54
|
if (isAddonConstructor(candidate)) return candidate;
|
|
55
|
-
if (isRecord$
|
|
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$
|
|
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$
|
|
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
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
const
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
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
|
-
|
|
9634
|
-
|
|
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);
|