Jason Tudisco 7077191c0c NIP-78 encrypted transport, instant UI, re-sync recovery
Nostr transport:
- Switch from kind 1 to NIP-78 kind 30078 (application-specific data)
- Relays store events persistently — no inventory protocol needed
- AES-256-GCM encryption via Web Crypto API (room key = shared secret)
- PBKDF2 key derivation (100k iterations) from room key
- Chunking for large events (images >48KB) with per-chunk encryption
- Auto re-publish missing local events on room join
- Manual "Re-sync" button for recovery of failed publishes

Performance (all variants):
- Emit 'change' immediately after local store write (IDB/NeDB/SQLite)
- Folder and Nostr writes run fire-and-forget in background
- Fast event hashing: fingerprint metadata only, skip full payload SHA-256
- Saving spinner in paste UI while write completes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 01:11:29 -06:00

94 lines
3.1 KiB
TypeScript

import { FolderSyncDB } from '../../nostr/src/index.ts';
import { initPasteApp } from '../shared.ts';
document.addEventListener('DOMContentLoaded', async () => {
const db = await FolderSyncDB.open({ autoSyncIntervalMs: 3000 });
await initPasteApp(db as any, 'nostr');
// ── Nostr room UI ──────────────────────────────────────────
const roomInput = document.querySelector('#room-input') as HTMLInputElement;
const joinBtn = document.querySelector('#join-room') as HTMLButtonElement;
const nostrStatus = document.querySelector('#nostr-status')!;
function updateNostrUI(connected: boolean, room?: string) {
if (connected && room) {
nostrStatus.textContent = `Connected: ${room}`;
nostrStatus.className = 'status ok';
joinBtn.textContent = 'Leave';
roomInput.disabled = true;
} else {
nostrStatus.textContent = 'Not connected';
nostrStatus.className = 'status';
joinBtn.textContent = 'Join room';
roomInput.disabled = false;
}
}
joinBtn.addEventListener('click', async () => {
if (db.isConnected()) {
db.leaveRoom();
updateNostrUI(false);
} else {
const key = roomInput.value.trim();
if (!key) {
nostrStatus.textContent = 'Enter a room key';
nostrStatus.className = 'status err';
return;
}
try {
nostrStatus.textContent = 'Connecting...';
nostrStatus.className = 'status';
await db.joinRoom(key);
updateNostrUI(true, key);
} catch (e: unknown) {
nostrStatus.textContent = 'Error: ' + (e as Error).message;
nostrStatus.className = 'status err';
}
}
});
roomInput.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Enter') joinBtn.click();
});
// ── Re-sync button ───────────────────────────────────────
const resyncBtn = document.querySelector('#resync-btn') as HTMLButtonElement;
resyncBtn.addEventListener('click', async () => {
if (!db.isConnected()) {
nostrStatus.textContent = 'Join a room first';
nostrStatus.className = 'status err';
return;
}
resyncBtn.disabled = true;
resyncBtn.textContent = 'Syncing...';
try {
await db.republishMissing();
await db.sync();
resyncBtn.textContent = 'Done!';
setTimeout(() => { resyncBtn.textContent = 'Re-sync'; resyncBtn.disabled = false; }, 2000);
} catch (e: unknown) {
resyncBtn.textContent = 'Re-sync';
resyncBtn.disabled = false;
nostrStatus.textContent = 'Re-sync error: ' + (e as Error).message;
nostrStatus.className = 'status err';
}
});
// Restore saved room
if (db.isConnected()) {
updateNostrUI(true, db.currentRoom ?? undefined);
roomInput.value = db.currentRoom ?? '';
} else {
updateNostrUI(false);
}
db.on('nostr:connected', (data: any) => {
updateNostrUI(true, data?.roomKey);
});
db.on('nostr:disconnected', () => {
updateNostrUI(false);
});
});