UNPKG

@helia/verified-fetch

Version:

A fetch-like API for obtaining verified & trustless IPFS content on the web

184 lines 7 kB
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