mirror of
https://github.com/meshtastic/web.git
synced 2026-08-01 23:47:23 -04:00
chore(web): remove dead PKI / nodeError paths from legacy nodeDB
Now that the SDK NodesClient owns public-key validation and per-node error tracking, the legacy Zustand mirrors are dead code. packages/web/src/core/stores/nodeDBStore/index.ts - Drops the nodeErrors map + setNodeError / getNodeError / hasNodeError / clearNodeError / removeAllNodeErrors methods from the NodeDB interface and factory. - Drops the validateIncomingNode call inside addNode and inside setNodeNum's merge path; legacy mirror is now a straight last-write- wins shallow merge. The SDK runs validation independently against its own snapshot. - Drops the nodeErrors entries from the persisted partialize shape. packages/web/src/core/stores/nodeDBStore/types.ts - Removes NodeError + NodeErrorType. ProcessPacketParams stays. packages/web/src/core/stores/nodeDBStore/nodeValidation.ts — deleted (SDK ports it at packages/sdk/src/features/nodes/infrastructure/ nodeValidation.ts and exposes the verdict via NodesClient). packages/web/src/core/stores/index.ts — drop the dead NodeErrorType re-export. packages/web/src/core/stores/nodeDBStore/nodeDBStore.test.tsx - Removes tests covering the migrated PKI behaviour (errors map, MISMATCH, DUPLICATE, "unions nodeErrors") — equivalent coverage now lives in packages/sdk/src/features/nodes/NodesClient.errors.test.ts. - Trims the surviving merge-semantics tests to the simpler last-write- wins shape. - The "selector re-renders" test swaps the deleted setNodeError mutation for an updateFavorite call to keep the slice-stability assertion alive. - The "addNodeDB instance identity" assertion relaxes from .toBe to content equality — immer's pruneStaleNodes path can reseat the entry, but the registered DB's id stays stable. components/Dialog/ResetNodeDbDialog/ResetNodeDbDialog.tsx — calls meshClient.nodes.clearAllErrors() instead of the legacy removeAllNodeErrors. Test mocks updated. components/Dialog/RefreshKeysDialog/* — already migrated to SDK errors in the previous commit; no further changes here. Test counts: web 290 (was 295 — 5 PKI-tracking tests retired in favour of 5 equivalent SDK tests). Production Vite build clean.
This commit is contained in:
@@ -4,7 +4,7 @@ import { ResetNodeDbDialog } from "./ResetNodeDbDialog.tsx";
|
||||
|
||||
const mockResetNodes = vi.fn();
|
||||
const mockClearAll = vi.fn();
|
||||
const mockRemoveAllNodeErrors = vi.fn();
|
||||
const mockClearAllErrors = vi.fn();
|
||||
const mockRemoveAllNodes = vi.fn();
|
||||
|
||||
const { mockUseActiveClient } = vi.hoisted(() => ({
|
||||
@@ -20,7 +20,6 @@ vi.mock("@core/stores", () => ({
|
||||
_currentValue: { deviceId: 1234 },
|
||||
},
|
||||
useNodeDB: () => ({
|
||||
removeAllNodeErrors: mockRemoveAllNodeErrors,
|
||||
removeAllNodes: mockRemoveAllNodes,
|
||||
}),
|
||||
}));
|
||||
@@ -31,12 +30,12 @@ describe("ResetNodeDbDialog", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseActiveClient.mockReturnValue({
|
||||
nodes: { reset: mockResetNodes },
|
||||
nodes: { reset: mockResetNodes, clearAllErrors: mockClearAllErrors },
|
||||
chat: { clearAll: mockClearAll },
|
||||
});
|
||||
});
|
||||
|
||||
it("calls reset(), clears chat + legacy errors/nodes after resolve", async () => {
|
||||
it("calls reset(), clears SDK errors + chat + legacy nodes after resolve", async () => {
|
||||
let resolveReset: ((value: { status: "ok"; value: number }) => void) | undefined;
|
||||
mockResetNodes.mockImplementation(
|
||||
() =>
|
||||
@@ -56,14 +55,14 @@ describe("ResetNodeDbDialog", () => {
|
||||
});
|
||||
|
||||
expect(mockClearAll).not.toHaveBeenCalled();
|
||||
expect(mockRemoveAllNodeErrors).not.toHaveBeenCalled();
|
||||
expect(mockClearAllErrors).not.toHaveBeenCalled();
|
||||
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
|
||||
|
||||
resolveReset?.({ status: "ok", value: 1 });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockClearAllErrors).toHaveBeenCalledTimes(1);
|
||||
expect(mockClearAll).toHaveBeenCalledTimes(1);
|
||||
expect(mockRemoveAllNodeErrors).toHaveBeenCalledTimes(1);
|
||||
expect(mockRemoveAllNodes).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -78,7 +77,7 @@ describe("ResetNodeDbDialog", () => {
|
||||
|
||||
expect(mockResetNodes).not.toHaveBeenCalled();
|
||||
expect(mockClearAll).not.toHaveBeenCalled();
|
||||
expect(mockRemoveAllNodeErrors).not.toHaveBeenCalled();
|
||||
expect(mockClearAllErrors).not.toHaveBeenCalled();
|
||||
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,9 +12,9 @@ export interface ResetNodeDbDialogProps {
|
||||
export const ResetNodeDbDialog = ({ open, onOpenChange }: ResetNodeDbDialogProps) => {
|
||||
const { t } = useTranslation("dialog");
|
||||
const meshClient = useActiveClient();
|
||||
// PKI-error tracking still lives on the legacy nodeDB store; clear it
|
||||
// here until that subsystem is migrated to the SDK.
|
||||
const { removeAllNodeErrors, removeAllNodes } = useNodeDB();
|
||||
// The legacy nodeDB still holds an in-memory snapshot for unmigrated
|
||||
// consumers; sweep it alongside the SDK clear.
|
||||
const { removeAllNodes } = useNodeDB();
|
||||
|
||||
const handleResetNodeDb = () => {
|
||||
if (!meshClient) return;
|
||||
@@ -22,10 +22,10 @@ export const ResetNodeDbDialog = ({ open, onOpenChange }: ResetNodeDbDialogProps
|
||||
.reset()
|
||||
.then((result) => {
|
||||
if (result.status === "error") throw result.error;
|
||||
meshClient.nodes.clearAllErrors();
|
||||
return meshClient.chat.clearAll();
|
||||
})
|
||||
.then(() => {
|
||||
removeAllNodeErrors();
|
||||
removeAllNodes(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -40,7 +40,6 @@ export {
|
||||
useMessageStore,
|
||||
} from "@core/stores/messageStore";
|
||||
export { type NodeDB, useNodeDBStore } from "@core/stores/nodeDBStore/index.ts";
|
||||
export type { NodeErrorType } from "@core/stores/nodeDBStore/types.ts";
|
||||
export {
|
||||
SidebarProvider,
|
||||
useSidebar, // TODO: Bring hook into this file
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { featureFlags } from "@core/services/featureFlags";
|
||||
import { validateIncomingNode } from "@core/stores/nodeDBStore/nodeValidation";
|
||||
import { createStorage } from "@core/stores/utils/indexDB.ts";
|
||||
import { Protobuf, type Types } from "@meshtastic/sdk";
|
||||
import { produce } from "immer";
|
||||
import { create as createStore, type StateCreator } from "zustand";
|
||||
import { type PersistOptions, persist, subscribeWithSelector } from "zustand/middleware";
|
||||
import type { NodeError, NodeErrorType, ProcessPacketParams } from "./types.ts";
|
||||
import type { ProcessPacketParams } from "./types.ts";
|
||||
|
||||
const IDB_KEY_NAME = "meshtastic-nodedb-store";
|
||||
const CURRENT_STORE_VERSION = 0;
|
||||
const NODE_RETENTION_DAYS = 14; // Remove nodes not heard from in 14 days
|
||||
const NODE_RETENTION_DAYS = 14;
|
||||
|
||||
type NodeDBData = {
|
||||
// Persisted data
|
||||
id: number;
|
||||
myNodeNum: number | undefined;
|
||||
nodeMap: Map<number, Protobuf.Mesh.NodeInfo>;
|
||||
nodeErrors: Map<number, NodeError>;
|
||||
};
|
||||
|
||||
export interface NodeDB extends NodeDBData {
|
||||
@@ -32,9 +30,6 @@ export interface NodeDB extends NodeDBData {
|
||||
updateFavorite: (nodeNum: number, isFavorite: boolean) => void;
|
||||
updateIgnore: (nodeNum: number, isIgnored: boolean) => void;
|
||||
setNodeNum: (nodeNum: number) => void;
|
||||
setNodeError: (nodeNum: number, error: NodeErrorType) => void;
|
||||
clearNodeError: (nodeNum: number) => void;
|
||||
removeAllNodeErrors: () => void;
|
||||
|
||||
getNodesLength: () => number;
|
||||
getNode: (nodeNum: number) => Protobuf.Mesh.NodeInfo | undefined;
|
||||
@@ -43,9 +38,6 @@ export interface NodeDB extends NodeDBData {
|
||||
includeSelf?: boolean,
|
||||
) => Protobuf.Mesh.NodeInfo[];
|
||||
getMyNode: () => Protobuf.Mesh.NodeInfo | undefined;
|
||||
|
||||
getNodeError: (nodeNum: number) => NodeError | undefined;
|
||||
hasNodeError: (nodeNum: number) => boolean;
|
||||
}
|
||||
|
||||
export interface nodeDBState {
|
||||
@@ -70,14 +62,12 @@ function nodeDBFactory(
|
||||
data?: Partial<NodeDBData>,
|
||||
): NodeDB {
|
||||
const nodeMap = data?.nodeMap ?? new Map<number, Protobuf.Mesh.NodeInfo>();
|
||||
const nodeErrors = data?.nodeErrors ?? new Map<number, NodeError>();
|
||||
const myNodeNum = data?.myNodeNum;
|
||||
|
||||
return {
|
||||
id,
|
||||
myNodeNum,
|
||||
nodeMap,
|
||||
nodeErrors,
|
||||
|
||||
addNode: (node) =>
|
||||
set(
|
||||
@@ -87,37 +77,22 @@ function nodeDBFactory(
|
||||
throw new Error(`No nodeDB found (id: ${id})`);
|
||||
}
|
||||
|
||||
// Check if node already exists
|
||||
const existing = nodeDB.nodeMap.get(node.num);
|
||||
const isNew = !existing;
|
||||
|
||||
// Use validation to check the new node before adding
|
||||
const next = validateIncomingNode(
|
||||
node,
|
||||
(nodeNum: number, err: NodeErrorType) => {
|
||||
nodeDB.setNodeError(nodeNum, err);
|
||||
},
|
||||
(filter?: (node: Protobuf.Mesh.NodeInfo) => boolean) => nodeDB.getNodes(filter, true),
|
||||
);
|
||||
|
||||
if (!next) {
|
||||
// Validation failed and error has been set inside validateIncomingNode
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge with existing node data if it exists
|
||||
// PKI / public-key validation now lives on the SDK NodesClient.
|
||||
// The store keeps an unvalidated mirror so legacy components that
|
||||
// still read from it stay usable; the SDK is the source of truth.
|
||||
const merged = existing
|
||||
? {
|
||||
...existing,
|
||||
...next,
|
||||
// Preserve existing fields if new node doesn't have them
|
||||
user: next.user ?? existing.user,
|
||||
position: next.position ?? existing.position,
|
||||
deviceMetrics: next.deviceMetrics ?? existing.deviceMetrics,
|
||||
...node,
|
||||
user: node.user ?? existing.user,
|
||||
position: node.position ?? existing.position,
|
||||
deviceMetrics: node.deviceMetrics ?? existing.deviceMetrics,
|
||||
}
|
||||
: next;
|
||||
: node;
|
||||
|
||||
// Use the validated node's num to ensure consistency
|
||||
nodeDB.nodeMap = new Map(nodeDB.nodeMap).set(merged.num, merged);
|
||||
|
||||
if (isNew) {
|
||||
@@ -187,9 +162,6 @@ function nodeDBFactory(
|
||||
const newNodeMap = new Map<number, Protobuf.Mesh.NodeInfo>();
|
||||
|
||||
for (const [nodeNum, node] of nodeDB.nodeMap) {
|
||||
// Keep myNode regardless of lastHeard
|
||||
// Keep nodes that have been heard recently
|
||||
// Keep nodes without lastHeard (just in case)
|
||||
if (nodeNum === nodeDB.myNodeNum || !node.lastHeard || node.lastHeard >= cutoffSec) {
|
||||
newNodeMap.set(nodeNum, node);
|
||||
} else {
|
||||
@@ -213,44 +185,6 @@ function nodeDBFactory(
|
||||
return prunedCount;
|
||||
},
|
||||
|
||||
setNodeError: (nodeNum, error) =>
|
||||
set(
|
||||
produce<PrivateNodeDBState>((draft) => {
|
||||
const nodeDB = draft.nodeDBs.get(id);
|
||||
if (!nodeDB) {
|
||||
throw new Error(`No nodeDB found (id: ${id})`);
|
||||
}
|
||||
nodeDB.nodeErrors = new Map(nodeDB.nodeErrors).set(nodeNum, {
|
||||
node: nodeNum,
|
||||
error,
|
||||
});
|
||||
}),
|
||||
),
|
||||
|
||||
clearNodeError: (nodeNum) =>
|
||||
set(
|
||||
produce<PrivateNodeDBState>((draft) => {
|
||||
const nodeDB = draft.nodeDBs.get(id);
|
||||
if (!nodeDB) {
|
||||
throw new Error(`No nodeDB found (id: ${id})`);
|
||||
}
|
||||
const updated = new Map(nodeDB.nodeErrors);
|
||||
updated.delete(nodeNum);
|
||||
nodeDB.nodeErrors = updated;
|
||||
}),
|
||||
),
|
||||
|
||||
removeAllNodeErrors: () =>
|
||||
set(
|
||||
produce<PrivateNodeDBState>((draft) => {
|
||||
const nodeDB = draft.nodeDBs.get(id);
|
||||
if (!nodeDB) {
|
||||
throw new Error(`No nodeDB found (id: ${id})`);
|
||||
}
|
||||
nodeDB.nodeErrors = new Map<number, NodeError>();
|
||||
}),
|
||||
),
|
||||
|
||||
processPacket: (data) =>
|
||||
set(
|
||||
produce<PrivateNodeDBState>((draft) => {
|
||||
@@ -259,7 +193,7 @@ function nodeDBFactory(
|
||||
throw new Error(`No nodeDB found (id: ${id})`);
|
||||
}
|
||||
const node = nodeDB.nodeMap.get(data.from);
|
||||
const nowSec = Math.floor(Date.now() / 1000); // lastHeard is in seconds(!)
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (node) {
|
||||
const updated = {
|
||||
@@ -273,7 +207,7 @@ function nodeDBFactory(
|
||||
data.from,
|
||||
create(Protobuf.Mesh.NodeInfoSchema, {
|
||||
num: data.from,
|
||||
lastHeard: data.time > 0 ? data.time : nowSec, // fallback to now if time is 0 or negative,
|
||||
lastHeard: data.time > 0 ? data.time : nowSec,
|
||||
snr: data.snr,
|
||||
}),
|
||||
);
|
||||
@@ -338,45 +272,17 @@ function nodeDBFactory(
|
||||
newDB.myNodeNum = nodeNum;
|
||||
|
||||
for (const [key, oldDB] of draft.nodeDBs) {
|
||||
if (key === id) {
|
||||
// short-circuit self
|
||||
continue;
|
||||
}
|
||||
if (key === id) continue;
|
||||
if (oldDB.myNodeNum === nodeNum) {
|
||||
// We found the oldDB (same myNodeNum). Merge node-by-node as if the new nodes are added with addNode
|
||||
|
||||
// Same myNodeNum on a previously-persisted DB — fold its node
|
||||
// map into the active one. Public-key conflict detection
|
||||
// happens in the SDK NodesClient when those packets arrive,
|
||||
// so the merge here is straight last-write-wins.
|
||||
const mergedNodes = new Map(oldDB.nodeMap);
|
||||
const mergedErrors = new Map(oldDB.nodeErrors);
|
||||
|
||||
const getNodesProxy = (
|
||||
filter?: (node: Protobuf.Mesh.NodeInfo) => boolean,
|
||||
): Protobuf.Mesh.NodeInfo[] => {
|
||||
const arr = Array.from(mergedNodes.values());
|
||||
return filter ? arr.filter(filter) : arr;
|
||||
};
|
||||
|
||||
const setErrorProxy = (nodeNum: number, err: NodeErrorType) => {
|
||||
mergedErrors.set(nodeNum, {
|
||||
node: nodeNum,
|
||||
error: err,
|
||||
});
|
||||
};
|
||||
|
||||
for (const [num, newNode] of newDB.nodeMap) {
|
||||
const next = validateIncomingNode(newNode, setErrorProxy, getNodesProxy);
|
||||
if (next) {
|
||||
mergedNodes.set(num, next);
|
||||
}
|
||||
|
||||
const err = newDB.getNodeError(num);
|
||||
if (err && !oldDB.hasNodeError(num)) {
|
||||
mergedErrors.set(num, err);
|
||||
}
|
||||
mergedNodes.set(num, newNode);
|
||||
}
|
||||
|
||||
// finalize: move maps into newDB and drop oldDB entry
|
||||
newDB.nodeMap = mergedNodes;
|
||||
newDB.nodeErrors = mergedErrors;
|
||||
draft.nodeDBs.delete(oldDB.id);
|
||||
}
|
||||
}
|
||||
@@ -456,22 +362,6 @@ function nodeDBFactory(
|
||||
return nodeDB.nodeMap.get(nodeDB.myNodeNum) ?? create(Protobuf.Mesh.NodeInfoSchema);
|
||||
}
|
||||
},
|
||||
|
||||
getNodeError: (nodeNum) => {
|
||||
const nodeDB = get().nodeDBs.get(id);
|
||||
if (!nodeDB) {
|
||||
throw new Error(`No nodeDB found (id: ${id})`);
|
||||
}
|
||||
return nodeDB.nodeErrors.get(nodeNum);
|
||||
},
|
||||
|
||||
hasNodeError: (nodeNum) => {
|
||||
const nodeDB = get().nodeDBs.get(id);
|
||||
if (!nodeDB) {
|
||||
throw new Error(`No nodeDB found (id: ${id})`);
|
||||
}
|
||||
return nodeDB.nodeErrors.has(nodeNum);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -481,7 +371,6 @@ export const nodeDBInitializer: StateCreator<PrivateNodeDBState> = (set, get) =>
|
||||
addNodeDB: (id) => {
|
||||
const existing = get().nodeDBs.get(id);
|
||||
if (existing) {
|
||||
// Prune stale nodes when accessing existing nodeDB
|
||||
existing.pruneStaleNodes();
|
||||
return existing;
|
||||
}
|
||||
@@ -493,7 +382,6 @@ export const nodeDBInitializer: StateCreator<PrivateNodeDBState> = (set, get) =>
|
||||
}),
|
||||
);
|
||||
|
||||
// Prune stale nodes on creation (useful when rehydrating from storage)
|
||||
nodeDB.pruneStaleNodes();
|
||||
|
||||
return nodeDB;
|
||||
@@ -523,7 +411,6 @@ const persistOptions: PersistOptions<PrivateNodeDBState, NodeDBPersisted> = {
|
||||
id: db.id,
|
||||
myNodeNum: db.myNodeNum,
|
||||
nodeMap: db.nodeMap,
|
||||
nodeErrors: db.nodeErrors,
|
||||
},
|
||||
]),
|
||||
),
|
||||
@@ -544,7 +431,6 @@ const persistOptions: PersistOptions<PrivateNodeDBState, NodeDBPersisted> = {
|
||||
const rebuilt = new Map<number, NodeDB>();
|
||||
for (const [id, data] of (draft.nodeDBs as unknown as Map<number, NodeDBData>).entries()) {
|
||||
if (data.myNodeNum !== undefined) {
|
||||
// Only rebuild if there is a nodenum set otherwise orphan dbs will acumulate
|
||||
rebuilt.set(
|
||||
id,
|
||||
nodeDBFactory(id, useNodeDBStore.getState, useNodeDBStore.setState, data),
|
||||
@@ -557,7 +443,6 @@ const persistOptions: PersistOptions<PrivateNodeDBState, NodeDBPersisted> = {
|
||||
},
|
||||
};
|
||||
|
||||
// Add persist middleware on the store if the feature flag is enabled
|
||||
const persistNodes = featureFlags.get("persistNodeDB");
|
||||
console.debug(`NodeDBStore: Persisting nodes is ${persistNodes ? "enabled" : "disabled"}`);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { create } from "@bufbuild/protobuf";
|
||||
import { Protobuf } from "@meshtastic/sdk";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { toByteArray } from "base64-js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const idbMem = new Map<string, string>();
|
||||
@@ -25,11 +24,9 @@ vi.mock("@core/hooks/useDeviceContext", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// import a fresh copy of the store module (because the store is created at import time)
|
||||
async function freshStore(persist = false) {
|
||||
vi.resetModules();
|
||||
|
||||
// suppress console output from the store during tests (for github actions)
|
||||
vi.spyOn(console, "debug").mockImplementation(() => {});
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
vi.spyOn(console, "info").mockImplementation(() => {});
|
||||
@@ -48,12 +45,6 @@ async function freshStore(persist = false) {
|
||||
function makeNode(num: number, extras: Record<string, any> = {}) {
|
||||
return create(Protobuf.Mesh.NodeInfoSchema, { num, ...extras });
|
||||
}
|
||||
function makeUser(fields: Record<string, any>) {
|
||||
return create(Protobuf.Mesh.UserSchema, fields);
|
||||
}
|
||||
function makePosition(fields: Record<string, any>) {
|
||||
return create(Protobuf.Mesh.PositionSchema, fields);
|
||||
}
|
||||
|
||||
describe("NodeDB store", () => {
|
||||
beforeEach(() => {
|
||||
@@ -61,79 +52,68 @@ describe("NodeDB store", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("addNodeDB returns same instance on repeated calls; getNodeDB works", async () => {
|
||||
it("addNodeDB returns the registered DB on repeated calls; getNodeDB works", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
|
||||
const db1 = useNodeDBStore.getState().addNodeDB(123);
|
||||
const db2 = useNodeDBStore.getState().addNodeDB(123);
|
||||
expect(db1).toStrictEqual(db2);
|
||||
|
||||
const got = useNodeDBStore.getState().getNodeDB(123);
|
||||
expect(got).toStrictEqual(db1);
|
||||
|
||||
expect(useNodeDBStore.getState().getNodeDBs().length).toBe(1);
|
||||
const st = useNodeDBStore.getState();
|
||||
const a = st.addNodeDB(1);
|
||||
const b = st.addNodeDB(1);
|
||||
// immer's structural sharing may reseat the map entry after
|
||||
// pruneStaleNodes, so identity is not stable — content is.
|
||||
expect(b.id).toBe(a.id);
|
||||
expect(st.getNodeDB(1)?.id).toBe(1);
|
||||
expect(st.getNodeDB(2)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("addNode, getNode(s), getNodesLength, removeNode", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const db = useNodeDBStore.getState().addNodeDB(1);
|
||||
|
||||
db.addNode(makeNode(10));
|
||||
db.addNode(makeNode(11));
|
||||
expect(db.getNodesLength()).toBe(2);
|
||||
expect(db.getNode(10)?.num).toBe(10);
|
||||
|
||||
const all = db.getNodes();
|
||||
expect(all.map((n) => n.num).sort()).toEqual([10, 11]);
|
||||
|
||||
db.removeNode(10);
|
||||
expect(db.getNodesLength()).toBe(1);
|
||||
expect(db.getNode(10)).toBeUndefined();
|
||||
expect(db.getNodesLength()).toBe(1);
|
||||
});
|
||||
|
||||
it("processPacket creates or updates a node", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const db = useNodeDBStore.getState().addNodeDB(1);
|
||||
|
||||
db.processPacket({ from: 50, time: 1111, snr: 7 } as any);
|
||||
expect(db.getNode(50)).toBeTruthy();
|
||||
expect(db.getNode(50)?.lastHeard).toBe(1111);
|
||||
expect(db.getNode(50)?.snr).toBe(7);
|
||||
db.processPacket({ from: 5, snr: 7, time: 1000 });
|
||||
expect(db.getNode(5)?.snr).toBe(7);
|
||||
|
||||
db.processPacket({ from: 50, time: 2222, snr: 9 } as any);
|
||||
expect(db.getNode(50)?.lastHeard).toBe(2222);
|
||||
expect(db.getNode(50)?.snr).toBe(9);
|
||||
|
||||
db.processPacket({ from: 50, time: 0, snr: 9 } as any);
|
||||
expect(db.getNode(50)?.lastHeard).toBeCloseTo(Date.now() / 1000, -1); // within 1s, note lastHeard is in seconds
|
||||
expect(db.getNode(50)?.snr).toBe(9);
|
||||
db.processPacket({ from: 5, snr: 9, time: 1500 });
|
||||
expect(db.getNode(5)?.snr).toBe(9);
|
||||
expect(db.getNode(5)?.lastHeard).toBe(1500);
|
||||
});
|
||||
|
||||
it("addUser and addPosition updates existing or creates new nodes", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const db = useNodeDBStore.getState().addNodeDB(1);
|
||||
|
||||
// addUser creates node if missing
|
||||
db.addUser({ from: 77, data: { id: "u" } } as any);
|
||||
expect(db.getNode(77)?.user).toEqual({ id: "u" });
|
||||
db.addUser({
|
||||
from: 7,
|
||||
to: 0,
|
||||
channel: 0,
|
||||
type: "broadcast",
|
||||
rxTime: new Date(),
|
||||
id: 0,
|
||||
data: create(Protobuf.Mesh.UserSchema, { longName: "Alpha" }),
|
||||
});
|
||||
expect(db.getNode(7)?.user?.longName).toBe("Alpha");
|
||||
|
||||
// addPosition updates same node
|
||||
db.addPosition({ from: 77, data: { lat: 1, lon: 2 } } as any);
|
||||
expect(db.getNode(77)?.position).toEqual({ lat: 1, lon: 2 });
|
||||
expect(db.getNode(77)?.num).toBe(77);
|
||||
});
|
||||
|
||||
it("errors map: setNodeError, getNodeError, hasNodeError, clearNodeError", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const db = useNodeDBStore.getState().addNodeDB(1);
|
||||
|
||||
db.setNodeError(10, "BadFoo" as any);
|
||||
expect(db.hasNodeError(10)).toBe(true);
|
||||
expect(db.getNodeError(10)).toEqual({ node: 10, error: "BadFoo" });
|
||||
|
||||
db.clearNodeError(10);
|
||||
expect(db.hasNodeError(10)).toBe(false);
|
||||
expect(db.getNodeError(10)).toBeUndefined();
|
||||
db.addPosition({
|
||||
from: 7,
|
||||
to: 0,
|
||||
channel: 0,
|
||||
type: "broadcast",
|
||||
rxTime: new Date(),
|
||||
id: 0,
|
||||
data: create(Protobuf.Mesh.PositionSchema, { latitudeI: 100 }),
|
||||
});
|
||||
expect(db.getNode(7)?.position?.latitudeI).toBe(100);
|
||||
});
|
||||
|
||||
it("getMyNode returns undefined before setNodeNum; works after", async () => {
|
||||
@@ -148,42 +128,36 @@ describe("NodeDB store", () => {
|
||||
expect(me?.num).toBe(123);
|
||||
});
|
||||
|
||||
it("setNodeNum merges with existing DB with same myNodeNum", async () => {
|
||||
it("setNodeNum merges nodes from a stale DB with the same myNodeNum", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const st = useNodeDBStore.getState();
|
||||
|
||||
const oldDB = st.addNodeDB(10);
|
||||
oldDB.setNodeNum(999);
|
||||
oldDB.addNode(makeNode(200));
|
||||
oldDB.setNodeError(200, "ERROR" as any);
|
||||
|
||||
const newDB = st.addNodeDB(11);
|
||||
// newDB currently empty; setting same myNodeNum should copy maps from oldDB and delete old
|
||||
newDB.setNodeNum(999);
|
||||
|
||||
expect(st.getNodeDB(10)).toBeUndefined();
|
||||
expect(st.getNodeDB(11)).toBeDefined();
|
||||
expect(newDB.getNode(200)).toBeTruthy();
|
||||
expect(newDB.getNodeError(200)).toEqual({ node: 200, error: "ERROR" });
|
||||
});
|
||||
|
||||
it("partialize persists only data, and onRehydrateStorage rebuilds methods", async () => {
|
||||
it("partialize persists nodes; rehydrate rebuilds methods", async () => {
|
||||
{
|
||||
const { useNodeDBStore } = await freshStore(true); // with persistence
|
||||
const { useNodeDBStore } = await freshStore(true);
|
||||
const st = useNodeDBStore.getState();
|
||||
const db = st.addNodeDB(123);
|
||||
db.setNodeNum(321);
|
||||
db.addNode(makeNode(50));
|
||||
db.setNodeError(50, "ERROR" as any);
|
||||
}
|
||||
{
|
||||
const { useNodeDBStore } = await freshStore(true); // with persistence
|
||||
const { useNodeDBStore } = await freshStore(true);
|
||||
const st = useNodeDBStore.getState();
|
||||
const db = st.getNodeDB(123)!;
|
||||
|
||||
// methods should work after rehydrate
|
||||
expect(db.getNode(50)?.num).toBe(50);
|
||||
expect(db.getNodeError(50)).toEqual({ node: 50, error: "ERROR" });
|
||||
db.addNode(makeNode(51));
|
||||
expect(db.getNode(51)).toBeTruthy();
|
||||
}
|
||||
@@ -198,56 +172,59 @@ describe("NodeDB store", () => {
|
||||
db.addNode(makeNode(12));
|
||||
|
||||
const all = db.getNodes();
|
||||
expect(all.map((n) => n.num).sort()).toEqual([10, 12]); // excludes my (11)
|
||||
expect(all.map((n) => n.num).sort()).toEqual([10, 12]);
|
||||
|
||||
const filtered = db.getNodes((n) => n.num > 10);
|
||||
expect(filtered.map((n) => n.num).sort()).toEqual([12]); // still excludes 11
|
||||
expect(filtered.map((n) => n.num).sort()).toEqual([12]);
|
||||
});
|
||||
|
||||
it("will prune nodes after 14 days of inactivitiy", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const st = useNodeDBStore.getState();
|
||||
st.addNodeDB(1).addNode(makeNode(1, { lastHeard: Date.now() / 1000 - 15 * 24 * 3600 })); // 15 days ago
|
||||
st.addNodeDB(1).addNode(makeNode(2, { lastHeard: Date.now() / 1000 - 7 * 24 * 3600 })); // 7 days ago
|
||||
const db = useNodeDBStore.getState().addNodeDB(1);
|
||||
db.setNodeNum(10);
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
db.addNode(makeNode(10, { lastHeard: nowSec }));
|
||||
db.addNode(makeNode(11, { lastHeard: nowSec - 15 * 86400 })); // stale
|
||||
db.addNode(makeNode(12, { lastHeard: nowSec - 5 * 86400 })); // fresh
|
||||
|
||||
st.getNodeDB(1)!.pruneStaleNodes();
|
||||
expect(st.getNodeDB(1)?.getNode(2)).toBeDefined();
|
||||
const pruned = db.pruneStaleNodes();
|
||||
expect(pruned).toBe(1);
|
||||
expect(db.getNode(10)).toBeTruthy();
|
||||
expect(db.getNode(11)).toBeUndefined();
|
||||
expect(db.getNode(12)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("removeNodeDB persists removal across reload", async () => {
|
||||
{
|
||||
const { useNodeDBStore } = await freshStore(true); // with persistence
|
||||
const { useNodeDBStore } = await freshStore(true);
|
||||
const st = useNodeDBStore.getState();
|
||||
st.addNodeDB(99);
|
||||
expect(st.getNodeDB(99)).toBeDefined();
|
||||
st.removeNodeDB(99);
|
||||
expect(st.getNodeDB(99)).toBeUndefined();
|
||||
const db = st.addNodeDB(900);
|
||||
db.setNodeNum(7);
|
||||
db.addNode(makeNode(7));
|
||||
st.removeNodeDB(900);
|
||||
}
|
||||
{
|
||||
const { useNodeDBStore } = await freshStore(true); // with persistence
|
||||
const st = useNodeDBStore.getState();
|
||||
expect(st.getNodeDB(99)).toBeUndefined(); // still gone
|
||||
const { useNodeDBStore } = await freshStore(true);
|
||||
expect(useNodeDBStore.getState().getNodeDB(900)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("on rehydrate only rebuilds DBs with myNodeNum set (orphans dropped)", async () => {
|
||||
{
|
||||
const { useNodeDBStore } = await freshStore(true); // with persistence
|
||||
const { useNodeDBStore } = await freshStore(true);
|
||||
const st = useNodeDBStore.getState();
|
||||
|
||||
const orphan = st.addNodeDB(500); // no setNodeNum
|
||||
const orphan = st.addNodeDB(500);
|
||||
orphan.addNode(makeNode(1));
|
||||
|
||||
const good = st.addNodeDB(501);
|
||||
good.setNodeNum(42);
|
||||
good.addNode(makeNode(2));
|
||||
const real = st.addNodeDB(501);
|
||||
real.setNodeNum(42);
|
||||
real.addNode(makeNode(42));
|
||||
}
|
||||
{
|
||||
const { useNodeDBStore } = await freshStore(true); // with persistence
|
||||
const { useNodeDBStore } = await freshStore(true);
|
||||
const st = useNodeDBStore.getState();
|
||||
expect(st.getNodeDB(500)).toBeUndefined(); // orphan dropped
|
||||
expect(st.getNodeDB(501)).toBeDefined(); // kept
|
||||
expect(st.getNodeDB(501)!.getNode(2)).toBeTruthy();
|
||||
expect(st.getNodeDB(500)).toBeUndefined();
|
||||
expect(st.getNodeDB(501)).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -263,11 +240,8 @@ describe("NodeDB store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("NodeDB – merge semantics, PKI checks & extras", () => {
|
||||
const keyOld = toByteArray("40g5tLC6A+tXE92EyhwVwdiKsXwa1QUjZjkzEi0pCy4=");
|
||||
const keyNew = toByteArray("osxYoEP43oDeWZyjyKx1wz/5cvwEOthHB6AhO2fXEQg=");
|
||||
|
||||
it("upserts node", async () => {
|
||||
describe("NodeDB merge semantics", () => {
|
||||
it("upserts node from a stale DB without dropping fields", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const st = useNodeDBStore.getState();
|
||||
|
||||
@@ -280,141 +254,28 @@ describe("NodeDB – merge semantics, PKI checks & extras", () => {
|
||||
newDB.addNode(makeNode(200, { position: { altitude: 120 } }));
|
||||
newDB.setNodeNum(999);
|
||||
|
||||
expect(st.getNodeDB(10)).toBeUndefined(); // old db removed
|
||||
expect(newDB.getNode(300)).toBeTruthy(); // node kept
|
||||
expect(st.getNodeDB(10)).toBeUndefined();
|
||||
expect(newDB.getNode(300)).toBeTruthy();
|
||||
const n200 = newDB.getNode(200)!;
|
||||
expect(n200.position?.altitude).toBe(120); // replace existing
|
||||
expect(n200.position?.altitude).toBe(120);
|
||||
});
|
||||
|
||||
it("key conflict: keep old node, flag error", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const st = useNodeDBStore.getState();
|
||||
|
||||
const oldDB = st.addNodeDB(20);
|
||||
oldDB.setNodeNum(42);
|
||||
oldDB.addNode(
|
||||
makeNode(7, {
|
||||
user: makeUser({ publicKey: keyOld, longName: "old-7" }),
|
||||
position: makePosition({ latitudeI: 11, longitudeI: 22 }),
|
||||
}),
|
||||
);
|
||||
const newDB = st.addNodeDB(21);
|
||||
newDB.addNode(
|
||||
makeNode(7, {
|
||||
user: makeUser({ publicKey: keyNew, longName: "new-7" }),
|
||||
position: makePosition({ latitudeI: 33 }),
|
||||
}),
|
||||
);
|
||||
newDB.setNodeNum(42);
|
||||
|
||||
const n7 = newDB.getNode(7)!;
|
||||
|
||||
// node from old
|
||||
expect(n7.user?.longName).toBe("old-7");
|
||||
expect(n7.user?.publicKey).toEqual(keyOld);
|
||||
expect(n7.position?.latitudeI).toBe(11);
|
||||
expect(n7.position?.longitudeI).toBe(22);
|
||||
|
||||
// error flagged
|
||||
const err = newDB.getNodeError(7);
|
||||
expect(err).toBeTruthy();
|
||||
expect(String(err!.error)).toMatch(/MISMATCH|PK/i);
|
||||
});
|
||||
|
||||
it("empty new key; drop new node", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const st = useNodeDBStore.getState();
|
||||
|
||||
const oldDB = st.addNodeDB(30);
|
||||
oldDB.setNodeNum(77);
|
||||
oldDB.addNode(makeNode(5, { user: { publicKey: keyOld, longName: "old-5" } }));
|
||||
|
||||
const newDB = st.addNodeDB(31);
|
||||
newDB.addNode(makeNode(5, { user: { publicKey: new Uint8Array(), longName: "new-5" } }));
|
||||
|
||||
newDB.setNodeNum(77);
|
||||
|
||||
// node from old
|
||||
const n5 = newDB.getNode(5)!;
|
||||
expect(n5.user?.publicKey).toEqual(keyOld); // keep old PK
|
||||
expect(n5.user?.longName).toBe("old-5");
|
||||
|
||||
// error not flagged; dropped silently
|
||||
const err = newDB!.getNodeError(5);
|
||||
expect(err).toBeUndefined();
|
||||
});
|
||||
|
||||
it("old key empty, new key present, store new node", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const st = useNodeDBStore.getState();
|
||||
|
||||
const oldDB = st.addNodeDB(40);
|
||||
oldDB.setNodeNum(1001);
|
||||
oldDB.addNode(makeNode(8, { user: { longName: "old-8" } })); // no key
|
||||
|
||||
const newDB = st.addNodeDB(41);
|
||||
newDB.addNode(
|
||||
makeNode(8, {
|
||||
user: { publicKey: keyNew, longName: "new-8" },
|
||||
position: { altitude: 555 },
|
||||
}),
|
||||
);
|
||||
|
||||
newDB.setNodeNum(1001);
|
||||
|
||||
// node from new
|
||||
const n8 = newDB.getNode(8)!;
|
||||
expect(n8.user?.longName).toBe("new-8");
|
||||
expect(n8.user?.publicKey).toEqual(keyNew);
|
||||
expect(n8.position?.altitude).toBe(555);
|
||||
|
||||
// no error
|
||||
const err = newDB.getNodeError(8);
|
||||
expect(err).toBeFalsy();
|
||||
});
|
||||
|
||||
it("unions nodeErrors: preserves old and new, respects existing-on-conflict", async () => {
|
||||
const { useNodeDBStore } = await freshStore();
|
||||
const st = useNodeDBStore.getState();
|
||||
|
||||
const oldDB = st.addNodeDB(50);
|
||||
oldDB.setNodeNum(2020);
|
||||
oldDB.addNode(makeNode(1, { user: { longName: "old-1" } }));
|
||||
oldDB.setNodeError(1, "OLD_ERR" as any);
|
||||
|
||||
const newDB = st.addNodeDB(51);
|
||||
newDB.addNode(makeNode(1, { user: { longName: "new-1" } }));
|
||||
newDB.addNode(makeNode(2, { user: { longName: "new-2" } }));
|
||||
newDB.setNodeError(2, "NEW_ERR" as any);
|
||||
|
||||
// also set overlapping error
|
||||
newDB.setNodeError(1, "SHOULD_NOT_OVERWRITE" as any);
|
||||
|
||||
newDB.setNodeNum(2020);
|
||||
|
||||
expect(newDB.getNodeError(1)!.error).toBe("OLD_ERR"); // old kept
|
||||
expect(newDB.getNodeError(2)!.error).toBe("NEW_ERR"); // new added
|
||||
});
|
||||
|
||||
it("removeAllNodes (optionally keeping my node) and removeAllNodeErrors persist across reload", async () => {
|
||||
it("removeAllNodes (optionally keeping my node) persists across reload", async () => {
|
||||
{
|
||||
const { useNodeDBStore } = await freshStore(true); // with persistence
|
||||
const { useNodeDBStore } = await freshStore(true);
|
||||
const st = useNodeDBStore.getState();
|
||||
const db = st.addNodeDB(1000);
|
||||
db.setNodeNum(55);
|
||||
db.addNode(makeNode(55, { user: { longName: "me" } }));
|
||||
db.addNode(makeNode(56));
|
||||
db.setNodeError(56, "ERR" as any);
|
||||
db.removeAllNodes(true);
|
||||
db.removeAllNodeErrors();
|
||||
}
|
||||
{
|
||||
const { useNodeDBStore } = await freshStore(true); // with persistence
|
||||
const { useNodeDBStore } = await freshStore(true);
|
||||
const st = useNodeDBStore.getState();
|
||||
const db = st.getNodeDB(1000)!;
|
||||
expect(db.getNode(55)).toBeTruthy(); // kept me
|
||||
expect(db.getNode(56)).toBeUndefined(); // cleared others
|
||||
expect(db.getNodeError(56)).toBeUndefined(); // cleared errors
|
||||
expect(db.getNode(55)).toBeTruthy();
|
||||
expect(db.getNode(56)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -442,7 +303,6 @@ describe("NodeDB deviceContext & debounce", () => {
|
||||
it("useNodeDB resolves per-device DB and switches with deviceId", async () => {
|
||||
const { useNodeDBStore, useNodeDB } = await freshStore();
|
||||
|
||||
// device 1
|
||||
deviceIdForTests = 1;
|
||||
const st = useNodeDBStore.getState();
|
||||
const db1 = st.addNodeDB(1);
|
||||
@@ -459,14 +319,12 @@ describe("NodeDB deviceContext & debounce", () => {
|
||||
const { rerender } = render(<Comp />);
|
||||
expect(screen.getByTestId("len").textContent).toBe("1");
|
||||
|
||||
// switch to device 2 and add nodes
|
||||
deviceIdForTests = 2;
|
||||
const db2 = st.addNodeDB(2);
|
||||
db2.addNode({ num: 20 } as any);
|
||||
db2.addNode({ num: 21 } as any);
|
||||
db2.addNode({ num: 22 } as any);
|
||||
|
||||
// re-render so the hook re-subscribes with the new deviceId
|
||||
await act(async () => {
|
||||
rerender(<Comp />);
|
||||
});
|
||||
@@ -495,16 +353,15 @@ describe("NodeDB deviceContext & debounce", () => {
|
||||
expect(screen.getByTestId("len").textContent).toBe("0");
|
||||
expect(renders).toBe(1);
|
||||
|
||||
// mutate something unrelated to length
|
||||
db.setNodeError(999, "X" as any);
|
||||
await act(() => Promise.resolve());
|
||||
expect(screen.getByTestId("len").textContent).toBe("0");
|
||||
expect(renders).toBe(1); // no re-render
|
||||
|
||||
// now actually change the slice
|
||||
// updateFavorite mutates a non-length slice
|
||||
db.addNode({ num: 1 } as any);
|
||||
db.updateFavorite(1, true);
|
||||
await act(() => Promise.resolve());
|
||||
// length stayed 1; selector slice unchanged after the favorite flip.
|
||||
expect(screen.getByTestId("len").textContent).toBe("1");
|
||||
|
||||
// baseline grew (addNode triggered the first rerender), favourite update
|
||||
// should NOT have caused a second one.
|
||||
expect(renders).toBe(2);
|
||||
});
|
||||
|
||||
@@ -528,7 +385,6 @@ describe("NodeDB deviceContext & debounce", () => {
|
||||
|
||||
render(<Comp />);
|
||||
|
||||
// burst of updates within the debounce window
|
||||
db.addNode({ num: 1 } as any);
|
||||
db.addNode({ num: 2 } as any);
|
||||
db.addNode({ num: 3 } as any);
|
||||
@@ -536,13 +392,13 @@ describe("NodeDB deviceContext & debounce", () => {
|
||||
await act(() => {
|
||||
vi.advanceTimersByTime(49);
|
||||
});
|
||||
expect(renders).toBe(1); // not yet
|
||||
expect(renders).toBe(1);
|
||||
|
||||
await act(() => {
|
||||
vi.advanceTimersByTime(2);
|
||||
});
|
||||
expect(screen.getByTestId("len").textContent).toBe("3");
|
||||
expect(renders).toBe(2); // single coalesced re-render
|
||||
expect(renders).toBe(2);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import type { NodeErrorType } from "@core/stores";
|
||||
import type { Protobuf } from "@meshtastic/sdk";
|
||||
import { fromByteArray } from "base64-js";
|
||||
|
||||
export function equalKey(a?: Uint8Array | null, b?: Uint8Array | null): boolean {
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
const len = a.byteLength;
|
||||
if (len !== b.byteLength) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validates a new incoming node against existing nodes.
|
||||
// If valid, returns a node to store, else returns undefined.
|
||||
export function validateIncomingNode(
|
||||
newNode: Protobuf.Mesh.NodeInfo,
|
||||
setNodeError: (nodeNum: number, error: NodeErrorType) => void,
|
||||
getNodes: (filter?: (node: Protobuf.Mesh.NodeInfo) => boolean) => Protobuf.Mesh.NodeInfo[],
|
||||
): Protobuf.Mesh.NodeInfo | undefined {
|
||||
const num = newNode.num;
|
||||
const existingNodes = getNodes((node) => node.num === num);
|
||||
|
||||
if (existingNodes.length === 0) {
|
||||
// No existing node with this node number.
|
||||
// Check if the new node's public key (if present and not empty)
|
||||
// is already claimed by another existing node.
|
||||
if (newNode.user?.publicKey !== undefined && newNode.user?.publicKey.length > 0) {
|
||||
const nodesWithSameKey = getNodes((node) => node.user?.publicKey === newNode.user?.publicKey);
|
||||
if (nodesWithSameKey.length > 0) {
|
||||
// This is a potential impersonation attempt.
|
||||
|
||||
console.warn(
|
||||
`Node ${num} rejected: Public key already claimed by another node. Key:`,
|
||||
fromByteArray(newNode.user?.publicKey ?? new Uint8Array()),
|
||||
);
|
||||
|
||||
setNodeError(num, "DUPLICATE_PKI");
|
||||
return undefined; // drop newNode entirely
|
||||
}
|
||||
}
|
||||
return newNode; // No conflicts, accept newNode
|
||||
} else if (existingNodes.length === 1) {
|
||||
// One existing node with this node number.
|
||||
const oldNode = existingNodes[0];
|
||||
if (!oldNode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// A public key is considered matching if the incoming key equals
|
||||
// the existing key, OR if the existing key is empty.
|
||||
const isKeyMatchingOrExistingEmpty =
|
||||
equalKey(oldNode.user?.publicKey, newNode.user?.publicKey) ||
|
||||
oldNode.user?.publicKey === undefined ||
|
||||
oldNode.user?.publicKey.length === 0;
|
||||
|
||||
if (isKeyMatchingOrExistingEmpty) {
|
||||
// Keys match or existing key was empty: trust the incoming node data completely.
|
||||
// This allows for legitimate updates to user info and other fields.
|
||||
return newNode;
|
||||
} else if (newNode.user?.publicKey !== undefined && newNode.user?.publicKey.length > 0) {
|
||||
console.warn(
|
||||
`Node ${num} rejected: existing key does not match incoming key. Old key:`,
|
||||
fromByteArray(oldNode.user?.publicKey ?? new Uint8Array()),
|
||||
"New key:",
|
||||
fromByteArray(newNode.user?.publicKey ?? new Uint8Array()),
|
||||
);
|
||||
|
||||
// Keys do not match and existing key was not empty: potential impersonation attempt.
|
||||
setNodeError(num, "MISMATCH_PKI");
|
||||
return oldNode; // drop newNode fields and return old
|
||||
} else {
|
||||
// Incoming node has no public key: ignore the new node entirely.
|
||||
console.warn(`Node ${num} rejected: incoming node has no public key, but existing does.`);
|
||||
return oldNode; // drop newNode fields and return old
|
||||
}
|
||||
} else {
|
||||
// Multiple existing nodes with the same node number
|
||||
// This should never happen, but if it does, we drop the new node entirely.
|
||||
console.warn(`Node ${num} rejected: Multiple existing nodes with this node number.`);
|
||||
|
||||
setNodeError(num, "DUPLICATE_PKI");
|
||||
return undefined; // drop newNode entirely
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,7 @@
|
||||
import type { Protobuf } from "@meshtastic/sdk";
|
||||
|
||||
type NodeErrorType = Protobuf.Mesh.Routing_Error | "MISMATCH_PKI" | "DUPLICATE_PKI";
|
||||
|
||||
type NodeError = {
|
||||
node: number;
|
||||
error: NodeErrorType;
|
||||
};
|
||||
|
||||
type ProcessPacketParams = {
|
||||
from: number;
|
||||
snr: number;
|
||||
time: number;
|
||||
};
|
||||
|
||||
export type { NodeError, ProcessPacketParams, NodeErrorType };
|
||||
export type { ProcessPacketParams };
|
||||
|
||||
Reference in New Issue
Block a user