mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Implements `scripts/agent/interop-test.mjs` to validate core Nostr operations (Video Post, View Event, Encrypted DM) against a small set of public relays. Ensures ephemeral keys are used and no private data is persisted. Features: - Ephemeral sender/receiver key generation. - Polyfills for WebSocket, localStorage, and Crypto for Node.js environment. - Verification of event signatures, IDs, and content shape. - DM encryption/decryption roundtrip verification. - Safe relay list usage (only 2 known relays).
199 lines
7.2 KiB
JavaScript
199 lines
7.2 KiB
JavaScript
|
|
import { WebSocket } from 'ws';
|
|
import * as crypto from 'node:crypto';
|
|
import * as NostrTools from 'nostr-tools';
|
|
|
|
// Polyfills
|
|
globalThis.WebSocket = WebSocket;
|
|
if (!globalThis.crypto) {
|
|
globalThis.crypto = crypto;
|
|
}
|
|
globalThis.NostrTools = NostrTools;
|
|
|
|
// Mock localStorage
|
|
const localStorageMock = {
|
|
store: {},
|
|
getItem: function(key) { return this.store[key] || null; },
|
|
setItem: function(key, value) { this.store[key] = String(value); },
|
|
removeItem: function(key) { delete this.store[key]; },
|
|
clear: function() { this.store = {}; }
|
|
};
|
|
globalThis.localStorage = localStorageMock;
|
|
|
|
// Prevent crash on unhandled rejection (e.g. from nostr-tools timeouts)
|
|
process.on('unhandledRejection', (reason, p) => {
|
|
// console.log('[TEST] Unhandled Rejection (suppressed):', reason);
|
|
});
|
|
|
|
// Configuration
|
|
const TEST_RELAYS = [
|
|
'wss://relay.damus.io',
|
|
'wss://nos.lol'
|
|
];
|
|
|
|
async function runTest() {
|
|
const { NostrClient } = await import('../../js/nostr/client.js');
|
|
// Removed unused imports
|
|
|
|
console.log('Starting Interop Test...');
|
|
const logs = [];
|
|
const log = (msg, data) => {
|
|
console.log(`[TEST] ${msg}`, data ? JSON.stringify(data, null, 2) : '');
|
|
logs.push({ msg, data });
|
|
};
|
|
|
|
try {
|
|
// -------------------------------------------------------------------------
|
|
// 1. Setup Sender
|
|
// -------------------------------------------------------------------------
|
|
log('Initializing Sender Client...');
|
|
const senderClient = new NostrClient();
|
|
|
|
// Explicitly set relays
|
|
senderClient.relays = [...TEST_RELAYS];
|
|
senderClient.writeRelays = [...TEST_RELAYS];
|
|
senderClient.readRelays = [...TEST_RELAYS];
|
|
log('Sender Relays set to:', senderClient.relays);
|
|
|
|
// Generate Ephemeral Key for Sender
|
|
const senderSecret = NostrTools.generateSecretKey();
|
|
const senderPrivKey = Buffer.from(senderSecret).toString('hex');
|
|
|
|
const { pubkey: senderPubkey } = await senderClient.registerPrivateKeySigner({ privateKey: senderPrivKey });
|
|
senderClient.pubkey = senderPubkey;
|
|
log('Sender Pubkey:', senderClient.pubkey);
|
|
|
|
await senderClient.init();
|
|
log('Sender Client Initialized.');
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 2. Publish Video Post
|
|
// -------------------------------------------------------------------------
|
|
log('Test A: Publish Video Post');
|
|
const videoRootId = `interop-test-${Date.now()}`;
|
|
const magnet = `magnet:?xt=urn:btih:${crypto.randomBytes(20).toString('hex')}&dn=test-video`;
|
|
const title = 'Interop Test Video ' + new Date().toISOString();
|
|
|
|
const videoData = {
|
|
title,
|
|
videoRootId,
|
|
magnet,
|
|
description: 'Automated interop test video',
|
|
mimeType: 'video/mp4'
|
|
};
|
|
|
|
log('Publishing video...', videoData);
|
|
const signedVideoEvent = await senderClient.publishVideo(videoData, senderClient.pubkey);
|
|
log('Video Published. Event ID:', signedVideoEvent.id);
|
|
|
|
// Verify
|
|
log('Verifying video event fetch...');
|
|
// Allow some propagation time
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
|
|
const fetchedVideo = await senderClient.getEventById(signedVideoEvent.id);
|
|
if (!fetchedVideo) throw new Error('Failed to fetch published video event');
|
|
if (fetchedVideo.id !== signedVideoEvent.id) throw new Error('Fetched event ID mismatch');
|
|
|
|
const isSigValid = NostrTools.verifyEvent(signedVideoEvent);
|
|
if (!isSigValid) throw new Error('Invalid event signature');
|
|
|
|
// Verify content shape
|
|
if (fetchedVideo.title !== title) throw new Error(`Title mismatch: expected "${title}", got "${fetchedVideo.title}"`);
|
|
if (fetchedVideo.magnet !== magnet) throw new Error(`Magnet mismatch: expected "${magnet}", got "${fetchedVideo.magnet}"`);
|
|
// Note: publishVideo generates a new videoRootId, so we check against the signed event's content, not our input
|
|
const signedContent = JSON.parse(signedVideoEvent.content);
|
|
if (fetchedVideo.videoRootId !== signedContent.videoRootId) throw new Error(`Root ID mismatch: expected "${signedContent.videoRootId}", got "${fetchedVideo.videoRootId}"`);
|
|
|
|
log('Test A PASSED: Video event published and verified (content matched).');
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 3. Publish View Event
|
|
// -------------------------------------------------------------------------
|
|
log('Test B: Publish View Event');
|
|
const pointer = {
|
|
type: 'e',
|
|
value: signedVideoEvent.id,
|
|
relay: TEST_RELAYS[0]
|
|
};
|
|
|
|
log('Publishing view event...', pointer);
|
|
const viewResult = await senderClient.publishViewEvent(pointer);
|
|
|
|
if (!viewResult.ok) throw new Error(`Failed to publish view event: ${viewResult.error}`);
|
|
log('View event published.');
|
|
|
|
// Verify View Event Visibility
|
|
log('Verifying view event visibility...');
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
|
|
// We can list view events for this pointer
|
|
const viewEvents = await senderClient.listVideoViewEvents(pointer, { limit: 5 });
|
|
const foundView = viewEvents.find(v => v.pubkey === senderPubkey); // It should be signed by sender
|
|
|
|
if (!foundView) {
|
|
log('Fetched View Events:', viewEvents);
|
|
throw new Error('Could not verify visibility of published view event.');
|
|
}
|
|
log('Test B PASSED: View event verified visible.');
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 4. Encrypted Direct Message (Sender -> Receiver)
|
|
// -------------------------------------------------------------------------
|
|
log('Test C: Encrypted DM');
|
|
|
|
// Setup Receiver
|
|
const receiverClient = new NostrClient();
|
|
receiverClient.relays = [...TEST_RELAYS];
|
|
receiverClient.writeRelays = [...TEST_RELAYS];
|
|
receiverClient.readRelays = [...TEST_RELAYS];
|
|
|
|
const receiverSecret = NostrTools.generateSecretKey();
|
|
const receiverPrivKey = Buffer.from(receiverSecret).toString('hex');
|
|
|
|
const { pubkey: receiverPubkey } = await receiverClient.registerPrivateKeySigner({ privateKey: receiverPrivKey });
|
|
receiverClient.pubkey = receiverPubkey;
|
|
log('Receiver Pubkey:', receiverPubkey);
|
|
|
|
await receiverClient.init();
|
|
|
|
// Sender sends DM
|
|
const messageContent = `Hello Interop ${Date.now()}`;
|
|
log(`Sender sending DM to ${receiverPubkey}: "${messageContent}"`);
|
|
|
|
// sendDirectMessage is on the sender client
|
|
const sendResult = await senderClient.sendDirectMessage(
|
|
NostrTools.nip19.npubEncode(receiverPubkey),
|
|
messageContent
|
|
);
|
|
|
|
if (!sendResult.ok) throw new Error(`Failed to send DM: ${sendResult.error}`);
|
|
log('DM Sent.');
|
|
|
|
// Wait for propagation
|
|
await new Promise(r => setTimeout(r, 5000));
|
|
|
|
// Receiver fetches DMs
|
|
log('Receiver fetching DMs...');
|
|
const dms = await receiverClient.listDirectMessages(receiverPubkey);
|
|
|
|
const foundDm = dms.find(dm => dm.plaintext === messageContent);
|
|
|
|
if (!foundDm) {
|
|
log('Fetched DMs:', dms.map(d => ({ plain: d.plaintext, sender: d.sender })));
|
|
throw new Error('Receiver could not find or decrypt the sent message.');
|
|
}
|
|
|
|
log('Test C PASSED: DM received and decrypted.');
|
|
|
|
console.log('\nAll Interop Tests PASSED!');
|
|
process.exit(0);
|
|
|
|
} catch (err) {
|
|
console.error('\nTest FAILED:', err);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
runTest();
|