@claw-network/node 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,267 @@
1
+ /**
2
+ * MessageStore — SQLite-backed inbox/outbox for P2P direct messaging.
3
+ *
4
+ * Inbox: messages received from other peers, waiting to be consumed by the local app.
5
+ * Outbox: messages pending delivery to offline peers, retried when the peer connects.
6
+ */
7
+ import Database from 'better-sqlite3';
8
+ import crypto from 'node:crypto';
9
+ // ── Schema ───────────────────────────────────────────────────────
10
+ const SCHEMA_SQL = `
11
+ CREATE TABLE IF NOT EXISTS inbox (
12
+ id TEXT PRIMARY KEY,
13
+ source_did TEXT NOT NULL,
14
+ target_did TEXT NOT NULL,
15
+ topic TEXT NOT NULL,
16
+ payload TEXT NOT NULL,
17
+ ttl_sec INTEGER NOT NULL DEFAULT 86400,
18
+ sent_at_ms INTEGER NOT NULL,
19
+ received_at_ms INTEGER NOT NULL,
20
+ consumed INTEGER NOT NULL DEFAULT 0,
21
+ priority INTEGER NOT NULL DEFAULT 0,
22
+ seq INTEGER NOT NULL DEFAULT 0
23
+ );
24
+
25
+ CREATE INDEX IF NOT EXISTS idx_inbox_topic ON inbox(topic, consumed);
26
+ CREATE INDEX IF NOT EXISTS idx_inbox_received ON inbox(received_at_ms);
27
+ CREATE INDEX IF NOT EXISTS idx_inbox_source ON inbox(source_did, consumed);
28
+ CREATE INDEX IF NOT EXISTS idx_inbox_unconsumed ON inbox(consumed, received_at_ms) WHERE consumed = 0;
29
+
30
+ CREATE TABLE IF NOT EXISTS outbox (
31
+ id TEXT PRIMARY KEY,
32
+ target_did TEXT NOT NULL,
33
+ topic TEXT NOT NULL,
34
+ payload TEXT NOT NULL,
35
+ ttl_sec INTEGER NOT NULL DEFAULT 86400,
36
+ sent_at_ms INTEGER NOT NULL,
37
+ attempts INTEGER NOT NULL DEFAULT 0,
38
+ last_attempt INTEGER,
39
+ priority INTEGER NOT NULL DEFAULT 0
40
+ );
41
+
42
+ CREATE INDEX IF NOT EXISTS idx_outbox_target ON outbox(target_did);
43
+ CREATE INDEX IF NOT EXISTS idx_outbox_retry ON outbox(attempts, last_attempt);
44
+
45
+ -- Deduplication table: stores idempotency keys with TTL for duplicate detection
46
+ CREATE TABLE IF NOT EXISTS dedup (
47
+ idempotency_key TEXT PRIMARY KEY,
48
+ message_id TEXT NOT NULL,
49
+ created_at_ms INTEGER NOT NULL
50
+ );
51
+
52
+ CREATE INDEX IF NOT EXISTS idx_dedup_created ON dedup(created_at_ms);
53
+
54
+ -- Sequence counter for ordered inbox replay
55
+ CREATE TABLE IF NOT EXISTS meta (
56
+ key TEXT PRIMARY KEY,
57
+ value TEXT NOT NULL
58
+ );
59
+
60
+ INSERT OR IGNORE INTO meta (key, value) VALUES ('inbox_seq', '0');
61
+
62
+ -- DID → PeerId mapping persistence (survives restarts)
63
+ CREATE TABLE IF NOT EXISTS did_peers (
64
+ did TEXT PRIMARY KEY,
65
+ peer_id TEXT NOT NULL,
66
+ updated_at_ms INTEGER NOT NULL
67
+ );
68
+
69
+ CREATE INDEX IF NOT EXISTS idx_did_peers_peer ON did_peers(peer_id);
70
+
71
+ -- Rate limiting: sliding-window event log (shared across processes via SQLite)
72
+ CREATE TABLE IF NOT EXISTS rate_limits (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ bucket TEXT NOT NULL,
75
+ ts_ms INTEGER NOT NULL
76
+ );
77
+
78
+ CREATE INDEX IF NOT EXISTS idx_rate_bucket_ts ON rate_limits(bucket, ts_ms);
79
+ `;
80
+ /** Deduplication window: 24 hours */
81
+ const DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
82
+ // ── Store ────────────────────────────────────────────────────────
83
+ export class MessageStore {
84
+ db;
85
+ constructor(dbPath) {
86
+ this.db = new Database(dbPath);
87
+ this.db.pragma('journal_mode = WAL');
88
+ this.db.exec(SCHEMA_SQL);
89
+ }
90
+ // ── Inbox ──────────────────────────────────────────────────────
91
+ /**
92
+ * Store an inbound message in the inbox. Returns the messageId.
93
+ * If `idempotencyKey` is provided, deduplicates — returns existing messageId on duplicate.
94
+ */
95
+ addToInbox(msg) {
96
+ // Deduplication check
97
+ if (msg.idempotencyKey) {
98
+ const existing = this.db.prepare('SELECT message_id FROM dedup WHERE idempotency_key = ?').get(msg.idempotencyKey);
99
+ if (existing)
100
+ return existing.message_id;
101
+ }
102
+ const id = `msg_${crypto.randomBytes(12).toString('hex')}`;
103
+ const now = Date.now();
104
+ const seq = this.nextSeq();
105
+ this.db.prepare(`
106
+ INSERT INTO inbox (id, source_did, target_did, topic, payload, ttl_sec, sent_at_ms, received_at_ms, priority, seq)
107
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
108
+ `).run(id, msg.sourceDid, msg.targetDid, msg.topic, msg.payload, msg.ttlSec ?? 86400, msg.sentAtMs ?? now, now, msg.priority ?? 0, seq);
109
+ // Record dedup key
110
+ if (msg.idempotencyKey) {
111
+ this.db.prepare('INSERT OR IGNORE INTO dedup (idempotency_key, message_id, created_at_ms) VALUES (?, ?, ?)').run(msg.idempotencyKey, id, now);
112
+ }
113
+ return id;
114
+ }
115
+ /** Increment and return the next inbox sequence number (monotonic). */
116
+ nextSeq() {
117
+ this.db.prepare("UPDATE meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE key = 'inbox_seq'").run();
118
+ const row = this.db.prepare("SELECT value FROM meta WHERE key = 'inbox_seq'").get();
119
+ return parseInt(row.value, 10);
120
+ }
121
+ /** Get the current (latest) inbox sequence number. */
122
+ currentSeq() {
123
+ const row = this.db.prepare("SELECT value FROM meta WHERE key = 'inbox_seq'").get();
124
+ return row ? parseInt(row.value, 10) : 0;
125
+ }
126
+ /** Fetch unconsumed inbox messages, ordered by priority then time. */
127
+ getInbox(opts = {}) {
128
+ const limit = Math.min(opts.limit ?? 100, 500);
129
+ let sql = 'SELECT id, source_did, topic, payload, received_at_ms, priority, seq FROM inbox WHERE consumed = 0';
130
+ const params = [];
131
+ if (opts.topic) {
132
+ sql += ' AND topic = ?';
133
+ params.push(opts.topic);
134
+ }
135
+ if (opts.sinceMs) {
136
+ sql += ' AND received_at_ms > ?';
137
+ params.push(opts.sinceMs);
138
+ }
139
+ if (opts.sinceSeq !== undefined) {
140
+ sql += ' AND seq > ?';
141
+ params.push(opts.sinceSeq);
142
+ }
143
+ // Higher priority first, then by received time
144
+ sql += ' ORDER BY priority DESC, received_at_ms ASC LIMIT ?';
145
+ params.push(limit);
146
+ const rows = this.db.prepare(sql).all(...params);
147
+ return rows.map((r) => ({
148
+ messageId: r.id,
149
+ sourceDid: r.source_did,
150
+ topic: r.topic,
151
+ payload: r.payload,
152
+ receivedAtMs: r.received_at_ms,
153
+ priority: r.priority,
154
+ seq: r.seq,
155
+ }));
156
+ }
157
+ /** Mark a message as consumed (acknowledged). */
158
+ consumeMessage(messageId) {
159
+ const result = this.db.prepare('UPDATE inbox SET consumed = 1 WHERE id = ? AND consumed = 0').run(messageId);
160
+ return result.changes > 0;
161
+ }
162
+ /** Delete consumed and expired messages in batches to avoid locking. Also cleans dedup table. */
163
+ cleanupInbox() {
164
+ const now = Date.now();
165
+ let total = 0;
166
+ // Delete in batches of 500 to avoid long table locks
167
+ const stmt = this.db.prepare('DELETE FROM inbox WHERE id IN (SELECT id FROM inbox WHERE consumed = 1 OR (sent_at_ms + ttl_sec * 1000) <= ? LIMIT 500)');
168
+ let changes;
169
+ do {
170
+ changes = stmt.run(now).changes;
171
+ total += changes;
172
+ } while (changes > 0);
173
+ // Clean expired dedup entries
174
+ const dedupCutoff = now - DEDUP_TTL_MS;
175
+ this.db.prepare('DELETE FROM dedup WHERE created_at_ms < ?').run(dedupCutoff);
176
+ return total;
177
+ }
178
+ // ── Outbox ─────────────────────────────────────────────────────
179
+ /** Queue a message for later delivery to an offline peer. */
180
+ addToOutbox(msg) {
181
+ const id = `msg_${crypto.randomBytes(12).toString('hex')}`;
182
+ const now = Date.now();
183
+ this.db.prepare(`
184
+ INSERT INTO outbox (id, target_did, topic, payload, ttl_sec, sent_at_ms, priority)
185
+ VALUES (?, ?, ?, ?, ?, ?, ?)
186
+ `).run(id, msg.targetDid, msg.topic, msg.payload, msg.ttlSec ?? 86400, now, msg.priority ?? 0);
187
+ return id;
188
+ }
189
+ /** Get pending outbox messages for a specific target DID, ordered by priority then time. */
190
+ getOutboxForTarget(targetDid, limit = 100) {
191
+ const now = Date.now();
192
+ const rows = this.db.prepare(`
193
+ SELECT id, target_did, topic, payload, ttl_sec, sent_at_ms, attempts, last_attempt
194
+ FROM outbox
195
+ WHERE target_did = ? AND (sent_at_ms + ttl_sec * 1000) > ?
196
+ ORDER BY priority DESC, sent_at_ms ASC LIMIT ?
197
+ `).all(targetDid, now, limit);
198
+ return rows.map((r) => ({
199
+ id: r.id,
200
+ targetDid: r.target_did,
201
+ topic: r.topic,
202
+ payload: r.payload,
203
+ ttlSec: r.ttl_sec,
204
+ sentAtMs: r.sent_at_ms,
205
+ attempts: r.attempts,
206
+ lastAttempt: r.last_attempt ?? 0,
207
+ }));
208
+ }
209
+ /** Increment attempt count for an outbox message. */
210
+ recordAttempt(messageId) {
211
+ this.db.prepare('UPDATE outbox SET attempts = attempts + 1, last_attempt = ? WHERE id = ?').run(Date.now(), messageId);
212
+ }
213
+ /** Remove a message from the outbox (successfully delivered). */
214
+ removeFromOutbox(messageId) {
215
+ const result = this.db.prepare('DELETE FROM outbox WHERE id = ?').run(messageId);
216
+ return result.changes > 0;
217
+ }
218
+ /** Clean up expired outbox entries in batches. */
219
+ cleanupOutbox() {
220
+ const now = Date.now();
221
+ let total = 0;
222
+ const stmt = this.db.prepare('DELETE FROM outbox WHERE id IN (SELECT id FROM outbox WHERE (sent_at_ms + ttl_sec * 1000) <= ? LIMIT 500)');
223
+ let changes;
224
+ do {
225
+ changes = stmt.run(now).changes;
226
+ total += changes;
227
+ } while (changes > 0);
228
+ return total;
229
+ }
230
+ /** Total count of inbox messages (for rate limiting / stats). */
231
+ inboxCount(targetDid) {
232
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM inbox WHERE target_did = ? AND consumed = 0').get(targetDid);
233
+ return row?.cnt ?? 0;
234
+ }
235
+ close() {
236
+ this.db.close();
237
+ }
238
+ // ── DID → PeerId Mapping ───────────────────────────────────
239
+ /** Persist or update a DID → PeerId mapping. */
240
+ upsertDidPeer(did, peerId) {
241
+ this.db.prepare('INSERT INTO did_peers (did, peer_id, updated_at_ms) VALUES (?, ?, ?) ON CONFLICT(did) DO UPDATE SET peer_id = excluded.peer_id, updated_at_ms = excluded.updated_at_ms').run(did, peerId, Date.now());
242
+ }
243
+ /** Load all persisted DID → PeerId mappings (including update timestamps for TTL). */
244
+ getAllDidPeers() {
245
+ const rows = this.db.prepare('SELECT did, peer_id, updated_at_ms FROM did_peers').all();
246
+ return rows.map((r) => ({ did: r.did, peerId: r.peer_id, updatedAtMs: r.updated_at_ms }));
247
+ }
248
+ /** Remove a DID mapping (e.g. when a peer is permanently gone). */
249
+ removeDidPeer(did) {
250
+ return this.db.prepare('DELETE FROM did_peers WHERE did = ?').run(did).changes > 0;
251
+ }
252
+ // ── Rate Limiting ──────────────────────────────────────────
253
+ /** Record a rate-limit event for the given bucket. */
254
+ recordRateEvent(bucket) {
255
+ this.db.prepare('INSERT INTO rate_limits (bucket, ts_ms) VALUES (?, ?)').run(bucket, Date.now());
256
+ }
257
+ /** Count rate-limit events for a bucket within the sliding window. */
258
+ countRateEvents(bucket, windowStartMs) {
259
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM rate_limits WHERE bucket = ? AND ts_ms > ?').get(bucket, windowStartMs);
260
+ return row.cnt;
261
+ }
262
+ /** Delete rate-limit events older than the given timestamp. */
263
+ pruneRateEvents(beforeMs) {
264
+ return this.db.prepare('DELETE FROM rate_limits WHERE ts_ms <= ?').run(beforeMs).changes;
265
+ }
266
+ }
267
+ //# sourceMappingURL=message-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-store.js","sourceRoot":"","sources":["../../src/services/message-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * MessagingService — orchestrates P2P direct messaging.
3
+ *
4
+ * Responsibilities:
5
+ * - Send messages to target DIDs via libp2p stream protocol
6
+ * - Receive inbound messages and store in inbox
7
+ * - Queue messages for offline peers in outbox, deliver on reconnect
8
+ * - Maintain DID → PeerId mapping via announce protocol
9
+ * - Periodic TTL cleanup of expired messages
10
+ */
11
+ import type { P2PNode } from '@claw-network/core';
12
+ import { MessageStore } from './message-store.js';
13
+ /** Topic used for delivery receipt notifications via WebSocket. */
14
+ export declare const RECEIPT_TOPIC: "_receipt";
15
+ /** Priority levels — higher number = higher priority. */
16
+ export declare enum MessagePriority {
17
+ LOW = 0,
18
+ NORMAL = 1,
19
+ HIGH = 2,
20
+ URGENT = 3
21
+ }
22
+ export interface SendResult {
23
+ messageId: string;
24
+ delivered: boolean;
25
+ compressed?: boolean;
26
+ encrypted?: boolean;
27
+ }
28
+ export interface MulticastResult {
29
+ results: Array<SendResult & {
30
+ targetDid: string;
31
+ }>;
32
+ }
33
+ export interface InboxQueryOptions {
34
+ topic?: string;
35
+ sinceMs?: number;
36
+ sinceSeq?: number;
37
+ limit?: number;
38
+ }
39
+ export interface InboxMessage {
40
+ messageId: string;
41
+ sourceDid: string;
42
+ topic: string;
43
+ payload: string;
44
+ receivedAtMs: number;
45
+ priority: number;
46
+ seq: number;
47
+ }
48
+ export interface SendOptions {
49
+ ttlSec?: number;
50
+ priority?: MessagePriority;
51
+ /** If true, compress payload > 1 KB with gzip before sending. */
52
+ compress?: boolean;
53
+ /** Recipient's X25519 public key hex for E2E encryption. */
54
+ encryptForKeyHex?: string;
55
+ /** Idempotency key for deduplication. */
56
+ idempotencyKey?: string;
57
+ /** Per-recipient X25519 public key hex map (DID → key) for multicast E2E encryption. */
58
+ recipientKeys?: Record<string, string>;
59
+ }
60
+ /** Callback for WebSocket subscribers — called when a new message arrives in the inbox. */
61
+ export type InboxSubscriber = (message: InboxMessage) => void;
62
+ export declare class MessagingService {
63
+ private readonly log;
64
+ private readonly store;
65
+ private readonly p2p;
66
+ private readonly localDid;
67
+ private cleanupTimer?;
68
+ /**
69
+ * DID → PeerId mapping. Populated via the did-announce protocol when
70
+ * peers connect. Persisted to SQLite and restored on startup.
71
+ */
72
+ private readonly didToPeerId;
73
+ private readonly peerIdToDid;
74
+ /** Tracks when each DID→PeerId mapping was last confirmed (for TTL-based re-resolve). */
75
+ private readonly didPeerUpdatedAt;
76
+ /** WebSocket subscribers that receive real-time inbox pushes. */
77
+ private readonly subscribers;
78
+ constructor(p2p: P2PNode, store: MessageStore, localDid: string);
79
+ start(): Promise<void>;
80
+ stop(): Promise<void>;
81
+ /**
82
+ * Send a message to a target DID.
83
+ * If the target peer is online and reachable, delivers directly.
84
+ * Otherwise queues in outbox for later delivery.
85
+ */
86
+ send(targetDid: string, topic: string, payload: string, opts?: SendOptions): Promise<SendResult>;
87
+ /**
88
+ * Send a message to multiple target DIDs (multicast).
89
+ * Each target is attempted independently — partial success is possible.
90
+ */
91
+ sendMulticast(targetDids: string[], topic: string, payload: string, opts?: SendOptions): Promise<MulticastResult>;
92
+ /** Query the local inbox. */
93
+ getInbox(opts?: InboxQueryOptions): InboxMessage[];
94
+ /** Acknowledge (consume) a message from inbox. */
95
+ ackMessage(messageId: string): boolean;
96
+ /** Flush outbox: attempt to deliver all pending messages for a specific DID with bounded concurrency. */
97
+ flushOutboxForDid(targetDid: string): Promise<number>;
98
+ /**
99
+ * Called when a peer connects. Announces our DID and flushes any
100
+ * pending outbox messages for that peer's DID.
101
+ */
102
+ onPeerConnected(peerId: string): Promise<void>;
103
+ /** Return the current DID→PeerId mapping (for debugging/status). */
104
+ getDidPeerMap(): Record<string, string>;
105
+ /** Register a subscriber for real-time inbox pushes. */
106
+ addSubscriber(cb: InboxSubscriber): void;
107
+ /** Remove a subscriber. */
108
+ removeSubscriber(cb: InboxSubscriber): void;
109
+ /** Number of active WS subscribers. */
110
+ get subscriberCount(): number;
111
+ /** Notify all subscribers of a new inbox message (non-blocking). */
112
+ private notifySubscribers;
113
+ /**
114
+ * Check rate limit for a DID. Throws if limit exceeded.
115
+ * Uses SQLite-backed sliding window for cross-process correctness.
116
+ */
117
+ enforceRateLimit(did: string): void;
118
+ /** Check if a DID is currently rate-limited (without consuming a slot). */
119
+ isRateLimited(did: string): boolean;
120
+ /**
121
+ * Enforce inbound rate limit for a peerId. Throws if limit exceeded.
122
+ * Prevents P2P peers from spamming without limit.
123
+ */
124
+ private enforceInboundRateLimit;
125
+ /**
126
+ * Enforce global aggregate inbound rate limit (all peers combined).
127
+ * Prevents total flooding even when spread across many peers.
128
+ */
129
+ private enforceGlobalInboundRateLimit;
130
+ /**
131
+ * Core rate-limit check: count events in the sliding window via SQLite,
132
+ * record a new event, throw if over limit.
133
+ */
134
+ private checkRateBucket;
135
+ /**
136
+ * Encode a payload: optionally compress (gzip) then optionally encrypt (X25519+AES-256-GCM).
137
+ *
138
+ * Returns:
139
+ * - `payloadBytes`: raw binary payload for FlatBuffers wire format (Uint8Array)
140
+ * - `storagePayload`: string representation for SQLite TEXT storage (backward compat)
141
+ * - `compressed` / `encrypted` flags
142
+ */
143
+ private encodePayload;
144
+ /**
145
+ * Decrypt an E2E-encrypted payload using the local node's X25519 private key.
146
+ * Returns the decrypted payload string or null if not encrypted / decryption fails.
147
+ */
148
+ static decryptPayload(payload: string, recipientPrivateKey: Uint8Array): string | null;
149
+ /**
150
+ * Decompress a gzip-compressed payload (base64-encoded gzip → utf-8 string).
151
+ * Returns the decompressed string, or null if decompression fails.
152
+ */
153
+ static decompressPayload(payload: string): string | null;
154
+ /** Get the current inbox sequence number (for WS replay). */
155
+ getCurrentSeq(): number;
156
+ private deliverDirect;
157
+ private handleInboundMessage;
158
+ private handleDidAnnounce;
159
+ private handleDidResolve;
160
+ /**
161
+ * Query connected peers to resolve an unknown DID → PeerId.
162
+ * Sends DID resolve requests to up to DID_RESOLVE_MAX_PEERS peers concurrently.
163
+ * Returns the first PeerId found, or null if none of the queried peers know the DID.
164
+ */
165
+ private resolveDidViaPeers;
166
+ /** Announce our DID to a specific peer. */
167
+ private announceDidToPeer;
168
+ /** Announce our DID to all currently connected peers. */
169
+ private announceToAll;
170
+ /**
171
+ * Deliver to multiple targets concurrently with bounded concurrency.
172
+ * Uses Promise.allSettled so one failure doesn't block others.
173
+ * Supports per-recipient E2E encryption when recipientKeys are provided.
174
+ */
175
+ private deliverMulticast;
176
+ /** Update in-memory maps AND persist the DID→PeerId mapping to SQLite. */
177
+ private registerDidPeer;
178
+ /** Check if a DID→PeerId mapping is older than the TTL threshold. */
179
+ private isStalePeerMapping;
180
+ /** Send a delivery receipt to the sender after receiving a message. */
181
+ private sendDeliveryReceipt;
182
+ /** Handle an incoming delivery receipt from a remote peer. */
183
+ private handleDeliveryReceipt;
184
+ }
185
+ export declare class RateLimitError extends Error {
186
+ readonly did: string;
187
+ readonly limit: number;
188
+ constructor(did: string, limit: number);
189
+ }
190
+ //# sourceMappingURL=messaging-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messaging-service.d.ts","sourceRoot":"","sources":["../../src/services/messaging-service.ts"],"names":[],"mappings":"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"}