@camstack/system 1.2.24 → 1.2.25

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.
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-BbOZZbVt.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-COgal7Jy.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
4
4
  let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Ct as resolveAddonClass, I as createUdsLoggerWithControl, L as LocalChildClient, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps } from "./manifest-python-deps-C9qhvMUi.mjs";
1
+ import { Ct as resolveAddonClass, I as createUdsLoggerWithControl, L as LocalChildClient, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps } from "./manifest-python-deps-julrfvyf.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as fs from "node:fs";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
3
  const require_dist = require("./dist-CS0pdC8l.js");
4
4
  const require_model_download_service = require("./model-download-service-hf0ookyy.js");
5
- const require_manifest_python_deps = require("./manifest-python-deps-BbOZZbVt.js");
5
+ const require_manifest_python_deps = require("./manifest-python-deps-COgal7Jy.js");
6
6
  const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
7
7
  const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
8
8
  const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
2
2
  import { B as logLevelAtMost, Dt as readinessKey, Et as parseJsonUnknown$1, F as isVoidInput, I as kebabToCamel, L as lifecycleJobSchema, M as isArrayOutputSchema, N as isCollectionArrayMethod, Ot as resolveCapMount, P as isObjectInput, Tt as parseJsonObject, V as looseSchema, W as objectInputDeclaresAddonId, _t as asString$1, c as RUNTIME_DEFAULTS, ct as DEVICE_STATUS_METHOD, ft as ReadinessRegistry, gt as asNumber, ht as asJsonObject$1, it as errMsg$1, jt as EventCategory$1, k as extractNestedAddonId, kt as scopeKey, m as addonSettingsCapability, ot as DATAPLANE_SECRET_HEADER$1, pt as ReadinessTimeoutError, q as procedureAuthKey, s as METHOD_ACCESS_MAP, st as DEVICE_SETTINGS_CONTRIBUTION_METHODS, t as ALL_CAPABILITY_DEFINITIONS, vt as createEvent, xt as expandCapMethods, yt as emitDownForOwnedCaps } from "./dist-CIek6F9X.mjs";
3
3
  import { a as downloadModel, c as getModelFilePath, d as contentTypeFor, f as createAuthenticatedFileServer, h as resolveFilePath, i as downloadFile, l as isModelDownloaded, m as parseTokenizedUrl, n as collectModelFiles, o as ensureModel, p as parseRangeHeader, r as deleteModelFromDisk, s as fetchJson, t as ModelDownloadService, u as createFileDataPlaneHandler } from "./model-download-service-Cp9f4dk6.mjs";
4
- import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-C9qhvMUi.mjs";
4
+ import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-julrfvyf.mjs";
5
5
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BkP504Vq.mjs";
6
6
  import { FilesystemStorageAddon, t as FilesystemStorageProvider } from "./builtins/sqlite-storage/filesystem-storage.addon.mjs";
7
7
  import { SqliteSettingsAddon, t as SqliteSettingsBackend } from "./builtins/sqlite-storage/sqlite-settings.addon.mjs";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Read-modify-write guard for the addon settings stores.
3
+ *
4
+ * ## The data loss this prevents
5
+ *
6
+ * Every settings mutation is `read current → merge patch → write back`. The
7
+ * read used to answer `{}` for BOTH "this addon has no settings yet" and "the
8
+ * read failed", so a failed read merged the patch onto nothing and the write
9
+ * persisted a blob containing ONLY the patch. Everything else in it was gone.
10
+ *
11
+ * It happened on 2026-07-31. The UDS transport stalled, `settings write failed
12
+ * (addon-settings/root) — transport error ... UDS request timed out after
13
+ * 60000ms` appeared in the log, and the recording configuration for all three
14
+ * recording cameras came back as `{"enabled": false, "bands": []}`. Two and a
15
+ * half hours of footage were never written, and nothing looked broken: an
16
+ * emptied config is indistinguishable from an operator switching recording off.
17
+ *
18
+ * The rule this module enforces: **a read that FAILED is not an empty value.**
19
+ * A miss still merges onto `{}` — that is a real empty. A failure refuses the
20
+ * write and says so, leaving the stored blob untouched so the next successful
21
+ * write starts from the truth.
22
+ */
23
+ /**
24
+ * Outcome of a settings read. `missing` and `failed` are deliberately distinct
25
+ * — collapsing them is the bug.
26
+ */
27
+ export type SettingsRead = {
28
+ readonly kind: 'value';
29
+ readonly value: Record<string, unknown>;
30
+ } | {
31
+ readonly kind: 'missing';
32
+ } | {
33
+ readonly kind: 'failed';
34
+ readonly error: string;
35
+ };
36
+ export interface ReadModifyWriteInput {
37
+ readonly read: () => Promise<SettingsRead>;
38
+ readonly write: (next: Record<string, unknown>) => Promise<void>;
39
+ readonly patch: Record<string, unknown>;
40
+ /** Called instead of writing when the read failed — always log this. */
41
+ readonly onRefused: (error: string) => void;
42
+ }
43
+ /**
44
+ * Merge `patch` over the current value and write it — unless the read failed,
45
+ * in which case write NOTHING.
46
+ *
47
+ * Returns whether the write was attempted, so a caller can surface the refusal.
48
+ */
49
+ export declare function readModifyWrite(input: ReadModifyWriteInput): Promise<boolean>;
@@ -1188,6 +1188,27 @@ function addSettingsActions(addon, actions) {
1188
1188
  }
1189
1189
  }
1190
1190
  //#endregion
1191
+ //#region src/kernel/moleculer/settings-read-guard.ts
1192
+ /**
1193
+ * Merge `patch` over the current value and write it — unless the read failed,
1194
+ * in which case write NOTHING.
1195
+ *
1196
+ * Returns whether the write was attempted, so a caller can surface the refusal.
1197
+ */
1198
+ async function readModifyWrite(input) {
1199
+ const current = await input.read();
1200
+ if (current.kind === "failed") {
1201
+ input.onRefused(current.error);
1202
+ return false;
1203
+ }
1204
+ const base = current.kind === "value" ? current.value : {};
1205
+ await input.write({
1206
+ ...base,
1207
+ ...input.patch
1208
+ });
1209
+ return true;
1210
+ }
1211
+ //#endregion
1191
1212
  //#region src/kernel/moleculer/device-cap-proxy.ts
1192
1213
  /**
1193
1214
  * Max number of accessory children spawned concurrently during a single
@@ -7171,8 +7192,18 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7171
7192
  bindingCache.clear();
7172
7193
  });
7173
7194
  const settingsApi = Reflect.get(api, "settingsStore");
7174
- const readBlob = async (coll, key, namespace) => {
7175
- if (!settingsApi) return {};
7195
+ /**
7196
+ * Read a settings blob, distinguishing a MISS from a FAILURE.
7197
+ *
7198
+ * Collapsing the two is a data-loss bug, not a style choice: every mutation
7199
+ * is read-modify-write, so a failed read that answers "empty" makes the write
7200
+ * persist the patch ALONE and destroy every field the patch does not mention.
7201
+ * That is what emptied the recording configuration on 2026-07-31 when the UDS
7202
+ * transport stalled — `{"enabled": false, "bands": []}`, two and a half hours
7203
+ * of footage never written, and nothing that looked like a fault.
7204
+ */
7205
+ const readBlobResult = async (coll, key, namespace) => {
7206
+ if (!settingsApi) return { kind: "missing" };
7176
7207
  try {
7177
7208
  const v = await settingsApi.get.query(namespace !== void 0 ? {
7178
7209
  namespace,
@@ -7182,11 +7213,40 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7182
7213
  collection: coll,
7183
7214
  key
7184
7215
  });
7185
- return v && typeof v === "object" ? v : {};
7186
- } catch {
7187
- return {};
7216
+ return v && typeof v === "object" ? {
7217
+ kind: "value",
7218
+ value: v
7219
+ } : { kind: "missing" };
7220
+ } catch (err) {
7221
+ return {
7222
+ kind: "failed",
7223
+ error: (0, _camstack_types_addon.errMsg)(err)
7224
+ };
7188
7225
  }
7189
7226
  };
7227
+ /**
7228
+ * Soft read for CONSUMERS: a failure reads as empty so a boot path degrades
7229
+ * to defaults instead of throwing. Safe here precisely because the result is
7230
+ * never written back — {@link readBlobResult} is what the write paths use.
7231
+ */
7232
+ const readBlob = async (coll, key, namespace) => {
7233
+ const r = await readBlobResult(coll, key, namespace);
7234
+ return r.kind === "value" ? r.value : {};
7235
+ };
7236
+ /** Read-modify-write that REFUSES to write when the read failed. */
7237
+ const mergeBlob = async (coll, key, patch, namespace) => {
7238
+ await readModifyWrite({
7239
+ read: () => readBlobResult(coll, key, namespace),
7240
+ write: (next) => writeBlob(coll, key, next, namespace),
7241
+ patch,
7242
+ onRefused: (error) => {
7243
+ scopedLogger.error(`settings merge REFUSED (${coll}/${key}) — read failed, refusing to overwrite with a partial blob`, { meta: {
7244
+ error,
7245
+ patchKeys: Object.keys(patch)
7246
+ } });
7247
+ }
7248
+ });
7249
+ };
7190
7250
  const writeBlob = async (coll, key, value, namespace) => {
7191
7251
  if (!settingsApi) return;
7192
7252
  try {
@@ -7225,10 +7285,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7225
7285
  },
7226
7286
  async writeAddonStore(patch) {
7227
7287
  return serializeRmw("addon-settings", "root", async () => {
7228
- await writeBlob("addon-settings", "root", {
7229
- ...await readBlob("addon-settings", "root", addonId),
7230
- ...patch
7231
- }, addonId);
7288
+ await mergeBlob("addon-settings", "root", patch, addonId);
7232
7289
  });
7233
7290
  },
7234
7291
  async readDeviceStore(deviceId) {
@@ -7237,10 +7294,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7237
7294
  async writeDeviceStore(deviceId, patch) {
7238
7295
  const key = String(deviceId);
7239
7296
  return serializeRmw("addon-devices", key, async () => {
7240
- await writeBlob("addon-devices", key, {
7241
- ...await readBlob("addon-devices", key, addonId),
7242
- ...patch
7243
- }, addonId);
7297
+ await mergeBlob("addon-devices", key, patch, addonId);
7244
7298
  });
7245
7299
  },
7246
7300
  async clearDeviceStore(deviceId) {
@@ -7260,10 +7314,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7260
7314
  },
7261
7315
  async setSection(section, patch) {
7262
7316
  return serializeRmw("sections", section, async () => {
7263
- await writeBlob("sections", section, {
7264
- ...await readBlob("sections", section),
7265
- ...patch
7266
- });
7317
+ await mergeBlob("sections", section, patch);
7267
7318
  });
7268
7319
  }
7269
7320
  };
@@ -1186,6 +1186,27 @@ function addSettingsActions(addon, actions) {
1186
1186
  }
1187
1187
  }
1188
1188
  //#endregion
1189
+ //#region src/kernel/moleculer/settings-read-guard.ts
1190
+ /**
1191
+ * Merge `patch` over the current value and write it — unless the read failed,
1192
+ * in which case write NOTHING.
1193
+ *
1194
+ * Returns whether the write was attempted, so a caller can surface the refusal.
1195
+ */
1196
+ async function readModifyWrite(input) {
1197
+ const current = await input.read();
1198
+ if (current.kind === "failed") {
1199
+ input.onRefused(current.error);
1200
+ return false;
1201
+ }
1202
+ const base = current.kind === "value" ? current.value : {};
1203
+ await input.write({
1204
+ ...base,
1205
+ ...input.patch
1206
+ });
1207
+ return true;
1208
+ }
1209
+ //#endregion
1189
1210
  //#region src/kernel/moleculer/device-cap-proxy.ts
1190
1211
  /**
1191
1212
  * Max number of accessory children spawned concurrently during a single
@@ -7169,8 +7190,18 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7169
7190
  bindingCache.clear();
7170
7191
  });
7171
7192
  const settingsApi = Reflect.get(api, "settingsStore");
7172
- const readBlob = async (coll, key, namespace) => {
7173
- if (!settingsApi) return {};
7193
+ /**
7194
+ * Read a settings blob, distinguishing a MISS from a FAILURE.
7195
+ *
7196
+ * Collapsing the two is a data-loss bug, not a style choice: every mutation
7197
+ * is read-modify-write, so a failed read that answers "empty" makes the write
7198
+ * persist the patch ALONE and destroy every field the patch does not mention.
7199
+ * That is what emptied the recording configuration on 2026-07-31 when the UDS
7200
+ * transport stalled — `{"enabled": false, "bands": []}`, two and a half hours
7201
+ * of footage never written, and nothing that looked like a fault.
7202
+ */
7203
+ const readBlobResult = async (coll, key, namespace) => {
7204
+ if (!settingsApi) return { kind: "missing" };
7174
7205
  try {
7175
7206
  const v = await settingsApi.get.query(namespace !== void 0 ? {
7176
7207
  namespace,
@@ -7180,11 +7211,40 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7180
7211
  collection: coll,
7181
7212
  key
7182
7213
  });
7183
- return v && typeof v === "object" ? v : {};
7184
- } catch {
7185
- return {};
7214
+ return v && typeof v === "object" ? {
7215
+ kind: "value",
7216
+ value: v
7217
+ } : { kind: "missing" };
7218
+ } catch (err) {
7219
+ return {
7220
+ kind: "failed",
7221
+ error: errMsg(err)
7222
+ };
7186
7223
  }
7187
7224
  };
7225
+ /**
7226
+ * Soft read for CONSUMERS: a failure reads as empty so a boot path degrades
7227
+ * to defaults instead of throwing. Safe here precisely because the result is
7228
+ * never written back — {@link readBlobResult} is what the write paths use.
7229
+ */
7230
+ const readBlob = async (coll, key, namespace) => {
7231
+ const r = await readBlobResult(coll, key, namespace);
7232
+ return r.kind === "value" ? r.value : {};
7233
+ };
7234
+ /** Read-modify-write that REFUSES to write when the read failed. */
7235
+ const mergeBlob = async (coll, key, patch, namespace) => {
7236
+ await readModifyWrite({
7237
+ read: () => readBlobResult(coll, key, namespace),
7238
+ write: (next) => writeBlob(coll, key, next, namespace),
7239
+ patch,
7240
+ onRefused: (error) => {
7241
+ scopedLogger.error(`settings merge REFUSED (${coll}/${key}) — read failed, refusing to overwrite with a partial blob`, { meta: {
7242
+ error,
7243
+ patchKeys: Object.keys(patch)
7244
+ } });
7245
+ }
7246
+ });
7247
+ };
7188
7248
  const writeBlob = async (coll, key, value, namespace) => {
7189
7249
  if (!settingsApi) return;
7190
7250
  try {
@@ -7223,10 +7283,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7223
7283
  },
7224
7284
  async writeAddonStore(patch) {
7225
7285
  return serializeRmw("addon-settings", "root", async () => {
7226
- await writeBlob("addon-settings", "root", {
7227
- ...await readBlob("addon-settings", "root", addonId),
7228
- ...patch
7229
- }, addonId);
7286
+ await mergeBlob("addon-settings", "root", patch, addonId);
7230
7287
  });
7231
7288
  },
7232
7289
  async readDeviceStore(deviceId) {
@@ -7235,10 +7292,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7235
7292
  async writeDeviceStore(deviceId, patch) {
7236
7293
  const key = String(deviceId);
7237
7294
  return serializeRmw("addon-devices", key, async () => {
7238
- await writeBlob("addon-devices", key, {
7239
- ...await readBlob("addon-devices", key, addonId),
7240
- ...patch
7241
- }, addonId);
7295
+ await mergeBlob("addon-devices", key, patch, addonId);
7242
7296
  });
7243
7297
  },
7244
7298
  async clearDeviceStore(deviceId) {
@@ -7258,10 +7312,7 @@ async function buildAddonContext(runtime, declaration, dataDir, options) {
7258
7312
  },
7259
7313
  async setSection(section, patch) {
7260
7314
  return serializeRmw("sections", section, async () => {
7261
- await writeBlob("sections", section, {
7262
- ...await readBlob("sections", section),
7263
- ...patch
7264
- });
7315
+ await mergeBlob("sections", section, patch);
7265
7316
  });
7266
7317
  }
7267
7318
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.24",
3
+ "version": "1.2.25",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",