
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
message-nodes
Advanced tools
A tiny TypeScript utility for modeling conversation threads as a map of nodes with lightweight relationships:
parent → points “up” the threadchild → points to the active child (the currently selected branch)root → thread identifier (root nodes have root === id)metadata → optional extra data you want to store per messageIt’s designed for chat UIs, branching conversations (“regenerate”), and tree-like message histories while keeping updates immutable (returns a new mappings object, preserves referential equality on no-ops where possible).
npm i message-nodes
(or)
yarn add message-nodes
Each message is a MessageNode:
export interface MessageNode<C = string, M = Record<string, any>> {
id: string;
role: string;
content: C;
root: string;
parent?: string | undefined;
child?: string | undefined;
metadata?: M | undefined;
}
Record<string, MessageNode>parent (branching), but:
child property is the active child in that branch setimport {
addNode,
branchNode,
getConversation,
getRoots,
makeRoot,
updateContent,
deleteNode,
type MessageNode,
} from "message-nodes";
type Meta = { model?: string; tokens?: number };
let mappings: Record<string, MessageNode<string, Meta>> = {};
// Create a root message (no parent => root === id)
mappings = addNode(mappings, "root-1", "system", "New chat");
// Add first user message under the root
mappings = addNode(mappings, "u1", "user", "Hello!", undefined, "root-1");
// Add assistant response (as active child of u1)
mappings = addNode(mappings, "a1", "assistant", "Hi there 👋", undefined, "u1", undefined, { model: "gpt" });
// Read the active conversation chain from the root
const convo = getConversation(mappings, "root-1"); // [u1, a1]
// Branch the assistant response (e.g., regenerate)
mappings = branchNode(mappings, "a1", "a2", "Alternative answer", { model: "gpt", tokens: 123 });
// Now u1.child points to "a2" (active branch)
const convo2 = getConversation(mappings, "root-1"); // [u1, a2]
// Update content (immutable)
mappings = updateContent(mappings, "a2", (prev) => prev + " ✅");
// Delete a node and all descendants
mappings = deleteNode(mappings, "u1");
hasNode(mappings, id): booleanReturns true if the node exists.
getNode(mappings, id): MessageNode | undefinedGets a node by id.
getRoot(mappings, id): MessageNode | undefinedWalks parent pointers until the top-most node.
Note:
getRootfinds the “top of chain” byparentpointers, while therootfield is a thread identifier you can rewrite withmakeRoot.
getRoots(mappings): MessageNode[]Returns all thread roots (node.root === node.id).
getConversation(mappings, rootId): MessageNode[]Returns the active chain from rootId following .child pointers.
[] (and warns)getAncestry(mappings, id): MessageNode[]Returns [node, parent, grandparent, ...] walking parent pointers.
Detects cycles.
getChildren(mappings, id): MessageNode[]Returns all direct children where msg.parent === id.
setChild(mappings, parentId, childId | undefined): Record<...>Sets a parent’s active child pointer only if:
child.parent === parentIdNo-op if it wouldn’t change anything.
nextChild(mappings, parentId): Record<...>Moves the parent’s active child to the “next” sibling among getChildren(parentId).
The ordering is whatever
Object.values(mappings)produces forgetChildren, so if you need deterministic order, keep your own ordering strategy (e.g. storecreatedAtin metadata and sort externally).
lastChild(mappings, parentId): Record<...>Moves the parent’s active child to the “previous” sibling.
addNode(mappings, id, role, content, root?, parent?, child?, metadata?): Record<...>Adds a new node, optionally linking it.
Behavior:
parent is not provided: the node becomes a root (root = id)parent is provided: root is inferred from getRoot(parent) (top of chain), and the parent’s active child is set to the new nodechild is provided: the child’s parent is set to the new nodeValidates that referenced parent, child, and root exist (when applicable), otherwise returns unchanged and warns.
branchNode(mappings, existingId, siblingId, content, metadata?): Record<...>Creates a sibling under the same parent as existingId.
role, root, and parentaddNode behavior)Great for “regenerate answer” branching.
updateContent(mappings, id, contentOrUpdater, metadataOrUpdater?): Record<...>Updates a node’s content (and optionally metadata) immutably.
content can be a value or (prev) => nextmetadata can be a value or (prev) => nextObject.is equal)deleteNode(mappings, id): Record<...>Deletes the node and all descendants (all nodes reachable via parent === id, recursively).
Also attempts to keep the parent thread usable:
child to another sibling if one exists.Cycle-safe.
unlinkNode(mappings, id): Record<...>Isolates the node:
parent, childroot = idmakeRoot(mappings, id): Record<...>Converts a node into a root without deleting its subtree.
root on the node and all descendants (following both the active .child chain and all direct children via getChildren), so the whole sub-tree becomes a new thread.// Suppose u1.child is currently "a1"
mappings = branchNode(mappings, "a1", "a2", "New answer");
mappings = updateContent(mappings, "a2", "Streaming...");
const roots = getRoots(mappings); // list of root nodes (threads)
type Meta = { selected?: boolean; createdAt?: number };
mappings = updateContent(
mappings,
"u1",
(c) => c,
(m) => ({ ...(m ?? {}), selected: true })
);
getConversation follows the active .child chain only. If you want “all nodes in a thread”, you can start from a root and traverse via getChildren recursively.nextChild / lastChild depend on the order returned by getChildren (which is based on object iteration). For deterministic ordering, store ordering metadata and build your own sorted child list.MIT License
Copyright (c) 2026 Dane Madsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
FAQs
A library for managing message nodes and their relationships.
We found that message-nodes demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.