@claw-network/node 0.2.1 → 0.3.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.
- package/README.md +157 -14
- package/dist/api/routes/messaging.d.ts +13 -0
- package/dist/api/routes/messaging.d.ts.map +1 -0
- package/dist/api/routes/messaging.js +181 -0
- package/dist/api/routes/messaging.js.map +1 -0
- package/dist/api/server.d.ts +1 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +6 -0
- package/dist/api/server.js.map +1 -1
- package/dist/api/types.d.ts +2 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js.map +1 -1
- package/dist/api/ws-messaging.d.ts +23 -0
- package/dist/api/ws-messaging.d.ts.map +1 -0
- package/dist/api/ws-messaging.js +124 -0
- package/dist/api/ws-messaging.js.map +1 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -22
- package/dist/index.js.map +1 -1
- package/dist/services/message-store.d.ts +90 -0
- package/dist/services/message-store.d.ts.map +1 -0
- package/dist/services/message-store.js +221 -0
- package/dist/services/message-store.js.map +1 -0
- package/dist/services/messaging-service.d.ts +174 -0
- package/dist/services/messaging-service.d.ts.map +1 -0
- package/dist/services/messaging-service.js +705 -0
- package/dist/services/messaging-service.js.map +1 -0
- package/package.json +4 -2
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
/** Deduplication window: 24 hours */
|
|
63
|
+
const DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
|
|
64
|
+
// ── Store ────────────────────────────────────────────────────────
|
|
65
|
+
export class MessageStore {
|
|
66
|
+
db;
|
|
67
|
+
constructor(dbPath) {
|
|
68
|
+
this.db = new Database(dbPath);
|
|
69
|
+
this.db.pragma('journal_mode = WAL');
|
|
70
|
+
this.db.exec(SCHEMA_SQL);
|
|
71
|
+
}
|
|
72
|
+
// ── Inbox ──────────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Store an inbound message in the inbox. Returns the messageId.
|
|
75
|
+
* If `idempotencyKey` is provided, deduplicates — returns existing messageId on duplicate.
|
|
76
|
+
*/
|
|
77
|
+
addToInbox(msg) {
|
|
78
|
+
// Deduplication check
|
|
79
|
+
if (msg.idempotencyKey) {
|
|
80
|
+
const existing = this.db.prepare('SELECT message_id FROM dedup WHERE idempotency_key = ?').get(msg.idempotencyKey);
|
|
81
|
+
if (existing)
|
|
82
|
+
return existing.message_id;
|
|
83
|
+
}
|
|
84
|
+
const id = `msg_${crypto.randomBytes(12).toString('hex')}`;
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const seq = this.nextSeq();
|
|
87
|
+
this.db.prepare(`
|
|
88
|
+
INSERT INTO inbox (id, source_did, target_did, topic, payload, ttl_sec, sent_at_ms, received_at_ms, priority, seq)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
90
|
+
`).run(id, msg.sourceDid, msg.targetDid, msg.topic, msg.payload, msg.ttlSec ?? 86400, msg.sentAtMs ?? now, now, msg.priority ?? 0, seq);
|
|
91
|
+
// Record dedup key
|
|
92
|
+
if (msg.idempotencyKey) {
|
|
93
|
+
this.db.prepare('INSERT OR IGNORE INTO dedup (idempotency_key, message_id, created_at_ms) VALUES (?, ?, ?)').run(msg.idempotencyKey, id, now);
|
|
94
|
+
}
|
|
95
|
+
return id;
|
|
96
|
+
}
|
|
97
|
+
/** Increment and return the next inbox sequence number (monotonic). */
|
|
98
|
+
nextSeq() {
|
|
99
|
+
this.db.prepare("UPDATE meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE key = 'inbox_seq'").run();
|
|
100
|
+
const row = this.db.prepare("SELECT value FROM meta WHERE key = 'inbox_seq'").get();
|
|
101
|
+
return parseInt(row.value, 10);
|
|
102
|
+
}
|
|
103
|
+
/** Get the current (latest) inbox sequence number. */
|
|
104
|
+
currentSeq() {
|
|
105
|
+
const row = this.db.prepare("SELECT value FROM meta WHERE key = 'inbox_seq'").get();
|
|
106
|
+
return row ? parseInt(row.value, 10) : 0;
|
|
107
|
+
}
|
|
108
|
+
/** Fetch unconsumed inbox messages, ordered by priority then time. */
|
|
109
|
+
getInbox(opts = {}) {
|
|
110
|
+
const limit = Math.min(opts.limit ?? 100, 500);
|
|
111
|
+
let sql = 'SELECT id, source_did, topic, payload, received_at_ms, priority, seq FROM inbox WHERE consumed = 0';
|
|
112
|
+
const params = [];
|
|
113
|
+
if (opts.topic) {
|
|
114
|
+
sql += ' AND topic = ?';
|
|
115
|
+
params.push(opts.topic);
|
|
116
|
+
}
|
|
117
|
+
if (opts.sinceMs) {
|
|
118
|
+
sql += ' AND received_at_ms > ?';
|
|
119
|
+
params.push(opts.sinceMs);
|
|
120
|
+
}
|
|
121
|
+
if (opts.sinceSeq !== undefined) {
|
|
122
|
+
sql += ' AND seq > ?';
|
|
123
|
+
params.push(opts.sinceSeq);
|
|
124
|
+
}
|
|
125
|
+
// Higher priority first, then by received time
|
|
126
|
+
sql += ' ORDER BY priority DESC, received_at_ms ASC LIMIT ?';
|
|
127
|
+
params.push(limit);
|
|
128
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
129
|
+
return rows.map((r) => ({
|
|
130
|
+
messageId: r.id,
|
|
131
|
+
sourceDid: r.source_did,
|
|
132
|
+
topic: r.topic,
|
|
133
|
+
payload: r.payload,
|
|
134
|
+
receivedAtMs: r.received_at_ms,
|
|
135
|
+
priority: r.priority,
|
|
136
|
+
seq: r.seq,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
/** Mark a message as consumed (acknowledged). */
|
|
140
|
+
consumeMessage(messageId) {
|
|
141
|
+
const result = this.db.prepare('UPDATE inbox SET consumed = 1 WHERE id = ? AND consumed = 0').run(messageId);
|
|
142
|
+
return result.changes > 0;
|
|
143
|
+
}
|
|
144
|
+
/** Delete consumed and expired messages in batches to avoid locking. Also cleans dedup table. */
|
|
145
|
+
cleanupInbox() {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
let total = 0;
|
|
148
|
+
// Delete in batches of 500 to avoid long table locks
|
|
149
|
+
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)');
|
|
150
|
+
let changes;
|
|
151
|
+
do {
|
|
152
|
+
changes = stmt.run(now).changes;
|
|
153
|
+
total += changes;
|
|
154
|
+
} while (changes > 0);
|
|
155
|
+
// Clean expired dedup entries
|
|
156
|
+
const dedupCutoff = now - DEDUP_TTL_MS;
|
|
157
|
+
this.db.prepare('DELETE FROM dedup WHERE created_at_ms < ?').run(dedupCutoff);
|
|
158
|
+
return total;
|
|
159
|
+
}
|
|
160
|
+
// ── Outbox ─────────────────────────────────────────────────────
|
|
161
|
+
/** Queue a message for later delivery to an offline peer. */
|
|
162
|
+
addToOutbox(msg) {
|
|
163
|
+
const id = `msg_${crypto.randomBytes(12).toString('hex')}`;
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
this.db.prepare(`
|
|
166
|
+
INSERT INTO outbox (id, target_did, topic, payload, ttl_sec, sent_at_ms, priority)
|
|
167
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
168
|
+
`).run(id, msg.targetDid, msg.topic, msg.payload, msg.ttlSec ?? 86400, now, msg.priority ?? 0);
|
|
169
|
+
return id;
|
|
170
|
+
}
|
|
171
|
+
/** Get pending outbox messages for a specific target DID, ordered by priority then time. */
|
|
172
|
+
getOutboxForTarget(targetDid, limit = 100) {
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
const rows = this.db.prepare(`
|
|
175
|
+
SELECT id, target_did, topic, payload, ttl_sec, sent_at_ms, attempts, last_attempt
|
|
176
|
+
FROM outbox
|
|
177
|
+
WHERE target_did = ? AND (sent_at_ms + ttl_sec * 1000) > ?
|
|
178
|
+
ORDER BY priority DESC, sent_at_ms ASC LIMIT ?
|
|
179
|
+
`).all(targetDid, now, limit);
|
|
180
|
+
return rows.map((r) => ({
|
|
181
|
+
id: r.id,
|
|
182
|
+
targetDid: r.target_did,
|
|
183
|
+
topic: r.topic,
|
|
184
|
+
payload: r.payload,
|
|
185
|
+
ttlSec: r.ttl_sec,
|
|
186
|
+
sentAtMs: r.sent_at_ms,
|
|
187
|
+
attempts: r.attempts,
|
|
188
|
+
lastAttempt: r.last_attempt ?? 0,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
/** Increment attempt count for an outbox message. */
|
|
192
|
+
recordAttempt(messageId) {
|
|
193
|
+
this.db.prepare('UPDATE outbox SET attempts = attempts + 1, last_attempt = ? WHERE id = ?').run(Date.now(), messageId);
|
|
194
|
+
}
|
|
195
|
+
/** Remove a message from the outbox (successfully delivered). */
|
|
196
|
+
removeFromOutbox(messageId) {
|
|
197
|
+
const result = this.db.prepare('DELETE FROM outbox WHERE id = ?').run(messageId);
|
|
198
|
+
return result.changes > 0;
|
|
199
|
+
}
|
|
200
|
+
/** Clean up expired outbox entries in batches. */
|
|
201
|
+
cleanupOutbox() {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
let total = 0;
|
|
204
|
+
const stmt = this.db.prepare('DELETE FROM outbox WHERE id IN (SELECT id FROM outbox WHERE (sent_at_ms + ttl_sec * 1000) <= ? LIMIT 500)');
|
|
205
|
+
let changes;
|
|
206
|
+
do {
|
|
207
|
+
changes = stmt.run(now).changes;
|
|
208
|
+
total += changes;
|
|
209
|
+
} while (changes > 0);
|
|
210
|
+
return total;
|
|
211
|
+
}
|
|
212
|
+
/** Total count of inbox messages (for rate limiting / stats). */
|
|
213
|
+
inboxCount(targetDid) {
|
|
214
|
+
const row = this.db.prepare('SELECT COUNT(*) as cnt FROM inbox WHERE target_did = ? AND consumed = 0').get(targetDid);
|
|
215
|
+
return row?.cnt ?? 0;
|
|
216
|
+
}
|
|
217
|
+
close() {
|
|
218
|
+
this.db.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//# 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,174 @@
|
|
|
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
|
+
/** Priority levels — higher number = higher priority. */
|
|
14
|
+
export declare enum MessagePriority {
|
|
15
|
+
LOW = 0,
|
|
16
|
+
NORMAL = 1,
|
|
17
|
+
HIGH = 2,
|
|
18
|
+
URGENT = 3
|
|
19
|
+
}
|
|
20
|
+
export interface SendResult {
|
|
21
|
+
messageId: string;
|
|
22
|
+
delivered: boolean;
|
|
23
|
+
compressed?: boolean;
|
|
24
|
+
encrypted?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface MulticastResult {
|
|
27
|
+
results: Array<SendResult & {
|
|
28
|
+
targetDid: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export interface InboxQueryOptions {
|
|
32
|
+
topic?: string;
|
|
33
|
+
sinceMs?: number;
|
|
34
|
+
sinceSeq?: number;
|
|
35
|
+
limit?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface InboxMessage {
|
|
38
|
+
messageId: string;
|
|
39
|
+
sourceDid: string;
|
|
40
|
+
topic: string;
|
|
41
|
+
payload: string;
|
|
42
|
+
receivedAtMs: number;
|
|
43
|
+
priority: number;
|
|
44
|
+
seq: number;
|
|
45
|
+
}
|
|
46
|
+
export interface SendOptions {
|
|
47
|
+
ttlSec?: number;
|
|
48
|
+
priority?: MessagePriority;
|
|
49
|
+
/** If true, compress payload > 1 KB with gzip before sending. */
|
|
50
|
+
compress?: boolean;
|
|
51
|
+
/** Recipient's X25519 public key hex for E2E encryption. */
|
|
52
|
+
encryptForKeyHex?: string;
|
|
53
|
+
/** Idempotency key for deduplication. */
|
|
54
|
+
idempotencyKey?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Callback for WebSocket subscribers — called when a new message arrives in the inbox. */
|
|
57
|
+
export type InboxSubscriber = (message: InboxMessage) => void;
|
|
58
|
+
export declare class MessagingService {
|
|
59
|
+
private readonly log;
|
|
60
|
+
private readonly store;
|
|
61
|
+
private readonly p2p;
|
|
62
|
+
private readonly localDid;
|
|
63
|
+
private cleanupTimer?;
|
|
64
|
+
/**
|
|
65
|
+
* DID → PeerId mapping. Populated via the did-announce protocol when
|
|
66
|
+
* peers connect. This is a best-effort cache; entries are never evicted
|
|
67
|
+
* but may become stale when peers go offline.
|
|
68
|
+
*/
|
|
69
|
+
private readonly didToPeerId;
|
|
70
|
+
private readonly peerIdToDid;
|
|
71
|
+
/** WebSocket subscribers that receive real-time inbox pushes. */
|
|
72
|
+
private readonly subscribers;
|
|
73
|
+
/** Sliding-window rate limiter: DID → array of timestamps (ms). */
|
|
74
|
+
private readonly rateBuckets;
|
|
75
|
+
/** Inbound rate limiter: peerId → array of timestamps. */
|
|
76
|
+
private readonly inboundRateBuckets;
|
|
77
|
+
/** Timer for periodic rate-bucket garbage collection. */
|
|
78
|
+
private rateBucketGcTimer?;
|
|
79
|
+
constructor(p2p: P2PNode, store: MessageStore, localDid: string);
|
|
80
|
+
start(): Promise<void>;
|
|
81
|
+
stop(): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Send a message to a target DID.
|
|
84
|
+
* If the target peer is online and reachable, delivers directly.
|
|
85
|
+
* Otherwise queues in outbox for later delivery.
|
|
86
|
+
*/
|
|
87
|
+
send(targetDid: string, topic: string, payload: string, opts?: SendOptions): Promise<SendResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Send a message to multiple target DIDs (multicast).
|
|
90
|
+
* Each target is attempted independently — partial success is possible.
|
|
91
|
+
*/
|
|
92
|
+
sendMulticast(targetDids: string[], topic: string, payload: string, opts?: SendOptions): Promise<MulticastResult>;
|
|
93
|
+
/** Query the local inbox. */
|
|
94
|
+
getInbox(opts?: InboxQueryOptions): InboxMessage[];
|
|
95
|
+
/** Acknowledge (consume) a message from inbox. */
|
|
96
|
+
ackMessage(messageId: string): boolean;
|
|
97
|
+
/** Flush outbox: attempt to deliver all pending messages for a specific DID with exponential backoff. */
|
|
98
|
+
flushOutboxForDid(targetDid: string): Promise<number>;
|
|
99
|
+
/**
|
|
100
|
+
* Called when a peer connects. Announces our DID and flushes any
|
|
101
|
+
* pending outbox messages for that peer's DID.
|
|
102
|
+
*/
|
|
103
|
+
onPeerConnected(peerId: string): Promise<void>;
|
|
104
|
+
/** Return the current DID→PeerId mapping (for debugging/status). */
|
|
105
|
+
getDidPeerMap(): Record<string, string>;
|
|
106
|
+
/** Register a subscriber for real-time inbox pushes. */
|
|
107
|
+
addSubscriber(cb: InboxSubscriber): void;
|
|
108
|
+
/** Remove a subscriber. */
|
|
109
|
+
removeSubscriber(cb: InboxSubscriber): void;
|
|
110
|
+
/** Number of active WS subscribers. */
|
|
111
|
+
get subscriberCount(): number;
|
|
112
|
+
/** Notify all subscribers of a new inbox message (non-blocking). */
|
|
113
|
+
private notifySubscribers;
|
|
114
|
+
/**
|
|
115
|
+
* Check rate limit for a DID. Throws if limit exceeded.
|
|
116
|
+
* Uses a sliding window of timestamps with binary search eviction.
|
|
117
|
+
*/
|
|
118
|
+
enforceRateLimit(did: string): void;
|
|
119
|
+
/** Check if a DID is currently rate-limited (without consuming a slot). */
|
|
120
|
+
isRateLimited(did: string): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Enforce inbound rate limit for a peerId. Throws if limit exceeded.
|
|
123
|
+
* Prevents P2P peers from spamming without limit.
|
|
124
|
+
*/
|
|
125
|
+
private enforceInboundRateLimit;
|
|
126
|
+
/**
|
|
127
|
+
* Core rate-limit check: evict expired entries via binary search,
|
|
128
|
+
* push new timestamp, throw if over limit.
|
|
129
|
+
*/
|
|
130
|
+
private checkRateBucket;
|
|
131
|
+
/** Binary search: find first index where timestamps[i] >= target. */
|
|
132
|
+
private bisectLeft;
|
|
133
|
+
/** Remove empty and stale rate-limit buckets to prevent memory leaks. */
|
|
134
|
+
private pruneRateBuckets;
|
|
135
|
+
/**
|
|
136
|
+
* Encode a payload: optionally compress (gzip) then optionally encrypt (X25519+AES-256-GCM).
|
|
137
|
+
* Returns the encoded string and flags indicating what was applied.
|
|
138
|
+
*/
|
|
139
|
+
private encodePayload;
|
|
140
|
+
/**
|
|
141
|
+
* Decrypt an E2E-encrypted payload using the local node's X25519 private key.
|
|
142
|
+
* Returns the decrypted payload string or null if not encrypted / decryption fails.
|
|
143
|
+
*/
|
|
144
|
+
static decryptPayload(payload: string, recipientPrivateKey: Uint8Array): string | null;
|
|
145
|
+
/**
|
|
146
|
+
* Decompress a gzip-compressed payload (base64-encoded gzip → utf-8 string).
|
|
147
|
+
* Returns the decompressed string, or null if decompression fails.
|
|
148
|
+
*/
|
|
149
|
+
static decompressPayload(payload: string): string | null;
|
|
150
|
+
/** Get the current inbox sequence number (for WS replay). */
|
|
151
|
+
getCurrentSeq(): number;
|
|
152
|
+
private deliverDirect;
|
|
153
|
+
private handleInboundMessage;
|
|
154
|
+
private handleDidAnnounce;
|
|
155
|
+
/** Announce our DID to a specific peer. */
|
|
156
|
+
private announceDidToPeer;
|
|
157
|
+
/** Announce our DID to all currently connected peers. */
|
|
158
|
+
private announceToAll;
|
|
159
|
+
/**
|
|
160
|
+
* Deliver to multiple targets concurrently with bounded concurrency.
|
|
161
|
+
* Uses Promise.allSettled so one failure doesn't block others.
|
|
162
|
+
*/
|
|
163
|
+
private deliverMulticast;
|
|
164
|
+
/** Send a delivery receipt to the sender after receiving a message. */
|
|
165
|
+
private sendDeliveryReceipt;
|
|
166
|
+
/** Handle an incoming delivery receipt from a remote peer. */
|
|
167
|
+
private handleDeliveryReceipt;
|
|
168
|
+
}
|
|
169
|
+
export declare class RateLimitError extends Error {
|
|
170
|
+
readonly did: string;
|
|
171
|
+
readonly limit: number;
|
|
172
|
+
constructor(did: string, limit: number);
|
|
173
|
+
}
|
|
174
|
+
//# 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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"}
|