diff --git a/admin.html b/admin.html index 82eeae1..1e91d8d 100644 --- a/admin.html +++ b/admin.html @@ -67,7 +67,7 @@

šŸ“” Fonstr Relay Admin

- +
diff --git a/bin/relay.js b/bin/relay.js deleted file mode 100755 index 0ffcaa3..0000000 --- a/bin/relay.js +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env node - -import minimist from 'minimist' -import { createServer } from '../index.js' - -const argv = minimist(process.argv.slice(2)) -const useHttps = argv.h || argv.https - -console.log(argv) - -let port -for (const arg of argv._) { - if (parseInt(arg)) { - port = parseInt(arg) - break - } -} - -if (!port) { - port = 4444 -} - -createServer({ port, useHttps }) diff --git a/deploy.sh b/deploy.sh deleted file mode 100644 index 5ebcf73..0000000 --- a/deploy.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash -# Fonstr VPS Quick Deploy Script - -echo "šŸš€ Fonstr Relay Deployment for VPS" -echo "==================================" - -# Update system -echo "šŸ“¦ Updating system packages..." -apt update && apt upgrade -y - -# Install Node.js (v20 LTS) -echo "šŸ“¦ Installing Node.js..." -curl -fsSL https://deb.nodesource.com/setup_20.x | bash - -apt install -y nodejs - -# Install PM2 for process management -echo "šŸ“¦ Installing PM2..." -npm install -g pm2 - -# Install fonstr -echo "šŸ“¦ Installing fonstr..." -npm install -g fonstr - -# Setup PM2 to run fonstr -echo "āš™ļø Configuring PM2..." -cat > ecosystem.config.js << 'EOF' -module.exports = { - apps: [{ - name: 'fonstr', - script: 'fonstr', - args: '--port 4444', - env: { - MAX_EVENTS: 5000 // More capacity on VPS - }, - restart_delay: 5000, - max_restarts: 10 - }] -} -EOF - -# Start fonstr with PM2 -pm2 start ecosystem.config.js -pm2 save -pm2 startup systemd -u $USER --hp $HOME - -# Setup firewall -echo "šŸ”’ Configuring firewall..." -ufw allow 4444/tcp -ufw allow 22/tcp -ufw --force enable - -# Setup nginx for WebSocket proxy (optional) -read -p "Install nginx for domain support? (y/n): " -n 1 -r -echo -if [[ $REPLY =~ ^[Yy]$ ]]; then - apt install -y nginx certbot python3-certbot-nginx - - cat > /etc/nginx/sites-available/fonstr << 'NGINX' -server { - listen 80; - server_name your-domain.com; - - location / { - proxy_pass http://localhost:4444; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 86400; - } -} -NGINX - - ln -s /etc/nginx/sites-available/fonstr /etc/nginx/sites-enabled/ - nginx -t && systemctl reload nginx - echo "šŸ“ Remember to update 'your-domain.com' in /etc/nginx/sites-available/fonstr" - echo "šŸ“ Then run: certbot --nginx -d your-domain.com" -fi - -# Display status -echo "" -echo "āœ… Deployment complete!" -echo "==================================" -pm2 status -echo "" -echo "šŸ“± Access your relay at:" -echo " ws://$(curl -s ifconfig.me):4444" -echo "" -echo "šŸ“Š Useful commands:" -echo " pm2 logs fonstr - View logs" -echo " pm2 restart fonstr - Restart relay" -echo " pm2 monit - Monitor resources" \ No newline at end of file diff --git a/index.js.legacy b/index.js.legacy deleted file mode 100755 index 800923c..0000000 --- a/index.js.legacy +++ /dev/null @@ -1,429 +0,0 @@ -import { validateEvent, verifySignature } from 'nostr-tools' -import fastify from 'fastify' -import fastifyWebsocket from '@fastify/websocket' -import { readFileSync } from 'fs' -import { generateDIDDocument, isValidPubkey } from './lib/did-endpoint.js' - -const MAX_EVENTS = parseInt(process.env.MAX_EVENTS) || 1000 // Configurable max events to prevent memory exhaustion - -function createServer ({ port, useHttps = false }) { - const events = [] - const subscribers = new Map() - - const httpsOptions = useHttps - ? { - key: readFileSync('privkey.pem'), - cert: readFileSync('fullchain.pem') - } - : undefined - - const fi = fastify({ - https: httpsOptions, - http2: false - }) - - fi.register(fastifyWebsocket) - - // DID endpoint - serves DID documents for Nostr public keys - // Support query parameter format: /.well-known/did.json?pubkey={pubkey} - fi.get('/.well-known/did.json', async (request, reply) => { - const { pubkey } = request.query - - if (!pubkey) { - return reply.code(400) - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Headers', 'Content-Type') - .send({ - error: 'Missing pubkey parameter', - message: 'Please provide a pubkey query parameter with a 64-character hex public key' - }) - } - - if (!isValidPubkey(pubkey)) { - return reply.code(400) - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Headers', 'Content-Type') - .send({ - error: 'Invalid pubkey format', - message: 'Public key must be a 64-character hexadecimal string' - }) - } - - try { - // Find metadata event (kind 0) for this pubkey - const metadataEvent = events.find(e => - e.kind === 0 && e.pubkey === pubkey - ) - - // Parse metadata if available - let profile = null - if (metadataEvent) { - try { - const metadata = JSON.parse(metadataEvent.content) - profile = { - ...metadata, - timestamp: metadataEvent.created_at - } - } catch (e) { - // Invalid metadata, ignore - } - } - - // Find most recent contact list event (kind 3) for this pubkey - const contactListEvents = events.filter(e => - e.kind === 3 && e.pubkey === pubkey - ) - const contactListEvent = contactListEvents.length > 0 - ? contactListEvents.reduce((latest, current) => - current.created_at > latest.created_at ? current : latest - ) - : null - - // Parse follows if available - let follows = null - if (contactListEvent) { - try { - // Extract pubkeys from kind 3 event tags (p tags) - const followPubkeys = contactListEvent.tags - .filter(tag => tag[0] === 'p' && tag[1] && /^[0-9a-f]{64}$/i.test(tag[1])) - .map(tag => tag[1].toLowerCase()) - - if (followPubkeys.length > 0) { - follows = { - pubkeys: followPubkeys, - count: followPubkeys.length - } - } - } catch (e) { - // Invalid contact list, ignore - } - } - - const didDocument = generateDIDDocument(pubkey, profile, follows) - return reply - .header('Content-Type', 'application/json') - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Headers', 'Content-Type') - .send(didDocument) - } catch (error) { - console.error('Error generating DID document:', error) - return reply.code(500) - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Headers', 'Content-Type') - .send({ - error: 'Failed to generate DID document', - message: error.message - }) - } - }) - - // OPTIONS support for CORS preflight - query parameter format - fi.options('/.well-known/did.json', async (request, reply) => { - return reply - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Headers', 'Content-Type') - .code(204) - .send() - }) - - // DID endpoint - path parameter format: /.well-known/did/nostr/{pubkey}.json - fi.get('/.well-known/did/nostr/:pubkey.json', async (request, reply) => { - const { pubkey } = request.params - - if (!isValidPubkey(pubkey)) { - return reply.code(400) - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Headers', 'Content-Type') - .send({ - error: 'Invalid pubkey format', - message: 'Public key must be a 64-character hexadecimal string' - }) - } - - try { - // Find metadata event (kind 0) for this pubkey - const metadataEvent = events.find(e => - e.kind === 0 && e.pubkey === pubkey - ) - - // Parse metadata if available - let profile = null - if (metadataEvent) { - try { - const metadata = JSON.parse(metadataEvent.content) - profile = { - ...metadata, - timestamp: metadataEvent.created_at - } - } catch (e) { - // Invalid metadata, ignore - } - } - - // Find most recent contact list event (kind 3) for this pubkey - const contactListEvents = events.filter(e => - e.kind === 3 && e.pubkey === pubkey - ) - const contactListEvent = contactListEvents.length > 0 - ? contactListEvents.reduce((latest, current) => - current.created_at > latest.created_at ? current : latest - ) - : null - - // Parse follows if available - let follows = null - if (contactListEvent) { - try { - // Extract pubkeys from kind 3 event tags (p tags) - const followPubkeys = contactListEvent.tags - .filter(tag => tag[0] === 'p' && tag[1] && /^[0-9a-f]{64}$/i.test(tag[1])) - .map(tag => tag[1].toLowerCase()) - - if (followPubkeys.length > 0) { - follows = { - pubkeys: followPubkeys, - count: followPubkeys.length - } - } - } catch (e) { - // Invalid contact list, ignore - } - } - - const didDocument = generateDIDDocument(pubkey, profile, follows) - return reply - .header('Content-Type', 'application/json') - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Headers', 'Content-Type') - .send(didDocument) - } catch (error) { - console.error('Error generating DID document:', error) - return reply.code(500) - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Headers', 'Content-Type') - .send({ - error: 'Failed to generate DID document', - message: error.message - }) - } - }) - - // OPTIONS support for CORS preflight - path parameter format - fi.options('/.well-known/did/nostr/:pubkey.json', async (request, reply) => { - return reply - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Allow-Methods', 'GET, OPTIONS') - .header('Access-Control-Allow-Headers', 'Content-Type') - .code(204) - .send() - }) - - fi.register(async function (fastify) { - fastify.get('/', { websocket: true }, async (con, req) => { - console.log('ws connection started') - - const { socket } = con - console.log('ws connection established') - - socket.on('message', async (message) => { - message = message?.toString() - console.log('received message', message) - - const [type, value, ...rest] = JSON.parse(message) - await processMessage(type, value, rest, socket, events, subscribers) - }) - - // ... - }) - }) - - fi.listen({ host: '0.0.0.0', port }, (err) => { - if (err) throw err - console.log(`listening on ${port}`) - }) -} - -export const eventPassesFilter = (event, filter) => { - // Standard Nostr filter fields (NIP-01) - 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 && event.created_at < filter.since) { - return false - } - - if (filter.until && event.created_at > filter.until) { - return false - } - - // Tag filters (#e, #p, etc.) - NIP-01 compliant - // Check all filter properties that start with '#' - for (const [key, values] of Object.entries(filter)) { - if (key.startsWith('#') && key.length === 2) { - const tagName = key[1] // Get the letter after '#' - const eventTagValues = event.tags - .filter(tag => tag[0] === tagName) - .map(tag => tag[1]) - - // The event must have at least one matching tag value - if (!values.some(v => eventTagValues.includes(v))) { - return false - } - } - } - - // Keep existing custom filters for backward compatibility (optional) - if (filter.from && event.from !== filter.from) { - return false - } - - if (filter.to && event.to !== filter.to) { - return false - } - - // Legacy support for single 'kind' (can remove if not needed) - if (filter.kind && event.kind !== filter.kind) { - return false - } - - return true -} - -function isReplaceableKind (kind) { - return (kind >= 10000 && kind < 20000) || kind === 0 || kind === 3 -} - -function isEphemeralKind (kind) { - return kind >= 20000 && kind < 30000 -} - -function isParameterizedReplaceable (kind) { - return kind >= 30000 && kind < 40000 -} - -function getDTagValue (tags) { - for (const tag of tags) { - if (tag[0] === 'd') { - return tag[1] - } - } - return null -} - -export const processMessage = async (type, value, rest, socket, events, subscribers) => { - switch (type) { - case 'EVENT': - const ok = validateEvent(value) - const veryOk = verifySignature(value) - console.log('ok', ok, veryOk) - if (ok && veryOk) { - if (isEphemeralKind(value.kind)) { - // Don't store the event, just process and forget - // ... (rest of your processing logic) - } else if (isReplaceableKind(value.kind) || isParameterizedReplaceable(value.kind)) { - // Find and replace the previous event with the same pubkey and kind - let indexToReplace = -1 - for (let i = 0; i < events.length; i++) { - if (events[i].pubkey === value.pubkey && events[i].kind === value.kind) { - if (isParameterizedReplaceable(value.kind)) { - const dTagValue = getDTagValue(value.tags) - const existingDTagValue = getDTagValue(events[i].tags) - if (dTagValue === existingDTagValue) { - indexToReplace = i - break; - } - } else { - indexToReplace = i - break; - } - } - } - if (indexToReplace !== -1) { - events[indexToReplace] = value - } else { - // Check memory cap before adding new event - if (events.length >= MAX_EVENTS) { - events.shift() // Remove oldest event (FIFO) - } - events.push(value) - } - // ... (rest of your processing logic) - } else { - // Regular event, just add to the list - // Check memory cap before adding new event - if (events.length >= MAX_EVENTS) { - events.shift() // Remove oldest event (FIFO) - } - events.push(value) - console.log('event ok') - // ... (rest of your processing logic) - } - - subscribers.forEach((filters, subscriber) => { - filters.forEach(filter => { - if (eventPassesFilter(value, filter)) { - subscriber.send(JSON.stringify(['EVENT', filter.subscription_id, value])) - } - }) - }) - socket.send(`["OK", ${value.id}, true, ""]`) - } else { - socket.send('["NOTICE", "Invalid event"]') - } - break - case 'REQ': - console.log('REQ') - const subscription_id = value - const filters = rest.map(filter => ({ ...filter, subscription_id })) - subscribers.set(socket, filters) - - filters.forEach(filter => { - const matchingEvents = events.filter(event => eventPassesFilter(event, filter)) - const limited = filter.limit ? matchingEvents.slice(-filter.limit) : matchingEvents - limited.forEach(event => socket.send(JSON.stringify(['EVENT', filter.subscription_id, event]))) - }) - - socket.send(JSON.stringify(['EOSE', subscription_id])) - break - case 'CLOSE': - const sub_id = value - if (subscribers.has(socket)) { - const updatedFilters = subscribers.get(socket).filter(filter => filter.subscription_id !== sub_id) - if (updatedFilters.length === 0) { - subscribers.delete(socket) - } else { - subscribers.set(socket, updatedFilters) - } - } - break - default: - socket.send('["NOTICE", "Unrecognized event"]') - console.log('Unrecognized event') - } -} - -// Compatibility with ES6 imports -export { createServer } - -// Compatibility with CommonJS require -const exportsObject = { createServer } -if (typeof module !== 'undefined') { - module.exports = exportsObject -} diff --git a/lib/did-endpoint.js b/lib/did-endpoint.js deleted file mode 100644 index 677f70b..0000000 --- a/lib/did-endpoint.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Nostr DID Document Generator - * Implements minimal DID document generation per https://nostrcg.github.io/did-nostr/ - */ - -/** - * Generates a DID document from a Nostr public key with optional profile and follows - * @param {string} pubkey - 64-character hex public key - * @param {object} profile - Optional profile object with user data - * @param {object} follows - Optional follows object with social graph data - * @returns {object} DID document - */ -export function generateDIDDocument(pubkey, profile = null, follows = null) { - // Validate pubkey format - if (!pubkey || typeof pubkey !== 'string' || !/^[0-9a-f]{64}$/i.test(pubkey)) { - throw new Error('Invalid public key: must be 64-character hex string') - } - - // Normalize to lowercase - const normalizedPubkey = pubkey.toLowerCase() - - // Generate Multikey representation - // 1. Add 0x02 prefix for compressed secp256k1 (even y-coordinate default) - // 2. Add multicodec varint for secp256k1-pub (0xe7, 0x01) - // 3. Encode with base16-lower (f prefix) - const publicKeyMultibase = 'fe70102' + normalizedPubkey - - // Construct DID - const did = `did:nostr:${normalizedPubkey}` - - // Generate DID document - const didDocument = { - '@context': [ - 'https://w3id.org/did', - 'https://w3id.org/nostr/context' - ], - id: did, - verificationMethod: [ - { - id: `${did}#key1`, - type: 'Multikey', - controller: did, - publicKeyMultibase: publicKeyMultibase - } - ], - authentication: ['#key1'], - assertionMethod: ['#key1'] - } - - // Add profile if provided - if (profile && typeof profile === 'object') { - didDocument.profile = {} - - // Standard profile fields from the spec - if (profile.name && typeof profile.name === 'string') { - didDocument.profile.name = profile.name - } - if (profile.about && typeof profile.about === 'string') { - didDocument.profile.about = profile.about - } - if (profile.picture && typeof profile.picture === 'string') { - didDocument.profile.picture = profile.picture - } - if (profile.nip05 && typeof profile.nip05 === 'string') { - didDocument.profile.nip05 = profile.nip05 - } - if (profile.lud16 && typeof profile.lud16 === 'string') { - didDocument.profile.lud16 = profile.lud16 - } - if (profile.website && typeof profile.website === 'string') { - didDocument.profile.website = profile.website - } - if (profile.timestamp && typeof profile.timestamp === 'number') { - didDocument.profile.timestamp = profile.timestamp - } - - // Remove profile if empty - if (Object.keys(didDocument.profile).length === 0) { - delete didDocument.profile - } - } - - // Add follows if provided (social graph from kind 3 events) - if (follows && typeof follows === 'object') { - // Add follows array - convert pubkeys to DIDs - if (Array.isArray(follows.pubkeys) && follows.pubkeys.length > 0) { - didDocument.follows = follows.pubkeys - .filter(pk => typeof pk === 'string' && /^[0-9a-f]{64}$/i.test(pk)) - .map(pk => `did:nostr:${pk.toLowerCase()}`) - - // Remove follows if empty after filtering - if (didDocument.follows.length === 0) { - delete didDocument.follows - } - } - - // Add followsCount if provided - if (typeof follows.count === 'number' && follows.count > 0) { - didDocument.followsCount = follows.count - } - - // Add service endpoint for complete follows if truncated - if (follows.truncated && follows.serviceEndpoint) { - if (!didDocument.service) { - didDocument.service = [] - } - didDocument.service.push({ - id: `${did}#follows-api`, - type: 'FollowsEndpoint', - serviceEndpoint: follows.serviceEndpoint - }) - } - } - - return didDocument -} - -/** - * Validates a hex public key - * @param {string} pubkey - Public key to validate - * @returns {boolean} True if valid - */ -export function isValidPubkey(pubkey) { - if (!pubkey || typeof pubkey !== 'string') { - return false - } - return /^[0-9a-f]{64}$/i.test(pubkey) -} \ No newline at end of file diff --git a/test/cors-test.html b/test/cors-test.html deleted file mode 100644 index ca27f16..0000000 --- a/test/cors-test.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - DID Endpoint CORS Test - - - -

DID Endpoint CORS Test

- -
- Instructions:
- 1. Start your fonstr server
- 2. Open this file in a browser
- 3. Click the test buttons below
- 4. Check that requests work without CORS errors -
- - - - - - -
- - - - \ No newline at end of file diff --git a/test/cors-test.js b/test/cors-test.js deleted file mode 100644 index 831c930..0000000 --- a/test/cors-test.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Basic CORS test using curl to verify headers - */ - -import { spawn } from 'child_process' - -const SERVER_URL = 'http://localhost:3000' -const TEST_PUBKEY = '124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2' - -console.log('CORS Test for DID Endpoints') -console.log('============================') - -function runCurlTest(description, url, method = 'GET') { - return new Promise((resolve, reject) => { - console.log(`\n${description}`) - console.log(`URL: ${url}`) - console.log(`Method: ${method}`) - - const curl = spawn('curl', [ - '-I', // Headers only - '-X', method, - '-H', 'Origin: https://example.com', // Simulate cross-origin request - url - ]) - - let output = '' - - curl.stdout.on('data', (data) => { - output += data.toString() - }) - - curl.stderr.on('data', (data) => { - output += data.toString() - }) - - curl.on('close', (code) => { - console.log(`Response Headers:`) - console.log(output) - - // Check for CORS headers - const hasAccessControlAllowOrigin = output.includes('Access-Control-Allow-Origin') - const hasAccessControlAllowMethods = output.includes('Access-Control-Allow-Methods') - - if (hasAccessControlAllowOrigin && hasAccessControlAllowMethods) { - console.log('āœ… CORS headers present') - resolve(true) - } else { - console.log('āŒ Missing CORS headers') - resolve(false) - } - }) - - curl.on('error', (err) => { - console.log(`āŒ Error: ${err.message}`) - resolve(false) - }) - }) -} - -async function runTests() { - console.log('Starting CORS tests...') - console.log('Make sure your fonstr server is running on port 3000') - - const tests = [ - { - description: 'Test 1: Query Parameter Format - GET', - url: `${SERVER_URL}/.well-known/did.json?pubkey=${TEST_PUBKEY}`, - method: 'GET' - }, - { - description: 'Test 2: Query Parameter Format - OPTIONS', - url: `${SERVER_URL}/.well-known/did.json`, - method: 'OPTIONS' - }, - { - description: 'Test 3: Path Parameter Format - GET', - url: `${SERVER_URL}/.well-known/did/nostr/${TEST_PUBKEY}.json`, - method: 'GET' - }, - { - description: 'Test 4: Path Parameter Format - OPTIONS', - url: `${SERVER_URL}/.well-known/did/nostr/${TEST_PUBKEY}.json`, - method: 'OPTIONS' - }, - { - description: 'Test 5: Error Response CORS', - url: `${SERVER_URL}/.well-known/did.json?pubkey=invalid`, - method: 'GET' - } - ] - - let passCount = 0 - - for (const test of tests) { - const passed = await runCurlTest(test.description, test.url, test.method) - if (passed) passCount++ - - // Small delay between tests - await new Promise(resolve => setTimeout(resolve, 500)) - } - - console.log(`\n============================`) - console.log(`CORS Test Results: ${passCount}/${tests.length} passed`) - - if (passCount === tests.length) { - console.log('šŸŽ‰ All CORS tests passed!') - } else { - console.log('āš ļø Some CORS tests failed. Check server configuration.') - } -} - -// Run tests -runTests().catch(console.error) \ No newline at end of file diff --git a/test/did-endpoint.test.js b/test/did-endpoint.test.js deleted file mode 100644 index 81e1049..0000000 --- a/test/did-endpoint.test.js +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Tests for DID endpoint functionality - */ - -import { generateDIDDocument, isValidPubkey } from '../lib/did-endpoint.js' - -// Test data -const validPubkey = '124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2' -const invalidPubkeys = [ - '', - null, - undefined, - '123', // Too short - '124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd', // 63 chars - '124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd22', // 65 chars - 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', // Invalid hex - '124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd!', // Invalid char -] - -// Test pubkey validation -console.log('Testing pubkey validation...') -console.assert(isValidPubkey(validPubkey) === true, 'Valid pubkey should return true') -invalidPubkeys.forEach(pubkey => { - console.assert(isValidPubkey(pubkey) === false, `Invalid pubkey "${pubkey}" should return false`) -}) -console.log('āœ“ Pubkey validation tests passed') - -// Test DID document generation -console.log('\nTesting DID document generation...') -const didDoc = generateDIDDocument(validPubkey) - -// Check structure -console.assert(didDoc['@context'] !== undefined, 'DID document should have @context') -console.assert(Array.isArray(didDoc['@context']), '@context should be an array') -console.assert(didDoc['@context'].includes('https://w3id.org/did'), '@context should include DID context') -console.assert(didDoc['@context'].includes('https://w3id.org/nostr/context'), '@context should include Nostr context') - -// Check ID -const expectedDid = `did:nostr:${validPubkey.toLowerCase()}` -console.assert(didDoc.id === expectedDid, `DID should be ${expectedDid}`) - -// Check verification method -console.assert(Array.isArray(didDoc.verificationMethod), 'verificationMethod should be an array') -console.assert(didDoc.verificationMethod.length === 1, 'Should have one verification method') - -const vm = didDoc.verificationMethod[0] -console.assert(vm.id === `${expectedDid}#key1`, 'Verification method ID should be correct') -console.assert(vm.type === 'Multikey', 'Verification method type should be Multikey') -console.assert(vm.controller === expectedDid, 'Controller should be the DID') - -// Check multibase encoding -const expectedMultibase = 'fe70102' + validPubkey.toLowerCase() -console.assert(vm.publicKeyMultibase === expectedMultibase, 'publicKeyMultibase should be correctly encoded') - -// Check authentication and assertion -console.assert(Array.isArray(didDoc.authentication), 'authentication should be an array') -console.assert(didDoc.authentication.includes('#key1'), 'authentication should reference #key1') -console.assert(Array.isArray(didDoc.assertionMethod), 'assertionMethod should be an array') -console.assert(didDoc.assertionMethod.includes('#key1'), 'assertionMethod should reference #key1') - -console.log('āœ“ DID document generation tests passed') - -// Test error handling -console.log('\nTesting error handling...') -invalidPubkeys.forEach(pubkey => { - try { - generateDIDDocument(pubkey) - console.assert(false, `Should throw error for invalid pubkey "${pubkey}"`) - } catch (error) { - console.assert(error.message.includes('Invalid public key'), 'Error message should mention invalid public key') - } -}) -console.log('āœ“ Error handling tests passed') - -// Test profile functionality -console.log('\nTesting profile functionality...') - -// Test with valid profile -const profileData = { - name: 'Alice', - about: 'Building the decentralized social web', - picture: 'https://example.com/alice.jpg', - nip05: 'alice@example.com', - lud16: 'alice@getalby.com', - website: 'https://alice.example.com', - timestamp: 1737906600 -} - -const didDocWithProfile = generateDIDDocument(validPubkey, profileData) -console.assert(didDocWithProfile.profile !== undefined, 'DID document should have profile field') -console.assert(didDocWithProfile.profile.name === 'Alice', 'Profile name should match') -console.assert(didDocWithProfile.profile.about === 'Building the decentralized social web', 'Profile about should match') -console.assert(didDocWithProfile.profile.picture === 'https://example.com/alice.jpg', 'Profile picture should match') -console.assert(didDocWithProfile.profile.nip05 === 'alice@example.com', 'Profile nip05 should match') -console.assert(didDocWithProfile.profile.lud16 === 'alice@getalby.com', 'Profile lud16 should match') -console.assert(didDocWithProfile.profile.website === 'https://alice.example.com', 'Profile website should match') -console.assert(didDocWithProfile.profile.timestamp === 1737906600, 'Profile timestamp should match') - -// Test with partial profile -const partialProfile = { name: 'Bob' } -const didDocPartial = generateDIDDocument(validPubkey, partialProfile) -console.assert(didDocPartial.profile.name === 'Bob', 'Partial profile name should match') -console.assert(didDocPartial.profile.about === undefined, 'Partial profile should not have undefined fields') - -// Test with empty profile -const emptyProfile = {} -const didDocEmpty = generateDIDDocument(validPubkey, emptyProfile) -console.assert(didDocEmpty.profile === undefined, 'Empty profile should be removed') - -// Test with invalid profile data types -const invalidProfile = { name: 123, about: null, timestamp: 'invalid' } -const didDocInvalid = generateDIDDocument(validPubkey, invalidProfile) -console.assert(didDocInvalid.profile === undefined, 'Invalid profile data should result in no profile') - -// Test without profile (backward compatibility) -const didDocNoProfile = generateDIDDocument(validPubkey) -console.assert(didDocNoProfile.profile === undefined, 'DID document without profile should work') - -console.log('āœ“ Profile functionality tests passed') - -// Test follows functionality -console.log('\nTesting follows functionality...') - -// Test with valid follows -const followsData = { - pubkeys: [ - '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', - '46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184147700e3a8' - ], - count: 2 -} - -const didDocWithFollows = generateDIDDocument(validPubkey, null, followsData) -console.assert(Array.isArray(didDocWithFollows.follows), 'DID document should have follows array') -console.assert(didDocWithFollows.follows.length === 2, 'Should have 2 follows') -console.assert(didDocWithFollows.follows[0].startsWith('did:nostr:'), 'Follows should be DIDs') -console.assert(didDocWithFollows.followsCount === 2, 'followsCount should match') - -// Test with complete DID document (profile + follows) -const didDocComplete = generateDIDDocument(validPubkey, profileData, followsData) -console.assert(didDocComplete.profile !== undefined, 'Complete DID should have profile') -console.assert(Array.isArray(didDocComplete.follows), 'Complete DID should have follows') -console.assert(didDocComplete.followsCount === 2, 'Complete DID should have followsCount') - -// Test with invalid pubkeys in follows (should filter out) -const invalidFollowsData = { - pubkeys: [ - '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // valid - 'invalid-pubkey', // invalid - '46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184147700e3a8' // valid - ], - count: 3 -} - -const didDocFiltered = generateDIDDocument(validPubkey, null, invalidFollowsData) -console.assert(didDocFiltered.follows.length === 2, 'Should filter out invalid pubkeys') -console.assert(didDocFiltered.followsCount === 3, 'followsCount should preserve original count') - -// Test with empty follows -const emptyFollowsData = { pubkeys: [], count: 0 } -const didDocEmptyFollows = generateDIDDocument(validPubkey, null, emptyFollowsData) -console.assert(didDocEmptyFollows.follows === undefined, 'Empty follows should be removed') -console.assert(didDocEmptyFollows.followsCount === undefined, 'Empty followsCount should be removed') - -// Test with truncated follows and service endpoint -const truncatedFollowsData = { - pubkeys: [ - '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', - '46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184147700e3a8' - ], - count: 5234, - truncated: true, - serviceEndpoint: 'https://api.example.com/did/follows/{did}' -} - -const didDocTruncated = generateDIDDocument(validPubkey, null, truncatedFollowsData) -console.assert(didDocTruncated.follows.length === 2, 'Should include truncated follows') -console.assert(didDocTruncated.followsCount === 5234, 'Should show total count') -console.assert(Array.isArray(didDocTruncated.service), 'Should have service array') -console.assert(didDocTruncated.service.some(s => s.type === 'FollowsEndpoint'), 'Should have FollowsEndpoint service') - -console.log('āœ“ Follows functionality tests passed') - -console.log('\nāœ… All tests passed!') - -// Output example DID documents -console.log('\nExample Minimal DID Document:') -console.log(JSON.stringify(didDoc, null, 2)) - -console.log('\nExample DID Document with Profile:') -console.log(JSON.stringify(didDocWithProfile, null, 2)) - -console.log('\nExample Complete DID Document (Profile + Follows):') -console.log(JSON.stringify(didDocComplete, null, 2)) \ No newline at end of file diff --git a/test/integration-test.js b/test/integration-test.js deleted file mode 100644 index 1f54a00..0000000 --- a/test/integration-test.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Integration test for DID endpoint with profile data - */ - -import { generateDIDDocument } from '../lib/did-endpoint.js' - -// Test the integration logic that would be in index.js -console.log('Testing DID endpoint integration with profile data...') - -const validPubkey = '124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2' - -// Simulate events array with a kind 0 event (like in relay memory) -const events = [ - { - id: 'test-event-id', - pubkey: validPubkey, - kind: 0, - content: JSON.stringify({ - name: 'Alice', - about: 'Building the decentralized social web', - picture: 'https://example.com/alice.jpg', - nip05: 'alice@example.com', - website: 'https://alice.example.com' - }), - created_at: 1737906600, - tags: [], - sig: 'fake-signature' - }, - { - id: 'contact-list-event', - pubkey: validPubkey, - kind: 3, - content: '', - created_at: 1737906800, - tags: [ - ['p', '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'], - ['p', '46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184147700e3a8'], - ['p', '82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2'] - ], - sig: 'fake-signature' - }, - { - id: 'other-event', - pubkey: 'different-pubkey-000000000000000000000000000000000000000000000000000', - kind: 1, - content: 'Hello world', - created_at: 1737906700, - tags: [], - sig: 'fake-signature' - } -] - -// Simulate the logic from index.js -function simulateDIDEndpoint(pubkey, events) { - // Find metadata event (kind 0) for this pubkey - const metadataEvent = events.find(e => - e.kind === 0 && e.pubkey === pubkey - ) - - // Parse metadata if available - let profile = null - if (metadataEvent) { - try { - const metadata = JSON.parse(metadataEvent.content) - profile = { - ...metadata, - timestamp: metadataEvent.created_at - } - } catch (e) { - // Invalid metadata, ignore - } - } - - // Find most recent contact list event (kind 3) for this pubkey - const contactListEvents = events.filter(e => - e.kind === 3 && e.pubkey === pubkey - ) - const contactListEvent = contactListEvents.length > 0 - ? contactListEvents.reduce((latest, current) => - current.created_at > latest.created_at ? current : latest - ) - : null - - // Parse follows if available - let follows = null - if (contactListEvent) { - try { - // Extract pubkeys from kind 3 event tags (p tags) - const followPubkeys = contactListEvent.tags - .filter(tag => tag[0] === 'p' && tag[1] && /^[0-9a-f]{64}$/i.test(tag[1])) - .map(tag => tag[1].toLowerCase()) - - if (followPubkeys.length > 0) { - follows = { - pubkeys: followPubkeys, - count: followPubkeys.length - } - } - } catch (e) { - // Invalid contact list, ignore - } - } - - return generateDIDDocument(pubkey, profile, follows) -} - -// Test 1: Pubkey with profile and follows data -const didComplete = simulateDIDEndpoint(validPubkey, events) -console.assert(didComplete.profile !== undefined, 'Should have profile when kind 0 event exists') -console.assert(didComplete.profile.name === 'Alice', 'Profile name should match kind 0 event') -console.assert(didComplete.profile.timestamp === 1737906600, 'Profile timestamp should match created_at') -console.assert(Array.isArray(didComplete.follows), 'Should have follows when kind 3 event exists') -console.assert(didComplete.follows.length === 3, 'Should have 3 follows from kind 3 event') -console.assert(didComplete.followsCount === 3, 'followsCount should match') -console.assert(didComplete.follows[0].startsWith('did:nostr:'), 'Follows should be DIDs') - -// Test 2: Pubkey without profile data -const unknownPubkey = '0000000000000000000000000000000000000000000000000000000000000000' -const didWithoutData = simulateDIDEndpoint(unknownPubkey, events) -console.assert(didWithoutData.profile === undefined, 'Should not have profile when no kind 0 event exists') -console.assert(didWithoutData.follows === undefined, 'Should not have follows when no kind 3 event exists') - -// Test 3: Pubkey with invalid JSON in kind 0 -const eventsWithInvalidJson = [ - { - id: 'invalid-event', - pubkey: validPubkey, - kind: 0, - content: 'invalid json', - created_at: 1737906600, - tags: [], - sig: 'fake-signature' - } -] -const didWithInvalidProfile = simulateDIDEndpoint(validPubkey, eventsWithInvalidJson) -console.assert(didWithInvalidProfile.profile === undefined, 'Should not have profile when JSON is invalid') - -// Test 4: Multiple kind 3 events - should use most recent -const eventsWithMultipleContacts = [ - ...events, - { - id: 'older-contact-list', - pubkey: validPubkey, - kind: 3, - content: '', - created_at: 1737906700, // Earlier timestamp - tags: [ - ['p', 'aaaa1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'] - ], - sig: 'fake-signature' - } -] - -const didWithMultipleContacts = simulateDIDEndpoint(validPubkey, eventsWithMultipleContacts) -console.assert(didWithMultipleContacts.follows.length === 3, 'Should use most recent contact list (3 follows, not 1)') - -console.log('āœ… Integration tests passed!') - -// Show example output -console.log('\nExample Complete DID with profile and follows:') -console.log(JSON.stringify(didComplete, null, 2)) \ No newline at end of file