@camstack/system 1.2.470 → 1.2.472

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 (93) hide show
  1. package/dist/addon-runner.js +248 -19
  2. package/dist/addon-runner.mjs +249 -20
  3. package/dist/addon-utils.mjs +1 -1
  4. package/dist/{agent-https-DmNQyAl7.mjs → agent-https-JhvY1axF.mjs} +1 -1
  5. package/dist/{agent-https-D_oo3PNv.js → agent-https-bnyFRK7Y.js} +1 -1
  6. package/dist/auth/user-manager.d.ts +2 -2
  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 +4 -4
  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 +5 -5
  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 +5 -5
  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/core-blocks.addon.js +2 -2
  26. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  27. package/dist/builtins/device-history/device-history.addon.js +1 -1
  28. package/dist/builtins/device-history/device-history.addon.mjs +1 -1
  29. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  30. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  31. package/dist/builtins/hub-forwarder/index.js +1 -1
  32. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  33. package/dist/builtins/keychain/index.js +3 -3
  34. package/dist/builtins/keychain/index.mjs +3 -3
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.d.ts +9 -25
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +240 -39
  37. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +237 -38
  38. package/dist/builtins/liveness-monitor/liveness-reporter.d.ts +28 -0
  39. package/dist/builtins/liveness-monitor/storage-root-access.d.ts +53 -0
  40. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  41. package/dist/builtins/local-auth/local-auth.addon.mjs +5 -5
  42. package/dist/builtins/local-network/local-network.addon.js +2 -2
  43. package/dist/builtins/local-network/local-network.addon.mjs +2 -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 +10 -10
  48. package/dist/builtins/platform-probe/coral-accelerators.d.ts +14 -4
  49. package/dist/builtins/platform-probe/index.js +9 -6
  50. package/dist/builtins/platform-probe/index.mjs +15 -12
  51. package/dist/builtins/record-entries/index.js +1 -1
  52. package/dist/builtins/record-entries/index.mjs +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  54. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  55. package/dist/builtins/snapshot/index.js +1 -1
  56. package/dist/builtins/snapshot/index.mjs +1 -1
  57. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  58. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +20 -20
  59. package/dist/builtins/sqlite-storage/index.js +1 -1
  60. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  61. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  62. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  63. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  64. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  65. package/dist/builtins/system-config/system-config.addon.js +3 -3
  66. package/dist/builtins/system-config/system-config.addon.mjs +115 -115
  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-B0tqvf0t.mjs → composition-sources-BY6rFuAE.mjs} +1 -1
  70. package/dist/{composition-sources-CRJffEiB.js → composition-sources-D3h-tNoJ.js} +1 -1
  71. package/dist/{constants-WF1AqoKF.js → constants-CqbO6e9i.js} +1 -1
  72. package/dist/{constants--A505iuO.mjs → constants-zBiEodEM.mjs} +1 -1
  73. package/dist/{coral-accelerators-Cri32b7q.js → coral-accelerators-BSVsBVzt.js} +46 -11
  74. package/dist/{coral-accelerators-Dl-GnMHn.mjs → coral-accelerators-DBlgWB8k.mjs} +52 -17
  75. package/dist/{custom-action-registry-BGYDjTqY.mjs → custom-action-registry-DZSCLBuT.mjs} +37 -37
  76. package/dist/{device-ca-DYmToMp8.mjs → device-ca-BWTWWBcc.mjs} +1 -1
  77. package/dist/{device-ca-R4MZZtc0.js → device-ca-CfiIg3a9.js} +1 -1
  78. package/dist/{dist-X1r44BdF.mjs → dist-CdFkkmXW.mjs} +6845 -1651
  79. package/dist/{dist-DRL3rEfN.js → dist-DX5Fu3mV.js} +6845 -1651
  80. package/dist/file-host-worker.js +1 -1
  81. package/dist/file-host-worker.mjs +1 -1
  82. package/dist/{hub-mdns-ciao-DUoTS6jN.js → hub-mdns-ciao-C174UDzO.js} +2 -2
  83. package/dist/{hub-mdns-ciao-CZV5Hion.mjs → hub-mdns-ciao-CYp1Gipm.mjs} +2 -2
  84. package/dist/index.js +235 -17
  85. package/dist/index.mjs +453 -235
  86. package/dist/kernel/hwaccel/hwaccel-resolver.d.ts +24 -4
  87. package/dist/kernel/hwaccel/vaapi-init-probe.d.ts +29 -0
  88. package/dist/kernel/moleculer/runner-process-group.d.ts +4 -0
  89. package/dist/{restore-archive-Df6PsOHP.js → restore-archive-B2jIFfu_.js} +1 -1
  90. package/dist/{restore-archive-X0ciGMmO.mjs → restore-archive-BX0gaKtG.mjs} +94 -94
  91. package/dist/{wal-maintenance-DYvzJiuK.js → wal-maintenance-Dxqd5igw.js} +0 -0
  92. package/dist/{wal-maintenance-D6J_MmZ8.mjs → wal-maintenance-KHzJXlF2.mjs} +0 -0
  93. package/package.json +1 -1
@@ -1,4 +1,7 @@
1
- import { $r as errMsg, Da as EventCategory, Ui as createEvent, ei as BaseAddon, it as RecordingStatusSchema, nt as RECORDING_SOURCE_CAMSTACK_ADDON } from "../../dist-X1r44BdF.mjs";
1
+ import { $r as errMsg, Da as EventCategory, Ui as createEvent, Vn as mayWriteToLocation, ei as BaseAddon, it as RecordingStatusSchema, nt as RECORDING_SOURCE_CAMSTACK_ADDON } from "../../dist-CdFkkmXW.mjs";
2
+ import * as fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { constants } from "node:fs";
2
5
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
6
  var NO_DEVICES = "liveness:no-devices";
4
7
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -50,6 +53,198 @@ function isLivenessFindingId(id) {
50
53
  return id === NO_DEVICES || id === ALL_OFFLINE || id.startsWith(NO_FOOTAGE_PREFIX);
51
54
  }
52
55
  //#endregion
56
+ //#region src/builtins/liveness-monitor/liveness-reporter.ts
57
+ /**
58
+ * Announce new liveness faults and their recoveries — never an unchanged fault.
59
+ *
60
+ * One ERROR line and one `SystemLivenessFailed` event per NEW finding id (the
61
+ * alert centre turns it into one persistent alert), one INFO line and one
62
+ * `SystemLivenessRecovered` when it is gone. Several families of checks run on
63
+ * different cadences (the device checks wait out the boot grace, the storage
64
+ * check does not), so a recovery is judged only within the family that was
65
+ * just evaluated: a storage check that found nothing must not "recover" a
66
+ * device finding it never looked at.
67
+ */
68
+ /** Event source stamped on every emission — the addon that observed it. */
69
+ var LIVENESS_SOURCE = {
70
+ type: "addon",
71
+ id: "liveness-monitor"
72
+ };
73
+ var LivenessReporter = class {
74
+ deps;
75
+ /** Finding ids currently reported, so recovery can be detected and announced. */
76
+ active = /* @__PURE__ */ new Set();
77
+ constructor(deps) {
78
+ this.deps = deps;
79
+ }
80
+ /**
81
+ * @param findings what the family found this evaluation
82
+ * @param inFamily which ids the family owns — only those can recover here
83
+ */
84
+ report(findings, inFamily) {
85
+ const current = new Set(findings.map((f) => f.id));
86
+ for (const finding of findings) {
87
+ if (this.active.has(finding.id)) continue;
88
+ this.active.add(finding.id);
89
+ this.deps.logger.error(`liveness: ${finding.title} — ${finding.message}`, {
90
+ ...finding.deviceId !== void 0 ? { tags: { deviceId: finding.deviceId } } : {},
91
+ meta: {
92
+ findingId: finding.id,
93
+ severity: finding.severity
94
+ }
95
+ });
96
+ this.deps.emit(createEvent(EventCategory.SystemLivenessFailed, LIVENESS_SOURCE, {
97
+ findingId: finding.id,
98
+ severity: finding.severity,
99
+ title: finding.title,
100
+ message: finding.message,
101
+ ...finding.deviceId !== void 0 ? { deviceId: finding.deviceId } : {},
102
+ ...finding.deviceName !== void 0 ? { name: finding.deviceName } : {}
103
+ }));
104
+ }
105
+ for (const id of [...this.active]) {
106
+ if (current.has(id) || !inFamily(id)) continue;
107
+ this.active.delete(id);
108
+ this.deps.logger.info(`liveness: recovered — ${id}`, { meta: { findingId: id } });
109
+ this.deps.emit(createEvent(EventCategory.SystemLivenessRecovered, LIVENESS_SOURCE, { findingId: id }));
110
+ }
111
+ }
112
+ };
113
+ //#endregion
114
+ //#region src/builtins/liveness-monitor/storage-root-access.ts
115
+ /**
116
+ * Can this node WRITE where its storage locations say media goes?
117
+ *
118
+ * 2026-10-23, a fresh install of the dev hub: the bind mounted at
119
+ * `/recordings` was `root:root 0755` while the hub runs as uid 1001. Nothing
120
+ * said so at boot; the first sign was an `EACCES` on `/recordings/artifacts`,
121
+ * and recordings would have failed one write at a time. A root the process
122
+ * cannot write is a fact known the moment the node starts, so it is reported
123
+ * then — once, naming the path, its owner, its mode and the uid — instead of
124
+ * as a stream of per-write errors.
125
+ *
126
+ * Only a location this node may WRITE (`mayWriteToLocation`), served by the
127
+ * local filesystem provider, on this node, with an absolute `basePath`, is
128
+ * judged. A root that does not exist yet is judged by its nearest existing
129
+ * ancestor: that is the directory the node would have to create it in.
130
+ *
131
+ * A probe that cannot answer (a stat that fails for a reason other than
132
+ * permission) is `unknown` and never a finding (D393): an unreadable fact is
133
+ * not an unwritable disk.
134
+ */
135
+ /** The local filesystem storage provider's id (`FilesystemStorageProvider.providerId`). */
136
+ var FILESYSTEM_PROVIDER_ID = "filesystem-storage";
137
+ var UNWRITABLE_PREFIX = "liveness:storage-root-unwritable:";
138
+ /** The hub-local, writable filesystem roots among `locations`, deduplicated by path. */
139
+ function storageRootTargets(locations, localNodeId) {
140
+ const byPath = /* @__PURE__ */ new Map();
141
+ for (const location of locations) {
142
+ if (location.providerId !== FILESYSTEM_PROVIDER_ID) continue;
143
+ if (location.nodeId !== void 0 && location.nodeId !== localNodeId) continue;
144
+ if (!mayWriteToLocation(location)) continue;
145
+ const basePath = location.config["basePath"];
146
+ if (typeof basePath !== "string" || !path.isAbsolute(basePath)) continue;
147
+ const root = path.resolve(basePath);
148
+ byPath.set(root, [...byPath.get(root) ?? [], location.id]);
149
+ }
150
+ return [...byPath.entries()].map(([root, locationIds]) => ({
151
+ path: root,
152
+ locationIds: locationIds.toSorted()
153
+ })).toSorted((a, b) => a.path.localeCompare(b.path));
154
+ }
155
+ function isMissing(err) {
156
+ return err instanceof Error && "code" in err && (err.code === "ENOENT" || err.code === "ENOTDIR");
157
+ }
158
+ /** Probe one root: the root itself, or the nearest ancestor that exists. */
159
+ async function probeStorageRoot(target, probe) {
160
+ let dir = target.path;
161
+ let stat;
162
+ for (;;) try {
163
+ stat = await probe.stat(dir);
164
+ break;
165
+ } catch (err) {
166
+ const parent = path.dirname(dir);
167
+ if (!isMissing(err) || parent === dir) return {
168
+ kind: "unknown",
169
+ target,
170
+ reason: `stat ${dir} failed: ${errMsg(err)}`
171
+ };
172
+ dir = parent;
173
+ }
174
+ try {
175
+ await probe.canWrite(dir);
176
+ return {
177
+ kind: "writable",
178
+ target
179
+ };
180
+ } catch (err) {
181
+ return {
182
+ kind: "unwritable",
183
+ target,
184
+ blockedAt: dir,
185
+ stat,
186
+ reason: errMsg(err)
187
+ };
188
+ }
189
+ }
190
+ function octalMode(mode) {
191
+ return (mode & 4095).toString(8).padStart(4, "0");
192
+ }
193
+ function processIdentity(probe) {
194
+ if (probe.processUid === null) return "this process";
195
+ return `uid ${String(probe.processUid)}` + (probe.processGid === null ? "" : ` (gid ${String(probe.processGid)})`);
196
+ }
197
+ /**
198
+ * One finding per DIRECTORY that refused — several roots under one unwritable
199
+ * mount (`/recordings` holding recordings, event media and artifacts) are one
200
+ * cause and one alert. Stable id = that directory.
201
+ */
202
+ function storageRootFindings(results, probe) {
203
+ const byBlocked = /* @__PURE__ */ new Map();
204
+ for (const result of results) {
205
+ if (result.kind !== "unwritable") continue;
206
+ const entry = byBlocked.get(result.blockedAt) ?? {
207
+ stat: result.stat,
208
+ roots: [],
209
+ locationIds: []
210
+ };
211
+ entry.roots.push(result.target.path);
212
+ entry.locationIds.push(...result.target.locationIds);
213
+ byBlocked.set(result.blockedAt, entry);
214
+ }
215
+ const uidFix = probe.processUid === null ? "<uid>" : String(probe.processUid);
216
+ const gidFix = probe.processGid === null ? "<gid>" : String(probe.processGid);
217
+ return [...byBlocked.entries()].toSorted(([a], [b]) => a.localeCompare(b)).map(([blockedAt, { stat, roots, locationIds }]) => {
218
+ const missing = roots.filter((r) => r !== blockedAt);
219
+ const below = missing.length > 0 ? ` (${missing.join(", ")} must be created inside it)` : "";
220
+ return {
221
+ id: `${UNWRITABLE_PREFIX}${blockedAt}`,
222
+ severity: "error",
223
+ title: "Storage location not writable",
224
+ message: `${blockedAt} is owned by ${String(stat.uid)}:${String(stat.gid)} with mode ${octalMode(stat.mode)}, and CamStack runs as ${processIdentity(probe)} — nothing can be written there${below}. Locations affected: ${locationIds.toSorted().join(", ")}. Fix the ownership on the host, e.g. chown -R ${uidFix}:${gidFix} <the host directory bound at ${blockedAt}>.`
225
+ };
226
+ });
227
+ }
228
+ function isStorageRootFindingId(id) {
229
+ return id.startsWith(UNWRITABLE_PREFIX);
230
+ }
231
+ /** The real probe: `fs.access(W_OK | X_OK)` as THIS process, and `fs.stat`. */
232
+ function nodeStorageRootProbe(fs) {
233
+ return {
234
+ canWrite: (dir) => fs.access(dir, constants.W_OK | constants.X_OK),
235
+ stat: async (dir) => {
236
+ const st = await fs.stat(dir);
237
+ return {
238
+ uid: st.uid,
239
+ gid: st.gid,
240
+ mode: st.mode
241
+ };
242
+ },
243
+ processUid: typeof process.getuid === "function" ? process.getuid() : null,
244
+ processGid: typeof process.getgid === "function" ? process.getgid() : null
245
+ };
246
+ }
247
+ //#endregion
53
248
  //#region src/builtins/liveness-monitor/liveness-monitor.addon.ts
54
249
  /**
55
250
  * liveness-monitor — the node states, out loud, when it has stopped working.
@@ -72,11 +267,8 @@ function isLivenessFindingId(id) {
72
267
  * cause the outage it was meant to report — this repo has already stopped a
73
268
  * hub that way once (the remedial-action watcher, 2026-07-30).
74
269
  */
75
- /** Event source stamped on every emission — the addon that observed it. */
76
- var LIVENESS_SOURCE = {
77
- type: "addon",
78
- id: "liveness-monitor"
79
- };
270
+ /** The node this monitor runs on (it is hub-only). */
271
+ var HUB_NODE_ID = "hub";
80
272
  /** How often the invariants are checked. */
81
273
  var TICK_INTERVAL_MS = 5 * 6e4;
82
274
  /**
@@ -99,13 +291,19 @@ var RECORDING_LOOKBACK_MS = 1440 * 6e4;
99
291
  var LivenessMonitorAddon = class extends BaseAddon {
100
292
  tickTimer = null;
101
293
  startedAtMs = 0;
102
- /** Finding ids currently reported, so recovery can be detected and announced. */
103
- active = /* @__PURE__ */ new Set();
294
+ reporter = null;
295
+ /** Roots whose writability could not be read, already said once. */
296
+ unknownRootsSaid = /* @__PURE__ */ new Set();
104
297
  constructor() {
105
298
  super({});
106
299
  }
107
300
  async onInitialize() {
108
301
  this.startedAtMs = Date.now();
302
+ this.reporter = new LivenessReporter({
303
+ logger: this.ctx.logger,
304
+ emit: (event) => this.ctx.eventBus.emit(event)
305
+ });
306
+ this.checkStorageRoots();
109
307
  this.tickTimer = setInterval(() => {
110
308
  this.tick();
111
309
  }, TICK_INTERVAL_MS);
@@ -125,11 +323,12 @@ var LivenessMonitorAddon = class extends BaseAddon {
125
323
  * without waiting out the interval.
126
324
  */
127
325
  async tick() {
128
- if (Date.now() - this.startedAtMs < BOOT_GRACE_MS) return [];
326
+ const storage = await this.checkStorageRoots();
327
+ if (Date.now() - this.startedAtMs < BOOT_GRACE_MS) return storage;
129
328
  try {
130
329
  const findings = evaluateLiveness(await this.gather());
131
- this.report(findings);
132
- return findings;
330
+ this.reporter?.report(findings, isLivenessFindingId);
331
+ return [...storage, ...findings];
133
332
  } catch (err) {
134
333
  this.ctx.logger.error("liveness-monitor: check failed — node health is UNKNOWN this tick", { meta: { error: errMsg(err) } });
135
334
  return [];
@@ -204,33 +403,33 @@ var LivenessMonitorAddon = class extends BaseAddon {
204
403
  this.ctx.logger.warn(`liveness: recording availability was unreadable (${window}) — this camera is not judged this tick`, { tags: { deviceId } });
205
404
  return null;
206
405
  }
207
- /** Announce new faults and recoveries; say nothing about an unchanged fault. */
208
- report(findings) {
209
- const current = new Set(findings.map((f) => f.id));
210
- for (const finding of findings) {
211
- if (this.active.has(finding.id)) continue;
212
- this.active.add(finding.id);
213
- this.ctx.logger.error(`liveness: ${finding.title} — ${finding.message}`, {
214
- ...finding.deviceId !== void 0 ? { tags: { deviceId: finding.deviceId } } : {},
215
- meta: {
216
- findingId: finding.id,
217
- severity: finding.severity
218
- }
219
- });
220
- this.ctx.eventBus.emit(createEvent(EventCategory.SystemLivenessFailed, LIVENESS_SOURCE, {
221
- findingId: finding.id,
222
- severity: finding.severity,
223
- title: finding.title,
224
- message: finding.message,
225
- ...finding.deviceId !== void 0 ? { deviceId: finding.deviceId } : {},
226
- ...finding.deviceName !== void 0 ? { name: finding.deviceName } : {}
227
- }));
228
- }
229
- for (const id of [...this.active]) {
230
- if (current.has(id)) continue;
231
- this.active.delete(id);
232
- this.ctx.logger.info(`liveness: recovered — ${id}`, { meta: { findingId: id } });
233
- this.ctx.eventBus.emit(createEvent(EventCategory.SystemLivenessRecovered, LIVENESS_SOURCE, { findingId: id }));
406
+ /**
407
+ * Every storage root this node writes to must be writable BY THIS PROCESS.
408
+ * Reported through the same finding path (one ERROR, one alert, a recovery
409
+ * when it is fixed). Never throws: an unreadable list is said and skipped.
410
+ */
411
+ async checkStorageRoots() {
412
+ const reporter = this.reporter;
413
+ if (reporter === null) return [];
414
+ try {
415
+ const locations = await this.ctx.api.storage.listLocations.query({});
416
+ const probe = nodeStorageRootProbe(fsp);
417
+ const results = await Promise.all(storageRootTargets(locations, HUB_NODE_ID).map((t) => probeStorageRoot(t, probe)));
418
+ for (const result of results) {
419
+ if (result.kind !== "unknown" || this.unknownRootsSaid.has(result.target.path)) continue;
420
+ this.unknownRootsSaid.add(result.target.path);
421
+ this.ctx.logger.warn("liveness: storage root writability could not be read — not judged", { meta: {
422
+ path: result.target.path,
423
+ locationIds: result.target.locationIds,
424
+ reason: result.reason
425
+ } });
426
+ }
427
+ const findings = storageRootFindings(results, probe);
428
+ reporter.report(findings, isStorageRootFindingId);
429
+ return findings;
430
+ } catch (err) {
431
+ this.ctx.logger.warn("liveness: storage locations unreadable — storage roots not checked", { meta: { error: errMsg(err) } });
432
+ return [];
234
433
  }
235
434
  }
236
435
  };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Announce new liveness faults and their recoveries — never an unchanged fault.
3
+ *
4
+ * One ERROR line and one `SystemLivenessFailed` event per NEW finding id (the
5
+ * alert centre turns it into one persistent alert), one INFO line and one
6
+ * `SystemLivenessRecovered` when it is gone. Several families of checks run on
7
+ * different cadences (the device checks wait out the boot grace, the storage
8
+ * check does not), so a recovery is judged only within the family that was
9
+ * just evaluated: a storage check that found nothing must not "recover" a
10
+ * device finding it never looked at.
11
+ */
12
+ import { type IScopedLogger, type SystemEvent } from "@camstack/types";
13
+ import type { LivenessFinding } from "./liveness-checks.js";
14
+ export interface LivenessReporterDeps {
15
+ readonly logger: IScopedLogger;
16
+ readonly emit: (event: SystemEvent) => void;
17
+ }
18
+ export declare class LivenessReporter {
19
+ private readonly deps;
20
+ /** Finding ids currently reported, so recovery can be detected and announced. */
21
+ private readonly active;
22
+ constructor(deps: LivenessReporterDeps);
23
+ /**
24
+ * @param findings what the family found this evaluation
25
+ * @param inFamily which ids the family owns — only those can recover here
26
+ */
27
+ report(findings: readonly LivenessFinding[], inFamily: (id: string) => boolean): void;
28
+ }
@@ -0,0 +1,53 @@
1
+ import { type StorageLocation } from "@camstack/types";
2
+ import type { LivenessFinding } from "./liveness-checks.js";
3
+ /** Owner and permission bits of a directory, as `stat` reports them. */
4
+ export interface DirStat {
5
+ readonly uid: number;
6
+ readonly gid: number;
7
+ readonly mode: number;
8
+ }
9
+ /** The two filesystem reads the check makes; injected so a test can run without root. */
10
+ export interface StorageRootProbe {
11
+ /** Resolves when the CURRENT process may write (and traverse) `dir`; rejects otherwise. */
12
+ readonly canWrite: (dir: string) => Promise<void>;
13
+ readonly stat: (dir: string) => Promise<DirStat>;
14
+ /** The process uid/gid, or `null` where the platform has none (Windows). */
15
+ readonly processUid: number | null;
16
+ readonly processGid: number | null;
17
+ }
18
+ /** One root this node must be able to write, and every location that writes under it. */
19
+ export interface StorageRootTarget {
20
+ readonly path: string;
21
+ readonly locationIds: readonly string[];
22
+ }
23
+ export type StorageRootAccess = {
24
+ readonly kind: "writable";
25
+ readonly target: StorageRootTarget;
26
+ } | {
27
+ readonly kind: "unwritable";
28
+ readonly target: StorageRootTarget;
29
+ /** The directory that refused: the root itself, or its nearest existing ancestor. */
30
+ readonly blockedAt: string;
31
+ readonly stat: DirStat;
32
+ readonly reason: string;
33
+ } | {
34
+ readonly kind: "unknown";
35
+ readonly target: StorageRootTarget;
36
+ readonly reason: string;
37
+ };
38
+ /** The hub-local, writable filesystem roots among `locations`, deduplicated by path. */
39
+ export declare function storageRootTargets(locations: readonly StorageLocation[], localNodeId: string): readonly StorageRootTarget[];
40
+ /** Probe one root: the root itself, or the nearest ancestor that exists. */
41
+ export declare function probeStorageRoot(target: StorageRootTarget, probe: StorageRootProbe): Promise<StorageRootAccess>;
42
+ /**
43
+ * One finding per DIRECTORY that refused — several roots under one unwritable
44
+ * mount (`/recordings` holding recordings, event media and artifacts) are one
45
+ * cause and one alert. Stable id = that directory.
46
+ */
47
+ export declare function storageRootFindings(results: readonly StorageRootAccess[], probe: StorageRootProbe): readonly LivenessFinding[];
48
+ export declare function isStorageRootFindingId(id: string): boolean;
49
+ /** The real probe: `fs.access(W_OK | X_OK)` as THIS process, and `fs.stat`. */
50
+ export declare function nodeStorageRootProbe(fs: {
51
+ readonly access: (p: string, mode: number) => Promise<void>;
52
+ readonly stat: (p: string) => Promise<DirStat>;
53
+ }): StorageRootProbe;
@@ -3,10 +3,10 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DRL3rEfN.js");
6
+ const require_dist = require("../../dist-DX5Fu3mV.js");
7
7
  const require_safe_buffer$1 = require("../../safe-buffer-D3aoa-7X.js");
8
8
  const require_ms$1 = require("../../ms-LB3giGoU.js");
9
- const require_device_ca = require("../../device-ca-R4MZZtc0.js");
9
+ const require_device_ca = require("../../device-ca-CfiIg3a9.js");
10
10
  const require_read_all_rows$1 = require("../../read-all-rows-CVoO1ONM.js");
11
11
  let zod = require("zod");
12
12
  let node_crypto = require("node:crypto");
@@ -1,15 +1,15 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { $ as PreferenceDeviceIdSchema, At as buildDeviceSignatureMessage, E as DEVICE_ENROLLMENT_TTL_MS, Ki as defineRecordKinds, O as DEVICE_NONCE_TTL_MS, Q as PREFERENCES_MAX_VALUE_BYTES, Qt as deviceCredentialsCapability, Ta as userError, ei as BaseAddon, et as PreferenceNamespaceSchema, gt as UserRecordSchema, h as CREDENTIAL_TOKEN_AUDIENCE, hn as formatDeviceRevoker, jr as userManagementCapability, kt as buildDeviceEnrollmentLink, lt as SsoBridgeClaimsSchema, mt as TokenScopeSchema, oa as makeSecretMarker, ra as isSecretMarker, st as ScopedTokenSchema, tt as PreferenceValueSchema, wn as isCredentialTokenPayload, wt as authProviderCapability, y as CredentialTokenClaimsSchema } from "../../dist-X1r44BdF.mjs";
2
+ import { $ as PreferenceDeviceIdSchema, At as buildDeviceSignatureMessage, E as DEVICE_ENROLLMENT_TTL_MS, Ki as defineRecordKinds, O as DEVICE_NONCE_TTL_MS, Q as PREFERENCES_MAX_VALUE_BYTES, Qt as deviceCredentialsCapability, Ta as userError, ei as BaseAddon, et as PreferenceNamespaceSchema, gt as UserRecordSchema, h as CREDENTIAL_TOKEN_AUDIENCE, hn as formatDeviceRevoker, jr as userManagementCapability, kt as buildDeviceEnrollmentLink, lt as SsoBridgeClaimsSchema, mt as TokenScopeSchema, oa as makeSecretMarker, ra as isSecretMarker, st as ScopedTokenSchema, tt as PreferenceValueSchema, wn as isCredentialTokenPayload, wt as authProviderCapability, y as CredentialTokenClaimsSchema } from "../../dist-CdFkkmXW.mjs";
3
3
  import { t as require_safe_buffer } from "../../safe-buffer-B1OjL1I9.mjs";
4
4
  import { t as require_ms } from "../../ms-CgH2OFlD.mjs";
5
- import { _ as setProcessJwtSigningKey, a as importDevicePublicKey, c as rebuildDeviceCaCertificate, d as JWT_SIGNING_KEY_REF, g as processJwtSigningKey, h as loadJwtSigningKey, i as generateDeviceCa, l as verifyDeviceLeaf, m as getProcessJwtSigningKeyState, n as DEVICE_CA_KEY_REF, o as issueDeviceLeaf, r as DEVICE_CA_SETTINGS_SECTION, s as loadDeviceCa, t as DEVICE_CA_FIELD, u as verifyDeviceSignature } from "../../device-ca-DYmToMp8.mjs";
5
+ import { _ as setProcessJwtSigningKey, a as importDevicePublicKey, c as rebuildDeviceCaCertificate, d as JWT_SIGNING_KEY_REF, g as processJwtSigningKey, h as loadJwtSigningKey, i as generateDeviceCa, l as verifyDeviceLeaf, m as getProcessJwtSigningKeyState, n as DEVICE_CA_KEY_REF, o as issueDeviceLeaf, r as DEVICE_CA_SETTINGS_SECTION, s as loadDeviceCa, t as DEVICE_CA_FIELD, u as verifyDeviceSignature } from "../../device-ca-BWTWWBcc.mjs";
6
6
  import { t as require_read_all_rows } from "../../read-all-rows-TaPqdWNF.mjs";
7
7
  import { z } from "zod";
8
8
  import * as crypto$1 from "node:crypto";
9
9
  import { X509Certificate, createHash, randomUUID, timingSafeEqual } from "node:crypto";
10
10
  import nodeCrypto from "crypto";
11
11
  import * as path$1 from "node:path";
12
- import * as fs from "node:fs";
12
+ import * as fs$1 from "node:fs";
13
13
  //#region ../../node_modules/bcryptjs/index.js
14
14
  /**
15
15
  * The random implementation to use as a fallback.
@@ -12064,8 +12064,8 @@ var LocalAuthAddon = class extends BaseAddon {
12064
12064
  users: this.userManager,
12065
12065
  dataDir: this.ctx.nodeDataDir,
12066
12066
  fs: {
12067
- exists: (filePath) => fs.existsSync(filePath),
12068
- remove: (filePath) => fs.rmSync(filePath, { force: true })
12067
+ exists: (filePath) => fs$1.existsSync(filePath),
12068
+ remove: (filePath) => fs$1.rmSync(filePath, { force: true })
12069
12069
  },
12070
12070
  logger: this.ctx.logger
12071
12071
  });
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-DRL3rEfN.js");
4
- const require_hub_mdns_ciao = require("../../hub-mdns-ciao-DUoTS6jN.js");
3
+ const require_dist = require("../../dist-DX5Fu3mV.js");
4
+ const require_hub_mdns_ciao = require("../../hub-mdns-ciao-C174UDzO.js");
5
5
  let zod = require("zod");
6
6
  let node_net = require("node:net");
7
7
  let node_os = require("node:os");
@@ -1,5 +1,5 @@
1
- import { $r as errMsg, Da as EventCategory, Fn as localNetworkCapability, Ft as checkRoleAddress, H as HUB_MDNS_TXT_KEYS, Nr as validateCustomAddress, Tn as isDefaultAppConnectionEndpoint, V as HUB_MDNS_SERVICE_TYPE, Z as NetworkTunnelStartedDataSchema, Zn as pickAppLinkHub, c as CAMSTACK_APP_LINK_ORIGIN, ei as BaseAddon, or as resolveAppConnections, pr as resolveNotificationTapAddress, ur as resolveHubAddressRoles, xn as isAppLinkBase, yr as shortHostname } from "../../dist-X1r44BdF.mjs";
2
- import { P as readLanHttpState, a as storeUploadedCertificate, d as servedHttpsLeafFingerprint, g as readTlsMode, h as readTlsAccessStatus, i as revertToGeneratedCertificate, j as applyLanHttp, k as DEFAULT_LAN_HTTP_PORT, m as readExtraSans, n as applyCertificateToListener, o as useStoredUploadedCertificate, r as regenerateCertificate, t as createCiaoPublisher, v as writeExtraSans, w as ensureTlsCert } from "../../hub-mdns-ciao-CZV5Hion.mjs";
1
+ import { $r as errMsg, Da as EventCategory, Fn as localNetworkCapability, Ft as checkRoleAddress, H as HUB_MDNS_TXT_KEYS, Nr as validateCustomAddress, Tn as isDefaultAppConnectionEndpoint, V as HUB_MDNS_SERVICE_TYPE, Z as NetworkTunnelStartedDataSchema, Zn as pickAppLinkHub, c as CAMSTACK_APP_LINK_ORIGIN, ei as BaseAddon, or as resolveAppConnections, pr as resolveNotificationTapAddress, ur as resolveHubAddressRoles, xn as isAppLinkBase, yr as shortHostname } from "../../dist-CdFkkmXW.mjs";
2
+ import { P as readLanHttpState, a as storeUploadedCertificate, d as servedHttpsLeafFingerprint, g as readTlsMode, h as readTlsAccessStatus, i as revertToGeneratedCertificate, j as applyLanHttp, k as DEFAULT_LAN_HTTP_PORT, m as readExtraSans, n as applyCertificateToListener, o as useStoredUploadedCertificate, r as regenerateCertificate, t as createCiaoPublisher, v as writeExtraSans, w as ensureTlsCert } from "../../hub-mdns-ciao-CYp1Gipm.mjs";
3
3
  import { z } from "zod";
4
4
  import { isIP } from "node:net";
5
5
  import * as os$1 from "node:os";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DRL3rEfN.js");
6
+ const require_dist = require("../../dist-DX5Fu3mV.js");
7
7
  const require_log_read_filter = require("../../log-read-filter-Bi066jwU.js");
8
8
  let zod = require("zod");
9
9
  //#region src/builtins/loki-logging/loki-payload.ts
@@ -1,4 +1,4 @@
1
- import { Ln as logDestinationCapability, ei as BaseAddon } from "../../dist-X1r44BdF.mjs";
1
+ import { Ln as logDestinationCapability, ei as BaseAddon } from "../../dist-CdFkkmXW.mjs";
2
2
  import { n as compileLogFilter, o as lineMatches } from "../../log-read-filter-D7Ev3zm-.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/builtins/loki-logging/loki-payload.ts
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DRL3rEfN.js");
6
+ const require_dist = require("../../dist-DX5Fu3mV.js");
7
7
  let node_fs_promises = require("node:fs/promises");
8
8
  let node_util = require("node:util");
9
9
  let node_os = require("node:os");
@@ -1,9 +1,9 @@
1
- import { $r as errMsg, Da as EventCategory, Ui as createEvent, Un as metricsProviderCapability, ei as BaseAddon, ht as UNATTRIBUTED_BUCKET_KEY, ir as reducePoints, mn as foldSnapshotByFunction, rt as ROOT_BUCKET_KEY, sr as resolveBucketMs } from "../../dist-X1r44BdF.mjs";
1
+ import { $r as errMsg, Da as EventCategory, Ui as createEvent, Un as metricsProviderCapability, ei as BaseAddon, ht as UNATTRIBUTED_BUCKET_KEY, ir as reducePoints, mn as foldSnapshotByFunction, rt as ROOT_BUCKET_KEY, sr as resolveBucketMs } from "../../dist-CdFkkmXW.mjs";
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import { promisify } from "node:util";
4
4
  import * as os$1 from "node:os";
5
5
  import { platform } from "node:os";
6
- import * as fs from "node:fs";
6
+ import * as fs$1 from "node:fs";
7
7
  import { constants, readFileSync } from "node:fs";
8
8
  import { execFile, execFileSync } from "node:child_process";
9
9
  //#region src/builtins/native-metrics/cgroup-memory.ts
@@ -1642,7 +1642,7 @@ var NativeMetricsProvider = class {
1642
1642
  let swapUsedBytes = 0;
1643
1643
  let swapTotalBytes = 0;
1644
1644
  if (IS_LINUX) try {
1645
- const meminfo = fs.readFileSync("/proc/meminfo", "utf-8");
1645
+ const meminfo = fs$1.readFileSync("/proc/meminfo", "utf-8");
1646
1646
  const swapTotal = parseInt(meminfo.match(/SwapTotal:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
1647
1647
  const swapFree = parseInt(meminfo.match(/SwapFree:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
1648
1648
  swapTotalBytes = swapTotal;
@@ -1720,7 +1720,7 @@ function sampleMacMemory(provider) {
1720
1720
  }
1721
1721
  function getDiskSpaceInternal(dirPath) {
1722
1722
  return new Promise((resolve, reject) => {
1723
- fs.statfs(dirPath, (err, stats) => {
1723
+ fs$1.statfs(dirPath, (err, stats) => {
1724
1724
  if (err) return reject(err);
1725
1725
  const totalBytes = stats.blocks * stats.bsize;
1726
1726
  const availableBytes = stats.bavail * stats.bsize;
@@ -1738,7 +1738,7 @@ function getDiskSpaceInternal(dirPath) {
1738
1738
  async function takeDiskIoSnapshot() {
1739
1739
  const now = Date.now();
1740
1740
  if (IS_LINUX) try {
1741
- const data = fs.readFileSync("/proc/diskstats", "utf-8");
1741
+ const data = fs$1.readFileSync("/proc/diskstats", "utf-8");
1742
1742
  let readOps = 0, readBytes = 0, writeOps = 0, writeBytes = 0;
1743
1743
  for (const line of data.split("\n")) {
1744
1744
  const parts = line.trim().split(/\s+/);
@@ -1769,7 +1769,7 @@ async function takeDiskIoSnapshot() {
1769
1769
  async function takeNetworkIoSnapshot() {
1770
1770
  const now = Date.now();
1771
1771
  if (IS_LINUX) try {
1772
- const data = fs.readFileSync("/proc/net/dev", "utf-8");
1772
+ const data = fs$1.readFileSync("/proc/net/dev", "utf-8");
1773
1773
  let rxBytes = 0, txBytes = 0, rxPackets = 0, txPackets = 0, rxErrors = 0, txErrors = 0;
1774
1774
  for (const line of data.split("\n")) {
1775
1775
  const match = line.match(/^\s*(\w+):\s+(.+)$/);
@@ -1830,7 +1830,7 @@ async function takeNetworkIoSnapshot() {
1830
1830
  }
1831
1831
  async function getCpuTemperatureInternal() {
1832
1832
  if (IS_LINUX) for (const p of ["/sys/class/thermal/thermal_zone0/temp", "/sys/class/hwmon/hwmon0/temp1_input"]) try {
1833
- const raw = fs.readFileSync(p, "utf-8").trim();
1833
+ const raw = fs$1.readFileSync(p, "utf-8").trim();
1834
1834
  const millideg = parseInt(raw, 10);
1835
1835
  if (!isNaN(millideg)) return Math.round(millideg / 100) / 10;
1836
1836
  } catch {
@@ -1881,10 +1881,10 @@ function getProcessResources() {
1881
1881
  let threadCount = 0;
1882
1882
  if (IS_LINUX) {
1883
1883
  try {
1884
- openFds = fs.readdirSync(`/proc/${process.pid}/fd`).length;
1884
+ openFds = fs$1.readdirSync(`/proc/${process.pid}/fd`).length;
1885
1885
  } catch {}
1886
1886
  try {
1887
- const match = fs.readFileSync(`/proc/${process.pid}/status`, "utf-8").match(/Threads:\s+(\d+)/);
1887
+ const match = fs$1.readFileSync(`/proc/${process.pid}/status`, "utf-8").match(/Threads:\s+(\d+)/);
1888
1888
  if (match) threadCount = parseInt(match[1], 10);
1889
1889
  } catch {}
1890
1890
  }
@@ -1899,7 +1899,7 @@ function getProcessResources() {
1899
1899
  function getPressure(resource) {
1900
1900
  if (!IS_LINUX) return null;
1901
1901
  try {
1902
- const data = fs.readFileSync(`/proc/pressure/${resource}`, "utf-8");
1902
+ const data = fs$1.readFileSync(`/proc/pressure/${resource}`, "utf-8");
1903
1903
  const parseLine = (prefix) => {
1904
1904
  const match = data.match(new RegExp(`${prefix} avg10=(\\d+\\.\\d+) avg60=(\\d+\\.\\d+) avg300=(\\d+\\.\\d+)`));
1905
1905
  if (!match) return null;
@@ -12,7 +12,7 @@ export interface CoralFs {
12
12
  * `null` and an empty listing both mean "the bus is not observable".
13
13
  */
14
14
  listUsbDevices(): readonly string[] | null;
15
- /** Read a `<sysfsNode>/<attr>` value (idVendor / idProduct), trimmed. */
15
+ /** Read a `<sysfsNode>/<attr>` value (idVendor / idProduct / busnum / devnum), trimmed. */
16
16
  readUsbAttr(device: string, attr: string): string;
17
17
  /** PCI function names under `/sys/bus/pci/devices` (e.g. `0000:03:00.0`). */
18
18
  listPciDevices(): readonly string[];
@@ -20,7 +20,7 @@ export interface CoralFs {
20
20
  readPciAttr(device: string, attr: string): string;
21
21
  /** Entry names under `/sys/class/apex` (the gasket/apex driver: `apex_0`, …). */
22
22
  listApexClass(): readonly string[];
23
- /** Whether this process can open a device node for read AND write. */
23
+ /** Whether this process can open a device node (`/dev/apex_N`, `/dev/bus/usb/…`) for read AND write. */
24
24
  canOpen(path: string): boolean;
25
25
  /** Whether a filesystem path exists (the libedgetpu delegate, `/dev/apex_N`). */
26
26
  exists(path: string): boolean;
@@ -38,7 +38,8 @@ export type CoralUsbBusView = "observed" | "unobservable";
38
38
  /**
39
39
  * Which evidence decided the Coral answer — logged by the probe so a node
40
40
  * that reports (or stops reporting) a Coral says WHY:
41
- * - `usb-ids` — a Coral USB id is on the bus;
41
+ * - `usb-ids` — a Coral USB id is on the bus (usable or not: its device node
42
+ * decides, see {@link CoralDetection.inaccessibleUsbNodes});
42
43
  * - `pci` — a PCIe/M.2 Coral (usable or not) is on the PCI bus;
43
44
  * - `delegate-fallback` — the USB bus is not observable and the libedgetpu
44
45
  * delegate is installed, so one USB unit is ASSUMED;
@@ -49,6 +50,13 @@ export interface CoralDetection {
49
50
  readonly info: CoralInfo | null;
50
51
  readonly usbBus: CoralUsbBusView;
51
52
  readonly decidedBy: CoralDecision;
53
+ /**
54
+ * The device node of every USB Coral sysfs lists but this process cannot
55
+ * open read/write (`/dev/bus/usb/<BBB>/<DDD>`, or `<sysfs entry>` when
56
+ * sysfs gave no bus/device number) — the evidence behind a USB
57
+ * `device-not-accessible`. Empty when every listed stick is usable.
58
+ */
59
+ readonly inaccessibleUsbNodes: readonly string[];
52
60
  }
53
61
  /**
54
62
  * Detect Coral Edge TPUs on USB and PCIe/M.2 — the answer only. See
@@ -58,7 +66,9 @@ export declare function classifyCoralAccelerator(fsx: CoralFs, delegateHost?: Ed
58
66
  /**
59
67
  * Detect Coral Edge TPUs on USB and PCIe/M.2, with the evidence that decided.
60
68
  *
61
- * - USB: every matching USB device counts (multiple Corals run concurrently).
69
+ * - USB: every matching USB device counts (multiple Corals run concurrently),
70
+ * usable only if its `/dev/bus/usb/<BBB>/<DDD>` node opens read/write — a
71
+ * listed stick whose node is missing or refused is `device-not-accessible`.
62
72
  * - PCIe: the apex driver's units, each usable only if its device node opens;
63
73
  * a Coral PCI function the driver did not bind is `driver-missing`.
64
74
  * - Fallback: when the USB bus is NOT OBSERVABLE (`/sys/bus/usb/devices`