|
| 1 | +// Follows hose: signature verification + p-tag derivation + relay harvesting. |
| 2 | +import test from 'node:test'; |
| 3 | +import assert from 'node:assert/strict'; |
| 4 | +import { schnorr } from '@noble/curves/secp256k1.js'; |
| 5 | +import { sha256 } from '@noble/hashes/sha2.js'; |
| 6 | +import { hexToBytes, bytesToHex } from '@noble/hashes/utils.js'; |
| 7 | +import { verifyEvent, parseFollows, relayUrlsFrom, canonicalizeRelayUrl } from '../src/hoses/follows.js'; |
| 8 | + |
| 9 | +const enc = new TextEncoder(); |
| 10 | +const PRIV = hexToBytes('0000000000000000000000000000000000000000000000000000000000000003'); |
| 11 | +const PUBKEY = bytesToHex(schnorr.getPublicKey(PRIV)); |
| 12 | +const PK = (n) => String(n).padStart(64, '0'); // a fake 64-hex pubkey |
| 13 | + |
| 14 | +function signed({ tags = [], content = '', created_at = 1000, kind = 3 } = {}) { |
| 15 | + const base = { pubkey: PUBKEY, created_at, kind, tags, content }; |
| 16 | + const id = bytesToHex(sha256(enc.encode(JSON.stringify([0, base.pubkey, base.created_at, base.kind, base.tags, base.content])))); |
| 17 | + const sig = bytesToHex(schnorr.sign(hexToBytes(id), PRIV)); |
| 18 | + return { ...base, id, sig }; |
| 19 | +} |
| 20 | + |
| 21 | +test('accepts a validly-signed kind-3 event (empty content ok)', () => { |
| 22 | + assert.equal(verifyEvent(signed()), true); |
| 23 | +}); |
| 24 | + |
| 25 | +test('rejects the wrong kind', () => { |
| 26 | + assert.equal(verifyEvent(signed({ kind: 0 })), false); |
| 27 | +}); |
| 28 | + |
| 29 | +test('rejects a tampered event', () => { |
| 30 | + const e = signed({ tags: [['p', PK(1)]] }); |
| 31 | + e.tags = [['p', PK(2)]]; // change after signing |
| 32 | + assert.equal(verifyEvent(e), false); |
| 33 | +}); |
| 34 | + |
| 35 | +test('rejects malformed input without throwing', () => { |
| 36 | + for (const bad of [null, undefined, {}, 42, 'x']) assert.equal(verifyEvent(bad), false); |
| 37 | +}); |
| 38 | + |
| 39 | +test('parseFollows: p tags only, 64-hex, lowercased, deduped', () => { |
| 40 | + const e = signed({ tags: [ |
| 41 | + ['p', PK('a').toUpperCase()], // uppercased -> lowercased |
| 42 | + ['p', PK('a')], // duplicate -> deduped |
| 43 | + ['p', PK('b')], |
| 44 | + ['e', PK('c')], // non-p tag -> ignored |
| 45 | + ['p', 'not-hex'], // invalid -> dropped |
| 46 | + ['p'], // malformed -> skipped |
| 47 | + ] }); |
| 48 | + assert.deepEqual(parseFollows(e), [PK('a'), PK('b')]); |
| 49 | +}); |
| 50 | + |
| 51 | +test('parseFollows: no tags -> empty', () => { |
| 52 | + assert.deepEqual(parseFollows(signed()), []); |
| 53 | +}); |
| 54 | + |
| 55 | +test('canonicalizeRelayUrl: trailing slash on origins, path preserved, junk dropped', () => { |
| 56 | + assert.equal(canonicalizeRelayUrl('wss://relay.example.com'), 'wss://relay.example.com/'); |
| 57 | + assert.equal(canonicalizeRelayUrl('wss://relay.example.com/'), 'wss://relay.example.com/'); |
| 58 | + assert.equal(canonicalizeRelayUrl('wss://relay.example.com/inbox'), 'wss://relay.example.com/inbox'); |
| 59 | + assert.equal(canonicalizeRelayUrl('https://not-a-relay.com'), null); |
| 60 | + assert.equal(canonicalizeRelayUrl('garbage'), null); |
| 61 | +}); |
| 62 | + |
| 63 | +test('relayUrlsFrom: parses the legacy relay map, canonicalizes + dedupes', () => { |
| 64 | + const content = JSON.stringify({ |
| 65 | + 'wss://relay.example.com': { read: true, write: true }, |
| 66 | + 'wss://relay.example.com/': { read: true, write: false }, // dedupes with above |
| 67 | + 'wss://other.example.com/inbox': { read: true, write: true }, |
| 68 | + 'https://bad.example.com': {}, // dropped (not ws/wss) |
| 69 | + }); |
| 70 | + assert.deepEqual( |
| 71 | + relayUrlsFrom(signed({ content })).sort(), |
| 72 | + ['wss://other.example.com/inbox', 'wss://relay.example.com/'], |
| 73 | + ); |
| 74 | +}); |
| 75 | + |
| 76 | +test('relayUrlsFrom: empty / non-JSON content -> empty', () => { |
| 77 | + assert.deepEqual(relayUrlsFrom(signed({ content: '' })), []); |
| 78 | + assert.deepEqual(relayUrlsFrom(signed({ content: 'not json' })), []); |
| 79 | +}); |
0 commit comments