UNPKG

ara-identity

Version:

Create and resolve decentralized identity based Ara identifiers.

354 lines (304 loc) 8.45 kB
const { dirname, resolve } = require('path') const { createSwarm } = require('ara-network/discovery') const { createCFS } = require('cfsnet/create') const isDomainName = require('is-domain-name') const isBrowser = require('is-browser') const { DID } = require('did-uri') const mkdirp = require('mkdirp') const crypto = require('ara-crypto') const debug = require('debug')('ara:identity:fs') const pify = require('pify') const pump = require('pump') const ram = require('random-access-memory') const fs = require('fs') const { toHex, writeCache } = require('./util') const { normalize } = require('./did') const rc = require('./rc')() const DISCOVERY_TIMEOUT = 5 * 1000 const CFS_UPDATE_TIMEOUT = 1 * 1000 /** * Joins a network swarm for an identity scoped to a given * filename. * @private * @param {String} identifier * @param {String} filename * @param {?(Object)} opts * @param {Function} onjoin * @return {Promise} */ async function joinNetwork(identifier, filename, opts, onjoin) { return pify(async (done) => { let retries = 8 let timeout = null let swarm = null let cfs = null let did = null let dat = null try { did = new DID(normalize(identifier)) } catch (err) { return close(err) } debug('network: open: %s: %s', did.identifier, filename) try { cfs = await createCFS({ sparseMetadata: false, shallow: true, storage: () => ram(), latest: true, sparse: true, key: Buffer.from(did.identifier, 'hex'), id: did.identifier, }) } catch (err) { return close(err) } try { swarm = createSwarm({ utp: false, id: cfs.discoveryKey, ...opts }) } catch (err) { return close(err) } try { swarm.join(cfs.discoveryKey, { announce: false }) } catch (err) { return close(err) } timeout = setTimeout(ontimeout, DISCOVERY_TIMEOUT) swarm.on('connection', onconnection) swarm.on('error', onerror) try { dat = createSwarm({ stream: () => cfs.replicate({ live: false }) }) dat.join(cfs.discoveryKey) } catch (err) { debug(err) } try { debug('network: access: %s: %s', did.identifier, filename) await cfs.access(filename) } catch (err) { await Promise.race([ new Promise((cb) => cfs.once('sync', cb)), new Promise((cb) => cfs.once('update', cb)), new Promise((cb) => setTimeout(cb, CFS_UPDATE_TIMEOUT)) ]) } clearTimeout(timeout) timeout = setTimeout(ontimeout, DISCOVERY_TIMEOUT) return onjoin(cfs, did, (err, result) => { debug('network: onjoin: %s: %s', did.identifier, filename) clearTimeout(timeout) close(err, result) }) async function close(err, result) { try { if (null !== timeout) { clearTimeout(timeout) } if (null !== cfs) { await cfs.close() } if (null !== swarm) { swarm.close() } if (null != dat) { dat.close() } done(err, result) debug('network: close: %s: %s', did.identifier, filename) } catch (err2) { done(err2, result) } } function onerror(err) { debug('network: onerror: %s: %s', did.identifier, filename, err) clearTimeout(timeout) close(err) } function ontimeout() { debug('network: ontimeout: %s: %s', did.identifier, filename) close(new NoEntityError(filename, 'open')) } function onconnection(connection, peer) { debug('network: onconnection: %s: %s', did.identifier, filename, peer) clearTimeout(timeout) timeout = setTimeout(ontimeout, DISCOVERY_TIMEOUT) const stream = cfs.replicate({ live: false }) pump(connection, stream, connection, (err) => { if (err) { debug( 'network: onconnection: onerror: %s: %s', did.identifier, filename, err ) if (0 === --retries) { close(err) } } }) } })() } /** * Resolves a filename path for an identity based on * a given identifier. * @private * @param {String} identifier * @param {String} filename * @return {String} */ function resolvePath(identifier, filename) { if (isDomainName(identifier)) { throw new Error('DNS resolvable names are not allowed') } const did = new DID(normalize(identifier)) const hash = toHex(crypto.blake2b(Buffer.from(did.identifier, 'hex'))) return resolve(rc.network.identity.root, hash, filename) } async function readFile(identifier, filename, opts) { const skipCache = Boolean(opts && false === opts.cache) if (false === skipCache) { try { const path = resolvePath(identifier, filename) return await pify(fs.readFile)(path, opts) } catch (err) { void err } } async function onjoin(cfs, did, done) { try { const buffer = await cfs.readFile(filename) if (false === isBrowser) { await writeCache(did.identifier, filename, buffer) } done(null, buffer) } catch (err) { done(new NoEntityError(filename, 'open')) } } if (!opts || false !== opts.network) { return joinNetwork(identifier, filename, opts, onjoin) } throw new NoEntityError(filename, 'open') } /** * Write a filename for an identity based on a given identifier * * @public * @param {String} identifier * @param {String} filename * @param {Buffer} buffer * @param {?(Object)} opts * @return {Promise} */ async function writeFile(identifier, filename, buffer, opts) { const path = resolvePath(identifier, filename) await mkdirp(dirname(path)) return pify(fs.writeFile)(path, buffer, opts) } async function stat(identifier, filename, opts) { if (!opts || false !== opts.cache) { const path = resolvePath(identifier, filename) try { return await pify(fs.stat)(path, opts) } catch (err) { void err } } async function onjoin(cfs, did, done) { try { done(null, await cfs.stat(filename)) } catch (err) { done(new NoEntityError(filename, 'stat')) } } if (!opts || false !== opts.network) { return joinNetwork(identifier, filename, opts, onjoin) } throw new NoEntityError(filename, 'stat') } async function lstat(identifier, filename, opts) { if (!opts || false !== opts.cache) { const path = resolvePath(identifier, filename) try { return await pify(fs.lstat)(path, opts) } catch (err) { void err } } async function onjoin(cfs, did, done) { try { done(null, await cfs.lstat(filename)) } catch (err) { done(new NoEntityError(filename, 'lstat')) } } if (false !== opts.network) { return joinNetwork(identifier, filename, opts, onjoin) } throw new NoEntityError(filename, 'lstat') } async function access(identifier, filename, opts) { if (!opts || false !== opts.cache) { try { const path = resolvePath(identifier, filename) return await pify(fs.access)(path) } catch (err) { void err } } async function onjoin(cfs, did, done) { try { done(null, await cfs.access(filename)) } catch (err) { done(new NoEntityError(filename, 'access')) } } if (!opts || false !== opts.network) { return joinNetwork(identifier, filename, opts, onjoin) } throw new NoEntityError(filename, 'access') } async function readdir(identifier, filename, opts) { if (!opts || false !== opts.cache) { try { const path = resolvePath(identifier, filename) return await pify(fs.readdir)(path, opts) } catch (err) { void err } } async function onjoin(cfs, did, done) { try { done(null, await cfs.readdir(filename, opts)) } catch (err) { done(new NoEntityError(filename, 'scandir')) } } if (!opts || false !== opts.network) { return joinNetwork(identifier, filename, opts, onjoin) } throw new NoEntityError(filename, 'scandir') } class NoEntityError extends Error { constructor(path, call) { super(`ENOENT: no such file or directory, ${call} '${path}'`) this.errno = -2 this.code = 'ENOENT' this.syscall = call this.path = path } } module.exports = { resolve: resolvePath, writeFile, readFile, readdir, access, lstat, stat, }