diff --git a/lib/handshake.js b/lib/handshake.js index 482303a..4854cce 100644 --- a/lib/handshake.js +++ b/lib/handshake.js @@ -9,7 +9,8 @@ const { LOCAL_RESOLVER_HOST, LOCAL_RESOLVER_PORT, CACHE_ENABLED, - CACHE_TTL_SECONDS + CACHE_TTL_SECONDS, + IPFS_GATEWAY } = config; // Setup cache @@ -18,6 +19,12 @@ const cache = new NodeCache({ checkperiod: CACHE_TTL_SECONDS * 0.2, }); +// Setup separate cache for IPNS with shorter TTL (IPNS can change more frequently) +const ipnsCache = new NodeCache({ + stdTTL: Math.min(CACHE_TTL_SECONDS, 300), // Max 5 minutes for IPNS + checkperiod: 60, +}); + /** * Resolve a Handshake domain to an IPFS CID * @param {string} domain - The Handshake domain to resolve @@ -299,14 +306,14 @@ async function resolveLocal(domain) { /** * Extract IPFS CID from DNS TXT records * @param {string[][]} records - Array of TXT record arrays - * @returns {string|null} - IPFS CID or null + * @returns {Promise} - IPFS CID or IPNS hash or null */ -function extractCidFromRecords(records) { +async function extractCidFromRecords(records) { if (!records || !records.length) { return null; } - // Flatten and look for ipfs= or ip6= prefixes + // Flatten and look for ipfs=, ip6=, or ipns= prefixes for (const recordSet of records) { for (const record of recordSet) { // Support multiple formats @@ -319,6 +326,18 @@ function extractCidFromRecords(records) { if (record.startsWith('ip6=')) { return record.substring(4); } + if (record.startsWith('ipns=')) { + const ipnsName = record.substring(5); + console.log(`Found IPNS record: ${ipnsName}`); + // Return IPNS hash prefixed with 'ipns:' to distinguish from IPFS CID + return `ipns:${ipnsName}`; + } + if (record.startsWith('ipns:')) { + const ipnsName = record.substring(5); + console.log(`Found IPNS record: ${ipnsName}`); + // Return IPNS hash prefixed with 'ipns:' to distinguish from IPFS CID + return `ipns:${ipnsName}`; + } // Log the record for debugging console.log(`Found TXT record: ${record}`); @@ -328,7 +347,6 @@ function extractCidFromRecords(records) { return null; } - /** * Clear the cache for a specific domain * @param {string} domain - The Handshake domain to clear from cache @@ -340,7 +358,46 @@ function clearCache(domain) { } } +/** + * Clear IPNS cache for a specific IPNS name (keeping for API compatibility) + * @param {string} ipnsName - The IPNS name to clear from cache + */ +function clearIpnsCache(ipnsName) { + // Since we're not caching IPNS resolutions anymore, just log + console.log(`IPNS cache clear requested for ${ipnsName} (no-op)`); +} + +/** + * Clear the cache for a specific domain + * @param {string} domain - The Handshake domain to clear from cache + */ +function clearCache(domain) { + if (CACHE_ENABLED) { + cache.del(`hns:${domain}`); + // Also clear any IPNS cache entries that might be related + const keys = ipnsCache.keys(); + keys.forEach(key => { + if (key.includes(domain)) { + ipnsCache.del(key); + } + }); + console.log(`Cache cleared for ${domain}`); + } +} + +/** + * Clear IPNS cache for a specific IPNS name + * @param {string} ipnsName - The IPNS name to clear from cache + */ +function clearIpnsCache(ipnsName) { + if (CACHE_ENABLED) { + ipnsCache.del(`ipns:${ipnsName}`); + console.log(`IPNS cache cleared for ${ipnsName}`); + } +} + module.exports = { resolveHandshake, - clearCache + clearCache, + clearIpnsCache }; diff --git a/lib/ipfs.js b/lib/ipfs.js index 946f612..01c6c66 100644 --- a/lib/ipfs.js +++ b/lib/ipfs.js @@ -24,26 +24,30 @@ const mimeTypes = { }; /** - * Fetch content from IPFS by CID and path - * @param {string} cid - IPFS Content Identifier - * @param {string} path - Optional path within the CID + * Fetch content from IPFS by CID and path, or from IPNS + * @param {string} cidOrIpns - IPFS Content Identifier or IPNS hash (prefixed with 'ipns:') + * @param {string} path - Optional path within the CID/IPNS * @returns {Promise<{data: Buffer, mimeType: string}|null>} - Content and MIME type or null */ -async function fetchFromIpfs(cid, path = '') { - const contentPath = path ? `${cid}/${path}` : cid; +async function fetchFromIpfs(cidOrIpns, path = '') { + const isIpns = cidOrIpns.startsWith('ipns:'); + const hash = isIpns ? cidOrIpns.substring(5) : cidOrIpns; + const contentType = isIpns ? 'ipns' : 'ipfs'; + const contentPath = path ? `${hash}/${path}` : hash; // Check cache first if (CACHE_ENABLED) { - const cachedContent = cache.get(`ipfs:${contentPath}`); + const cacheKey = `${contentType}:${contentPath}`; + const cachedContent = cache.get(cacheKey); if (cachedContent) { - console.log(`Cache hit for IPFS content: ${contentPath}`); + console.log(`Cache hit for ${contentType.toUpperCase()} content: ${contentPath}`); return cachedContent; } } try { - // Use the HTTP gateway directly instead of the IPFS client - const result = await fetchViaGateway(cid, path); + // Use the HTTP gateway directly + const result = await fetchViaGateway(hash, path, contentType); if (!result) { return null; @@ -56,25 +60,29 @@ async function fetchFromIpfs(cid, path = '') { // Cache the result if (CACHE_ENABLED) { - cache.set(`ipfs:${contentPath}`, result); + const cacheKey = `${contentType}:${contentPath}`; + // Use shorter TTL for IPNS content since it can change + const ttl = isIpns ? Math.min(CACHE_TTL_SECONDS, 300) : CACHE_TTL_SECONDS; + cache.set(cacheKey, result, ttl); } return result; } catch (error) { - console.error(`Error fetching ${contentPath} from IPFS:`, error); + console.error(`Error fetching ${contentPath} from ${contentType.toUpperCase()}:`, error); return null; } } /** * Fetch content via IPFS HTTP gateway - * @param {string} cid - IPFS Content Identifier - * @param {string} path - Path within the CID + * @param {string} hash - IPFS CID or IPNS hash + * @param {string} path - Path within the content + * @param {string} contentType - Either 'ipfs' or 'ipns' * @returns {Promise<{data: Buffer, mimeType: string}|null>} - Content and MIME type or null */ -async function fetchViaGateway(cid, path) { +async function fetchViaGateway(hash, path, contentType = 'ipfs') { try { - const url = new URL(`${IPFS_GATEWAY}/ipfs/${cid}${path ? '/' + path : ''}`); + const url = new URL(`${IPFS_GATEWAY}/${contentType}/${hash}${path ? '/' + path : ''}`); console.log(`Fetching from IPFS gateway: ${url}`); const response = await fetch(url); diff --git a/public/index.html b/public/index.html index d3e442b..f743ff8 100644 --- a/public/index.html +++ b/public/index.html @@ -157,7 +157,7 @@
  1. Get the IPFS hash of your content using any IPFS content hosting provider
  2. Update your Handshake domain's DNS records with a TXT record
  3. -
  4. Use the format: ipfs=Qm...your-ipfs-hash
  5. +
  6. Use the format: ipfs=Qm...your-ipfs-hash or using IPNS ipns=your-ipns-hash
  7. Add an A record to point your domain to this portal 192.9.167.104
    Alternatively you can use this ALIAS ipfs.hnshosting
  8. diff --git a/server.js b/server.js index 817266e..5af3a01 100644 --- a/server.js +++ b/server.js @@ -3,7 +3,7 @@ const express = require('express'); const morgan = require('morgan'); const cors = require('cors'); const path = require('path'); -const { resolveHandshake, clearCache } = require('./lib/handshake'); +const { resolveHandshake, clearCache, clearIpnsCache } = require('./lib/handshake'); const { fetchFromIpfs } = require('./lib/ipfs'); const { PORT } = require('./config'); @@ -534,6 +534,39 @@ app.get('/api/refresh/:domain', async (req, res) => { } }); +// New API route to refresh IPNS cache specifically +app.get('/api/refresh-ipns/:ipnsName', async (req, res) => { + try { + const ipnsName = req.params.ipnsName; + + // Validate IPNS name format (basic validation) + if (!ipnsName.match(/^[a-zA-Z0-9]+$/)) { + return res.status(400).json({ + success: false, + message: 'Invalid IPNS name format' + }); + } + + console.log(`Refreshing IPNS cache for: ${ipnsName}`); + // Clear IPNS cache + clearIpnsCache(ipnsName); + + // Return success response + res.json({ + success: true, + message: `IPNS cache refresh initiated for ${ipnsName}`, + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error('IPNS refresh error:', error); + res.status(500).json({ + success: false, + message: 'Server error during IPNS refresh operation' + }); + } +}); + // Catch-all route to handle SPA navigation app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html'));