fix: serialize proto uint64 fields as strings in device info (v0.7.1)
CI / build + unit (pull_request) Successful in 1m17s

This commit is contained in:
2026-09-24 21:08:45 +00:00
parent 4beaa377c4
commit 43f0ede1eb
4 changed files with 51 additions and 5 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog # 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 ## 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. - 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
View File
@@ -1,6 +1,6 @@
{ {
"name": "mesh-bot", "name": "mesh-bot",
"version": "0.7.0", "version": "0.7.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
+26
View File
@@ -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 () => { it('summarizes NodeInfo snr/hopsAway/viaMqtt when present', async () => {
const bridge = await startBridge(); const bridge = await startBridge();
process.env.MESH_HOST = '127.0.0.1'; process.env.MESH_HOST = '127.0.0.1';
+18 -4
View File
@@ -76,6 +76,19 @@ export interface DeviceInfo {
moduleConfig: Record<string, Record<string, unknown>>; 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 // key material must never leave the process — replaced with a marker so callers can see the field exists
const REDACTED = 'REDACTED'; const REDACTED = 'REDACTED';
/** Config sections that carry secrets: security (PKI keys) and network (wifi psk). */ /** 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 * 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, * 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 * 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 { deviceInfo(): DeviceInfo {
const out: DeviceInfo = { const out: DeviceInfo = {
@@ -180,12 +194,12 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
}; };
if (this.metadata) { if (this.metadata) {
out.firmwareVersion = this.metadata.firmwareVersion; out.firmwareVersion = this.metadata.firmwareVersion;
out.metadata = this.metadata; out.metadata = jsonSafe(this.metadata);
} }
for (const [name, value] of this.configSections) 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) for (const [name, value] of this.moduleConfigSections)
out.moduleConfig[name] = value; out.moduleConfig[name] = jsonSafe(value);
return out; return out;
} }