Files
next.orly.dev/pkg/protocol/directory-client/src/helpers.ts
mleku 8e15ca7e2f Enhance Directory Client Library for NIP-XX Protocol
- Introduced a TypeScript client library for the Distributed Directory Consensus Protocol (NIP-XX), providing a high-level API for managing directory events, identity resolution, and trust calculations.
- Implemented core functionalities including event parsing, trust score aggregation, and replication filtering, mirroring the Go implementation.
- Added comprehensive documentation and development guides for ease of use and integration.
- Updated the `.gitignore` to include additional dependencies and build artifacts for the TypeScript client.
- Enhanced validation mechanisms for group tag names and trust levels, ensuring robust input handling and security.
- Created a new `bun.lock` file to manage package dependencies effectively.
2025-10-25 14:12:09 +01:00

283 lines
7.0 KiB
TypeScript

/**
* Helper utilities for the Directory Consensus Protocol
*/
import type { NostrEvent } from 'applesauce-core/helpers';
import type { EventStore } from 'applesauce-core';
import type {
RelayIdentity,
TrustAct,
GroupTagAct,
TrustLevel,
} from './types.js';
import { EventKinds } from './types.js';
import {
parseRelayIdentity,
parseTrustAct,
parseGroupTagAct,
} from './parsers.js';
import { Observable, combineLatest, map } from 'rxjs';
/**
* Trust calculator for computing aggregate trust scores
*/
export class TrustCalculator {
private acts: Map<string, TrustAct[]> = new Map();
/**
* Add a trust act to the calculator
*/
public addAct(act: TrustAct): void {
const key = act.targetPubkey;
if (!this.acts.has(key)) {
this.acts.set(key, []);
}
this.acts.get(key)!.push(act);
}
/**
* Calculate aggregate trust score for a pubkey
*
* @param pubkey - The public key to calculate trust for
* @returns Numeric trust score (0-100)
*/
public calculateTrust(pubkey: string): number {
const acts = this.acts.get(pubkey) || [];
if (acts.length === 0) return 0;
// Simple weighted average: high=100, medium=50, low=25
const weights: Record<TrustLevel, number> = {
[TrustLevel.High]: 100,
[TrustLevel.Medium]: 50,
[TrustLevel.Low]: 25,
};
let total = 0;
let count = 0;
for (const act of acts) {
// Skip expired acts
if (act.expiry && act.expiry < new Date()) {
continue;
}
total += weights[act.trustLevel];
count++;
}
return count > 0 ? total / count : 0;
}
/**
* Get all acts for a pubkey
*/
public getActs(pubkey: string): TrustAct[] {
return this.acts.get(pubkey) || [];
}
/**
* Clear all acts
*/
public clear(): void {
this.acts.clear();
}
}
/**
* Replication filter for managing which events to replicate
*/
export class ReplicationFilter {
private trustedRelays: Set<string> = new Set();
private trustCalculator: TrustCalculator;
private minTrustScore: number;
constructor(minTrustScore = 50) {
this.trustCalculator = new TrustCalculator();
this.minTrustScore = minTrustScore;
}
/**
* Add a trust act to influence replication decisions
*/
public addTrustAct(act: TrustAct): void {
this.trustCalculator.addAct(act);
// Update trusted relays based on trust score
const score = this.trustCalculator.calculateTrust(act.targetPubkey);
if (score >= this.minTrustScore) {
this.trustedRelays.add(act.targetPubkey);
} else {
this.trustedRelays.delete(act.targetPubkey);
}
}
/**
* Check if a relay is trusted enough for replication
*/
public shouldReplicate(pubkey: string): boolean {
return this.trustedRelays.has(pubkey);
}
/**
* Get all trusted relay pubkeys
*/
public getTrustedRelays(): string[] {
return Array.from(this.trustedRelays);
}
/**
* Get trust score for a relay
*/
public getTrustScore(pubkey: string): number {
return this.trustCalculator.calculateTrust(pubkey);
}
}
/**
* Helper to find all relay identities in an event store
*/
export function findRelayIdentities(eventStore: EventStore): Observable<RelayIdentity[]> {
return eventStore.stream({ kinds: [EventKinds.RelayIdentityAnnouncement] }).pipe(
map(events => {
const identities: RelayIdentity[] = [];
for (const event of events as any) {
try {
identities.push(parseRelayIdentity(event));
} catch (err) {
// Skip invalid events
console.warn('Invalid relay identity:', err);
}
}
return identities;
})
);
}
/**
* Helper to find all trust acts for a specific relay
*/
export function findTrustActsForRelay(
eventStore: EventStore,
targetPubkey: string
): Observable<TrustAct[]> {
return eventStore.stream({ kinds: [EventKinds.TrustAct] }).pipe(
map(events => {
const acts: TrustAct[] = [];
for (const event of events as any) {
try {
const act = parseTrustAct(event);
if (act.targetPubkey === targetPubkey) {
acts.push(act);
}
} catch (err) {
// Skip invalid events
console.warn('Invalid trust act:', err);
}
}
return acts;
})
);
}
/**
* Helper to find all group tag acts for a specific relay
*/
export function findGroupTagActsForRelay(
eventStore: EventStore,
targetPubkey: string
): Observable<GroupTagAct[]> {
return eventStore.stream({ kinds: [EventKinds.GroupTagAct] }).pipe(
map(events => {
const acts: GroupTagAct[] = [];
for (const event of events as any) {
try {
const act = parseGroupTagAct(event);
if (act.targetPubkey === targetPubkey) {
acts.push(act);
}
} catch (err) {
// Skip invalid events
console.warn('Invalid group tag act:', err);
}
}
return acts;
})
);
}
/**
* Helper to build a trust graph from an event store
*/
export function buildTrustGraph(eventStore: EventStore): Observable<Map<string, TrustAct[]>> {
return eventStore.stream({ kinds: [EventKinds.TrustAct] }).pipe(
map(events => {
const graph = new Map<string, TrustAct[]>();
for (const event of events as any) {
try {
const act = parseTrustAct(event);
const source = event.pubkey;
if (!graph.has(source)) {
graph.set(source, []);
}
graph.get(source)!.push(act);
} catch (err) {
// Skip invalid events
console.warn('Invalid trust act:', err);
}
}
return graph;
})
);
}
/**
* Helper to check if an event is a directory event
*/
export function isDirectoryEvent(event: NostrEvent): boolean {
return Object.values(EventKinds).includes(event.kind as any);
}
/**
* Helper to filter directory events from a stream
*/
export function filterDirectoryEvents(eventStore: EventStore): Observable<NostrEvent> {
return eventStore.stream({ kinds: Object.values(EventKinds) });
}
/**
* Format a relay URL to canonical format (with trailing slash)
*/
export function normalizeRelayURL(url: string): string {
const trimmed = url.trim();
return trimmed.endsWith('/') ? trimmed : `${trimmed}/`;
}
/**
* Extract relay URL from a NIP-11 URL
*/
export function extractRelayURL(nip11URL: string): string {
try {
const url = new URL(nip11URL);
// Convert http(s) to ws(s)
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
return normalizeRelayURL(`${protocol}//${url.host}${url.pathname}`);
} catch (err) {
throw new Error(`Invalid NIP-11 URL: ${nip11URL}`);
}
}
/**
* Create a NIP-11 URL from a relay WebSocket URL
*/
export function createNIP11URL(relayURL: string): string {
try {
const url = new URL(relayURL);
// Convert ws(s) to http(s)
const protocol = url.protocol === 'wss:' ? 'https:' : 'http:';
return `${protocol}//${url.host}${url.pathname}`;
} catch (err) {
throw new Error(`Invalid relay URL: ${relayURL}`);
}
}