mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-28 08:22:03 +00:00
- core RelayPool over the platform WebSocket (global in Node 22+/browsers, no dependency): multi-relay publish with per-relay OK collection, fetch aggregation until EOSE with dedupe and signature verification on ingest, connect timeouts, dead-relay tolerance. - Sync flows mirroring nostr/snapshot.py: publishSnapshot (chunk events -> manifest with HMAC identity + nonce), fetchLatestSnapshot (newest-first, per-chunk hash verify, falls through incomplete manifests), publishDelta, fetchDeltasSince (created_at replay order). - CLI nostr group: get-pubkey (matches app-1237 fixture npub), list/add/ remove-relay (config-backed, ws:// validation, last-relay guard), sync (publish local vault), restore (snapshot + delta merge -> local vault). - Tested against an in-process NIP-01 mock relay: core round trip incl. corrupted-chunk rejection, and CLI end-to-end sync -> destroy local vault -> restore with secrets intact. Transport tests are Node-only (jsdom WebSocket can't connect; real browsers come with Milestone 6). - 151 core + 32 CLI tests green. Milestone 5 CLI surface is complete. Co-Authored-By: Claude Fable 5 <[email protected]>
74 lines
2.7 KiB
TypeScript
74 lines
2.7 KiB
TypeScript
/**
|
|
* In-process NIP-01 relay for tests: stores events, answers REQ with
|
|
* matching EVENTs + EOSE, acks EVENT with OK. Test-only (uses `ws`).
|
|
*/
|
|
|
|
import { WebSocketServer, type WebSocket as WsSocket } from "ws";
|
|
import type { AddressInfo } from "node:net";
|
|
import type { Filter, NostrEvent } from "@seedpass/core";
|
|
|
|
function matches(event: NostrEvent, filter: Filter): boolean {
|
|
if (filter.ids && !filter.ids.includes(event.id)) return false;
|
|
if (filter.authors && !filter.authors.includes(event.pubkey)) return false;
|
|
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
if (filter.since !== undefined && event.created_at < filter.since) return false;
|
|
if (filter.until !== undefined && event.created_at > filter.until) return false;
|
|
for (const tagName of ["#d", "#e"] as const) {
|
|
const wanted = filter[tagName];
|
|
if (wanted) {
|
|
const values = event.tags.filter((t) => t[0] === tagName[1]).map((t) => t[1]);
|
|
if (!wanted.some((w) => values.includes(w))) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export class MockRelay {
|
|
readonly events: NostrEvent[] = [];
|
|
private server: WebSocketServer | null = null;
|
|
url = "";
|
|
|
|
async start(): Promise<string> {
|
|
this.server = new WebSocketServer({ port: 0 });
|
|
await new Promise<void>((resolve) => this.server!.once("listening", resolve));
|
|
const { port } = this.server.address() as AddressInfo;
|
|
this.url = `ws://127.0.0.1:${port}`;
|
|
this.server.on("connection", (socket: WsSocket) => {
|
|
socket.on("message", (raw) => {
|
|
let msg: unknown[];
|
|
try {
|
|
msg = JSON.parse(String(raw)) as unknown[];
|
|
} catch {
|
|
return;
|
|
}
|
|
if (msg[0] === "EVENT") {
|
|
const event = msg[1] as NostrEvent;
|
|
this.events.push(event);
|
|
socket.send(JSON.stringify(["OK", event.id, true, ""]));
|
|
} else if (msg[0] === "REQ") {
|
|
const subId = String(msg[1]);
|
|
const filters = msg.slice(2) as Filter[];
|
|
let sent = 0;
|
|
for (const event of this.events) {
|
|
if (filters.some((f) => matches(event, f))) {
|
|
const limit = Math.min(...filters.map((f) => f.limit ?? Infinity));
|
|
if (sent >= limit) break;
|
|
socket.send(JSON.stringify(["EVENT", subId, event]));
|
|
sent++;
|
|
}
|
|
}
|
|
socket.send(JSON.stringify(["EOSE", subId]));
|
|
}
|
|
// CLOSE is a no-op for the mock
|
|
});
|
|
});
|
|
return this.url;
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
for (const client of this.server?.clients ?? []) client.terminate();
|
|
await new Promise<void>((resolve) => this.server?.close(() => resolve()));
|
|
this.server = null;
|
|
}
|
|
}
|