helpers.ts raw
1 /**
2 * Helper utilities for the Directory Consensus Protocol
3 */
4
5 import type { NostrEvent } from 'applesauce-core/helpers';
6 import type { EventStore } from 'applesauce-core';
7 import type {
8 RelayIdentity,
9 TrustAct,
10 GroupTagAct,
11 TrustLevel,
12 } from './types.js';
13 import { EventKinds } from './types.js';
14 import {
15 parseRelayIdentity,
16 parseTrustAct,
17 parseGroupTagAct,
18 } from './parsers.js';
19 import { Observable, combineLatest, map } from 'rxjs';
20
21 /**
22 * Trust calculator for computing aggregate trust scores
23 */
24 export class TrustCalculator {
25 private acts: Map<string, TrustAct[]> = new Map();
26
27 /**
28 * Add a trust act to the calculator
29 */
30 public addAct(act: TrustAct): void {
31 const key = act.targetPubkey;
32 if (!this.acts.has(key)) {
33 this.acts.set(key, []);
34 }
35 this.acts.get(key)!.push(act);
36 }
37
38 /**
39 * Calculate aggregate trust score for a pubkey
40 *
41 * @param pubkey - The public key to calculate trust for
42 * @returns Numeric trust score (0-100)
43 */
44 public calculateTrust(pubkey: string): number {
45 const acts = this.acts.get(pubkey) || [];
46 if (acts.length === 0) return 0;
47
48 // Simple weighted average: high=100, medium=50, low=25
49 const weights: Record<TrustLevel, number> = {
50 [TrustLevel.High]: 100,
51 [TrustLevel.Medium]: 50,
52 [TrustLevel.Low]: 25,
53 };
54
55 let total = 0;
56 let count = 0;
57
58 for (const act of acts) {
59 // Skip expired acts
60 if (act.expiry && act.expiry < new Date()) {
61 continue;
62 }
63
64 total += weights[act.trustLevel];
65 count++;
66 }
67
68 return count > 0 ? total / count : 0;
69 }
70
71 /**
72 * Get all acts for a pubkey
73 */
74 public getActs(pubkey: string): TrustAct[] {
75 return this.acts.get(pubkey) || [];
76 }
77
78 /**
79 * Clear all acts
80 */
81 public clear(): void {
82 this.acts.clear();
83 }
84 }
85
86 /**
87 * Replication filter for managing which events to replicate
88 */
89 export class ReplicationFilter {
90 private trustedRelays: Set<string> = new Set();
91 private trustCalculator: TrustCalculator;
92 private minTrustScore: number;
93
94 constructor(minTrustScore = 50) {
95 this.trustCalculator = new TrustCalculator();
96 this.minTrustScore = minTrustScore;
97 }
98
99 /**
100 * Add a trust act to influence replication decisions
101 */
102 public addTrustAct(act: TrustAct): void {
103 this.trustCalculator.addAct(act);
104
105 // Update trusted relays based on trust score
106 const score = this.trustCalculator.calculateTrust(act.targetPubkey);
107 if (score >= this.minTrustScore) {
108 this.trustedRelays.add(act.targetPubkey);
109 } else {
110 this.trustedRelays.delete(act.targetPubkey);
111 }
112 }
113
114 /**
115 * Check if a relay is trusted enough for replication
116 */
117 public shouldReplicate(pubkey: string): boolean {
118 return this.trustedRelays.has(pubkey);
119 }
120
121 /**
122 * Get all trusted relay pubkeys
123 */
124 public getTrustedRelays(): string[] {
125 return Array.from(this.trustedRelays);
126 }
127
128 /**
129 * Get trust score for a relay
130 */
131 public getTrustScore(pubkey: string): number {
132 return this.trustCalculator.calculateTrust(pubkey);
133 }
134 }
135
136 /**
137 * Helper to find all relay identities in an event store
138 */
139 export function findRelayIdentities(eventStore: EventStore): Observable<RelayIdentity[]> {
140 return eventStore.stream({ kinds: [EventKinds.RelayIdentityAnnouncement] }).pipe(
141 map(events => {
142 const identities: RelayIdentity[] = [];
143 for (const event of events as any) {
144 try {
145 identities.push(parseRelayIdentity(event));
146 } catch (err) {
147 // Skip invalid events
148 console.warn('Invalid relay identity:', err);
149 }
150 }
151 return identities;
152 })
153 );
154 }
155
156 /**
157 * Helper to find all trust acts for a specific relay
158 */
159 export function findTrustActsForRelay(
160 eventStore: EventStore,
161 targetPubkey: string
162 ): Observable<TrustAct[]> {
163 return eventStore.stream({ kinds: [EventKinds.TrustAct] }).pipe(
164 map(events => {
165 const acts: TrustAct[] = [];
166 for (const event of events as any) {
167 try {
168 const act = parseTrustAct(event);
169 if (act.targetPubkey === targetPubkey) {
170 acts.push(act);
171 }
172 } catch (err) {
173 // Skip invalid events
174 console.warn('Invalid trust act:', err);
175 }
176 }
177 return acts;
178 })
179 );
180 }
181
182 /**
183 * Helper to find all group tag acts for a specific relay
184 */
185 export function findGroupTagActsForRelay(
186 eventStore: EventStore,
187 targetPubkey: string
188 ): Observable<GroupTagAct[]> {
189 return eventStore.stream({ kinds: [EventKinds.GroupTagAct] }).pipe(
190 map(events => {
191 const acts: GroupTagAct[] = [];
192 for (const event of events as any) {
193 try {
194 const act = parseGroupTagAct(event);
195 if (act.targetPubkey === targetPubkey) {
196 acts.push(act);
197 }
198 } catch (err) {
199 // Skip invalid events
200 console.warn('Invalid group tag act:', err);
201 }
202 }
203 return acts;
204 })
205 );
206 }
207
208 /**
209 * Helper to build a trust graph from an event store
210 */
211 export function buildTrustGraph(eventStore: EventStore): Observable<Map<string, TrustAct[]>> {
212 return eventStore.stream({ kinds: [EventKinds.TrustAct] }).pipe(
213 map(events => {
214 const graph = new Map<string, TrustAct[]>();
215 for (const event of events as any) {
216 try {
217 const act = parseTrustAct(event);
218 const source = event.pubkey;
219 if (!graph.has(source)) {
220 graph.set(source, []);
221 }
222 graph.get(source)!.push(act);
223 } catch (err) {
224 // Skip invalid events
225 console.warn('Invalid trust act:', err);
226 }
227 }
228 return graph;
229 })
230 );
231 }
232
233 /**
234 * Helper to check if an event is a directory event
235 */
236 export function isDirectoryEvent(event: NostrEvent): boolean {
237 return Object.values(EventKinds).includes(event.kind as any);
238 }
239
240 /**
241 * Helper to filter directory events from a stream
242 */
243 export function filterDirectoryEvents(eventStore: EventStore): Observable<NostrEvent> {
244 return eventStore.stream({ kinds: Object.values(EventKinds) });
245 }
246
247 /**
248 * Format a relay URL to canonical format (with trailing slash)
249 */
250 export function normalizeRelayURL(url: string): string {
251 const trimmed = url.trim();
252 return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
253 }
254
255 /**
256 * Extract relay URL from a NIP-11 URL
257 */
258 export function extractRelayURL(nip11URL: string): string {
259 try {
260 const url = new URL(nip11URL);
261 // Convert http(s) to ws(s)
262 const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
263 return normalizeRelayURL(`${protocol}//${url.host}${url.pathname}`);
264 } catch (err) {
265 throw new Error(`Invalid NIP-11 URL: ${nip11URL}`);
266 }
267 }
268
269 /**
270 * Create a NIP-11 URL from a relay WebSocket URL
271 */
272 export function createNIP11URL(relayURL: string): string {
273 try {
274 const url = new URL(relayURL);
275 // Convert ws(s) to http(s)
276 const protocol = url.protocol === 'wss:' ? 'https:' : 'http:';
277 return `${protocol}//${url.host}${url.pathname}`;
278 } catch (err) {
279 throw new Error(`Invalid relay URL: ${relayURL}`);
280 }
281 }
282
283