From 43f0ede1eb0bdecd823a5c8cfa4d7e405ae0aa7f Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Thu, 24 Sep 2026 21:08:45 +0000 Subject: [PATCH] fix: serialize proto uint64 fields as strings in device info (v0.7.1) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- src/mesh/mesh.service.spec.ts | 26 ++++++++++++++++++++++++++ src/mesh/mesh.service.ts | 22 ++++++++++++++++++---- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04ed79c..cd289da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/package.json b/package.json index 32b3277..c2653d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mesh-bot", - "version": "0.7.0", + "version": "0.7.1", "description": "", "author": "", "private": true, diff --git a/src/mesh/mesh.service.spec.ts b/src/mesh/mesh.service.spec.ts index b4c1b92..c1eb858 100644 --- a/src/mesh/mesh.service.spec.ts +++ b/src/mesh/mesh.service.spec.ts @@ -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'; diff --git a/src/mesh/mesh.service.ts b/src/mesh/mesh.service.ts index acf5558..cb93856 100644 --- a/src/mesh/mesh.service.ts +++ b/src/mesh/mesh.service.ts @@ -76,6 +76,19 @@ export interface DeviceInfo { moduleConfig: Record>; } +// 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(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 = {}; + 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; } -- 2.54.0