Files
bitvid/scripts/agent/interop-test.mjs
T
google-labs-jules[bot] 82f92d420f feat(ai): add interop-test.mjs for Nostr protocol validation
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).
2026-01-18 07:11:08 +00:00

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();