Compare commits

...

2 Commits

Author SHA1 Message Date
2ba2ef3bef feat: Add IPNS support 2025-07-26 13:57:13 +10:00
a7d18d529e feat: Add video link to index 2025-07-04 12:23:44 +10:00
4 changed files with 125 additions and 23 deletions

View File

@@ -9,7 +9,8 @@ const {
LOCAL_RESOLVER_HOST, LOCAL_RESOLVER_HOST,
LOCAL_RESOLVER_PORT, LOCAL_RESOLVER_PORT,
CACHE_ENABLED, CACHE_ENABLED,
CACHE_TTL_SECONDS CACHE_TTL_SECONDS,
IPFS_GATEWAY
} = config; } = config;
// Setup cache // Setup cache
@@ -18,6 +19,12 @@ const cache = new NodeCache({
checkperiod: CACHE_TTL_SECONDS * 0.2, 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 * Resolve a Handshake domain to an IPFS CID
* @param {string} domain - The Handshake domain to resolve * @param {string} domain - The Handshake domain to resolve
@@ -299,14 +306,14 @@ async function resolveLocal(domain) {
/** /**
* Extract IPFS CID from DNS TXT records * Extract IPFS CID from DNS TXT records
* @param {string[][]} records - Array of TXT record arrays * @param {string[][]} records - Array of TXT record arrays
* @returns {string|null} - IPFS CID or null * @returns {Promise<string|null>} - IPFS CID or IPNS hash or null
*/ */
function extractCidFromRecords(records) { async function extractCidFromRecords(records) {
if (!records || !records.length) { if (!records || !records.length) {
return null; 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 recordSet of records) {
for (const record of recordSet) { for (const record of recordSet) {
// Support multiple formats // Support multiple formats
@@ -319,6 +326,18 @@ function extractCidFromRecords(records) {
if (record.startsWith('ip6=')) { if (record.startsWith('ip6=')) {
return record.substring(4); 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 // Log the record for debugging
console.log(`Found TXT record: ${record}`); console.log(`Found TXT record: ${record}`);
@@ -328,7 +347,6 @@ function extractCidFromRecords(records) {
return null; return null;
} }
/** /**
* Clear the cache for a specific domain * Clear the cache for a specific domain
* @param {string} domain - The Handshake domain to clear from cache * @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 = { module.exports = {
resolveHandshake, resolveHandshake,
clearCache clearCache,
clearIpnsCache
}; };

View File

@@ -24,26 +24,30 @@ const mimeTypes = {
}; };
/** /**
* Fetch content from IPFS by CID and path * Fetch content from IPFS by CID and path, or from IPNS
* @param {string} cid - IPFS Content Identifier * @param {string} cidOrIpns - IPFS Content Identifier or IPNS hash (prefixed with 'ipns:')
* @param {string} path - Optional path within the CID * @param {string} path - Optional path within the CID/IPNS
* @returns {Promise<{data: Buffer, mimeType: string}|null>} - Content and MIME type or null * @returns {Promise<{data: Buffer, mimeType: string}|null>} - Content and MIME type or null
*/ */
async function fetchFromIpfs(cid, path = '') { async function fetchFromIpfs(cidOrIpns, path = '') {
const contentPath = path ? `${cid}/${path}` : cid; 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 // Check cache first
if (CACHE_ENABLED) { if (CACHE_ENABLED) {
const cachedContent = cache.get(`ipfs:${contentPath}`); const cacheKey = `${contentType}:${contentPath}`;
const cachedContent = cache.get(cacheKey);
if (cachedContent) { if (cachedContent) {
console.log(`Cache hit for IPFS content: ${contentPath}`); console.log(`Cache hit for ${contentType.toUpperCase()} content: ${contentPath}`);
return cachedContent; return cachedContent;
} }
} }
try { try {
// Use the HTTP gateway directly instead of the IPFS client // Use the HTTP gateway directly
const result = await fetchViaGateway(cid, path); const result = await fetchViaGateway(hash, path, contentType);
if (!result) { if (!result) {
return null; return null;
@@ -56,25 +60,29 @@ async function fetchFromIpfs(cid, path = '') {
// Cache the result // Cache the result
if (CACHE_ENABLED) { 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; return result;
} catch (error) { } catch (error) {
console.error(`Error fetching ${contentPath} from IPFS:`, error); console.error(`Error fetching ${contentPath} from ${contentType.toUpperCase()}:`, error);
return null; return null;
} }
} }
/** /**
* Fetch content via IPFS HTTP gateway * Fetch content via IPFS HTTP gateway
* @param {string} cid - IPFS Content Identifier * @param {string} hash - IPFS CID or IPNS hash
* @param {string} path - Path within the CID * @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 * @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 { 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}`); console.log(`Fetching from IPFS gateway: ${url}`);
const response = await fetch(url); const response = await fetch(url);

View File

@@ -157,13 +157,17 @@
<ol> <ol>
<li>Get the IPFS hash of your content using any IPFS content hosting provider</li> <li>Get the IPFS hash of your content using any IPFS content hosting provider</li>
<li>Update your Handshake domain's DNS records with a TXT record</li> <li>Update your Handshake domain's DNS records with a TXT record</li>
<li>Use the format: <code>ipfs=Qm...your-ipfs-hash</code></li> <li>Use the format: <code>ipfs=Qm...your-ipfs-hash</code> or using IPNS <code>ipns=your-ipns-hash</code></li>
<li>Add an A record to point your domain to this portal <code>192.9.167.104</code> <br> <li>Add an A record to point your domain to this portal <code>192.9.167.104</code> <br>
Alternatively you can use this ALIAS <code>ipfs.hnshosting</code></li> Alternatively you can use this ALIAS <code>ipfs.hnshosting</code></li>
</li> </li>
<li>Wait a few minutes and you should be able to see your IPFS content on the domain</li> <li>Wait a few minutes and you should be able to see your IPFS content on the domain</li>
</ol> </ol>
<a href="https://www.youtube.com/watch?v=78oa_a1zlLk" target="_blank" class="video-tutorial">
<strong>Watch a video tutorial</strong>
</a>
</section> </section>
<section class="info-section"> <section class="info-section">

View File

@@ -3,7 +3,7 @@ const express = require('express');
const morgan = require('morgan'); const morgan = require('morgan');
const cors = require('cors'); const cors = require('cors');
const path = require('path'); const path = require('path');
const { resolveHandshake, clearCache } = require('./lib/handshake'); const { resolveHandshake, clearCache, clearIpnsCache } = require('./lib/handshake');
const { fetchFromIpfs } = require('./lib/ipfs'); const { fetchFromIpfs } = require('./lib/ipfs');
const { PORT } = require('./config'); 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 // Catch-all route to handle SPA navigation
app.get('*', (req, res) => { app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html')); res.sendFile(path.join(__dirname, 'public', 'index.html'));