Skip to content

feat(meshcore): decode unencrypted packet data in packet-log export (#3937)#3939

Merged
Yeraze merged 1 commit into
mainfrom
feat/3937-meshcore-export-decode
Jul 5, 2026
Merged

feat(meshcore): decode unencrypted packet data in packet-log export (#3937)#3939
Yeraze merged 1 commit into
mainfrom
feat/3937-meshcore-export-decode

Conversation

@Yeraze

@Yeraze Yeraze commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Closes #3937

Summary

Issue #3937 has two parts:

  1. Retention >= 7 days — already supported today, no code change. The Max age (h) control in the MeshCore Packet Monitor filter panel accepts up to 720h (30 days); setting it to 168 gives a 7-day window. (Raise Max count alongside it for long windows, since it trims per-source regardless of age.)
  2. Unencrypted data (e.g. ADVERT payloads) in the export — a real gap, fixed here.

The gap

GET /api/sources/:id/meshcore/packets/export previously serialized only the raw DB row per packet (rawHex, payloadType, routeType, SNR/RSSI, etc.). Consumers had to manually decode rawHex for every ADVERT line themselves, even though MeshMonitor already ships a full, dependency-free decoder (src/utils/meshcorePacketDecode.ts) that powers the Packet Monitor UI's "click to decode" modal.

Change

Reuse the existing isomorphic decodeMeshCorePacket() server-side in the export route to attach a decoded field to each exported JSONL line:

  • Full ADVERT decode when payloadType === 0x04: name, lat/lon, pubkey, timestamp, signature, flags.
  • ACK codes.
  • Plaintext dest/src hash prefix for encrypted message payloads (TXT_MSG/GRP_TXT/etc.) — the ciphertext body stays undecoded by design.
  • rawHex and all existing raw fields are preserved, so nothing is lost for callers doing their own analysis.

Decode-at-export-time only — no schema/migration change, no change to what's persisted. Decoding never throws; parse issues surface as null / a .errors array.

Testing

  • Added 2 route tests (meshcoreRoutes.test.ts): one asserts a full ADVERT line is decoded (name Repeater-1, lat 37.5 / lon -122.25, pubkey) with rawHex preserved; one asserts short/encrypted rows still get a decoded header block with no advert.
  • Full Vitest suite: success: true, 0 failed tests, 0 failed suites (8009 passed).
  • tsc -p tsconfig.server.json --noEmit: no new errors from this change (only pre-existing TelemetryChart.tsx frontend errors present on main).
  • ESLint on changed files: 0 errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n

…3937)

The MeshCore packet-log export endpoint (GET
/api/sources/:id/meshcore/packets/export) previously serialized only the
raw DB row per packet, so anyone exporting had to manually decode `rawHex`
for every ADVERT line even though MeshMonitor already knows how (the Packet
Monitor UI's "click to decode" modal uses the same decoder).

Reuse the existing isomorphic `decodeMeshCorePacket()` server-side to attach
a `decoded` field to each exported JSONL line. This surfaces the full ADVERT
decode (name, lat/lon, pubkey, timestamp, flags), ACK codes, and the
plaintext dest/src hash prefix of encrypted messages. Encrypted message
bodies remain undecoded by design. `rawHex` is preserved so nothing is lost
for callers doing their own analysis.

This is decode-at-export-time only — no schema/migration change and no change
to what's persisted.

Note: part 1 of the issue (retention >= 7 days) needs no code change; the
Max age (h) filter already supports up to 720h (30 days).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(meshcore): decode unencrypted packet data in packet-log export (#3937)

  • Read changed files
  • Review code quality and best practices
  • Check for potential bugs or issues
  • Evaluate performance considerations
  • Assess security concerns
  • Evaluate test coverage

Overall Assessment

Clean, well-scoped change. The implementation correctly reuses the existing isomorphic decoder rather than duplicating logic, and the backward-compatible approach (adding decoded alongside existing fields) is the right call for a streaming export endpoint.


Code Quality

meshcoreRoutes.ts — lines 3639–3649`

The core change is a single, well-placed line:

const decoded = decodeMeshCorePacket(packet.rawHex);
res.write(JSON.stringify({ ...packet, decoded }) + '\n');

This is correct. The decoder returns null for empty/null rawHex (and never throws), so decoded: null serializes cleanly in the JSONL output, which is the right sentinel for callers.

Minor: res.status(500).json(...) after streaming has started (line 3655)

This is a pre-existing issue (not introduced by this PR), but worth noting: if meshcorePacketLogService.getPackets() throws after res.setHeader() has been called but before res.write(), the 500 error response would still work. However, if the error occurs mid-stream (after res.write() has been called at least once), Express cannot send a 500 status because headers are already committed. This PR's change (calling decodeMeshCorePacket inside the loop) does not throw — the decoder is guarded with try/catch internally — so this is safe in practice, but the architectural risk is pre-existing.


Potential Bugs

None introduced by this PR. The decoder (decodeMeshCorePacket) is already well-tested in meshcorePacketDecode.test.ts and handles all edge cases (null, empty string, truncated bytes) without throwing.

One subtle correctness note: decodeMeshCorePacket re-derives payloadType from the raw header byte (line 152 of the decoder), while the DB row already has payloadType as a stored column. If there's ever a discrepancy between the stored payloadType and what's in rawHex, the decoded header.payloadType reflects the wire data, not the DB field. This is actually the correct behavior for an export — the raw wire truth is more authoritative — but it's worth knowing.


Performance

The decode runs synchronously per packet in the export loop. For the default cap of 1,000 packets this is negligible. For very large exports (e.g., meshcore_packet_log_max_count at its maximum), adding CPU work per packet could slow the response, but the decoder is O(n) on packet size (not count), and individual packet sizes are small. No concern here.


Security

No new attack surface. The decoder is purely a bytes-to-struct operation on data already stored in the DB, uses no eval/dynamic dispatch, and cannot make network calls. The export endpoint's existing auth (requirePermission('packetmonitor', 'read')) is unchanged. The decoded output exposes nothing that wasn't already obtainable by a client doing their own hex decode.


Test Coverage

Two new test cases were added. Coverage is solid for the happy path:

  1. ADVERT decode — verifies name, lat, lon, publicKey, errors: [], and rawHex preservation. Good.
  2. Short/encrypted payload — verifies decoded is defined but advert is absent. Good.

Gap: no test for rawHex: null packets. The DB schema allows rawHex to be null (it's stored as-is from the companion push). When rawHex is null, decodeMeshCorePacket returns null, so the JSONL line will have decoded: null. This is correct behavior, but a test asserting decoded === null for a null-rawHex row would close the loop and document the contract explicitly.

Gap: the "short/undecodable" test (line 2384) uses rawHex: 'cafe' (2 bytes). The description says "short/undecodable payload" but 'cafe' is actually a valid 2-byte sequence that decodes a partial header — it won't return null, it returns a result with the header decoded and a truncated path. The test asserts decoded.payload.advert is undefined (correct), but the test name slightly overstates the case. Not a bug, just a documentation note.


Summary

Recommend merging. The change is minimal, correct, backward-compatible, and well-tested. The one suggested improvement (a null-rawHex test case) is nice-to-have, not a blocker.

@Yeraze Yeraze merged commit 4013cff into main Jul 5, 2026
19 checks passed
@Yeraze Yeraze deleted the feat/3937-meshcore-export-decode branch July 5, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] Improvements to Meshcore packet log

1 participant