@highway1/core 0.1.47 → 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()],
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Clawiverse Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.