@helia/verified-fetch
Version:
A fetch-like API for obtaining verified & trustless IPFS content on the web
184 lines • 7 kB
JavaScript
import { DoesNotExistError } from '@helia/unixfs/errors';
import * as dagCbor from '@ipld/dag-cbor';
import * as dagJson from '@ipld/dag-json';
import * as dagPb from '@ipld/dag-pb';
import { peerIdFromString } from '@libp2p/peer-id';
import { InvalidParametersError, walkPath } from 'ipfs-unixfs-exporter';
import toBuffer from 'it-to-buffer';
import { CID } from 'multiformats/cid';
import * as json from 'multiformats/codecs/json';
import * as raw from 'multiformats/codecs/raw';
import QuickLRU from 'quick-lru';
import { SESSION_CACHE_MAX_SIZE, SESSION_CACHE_TTL_MS, CODEC_CBOR, CODEC_IDENTITY } from "./constants.js";
import { resourceToSessionCacheKey } from "./utils/resource-to-cache-key.js";
import { ServerTiming } from "./utils/server-timing.js";
// 1 year in seconds for ipfs content
const IPFS_CONTENT_TTL = 29030400;
const ENTITY_CODECS = [
CODEC_CBOR,
json.code,
raw.code
];
/**
* These are supported by the UnixFS exporter
*/
const EXPORTABLE_CODECS = [
dagPb.code,
dagCbor.code,
dagJson.code,
raw.code
];
function basicEntry(type, cid, bytes) {
return {
name: cid.toString(),
path: cid.toString(),
depth: 0,
type,
node: bytes,
cid,
size: BigInt(bytes.byteLength),
content: async function* () {
yield bytes;
}
};
}
export class URLResolver {
components;
blockstoreSessions;
constructor(components, init = {}) {
this.components = components;
this.blockstoreSessions = new QuickLRU({
maxSize: init.sessionCacheSize ?? SESSION_CACHE_MAX_SIZE,
maxAge: init.sessionTTLms ?? SESSION_CACHE_TTL_MS,
onEviction: (key, store) => {
store.close();
}
});
}
async resolve(url, serverTiming = new ServerTiming(), options = {}) {
if (url.protocol === 'ipfs:') {
return this.resolveIPFSPath(url, serverTiming, options);
}
if (url.protocol === 'ipns:') {
return this.resolveIPNSName(url, serverTiming, options);
}
if (url.protocol === 'dnslink:') {
return this.resolveDNSLink(url, serverTiming, options);
}
throw new InvalidParametersError(`Invalid resource. Unsupported protocol in URL, must be ipfs:, ipns:, or dnslink: ${url}`);
}
getBlockstore(root, options = {}) {
if (options.session === false) {
return this.components.helia.blockstore;
}
const key = resourceToSessionCacheKey(root);
let session = this.blockstoreSessions.get(key);
if (session == null) {
session = this.components.helia.blockstore.createSession(root, options);
this.blockstoreSessions.set(key, session);
}
return session;
}
async resolveDNSLink(url, serverTiming, options) {
const results = await serverTiming.time('dnsLink.resolve', `Resolve DNSLink ${url.hostname}`, this.components.dnsLink.resolve(url.hostname, options));
const result = results?.[0];
if (result == null) {
throw new TypeError(`Invalid resource. Cannot resolve DNSLink from domain: ${url.hostname}`);
}
// dnslink resolved to IPNS name
if (result.namespace === 'ipns') {
return this.resolveIPNSName(url, serverTiming, options);
}
// dnslink resolved to CID
if (result.namespace !== 'ipfs') {
// @ts-expect-error result namespace should only be ipns or ipfs
throw new TypeError(`Invalid resource. Unexpected DNSLink namespace ${result.namespace} from domain: ${domain}`);
}
if (result.path != null && (url.pathname !== '' && url.pathname !== '/')) {
// path conflict?
}
const ipfsUrl = new URL(`ipfs://${result.cid}/${url.pathname}`);
const ipfsResult = await this.resolveIPFSPath(ipfsUrl, serverTiming, options);
return {
...ipfsResult,
url,
ttl: result.answer.TTL
};
}
async resolveIPNSName(url, serverTiming, options) {
const peerId = peerIdFromString(url.hostname);
const result = await serverTiming.time('ipns.resolve', `Resolve IPNS name ${peerId}`, this.components.ipnsResolver.resolve(peerId, options));
if (result.path != null && (url.pathname !== '' && url.pathname !== '/')) {
// path conflict?
}
const ipfsUrl = new URL(`ipfs://${result.cid}/${url.pathname}`);
const ipfsResult = await this.resolveIPFSPath(ipfsUrl, serverTiming, options);
return {
...ipfsResult,
url,
// IPNS ttl is in nanoseconds, convert to seconds
ttl: Number((result.record.ttl ?? 0n) / BigInt(1e9))
};
}
async resolveIPFSPath(url, serverTiming, options) {
const walkPathResult = await serverTiming.time('ipfs.resolve', '', this.walkPath(url, options));
return {
...walkPathResult,
url,
ttl: IPFS_CONTENT_TTL,
blockstore: walkPathResult.blockstore
};
}
async walkPath(url, options = {}) {
const cid = CID.parse(url.hostname);
const blockstore = this.getBlockstore(cid, options);
if (EXPORTABLE_CODECS.includes(cid.code)) {
const ipfsRoots = [];
let terminalElement;
const ipfsPath = toIPFSPath(url);
// @ts-expect-error offline is a helia option
for await (const entry of walkPath(ipfsPath, blockstore, {
...options,
offline: options.onlyIfCached === true,
extended: options.isRawBlockRequest !== true
})) {
ipfsRoots.push(entry.cid);
terminalElement = entry;
}
if (terminalElement == null) {
throw new DoesNotExistError('No terminal element found');
}
return {
ipfsRoots,
terminalElement,
blockstore
};
}
let bytes;
if (cid.multihash.code === CODEC_IDENTITY) {
bytes = cid.multihash.digest;
}
else {
bytes = await toBuffer(blockstore.get(cid, options));
}
// entity codecs contain all the bytes for an entity in one block and no
// path walking outside of that block is possible
if (ENTITY_CODECS.includes(cid.code)) {
return {
ipfsRoots: [cid],
terminalElement: basicEntry('object', cid, bytes),
blockstore
};
}
// may be an unknown codec
return {
ipfsRoots: [cid],
terminalElement: basicEntry('raw', cid, bytes),
blockstore
};
}
}
function toIPFSPath(url) {
return `/ipfs/${url.hostname}${decodeURI(url.pathname)}`;
}
//# sourceMappingURL=url-resolver.js.map