@highway1/core 0.1.46 → 0.1.48
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/dist/index.d.ts +308 -1
- package/dist/index.js +3430 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +5 -0
- package/src/messaging/defense.ts +236 -0
- package/src/messaging/index.ts +5 -0
- package/src/messaging/queue.ts +181 -0
- package/src/messaging/rate-limiter.ts +85 -0
- package/src/messaging/router.ts +38 -7
- package/src/messaging/storage.ts +281 -0
- package/src/messaging/types.ts +149 -0
- package/src/transport/node.ts +3 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Storage - LevelDB operations for message queue
|
|
3
|
+
*
|
|
4
|
+
* Key schema:
|
|
5
|
+
* msg:inbound:{timestamp}:{id} → StoredMessage
|
|
6
|
+
* msg:outbound:{timestamp}:{id} → StoredMessage
|
|
7
|
+
* block:{did} → BlocklistEntry
|
|
8
|
+
* allow:{did} → AllowlistEntry
|
|
9
|
+
* seen:{messageId} → SeenEntry
|
|
10
|
+
* rate:{did} → RateLimitState
|
|
11
|
+
* idx:from:{did}:{timestamp}:{id} → '1'
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Level } from 'level';
|
|
15
|
+
import { createLogger } from '../utils/logger.js';
|
|
16
|
+
import type {
|
|
17
|
+
StoredMessage,
|
|
18
|
+
BlocklistEntry,
|
|
19
|
+
AllowlistEntry,
|
|
20
|
+
SeenEntry,
|
|
21
|
+
RateLimitState,
|
|
22
|
+
MessageFilter,
|
|
23
|
+
PaginationOptions,
|
|
24
|
+
MessagePage,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
|
|
27
|
+
const logger = createLogger('message-storage');
|
|
28
|
+
|
|
29
|
+
export class MessageStorage {
|
|
30
|
+
private db: Level<string, any>;
|
|
31
|
+
private ready = false;
|
|
32
|
+
|
|
33
|
+
constructor(dbPath: string) {
|
|
34
|
+
this.db = new Level<string, any>(dbPath, { valueEncoding: 'json' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async open(): Promise<void> {
|
|
38
|
+
await this.db.open();
|
|
39
|
+
this.ready = true;
|
|
40
|
+
logger.info('Message storage opened');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async close(): Promise<void> {
|
|
44
|
+
if (this.ready) {
|
|
45
|
+
await this.db.close();
|
|
46
|
+
this.ready = false;
|
|
47
|
+
logger.info('Message storage closed');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Message Operations ───────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
async putMessage(msg: StoredMessage): Promise<void> {
|
|
54
|
+
const ts = String(msg.receivedAt ?? msg.sentAt ?? Date.now()).padStart(16, '0');
|
|
55
|
+
const key = `msg:${msg.direction}:${ts}:${msg.envelope.id}`;
|
|
56
|
+
await this.db.put(key, msg);
|
|
57
|
+
|
|
58
|
+
// Secondary index by sender
|
|
59
|
+
const idxKey = `idx:from:${msg.envelope.from}:${ts}:${msg.envelope.id}`;
|
|
60
|
+
await this.db.put(idxKey, '1');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getMessage(id: string): Promise<StoredMessage | null> {
|
|
64
|
+
// Scan both directions since we don't know the timestamp
|
|
65
|
+
for (const direction of ['inbound', 'outbound'] as const) {
|
|
66
|
+
const prefix = `msg:${direction}:`;
|
|
67
|
+
for await (const [, value] of this.db.iterator<string, StoredMessage>({
|
|
68
|
+
gte: prefix,
|
|
69
|
+
lte: prefix + '\xff',
|
|
70
|
+
valueEncoding: 'json',
|
|
71
|
+
})) {
|
|
72
|
+
if (value.envelope.id === id) return value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async updateMessage(id: string, updates: Partial<StoredMessage>): Promise<void> {
|
|
79
|
+
const msg = await this.getMessage(id);
|
|
80
|
+
if (!msg) return;
|
|
81
|
+
const ts = String(msg.receivedAt ?? msg.sentAt ?? Date.now()).padStart(16, '0');
|
|
82
|
+
const key = `msg:${msg.direction}:${ts}:${id}`;
|
|
83
|
+
await this.db.put(key, { ...msg, ...updates });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async deleteMessage(id: string): Promise<void> {
|
|
87
|
+
const msg = await this.getMessage(id);
|
|
88
|
+
if (!msg) return;
|
|
89
|
+
const ts = String(msg.receivedAt ?? msg.sentAt ?? Date.now()).padStart(16, '0');
|
|
90
|
+
const key = `msg:${msg.direction}:${ts}:${id}`;
|
|
91
|
+
const idxKey = `idx:from:${msg.envelope.from}:${ts}:${id}`;
|
|
92
|
+
await this.db.batch([
|
|
93
|
+
{ type: 'del', key },
|
|
94
|
+
{ type: 'del', key: idxKey },
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async queryMessages(
|
|
99
|
+
direction: 'inbound' | 'outbound',
|
|
100
|
+
filter: MessageFilter = {},
|
|
101
|
+
pagination: PaginationOptions = {}
|
|
102
|
+
): Promise<MessagePage> {
|
|
103
|
+
const { limit = 50, offset = 0 } = pagination;
|
|
104
|
+
const prefix = `msg:${direction}:`;
|
|
105
|
+
const results: StoredMessage[] = [];
|
|
106
|
+
let total = 0;
|
|
107
|
+
let skipped = 0;
|
|
108
|
+
|
|
109
|
+
for await (const [, value] of this.db.iterator<string, StoredMessage>({
|
|
110
|
+
gte: prefix,
|
|
111
|
+
lte: prefix + '\xff',
|
|
112
|
+
reverse: true, // newest first
|
|
113
|
+
valueEncoding: 'json',
|
|
114
|
+
})) {
|
|
115
|
+
if (!this.matchesFilter(value, filter)) continue;
|
|
116
|
+
total++;
|
|
117
|
+
if (skipped < offset) { skipped++; continue; }
|
|
118
|
+
if (results.length < limit) results.push(value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
messages: results,
|
|
123
|
+
total,
|
|
124
|
+
hasMore: total > offset + results.length,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private matchesFilter(msg: StoredMessage, filter: MessageFilter): boolean {
|
|
129
|
+
if (filter.fromDid) {
|
|
130
|
+
const froms = Array.isArray(filter.fromDid) ? filter.fromDid : [filter.fromDid];
|
|
131
|
+
if (!froms.includes(msg.envelope.from)) return false;
|
|
132
|
+
}
|
|
133
|
+
if (filter.toDid) {
|
|
134
|
+
const tos = Array.isArray(filter.toDid) ? filter.toDid : [filter.toDid];
|
|
135
|
+
if (!tos.includes(msg.envelope.to)) return false;
|
|
136
|
+
}
|
|
137
|
+
if (filter.protocol) {
|
|
138
|
+
const protos = Array.isArray(filter.protocol) ? filter.protocol : [filter.protocol];
|
|
139
|
+
if (!protos.includes(msg.envelope.protocol)) return false;
|
|
140
|
+
}
|
|
141
|
+
if (filter.type && msg.envelope.type !== filter.type) return false;
|
|
142
|
+
if (filter.unreadOnly && msg.readAt != null) return false;
|
|
143
|
+
if (filter.status) {
|
|
144
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
145
|
+
if (!statuses.includes(msg.status)) return false;
|
|
146
|
+
}
|
|
147
|
+
if (filter.maxAge) {
|
|
148
|
+
const age = Date.now() - (msg.receivedAt ?? msg.sentAt ?? 0);
|
|
149
|
+
if (age > filter.maxAge) return false;
|
|
150
|
+
}
|
|
151
|
+
if (filter.minTrustScore != null && (msg.trustScore ?? 0) < filter.minTrustScore) return false;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async countMessages(direction: 'inbound' | 'outbound', filter: MessageFilter = {}): Promise<number> {
|
|
156
|
+
const prefix = `msg:${direction}:`;
|
|
157
|
+
let count = 0;
|
|
158
|
+
for await (const [, value] of this.db.iterator<string, StoredMessage>({
|
|
159
|
+
gte: prefix,
|
|
160
|
+
lte: prefix + '\xff',
|
|
161
|
+
valueEncoding: 'json',
|
|
162
|
+
})) {
|
|
163
|
+
if (this.matchesFilter(value, filter)) count++;
|
|
164
|
+
}
|
|
165
|
+
return count;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Blocklist ────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
async putBlock(entry: BlocklistEntry): Promise<void> {
|
|
171
|
+
await this.db.put(`block:${entry.did}`, entry);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getBlock(did: string): Promise<BlocklistEntry | null> {
|
|
175
|
+
try {
|
|
176
|
+
return await this.db.get(`block:${did}`);
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async deleteBlock(did: string): Promise<void> {
|
|
183
|
+
try { await this.db.del(`block:${did}`); } catch { /* not found */ }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async listBlocked(): Promise<BlocklistEntry[]> {
|
|
187
|
+
const results: BlocklistEntry[] = [];
|
|
188
|
+
for await (const [, value] of this.db.iterator<string, BlocklistEntry>({
|
|
189
|
+
gte: 'block:',
|
|
190
|
+
lte: 'block:\xff',
|
|
191
|
+
valueEncoding: 'json',
|
|
192
|
+
})) {
|
|
193
|
+
results.push(value);
|
|
194
|
+
}
|
|
195
|
+
return results;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Allowlist ────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
async putAllow(entry: AllowlistEntry): Promise<void> {
|
|
201
|
+
await this.db.put(`allow:${entry.did}`, entry);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async getAllow(did: string): Promise<AllowlistEntry | null> {
|
|
205
|
+
try {
|
|
206
|
+
return await this.db.get(`allow:${did}`);
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async deleteAllow(did: string): Promise<void> {
|
|
213
|
+
try { await this.db.del(`allow:${did}`); } catch { /* not found */ }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async listAllowed(): Promise<AllowlistEntry[]> {
|
|
217
|
+
const results: AllowlistEntry[] = [];
|
|
218
|
+
for await (const [, value] of this.db.iterator<string, AllowlistEntry>({
|
|
219
|
+
gte: 'allow:',
|
|
220
|
+
lte: 'allow:\xff',
|
|
221
|
+
valueEncoding: 'json',
|
|
222
|
+
})) {
|
|
223
|
+
results.push(value);
|
|
224
|
+
}
|
|
225
|
+
return results;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── Seen Cache ───────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
async putSeen(entry: SeenEntry): Promise<void> {
|
|
231
|
+
await this.db.put(`seen:${entry.messageId}`, entry);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async getSeen(messageId: string): Promise<SeenEntry | null> {
|
|
235
|
+
try {
|
|
236
|
+
return await this.db.get(`seen:${messageId}`);
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async cleanupSeen(maxAgeMs: number): Promise<void> {
|
|
243
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
244
|
+
const toDelete: string[] = [];
|
|
245
|
+
for await (const [key, value] of this.db.iterator<string, SeenEntry>({
|
|
246
|
+
gte: 'seen:',
|
|
247
|
+
lte: 'seen:\xff',
|
|
248
|
+
valueEncoding: 'json',
|
|
249
|
+
})) {
|
|
250
|
+
if (value.seenAt < cutoff) toDelete.push(key);
|
|
251
|
+
}
|
|
252
|
+
await this.db.batch(toDelete.map((key) => ({ type: 'del' as const, key })));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Rate Limit State ─────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
async putRateLimit(state: RateLimitState): Promise<void> {
|
|
258
|
+
await this.db.put(`rate:${state.did}`, state);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async getRateLimit(did: string): Promise<RateLimitState | null> {
|
|
262
|
+
try {
|
|
263
|
+
return await this.db.get(`rate:${did}`);
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async cleanupRateLimits(maxAgeMs: number): Promise<void> {
|
|
270
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
271
|
+
const toDelete: string[] = [];
|
|
272
|
+
for await (const [key, value] of this.db.iterator<string, RateLimitState>({
|
|
273
|
+
gte: 'rate:',
|
|
274
|
+
lte: 'rate:\xff',
|
|
275
|
+
valueEncoding: 'json',
|
|
276
|
+
})) {
|
|
277
|
+
if (value.lastRefill < cutoff) toDelete.push(key);
|
|
278
|
+
}
|
|
279
|
+
await this.db.batch(toDelete.map((key) => ({ type: 'del' as const, key })));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Queue Types
|
|
3
|
+
*
|
|
4
|
+
* Types for message queue, storage, and filtering operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MessageEnvelope } from './envelope.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Message direction
|
|
11
|
+
*/
|
|
12
|
+
export type MessageDirection = 'inbound' | 'outbound';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Message status
|
|
16
|
+
*/
|
|
17
|
+
export type MessageStatus = 'pending' | 'delivered' | 'failed' | 'archived';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Stored message with metadata
|
|
21
|
+
*/
|
|
22
|
+
export interface StoredMessage {
|
|
23
|
+
envelope: MessageEnvelope;
|
|
24
|
+
direction: MessageDirection;
|
|
25
|
+
status: MessageStatus;
|
|
26
|
+
receivedAt?: number;
|
|
27
|
+
sentAt?: number;
|
|
28
|
+
readAt?: number;
|
|
29
|
+
trustScore?: number;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Message filter for queries
|
|
35
|
+
*/
|
|
36
|
+
export interface MessageFilter {
|
|
37
|
+
fromDid?: string | string[];
|
|
38
|
+
toDid?: string | string[];
|
|
39
|
+
protocol?: string | string[];
|
|
40
|
+
minTrustScore?: number;
|
|
41
|
+
maxAge?: number; // milliseconds
|
|
42
|
+
type?: 'request' | 'response' | 'notification';
|
|
43
|
+
unreadOnly?: boolean;
|
|
44
|
+
status?: MessageStatus | MessageStatus[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Pagination options
|
|
49
|
+
*/
|
|
50
|
+
export interface PaginationOptions {
|
|
51
|
+
limit?: number;
|
|
52
|
+
offset?: number;
|
|
53
|
+
startKey?: string; // For cursor-based pagination
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Paginated message results
|
|
58
|
+
*/
|
|
59
|
+
export interface MessagePage {
|
|
60
|
+
messages: StoredMessage[];
|
|
61
|
+
total: number;
|
|
62
|
+
hasMore: boolean;
|
|
63
|
+
nextKey?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Blocklist entry
|
|
68
|
+
*/
|
|
69
|
+
export interface BlocklistEntry {
|
|
70
|
+
did: string;
|
|
71
|
+
reason: string;
|
|
72
|
+
blockedAt: number;
|
|
73
|
+
blockedBy: string; // Local agent DID
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Allowlist entry
|
|
78
|
+
*/
|
|
79
|
+
export interface AllowlistEntry {
|
|
80
|
+
did: string;
|
|
81
|
+
addedAt: number;
|
|
82
|
+
note?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Seen cache entry (for deduplication)
|
|
87
|
+
*/
|
|
88
|
+
export interface SeenEntry {
|
|
89
|
+
messageId: string;
|
|
90
|
+
seenAt: number;
|
|
91
|
+
fromDid: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Rate limit state
|
|
96
|
+
*/
|
|
97
|
+
export interface RateLimitState {
|
|
98
|
+
did: string;
|
|
99
|
+
tokens: number;
|
|
100
|
+
lastRefill: number;
|
|
101
|
+
totalRequests: number;
|
|
102
|
+
firstSeen: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Defense check result
|
|
107
|
+
*/
|
|
108
|
+
export interface DefenseResult {
|
|
109
|
+
allowed: boolean;
|
|
110
|
+
reason?: 'blocked' | 'duplicate' | 'trust_too_low' | 'rate_limited' | 'invalid';
|
|
111
|
+
trustScore?: number;
|
|
112
|
+
remainingTokens?: number;
|
|
113
|
+
resetTime?: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Rate limit result
|
|
118
|
+
*/
|
|
119
|
+
export interface RateLimitResult {
|
|
120
|
+
allowed: boolean;
|
|
121
|
+
remaining: number;
|
|
122
|
+
resetTime: number;
|
|
123
|
+
limit: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Queue statistics
|
|
128
|
+
*/
|
|
129
|
+
export interface QueueStats {
|
|
130
|
+
inboxTotal: number;
|
|
131
|
+
inboxUnread: number;
|
|
132
|
+
outboxPending: number;
|
|
133
|
+
outboxFailed: number;
|
|
134
|
+
blockedAgents: number;
|
|
135
|
+
allowedAgents: number;
|
|
136
|
+
rateLimitedAgents: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Subscription callback
|
|
141
|
+
*/
|
|
142
|
+
export type MessageCallback = (message: StoredMessage) => void | Promise<void>;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Subscription filter
|
|
146
|
+
*/
|
|
147
|
+
export interface SubscriptionFilter extends MessageFilter {
|
|
148
|
+
webhookUrl?: string;
|
|
149
|
+
}
|
package/src/transport/node.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createLibp2p, Libp2p } from 'libp2p';
|
|
2
2
|
import { tcp } from '@libp2p/tcp';
|
|
3
|
+
import { webSockets } from '@libp2p/websockets';
|
|
3
4
|
import { noise } from '@chainsafe/libp2p-noise';
|
|
4
5
|
import { mplex } from '@libp2p/mplex';
|
|
5
6
|
import { kadDHT, passthroughMapper } from '@libp2p/kad-dht';
|
|
@@ -48,7 +49,7 @@ export async function createNode(
|
|
|
48
49
|
): Promise<ClawiverseNode> {
|
|
49
50
|
try {
|
|
50
51
|
const {
|
|
51
|
-
listenAddresses = ['/ip4/0.0.0.0/tcp/0'],
|
|
52
|
+
listenAddresses = ['/ip4/0.0.0.0/tcp/0', '/ip4/0.0.0.0/tcp/0/ws'], // CVP-0010 §5: Add WebSocket listener
|
|
52
53
|
bootstrapPeers = [],
|
|
53
54
|
enableDHT = true,
|
|
54
55
|
enableRelay = false,
|
|
@@ -69,6 +70,7 @@ export async function createNode(
|
|
|
69
70
|
...(privateKey ? { privateKey } : {}),
|
|
70
71
|
transports: [
|
|
71
72
|
tcp(),
|
|
73
|
+
webSockets(), // CVP-0010 §5: WebSocket for firewall traversal
|
|
72
74
|
circuitRelayTransport(),
|
|
73
75
|
],
|
|
74
76
|
connectionEncrypters: [noise()],
|