UNPKG

ipfs-core

Version:

JavaScript implementation of the IPFS specification

241 lines (194 loc) 7.1 kB
import { isPeerId } from '@libp2p/interface-peer-id' import { notFoundError } from 'datastore-core/errors' import errcode from 'err-code' import { logger } from '@libp2p/logger' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' import * as ipns from 'ipns' const log = logger('ipfs:ipns:publisher') /** * @typedef {import('@libp2p/interface-keys').PrivateKey} PrivateKey * @typedef {import('@libp2p/interface-keys').PublicKey} PublicKey * @typedef {import('ipns').IPNSEntry} IPNSEntry * @typedef {import('@libp2p/interface-peer-id').PeerId} PeerId * @typedef {import('@libp2p/interfaces').AbortOptions} AbortOptions */ const ERR_NOT_FOUND = notFoundError().code const defaultRecordLifetime = 60 * 60 * 1000 // IpnsPublisher is capable of publishing and resolving names to the IPFS routing system. export class IpnsPublisher { /** * @param {import('ipfs-core-types/src/utils').BufferStore} routing * @param {import('interface-datastore').Datastore} datastore */ constructor (routing, datastore) { this._routing = routing this._datastore = datastore } /** * Publish record with a eol * * @param {PeerId} peerId * @param {Uint8Array} value * @param {number} lifetime * @param {AbortOptions} [options] */ async publishWithEOL (peerId, value, lifetime, options) { const record = await this._updateOrCreateRecord(peerId, value, lifetime, options) return this._putRecordToRouting(record, peerId, options) } /** * Accepts a keypair, as well as a value (ipfsPath), and publishes it out to the routing system * * @param {PeerId} peerId * @param {Uint8Array} value * @param {AbortOptions} options */ publish (peerId, value, options) { return this.publishWithEOL(peerId, value, defaultRecordLifetime, options) } /** * @param {Uint8Array} record * @param {PeerId} peerId * @param {AbortOptions} [options] */ async _putRecordToRouting (record, peerId, options) { if (!(isPeerId(peerId))) { const errMsg = 'peerId received is not valid' log.error(errMsg) throw errcode(new Error(errMsg), 'ERR_INVALID_PEER_ID') } if (peerId.publicKey == null) { throw errcode(new Error('Public key was missing'), 'ERR_MISSING_PUBLIC_KEY') } const routingKey = ipns.peerIdToRoutingKey(peerId) await this._publishEntry(routingKey, record, options) return record } /** * @param {Uint8Array} key * @param {Uint8Array} entry * @param {AbortOptions} [options] */ async _publishEntry (key, entry, options) { // Add record to routing (buffer key) try { const res = await this._routing.put(key, entry, options) log(`ipns record for ${uint8ArrayToString(key, 'base32')} was stored in the routing`) return res } catch (/** @type {any} */err) { const errMsg = `ipns record for ${uint8ArrayToString(key, 'base32')} could not be stored in the routing - ${err.stack}` log.error(errMsg) log.error(err) throw errcode(new Error(errMsg), 'ERR_PUTTING_TO_ROUTING') } } /** * Returns the record this node has published corresponding to the given peer ID. * * If `checkRouting` is true and we have no existing record, this method will check the routing system for any existing records. * * @param {PeerId} peerId * @param {object} options * @param {boolean} [options.checkRouting] */ async _getPublished (peerId, options = {}) { if (!(isPeerId(peerId))) { const errMsg = 'peerId received is not valid' log.error(errMsg) throw errcode(new Error(errMsg), 'ERR_INVALID_PEER_ID') } const checkRouting = options.checkRouting !== false try { const dsVal = await this._datastore.get(ipns.getLocalKey(peerId.toBytes())) // unmarshal data return this._unmarshalData(dsVal) } catch (/** @type {any} */ err) { if (err.code !== ERR_NOT_FOUND) { const errMsg = `unexpected error getting the ipns record ${peerId.toString()} from datastore` log.error(errMsg) throw errcode(new Error(errMsg), 'ERR_UNEXPECTED_DATASTORE_RESPONSE') } if (!checkRouting) { throw errcode(err, 'ERR_NOT_FOUND_AND_CHECK_ROUTING_NOT_ENABLED') } // Try to get from routing try { const routingKey = ipns.peerIdToRoutingKey(peerId) const res = await this._routing.get(routingKey) // unmarshal data return this._unmarshalData(res) } catch (/** @type {any} */ err) { log.error(err) throw err } } } /** * @param {Uint8Array} data */ _unmarshalData (data) { try { return ipns.unmarshal(data) } catch (/** @type {any} */ err) { throw errcode(err, 'ERR_INVALID_RECORD_DATA') } } /** * @param {PeerId} peerId * @param {Uint8Array} value * @param {number} lifetime * @param {AbortOptions} [options] */ async _updateOrCreateRecord (peerId, value, lifetime, options) { if (!(isPeerId(peerId))) { const errMsg = 'peerId received is not valid' log.error(errMsg) throw errcode(new Error(errMsg), 'ERR_INVALID_PEER_ID') } const getPublishedOptions = { checkRouting: true } /** @type {IPNSEntry | undefined} */ let record try { record = await this._getPublished(peerId, getPublishedOptions) } catch (/** @type {any} */ err) { if (err.code !== ERR_NOT_FOUND) { const errMsg = `unexpected error when determining the last published IPNS record for ${peerId.toString()} ${err.stack}` log.error(errMsg) throw errcode(new Error(errMsg), 'ERR_DETERMINING_PUBLISHED_RECORD') } } // Determinate the record sequence number let seqNumber = 0n if (record && record.sequence !== undefined) { // Increment if the published value is different seqNumber = uint8ArrayEquals(record.value, value) ? record.sequence : record.sequence + BigInt(1) } /** @type {IPNSEntry} */ let entryData try { // Create record entryData = await ipns.create(peerId, value, seqNumber, lifetime) } catch (/** @type {any} */ err) { const errMsg = `ipns record for ${value} could not be created` log.error(err) throw errcode(new Error(errMsg), 'ERR_CREATING_IPNS_RECORD') } // TODO IMPROVEMENT - set ttl (still experimental feature for go) try { // Marshal record const data = ipns.marshal(entryData) // Store the new record await this._datastore.put(ipns.getLocalKey(peerId.toBytes()), data, options) log(`ipns record for ${uint8ArrayToString(value, 'base32')} was stored in the datastore`) return data } catch (/** @type {any} */ err) { const errMsg = `ipns record for ${value} could not be stored in the datastore` log.error(errMsg) throw errcode(new Error(errMsg), 'ERR_STORING_IN_DATASTORE') } } } IpnsPublisher.defaultRecordLifetime = defaultRecordLifetime