@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.
@@ -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
+ }
@@ -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()],