Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba5e60daf0 |
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## v0.7.1 — 2026-09-24 21:10
|
||||
|
||||
- Fix `GET /api/device` and MCP `device_info` crashing with `TypeError: Do not know how to serialize a BigInt` on live hardware (fw 2.7.15): protobuf-es decodes proto `uint64` fields as native JS `bigint`, which Express' `res.json`/`JSON.stringify` reject, and `MeshService.deviceInfo()` passed raw decoded proto objects through unconverted. Known affected fields: `Config.PowerConfig.powermonEnables` (in `localConfig.power`) and host-metrics byte counters (`freememBytes`/`diskfree*`).
|
||||
- Fix at the single choke point: one small recursive converter (`jsonSafe`, bigint → decimal string, plain objects/arrays/primitives pass through) applied in `deviceInfo()` to everything that comes from raw decoded proto messages — DeviceMetadata plus every localConfig and moduleConfig section. No per-field patching; any uint64 field in those messages is covered generically. Values that were bigint now serialize as strings (e.g. `"powermonEnables": "123"`); no other endpoint/shape changes, redaction logic untouched.
|
||||
- Tests: new device-info spec feeds a power config section frame with `powermonEnables` set to a real bigint through the fake bridge and asserts the value comes back as its string form (`"123"`) and that `JSON.stringify(deviceInfo())` succeeds — the old fixtures never set a uint64 field, which is why this slipped past the suite.
|
||||
|
||||
## v0.7.0 — 2026-09-24 20:21
|
||||
|
||||
- New `GET /api/device` endpoint (and matching MCP tool `device_info`, now seven tools): device info captured during the initial sync — this device's node number, firmware version and metadata (`meshtastic.DeviceMetadata` as-is; it carries no key material), plus per-section config and module-config messages keyed by section name. In-memory only, reset on every bridge reconnect like channels/nodes.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mesh-bot",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -718,6 +718,32 @@ describe('MeshService device info', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('serializes proto uint64 fields as strings (JSON-safe)', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
await waitFor(
|
||||
() => bridge.sockets.length === 1 && svc.status().connected,
|
||||
);
|
||||
|
||||
// powermonEnables is a proto uint64 — protobuf-es decodes it as native bigint
|
||||
bridge.sockets[0].write(
|
||||
configSectionFrame('power', { powermonEnables: 123n }),
|
||||
);
|
||||
|
||||
await waitFor(() => svc.deviceInfo().localConfig.power !== undefined);
|
||||
const info = svc.deviceInfo();
|
||||
expect(info.localConfig.power?.powermonEnables).toBe('123'); // bigint -> decimal string
|
||||
expect(() => JSON.stringify(info)).not.toThrow(); // no "Do not know how to serialize a BigInt"
|
||||
} finally {
|
||||
svc.onModuleDestroy();
|
||||
bridge.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('summarizes NodeInfo snr/hopsAway/viaMqtt when present', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
|
||||
@@ -76,6 +76,19 @@ export interface DeviceInfo {
|
||||
moduleConfig: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
// protobuf-es decodes proto uint64 fields as native bigint, which JSON.stringify rejects;
|
||||
// recursively convert so deviceInfo() output is always JSON-safe (bigint -> decimal string)
|
||||
function jsonSafe<T>(value: T): T {
|
||||
if (typeof value === 'bigint') return String(value) as T;
|
||||
if (Array.isArray(value)) return value.map(jsonSafe) as T;
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value)) out[k] = jsonSafe(v);
|
||||
return out as T;
|
||||
}
|
||||
return value; // primitives + undefined pass through
|
||||
}
|
||||
|
||||
// key material must never leave the process — replaced with a marker so callers can see the field exists
|
||||
const REDACTED = 'REDACTED';
|
||||
/** Config sections that carry secrets: security (PKI keys) and network (wifi psk). */
|
||||
@@ -170,7 +183,8 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
|
||||
* metadata (DeviceMetadata as-is — it carries no key material), plus per-section config and
|
||||
* module-config messages keyed by section name. In-memory only; empty until the sync completes,
|
||||
* reset on every bridge reconnect. Key material is redacted: security publicKey/privateKey/adminKey
|
||||
* and network wifiPsk are replaced with "REDACTED" — never exposed over HTTP.
|
||||
* and network wifiPsk are replaced with "REDACTED" — never exposed over HTTP. Proto uint64 fields
|
||||
* (e.g. power.powermonEnables) come back as decimal strings so the output always JSON-serializes.
|
||||
*/
|
||||
deviceInfo(): DeviceInfo {
|
||||
const out: DeviceInfo = {
|
||||
@@ -180,12 +194,12 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
|
||||
};
|
||||
if (this.metadata) {
|
||||
out.firmwareVersion = this.metadata.firmwareVersion;
|
||||
out.metadata = this.metadata;
|
||||
out.metadata = jsonSafe(this.metadata);
|
||||
}
|
||||
for (const [name, value] of this.configSections)
|
||||
out.localConfig[name] = redactSection(name, value);
|
||||
out.localConfig[name] = jsonSafe(redactSection(name, value));
|
||||
for (const [name, value] of this.moduleConfigSections)
|
||||
out.moduleConfig[name] = value;
|
||||
out.moduleConfig[name] = jsonSafe(value);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user