From 7ca739b26065a872cf5f27f6b6c0c3b8ddddb355 Mon Sep 17 00:00:00 2001 From: Randall Hand Date: Sun, 5 Jul 2026 16:04:28 -0400 Subject: [PATCH] feat(messages): surface XEdDSA packet signing status (firmware 2.8+) (#3923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firmware 2.8 adds XEdDSA broadcast signing. The official mobile clients show a verified-signature shield in message details; MeshMonitor exposed nothing. This adds the flag end-to-end and renders a green shield 🛡️ wherever per-message metadata is shown. - Bump the `protobufs` submodule to master HEAD (v2.7.26 predates the field; no release tag carries it yet) to expose `MeshPacket.xeddsa_signed` (field 22). Protobufs are parsed at runtime, so the bump alone makes the wire field decodable. - Read `xeddsaSigned` off the decoded packet in the message-build path (camelCase + snake_case defensive read) and thread it through the TextMessage / DbMessage / MeshMessage types. - Persist it on the `messages` table via migration 113 across all three backends (SQLite/PostgreSQL/MySQL), mirroring the viaMqtt/viaStoreForward nullable-boolean pattern. - Serialize it in both DB->MeshMessage paths (server.ts + getRecentMessages) and the unified per-reception view so it survives reloads, plus realtime WebSocket via the built message object. - Render a shield in HopCountDisplay (MessagesTab/ChannelsTab) and in the UnifiedMessagesPage reception-stats modal. i18n key with English fallback. Tests: HopCountDisplay shield rendering, message repository round-trip persistence, migration registry count (112 -> 113). Full Vitest suite passes (0 failures); server tsc clean (no new errors); vite build OK. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n --- protobufs | 2 +- public/locales/en.json | 1 + src/components/ChannelsTab.tsx | 1 + src/components/HopCountDisplay.test.tsx | 38 +++++++++++++++++++ src/components/HopCountDisplay.tsx | 22 ++++++++++- src/components/MessagesTab.tsx | 2 + src/db/migrations.test.ts | 12 +++--- src/db/migrations.ts | 18 +++++++++ src/db/repositories/messages.insert.test.ts | 21 ++++++++++ src/db/repositories/messages.ts | 2 + src/db/schema/messages.ts | 4 ++ src/db/types.ts | 2 + src/pages/UnifiedMessagesPage.tsx | 12 ++++++ src/server/meshtasticManager.ts | 6 +++ .../migrations/113_add_xeddsa_signed.ts | 38 +++++++++++++++++++ src/server/protobufLoader.ts | 7 ++++ src/server/routes/unifiedRoutes.ts | 5 +++ src/server/server.ts | 1 + src/services/database.ts | 4 ++ src/types/message.ts | 1 + 20 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 src/server/migrations/113_add_xeddsa_signed.ts diff --git a/protobufs b/protobufs index da60cee58..93b643694 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit da60cee584c6dc1efbb4a3809b98666505179b85 +Subproject commit 93b643694865d8685f690663b58eae5233a1eca8 diff --git a/public/locales/en.json b/public/locales/en.json index 8fd3de614..0016a9306 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -2856,6 +2856,7 @@ "messages.signal_info": "Signal strength (SNR/RSSI) - Direct message", "messages.click_for_relay": "Click to see potential relay nodes", "messages.via_mqtt": "Received via MQTT", + "messages.xeddsa_signed": "Cryptographically signed (XEdDSA)", "link_preview.loading": "Loading preview...", "link_preview.image_alt": "Link preview", diff --git a/src/components/ChannelsTab.tsx b/src/components/ChannelsTab.tsx index 3a435f401..1f7f32af4 100644 --- a/src/components/ChannelsTab.tsx +++ b/src/components/ChannelsTab.tsx @@ -1116,6 +1116,7 @@ export default function ChannelsTab({ relayNode={msg.relayNode} viaMqtt={msg.viaMqtt} viaStoreForward={msg.viaStoreForward} + xeddsaSigned={msg.xeddsaSigned} onClick={() => handleRelayClick(msg)} /> diff --git a/src/components/HopCountDisplay.test.tsx b/src/components/HopCountDisplay.test.tsx index 63445e88e..e25100961 100644 --- a/src/components/HopCountDisplay.test.tsx +++ b/src/components/HopCountDisplay.test.tsx @@ -58,6 +58,44 @@ describe('HopCountDisplay', () => { }); }); + describe('XEdDSA signed indicator', () => { + it('shows signed shield when xeddsaSigned is true', () => { + render(); + const shield = screen.getByLabelText(/xeddsa_signed/i); + expect(shield).toBeDefined(); + expect(shield.textContent).toBe('🛡️'); + }); + + it('does not show shield when xeddsaSigned is false', () => { + render(); + expect(screen.queryByLabelText(/xeddsa_signed/i)).toBeNull(); + }); + + it('does not show shield when xeddsaSigned is undefined', () => { + render(); + expect(screen.queryByLabelText(/xeddsa_signed/i)).toBeNull(); + }); + + it('shows shield alongside hop count when hops are available', () => { + render(); + expect(screen.getByLabelText(/xeddsa_signed/i)).toBeDefined(); + expect(screen.getByText(/hops/i)).toBeDefined(); + }); + + it('shows shield with SNR/RSSI for direct messages (0 hops)', () => { + render(); + expect(screen.getByLabelText(/xeddsa_signed/i)).toBeDefined(); + expect(screen.getByText(/9\.5 dB/)).toBeDefined(); + }); + + it('shows shield alongside MQTT and S&F indicators', () => { + render(); + expect(screen.getByLabelText(/xeddsa_signed/i)).toBeDefined(); + expect(screen.getByLabelText(/mqtt/i)).toBeDefined(); + expect(screen.getByLabelText(/store_forward/i)).toBeDefined(); + }); + }); + describe('MQTT indicator', () => { it('shows MQTT icon when viaMqtt is true', () => { render(); diff --git a/src/components/HopCountDisplay.tsx b/src/components/HopCountDisplay.tsx index 47f605c14..d2776adb3 100644 --- a/src/components/HopCountDisplay.tsx +++ b/src/components/HopCountDisplay.tsx @@ -9,6 +9,7 @@ interface HopCountDisplayProps { relayNode?: number; viaMqtt?: boolean; viaStoreForward?: boolean; + xeddsaSigned?: boolean; onClick?: () => void; } @@ -27,10 +28,25 @@ const HopCountDisplay: React.FC = ({ relayNode, viaMqtt, viaStoreForward, + xeddsaSigned, onClick, }) => { const { t } = useTranslation(); + // XEdDSA signing indicator (firmware 2.8+): green shield shown when the + // receiving node cryptographically verified the broadcast's signature, + // matching the official mobile clients' verified-signature indicator. + const SignedIndicator = xeddsaSigned ? ( + + 🛡️ + + ) : null; + // Store & Forward indicator component const StoreForwardIndicator = viaStoreForward ? ( = ({ // Return null if either hop value is missing (but show indicators if present) if (hopStart === undefined || hopLimit === undefined) { - return <>{StoreForwardIndicator}{MqttIndicator}; + return <>{SignedIndicator}{StoreForwardIndicator}{MqttIndicator}; } const hopCount = hopStart - hopLimit; // Guard against malformed data (negative hop counts) if (hopCount < 0) { - return <>{StoreForwardIndicator}{MqttIndicator}; + return <>{SignedIndicator}{StoreForwardIndicator}{MqttIndicator}; } // Check if this hop count is clickable (has relay node info) @@ -94,6 +110,7 @@ const HopCountDisplay: React.FC = ({ ({parts.join(' / ')}) + {SignedIndicator} {StoreForwardIndicator} {MqttIndicator} @@ -109,6 +126,7 @@ const HopCountDisplay: React.FC = ({ > ({t('messages.hops', { count: hopCount, hopStart: hopStart })}) + {SignedIndicator} {StoreForwardIndicator} {MqttIndicator} diff --git a/src/components/MessagesTab.tsx b/src/components/MessagesTab.tsx index e39cefb54..38437ec81 100644 --- a/src/components/MessagesTab.tsx +++ b/src/components/MessagesTab.tsx @@ -1467,6 +1467,7 @@ const MessagesTab: React.FC = ({ relayNode={msg.relayNode} viaMqtt={msg.viaMqtt} viaStoreForward={msg.viaStoreForward} + xeddsaSigned={msg.xeddsaSigned} onClick={() => handleRelayClick(msg)} /> @@ -1578,6 +1579,7 @@ const MessagesTab: React.FC = ({ relayNode={msg.relayNode} viaMqtt={msg.viaMqtt} viaStoreForward={msg.viaStoreForward} + xeddsaSigned={msg.xeddsaSigned} onClick={() => handleRelayClick(msg)} /> diff --git a/src/db/migrations.test.ts b/src/db/migrations.test.ts index 3b8b0c81c..ccb185086 100644 --- a/src/db/migrations.test.ts +++ b/src/db/migrations.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest'; import { registry } from './migrations.js'; describe('migrations registry', () => { - it('has all 112 migrations registered', () => { - expect(registry.count()).toBe(112); + it('has all 113 migrations registered', () => { + expect(registry.count()).toBe(113); }); // Bumping these counts: when adding a new migration, increment to +1 and @@ -15,14 +15,14 @@ describe('migrations registry', () => { expect(all[0].name).toContain('v37_baseline'); }); - it('last migration is add_notes_to_nodes', () => { + it('last migration is add_xeddsa_signed', () => { const all = registry.getAll(); const last = all[all.length - 1]; - expect(last.number).toBe(112); - expect(last.name).toContain('add_notes_to_nodes'); + expect(last.number).toBe(113); + expect(last.name).toContain('add_xeddsa_signed'); }); - it('migrations are sequentially numbered from 1 to 112', () => { + it('migrations are sequentially numbered from 1 to 113', () => { const all = registry.getAll(); for (let i = 0; i < all.length; i++) { expect(all[i].number).toBe(i + 1); diff --git a/src/db/migrations.ts b/src/db/migrations.ts index eacd1d62f..f1003560e 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -127,6 +127,7 @@ import { migration as clampFutureTracerouteMigration, runMigration109Postgres as import { migration as meshcorePositionHistoryMigration, runMigration110Postgres as runMeshcorePositionHistoryPostgres, runMigration110Mysql as runMeshcorePositionHistoryMysql } from '../server/migrations/110_add_meshcore_position_history.js'; import { migration as meshcoreNodePositionSourceMigration, runMigration111Postgres as runMeshcoreNodePositionSourcePostgres, runMigration111Mysql as runMeshcoreNodePositionSourceMysql } from '../server/migrations/111_meshcore_node_position_source.js'; import { migration as nodeNotesMigration, runMigration112Postgres as runNodeNotesPostgres, runMigration112Mysql as runNodeNotesMysql } from '../server/migrations/112_add_notes_to_nodes.js'; +import { migration as xeddsaSignedMigration, runMigration113Postgres as runXeddsaSignedPostgres, runMigration113Mysql as runXeddsaSignedMysql } from '../server/migrations/113_add_xeddsa_signed.js'; // ============================================================================ // Registry @@ -1782,3 +1783,20 @@ registry.register({ postgres: (client) => runNodeNotesPostgres(client), mysql: (pool) => runNodeNotesMysql(pool), }); + +// --------------------------------------------------------------------------- +// Migration 113: `xeddsaSigned` flag on `messages` (#3923) +// Records whether a received broadcast carried a cryptographically verified +// XEdDSA signature (Meshtastic firmware 2.8+, MeshPacket field 22). Surfaced +// as a "signed" shield in the message UI, mirroring the official mobile +// clients. Nullable; existing rows keep NULL. +// --------------------------------------------------------------------------- + +registry.register({ + number: 113, + name: 'add_xeddsa_signed', + settingsKey: 'migration_113_add_xeddsa_signed', + sqlite: (db) => xeddsaSignedMigration.up(db), + postgres: (client) => runXeddsaSignedPostgres(client), + mysql: (pool) => runXeddsaSignedMysql(pool), +}); diff --git a/src/db/repositories/messages.insert.test.ts b/src/db/repositories/messages.insert.test.ts index 9d1e1781c..c21df9ced 100644 --- a/src/db/repositories/messages.insert.test.ts +++ b/src/db/repositories/messages.insert.test.ts @@ -89,4 +89,25 @@ describe('MessagesRepository.insertMessage duplicate detection', () => { expect(result1).toBe(true); expect(result2).toBe(true); }); + + it('persists the xeddsaSigned flag (#3923)', async () => { + await repo.insertMessage({ ...makeMessage('msg-signed'), xeddsaSigned: true } as any); + await repo.insertMessage({ ...makeMessage('msg-unsigned'), xeddsaSigned: undefined } as any); + + const signed = db + .prepare('SELECT xeddsaSigned FROM messages WHERE id = ?') + .get('msg-signed') as { xeddsaSigned: number | null }; + const unsigned = db + .prepare('SELECT xeddsaSigned FROM messages WHERE id = ?') + .get('msg-unsigned') as { xeddsaSigned: number | null }; + + // SQLite stores booleans as 1 / NULL for absent. + expect(signed.xeddsaSigned).toBe(1); + expect(unsigned.xeddsaSigned == null).toBe(true); + + // And it survives the async read path. + const rows = await repo.getMessages(50, 0); + const readBack = rows.find((r) => r.id === 'msg-signed'); + expect(readBack?.xeddsaSigned).toBe(true); + }); }); diff --git a/src/db/repositories/messages.ts b/src/db/repositories/messages.ts index f8e6bb6d0..fbe85b2ac 100644 --- a/src/db/repositories/messages.ts +++ b/src/db/repositories/messages.ts @@ -42,6 +42,7 @@ export class MessagesRepository extends BaseRepository { emoji: messageData.emoji ?? null, viaMqtt: messageData.viaMqtt ?? null, viaStoreForward: messageData.viaStoreForward ?? null, + xeddsaSigned: messageData.xeddsaSigned ?? null, rxSnr: messageData.rxSnr ?? null, rxRssi: messageData.rxRssi ?? null, ackFailed: messageData.ackFailed ?? null, @@ -351,6 +352,7 @@ export class MessagesRepository extends BaseRepository { emoji: messageData.emoji ?? null, viaMqtt: messageData.viaMqtt ?? null, viaStoreForward: (messageData as any).viaStoreForward ?? null, + xeddsaSigned: (messageData as any).xeddsaSigned ?? null, rxSnr: messageData.rxSnr ?? null, rxRssi: messageData.rxRssi ?? null, ackFailed: (messageData as any).ackFailed ?? null, diff --git a/src/db/schema/messages.ts b/src/db/schema/messages.ts index d2dd8a698..4644869f1 100644 --- a/src/db/schema/messages.ts +++ b/src/db/schema/messages.ts @@ -27,6 +27,8 @@ export const messagesSqlite = sqliteTable('messages', { emoji: integer('emoji'), viaMqtt: integer('viaMqtt', { mode: 'boolean' }), viaStoreForward: integer('viaStoreForward', { mode: 'boolean' }), + // XEdDSA packet signing (firmware 2.8+): broadcast had a verified signature. + xeddsaSigned: integer('xeddsaSigned', { mode: 'boolean' }), rxSnr: real('rxSnr'), rxRssi: real('rxRssi'), // Delivery tracking @@ -68,6 +70,7 @@ export const messagesPostgres = pgTable('messages', { emoji: pgInteger('emoji'), viaMqtt: pgBoolean('viaMqtt'), viaStoreForward: pgBoolean('viaStoreForward'), + xeddsaSigned: pgBoolean('xeddsaSigned'), rxSnr: pgReal('rxSnr'), rxRssi: pgReal('rxRssi'), // Delivery tracking @@ -107,6 +110,7 @@ export const messagesMysql = mysqlTable('messages', { emoji: myInt('emoji'), viaMqtt: myBoolean('viaMqtt'), viaStoreForward: myBoolean('viaStoreForward'), + xeddsaSigned: myBoolean('xeddsaSigned'), rxSnr: myDouble('rxSnr'), rxRssi: myDouble('rxRssi'), // Delivery tracking diff --git a/src/db/types.ts b/src/db/types.ts index 985d4f62a..76c7c8dad 100644 --- a/src/db/types.ts +++ b/src/db/types.ts @@ -127,6 +127,8 @@ export interface DbMessage { emoji?: number | null; viaMqtt?: boolean | null; viaStoreForward?: boolean | null; + /** Broadcast carried a verified XEdDSA signature (firmware 2.8+). NULL for pre-migration rows / unsigned traffic. */ + xeddsaSigned?: boolean | null; rxSnr?: number | null; rxRssi?: number | null; ackFailed?: boolean | null; diff --git a/src/pages/UnifiedMessagesPage.tsx b/src/pages/UnifiedMessagesPage.tsx index 8b703935e..769a5a058 100644 --- a/src/pages/UnifiedMessagesPage.tsx +++ b/src/pages/UnifiedMessagesPage.tsx @@ -47,6 +47,8 @@ interface Reception { rxRssi: number | null; rxTime: number | null; timestamp: number; + /** Broadcast carried a verified XEdDSA signature on this reception (firmware 2.8+). */ + xeddsaSigned?: boolean | null; } interface UnifiedMessage { @@ -670,6 +672,16 @@ export default function UnifiedMessagesPage() { style={{ background: sourceColor(r.sourceId) }} /> {r.sourceName} + {r.xeddsaSigned ? ( + + 🛡️ + + ) : null} {hopDisplay(r.hopStart, r.hopLimit, t)} {r.rxSnr != null ? `${r.rxSnr.toFixed(1)} dB` : '—'} diff --git a/src/server/meshtasticManager.ts b/src/server/meshtasticManager.ts index 7b36e0022..5d766c8b0 100644 --- a/src/server/meshtasticManager.ts +++ b/src/server/meshtasticManager.ts @@ -373,6 +373,7 @@ type TextMessage = { createdAt: number; decryptedBy?: 'node' | 'server' | null; // Decryption source - 'server' means read-only viaStoreForward?: boolean; // Message received via Store & Forward replay + xeddsaSigned?: boolean; // Broadcast had a cryptographically verified XEdDSA signature (firmware 2.8+) sourceIp?: string | null; // Per-message ingress attribution (client IP for HTTP injects) sourcePath?: 'http_api' | 'tcp_radio' | 'mqtt_bridge' | 'system' | null; spoofSuspected?: boolean; // #2584 — claims from == our local node but arrived over RF @@ -5871,6 +5872,10 @@ class MeshtasticManager implements ISourceManager { createdAt: Date.now(), decryptedBy: context?.decryptedBy ?? null, // Track decryption source - 'server' means read-only viaStoreForward: context?.viaStoreForward === true ? true : undefined, // Message received via Store & Forward replay + // XEdDSA signing (firmware 2.8+): the node reports whether it + // cryptographically verified this broadcast's signature. Read both + // camelCase and snake_case for protobufjs decode-shape safety. + xeddsaSigned: ((meshPacket as any).xeddsaSigned === true || (meshPacket as any).xeddsa_signed === true) ? true : undefined, // Inbound radio path — message arrived from a meshtastic node over TCP. // MQTT-bridged inbound packets are flagged via viaMqtt above; we still // attribute the row's ingress to 'tcp_radio' because it arrived via @@ -14286,6 +14291,7 @@ class MeshtasticManager implements ISourceManager { replyId: msg.replyId ?? undefined, emoji: msg.emoji ?? undefined, viaMqtt: Boolean(msg.viaMqtt), + xeddsaSigned: (msg as any).xeddsaSigned ? true : undefined, rxSnr: msg.rxSnr ?? undefined, rxRssi: msg.rxRssi ?? undefined, // Include delivery tracking fields diff --git a/src/server/migrations/113_add_xeddsa_signed.ts b/src/server/migrations/113_add_xeddsa_signed.ts new file mode 100644 index 000000000..3744e168b --- /dev/null +++ b/src/server/migrations/113_add_xeddsa_signed.ts @@ -0,0 +1,38 @@ +/** + * Migration 113: Add xeddsaSigned column to messages table + * + * Adds a boolean column recording whether a received broadcast carried a + * cryptographically verified XEdDSA signature (Meshtastic firmware 2.8+, + * MeshPacket field 22 `xeddsa_signed`). Follows the same additive, nullable + * pattern as the existing viaMqtt / viaStoreForward columns. + */ +import type { Database } from 'better-sqlite3'; + +// SQLite migration +export const migration = { + up(db: Database) { + try { + db.exec(`ALTER TABLE messages ADD COLUMN "xeddsaSigned" INTEGER`); + } catch (e: any) { + if (!e.message?.includes('duplicate column')) throw e; + } + }, +}; + +// PostgreSQL migration +export async function runMigration113Postgres(client: any): Promise { + await client.query(` + ALTER TABLE messages ADD COLUMN IF NOT EXISTS "xeddsaSigned" BOOLEAN + `); +} + +// MySQL migration +export async function runMigration113Mysql(pool: any): Promise { + const [rows] = await pool.query(` + SELECT COLUMN_NAME FROM information_schema.COLUMNS + WHERE TABLE_NAME = 'messages' AND COLUMN_NAME = 'xeddsaSigned' + `); + if ((rows as any[]).length === 0) { + await pool.query(`ALTER TABLE messages ADD COLUMN \`xeddsaSigned\` BOOLEAN`); + } +} diff --git a/src/server/protobufLoader.ts b/src/server/protobufLoader.ts index 211eeb3c1..290694201 100644 --- a/src/server/protobufLoader.ts +++ b/src/server/protobufLoader.ts @@ -87,6 +87,13 @@ export interface MeshPacket { encrypted?: Uint8Array; /** Transport mechanism - see TransportMechanism enum in constants/meshtastic.ts */ transportMechanism?: number; + /** + * XEdDSA packet signing (firmware 2.8+). True when the receiving node + * cryptographically verified the broadcast's signature (MeshPacket field 22, + * `xeddsa_signed`). Absent/false on packets from pre-2.8 firmware or unsigned + * traffic. + */ + xeddsaSigned?: boolean; } export interface Data { diff --git a/src/server/routes/unifiedRoutes.ts b/src/server/routes/unifiedRoutes.ts index 345d879b7..2c2652074 100644 --- a/src/server/routes/unifiedRoutes.ts +++ b/src/server/routes/unifiedRoutes.ts @@ -426,6 +426,8 @@ router.get('/messages', async (req: Request, res: Response) => { rxRssi: number | null; rxTime: number | null; timestamp: number; + /** Broadcast carried a verified XEdDSA signature on this reception (firmware 2.8+). */ + xeddsaSigned: boolean | null; }; type Merged = { dedupKey: string; @@ -539,6 +541,8 @@ router.get('/messages', async (req: Request, res: Response) => { rxRssi: m.rssi ?? null, rxTime: null, timestamp: m.timestamp, + // MeshCore has no XEdDSA packet signing concept. + xeddsaSigned: null, }; // MeshCore ids are unique within a source; we don't cross-source-dedup // MeshCore (each radio is a distinct receiver), so key by source + id. @@ -763,6 +767,7 @@ router.get('/messages', async (req: Request, res: Response) => { rxRssi: m.rxRssi ?? null, rxTime, timestamp: m.timestamp, + xeddsaSigned: (m as any).xeddsaSigned ?? null, }; const existing = merged.get(dedupKey); diff --git a/src/server/server.ts b/src/server/server.ts index 582e44e2b..6691b88c3 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -2514,6 +2514,7 @@ function transformDbMessageToMeshMessage(msg: DbMessage): MeshMessage { replyId: msg.replyId ?? undefined, emoji: msg.emoji ?? undefined, viaMqtt: Boolean((msg as any).viaMqtt), + xeddsaSigned: (msg as any).xeddsaSigned ? true : undefined, rxSnr: msg.rxSnr ?? undefined, rxRssi: msg.rxRssi ?? undefined, requestId: (msg as any).requestId, diff --git a/src/services/database.ts b/src/services/database.ts index 5c7169715..295c60684 100644 --- a/src/services/database.ts +++ b/src/services/database.ts @@ -135,6 +135,8 @@ export interface DbMessage { replyId?: number; emoji?: number; viaMqtt?: boolean; + /** Broadcast carried a verified XEdDSA signature (firmware 2.8+). */ + xeddsaSigned?: boolean; rxSnr?: number; rxRssi?: number; createdAt: number; @@ -3079,6 +3081,7 @@ class DatabaseService { replyId: msg.replyId ?? undefined, emoji: msg.emoji ?? undefined, viaMqtt: msg.viaMqtt ?? undefined, + xeddsaSigned: msg.xeddsaSigned ?? undefined, rxSnr: msg.rxSnr ?? undefined, rxRssi: msg.rxRssi ?? undefined, ackFailed: msg.ackFailed ?? undefined, @@ -8741,6 +8744,7 @@ class DatabaseService { replyId: result.replyId ?? undefined, emoji: result.emoji ?? undefined, viaMqtt: result.viaMqtt ?? undefined, + xeddsaSigned: result.xeddsaSigned ?? undefined, rxSnr: result.rxSnr ?? undefined, rxRssi: result.rxRssi ?? undefined, ackFailed: result.ackFailed ?? undefined, diff --git a/src/types/message.ts b/src/types/message.ts index 87262f309..9a29ce139 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -37,6 +37,7 @@ export interface MeshMessage { emoji?: number; viaMqtt?: boolean; // Whether message was received via MQTT bridge viaStoreForward?: boolean; // Whether message was received via Store & Forward replay + xeddsaSigned?: boolean; // Broadcast carried a cryptographically verified XEdDSA signature (firmware 2.8+) rxSnr?: number; // SNR of received packet (for direct messages) rxRssi?: number; // RSSI of received packet (for direct messages) // Enhanced delivery tracking