const crypto = require('node:crypto') const base58 = require('bs58') const b4a = require('b4a') // @ts-ignore const stringify = require('json-canon') const Keypair = require('ppppp-keypair') // @ts-ignore const union = require('set.prototype.union') const { stripIdentity } = require('./strip') const isFeedRoot = require('./is-feed-root') const { getMsgId, getMsgHash } = require('./get-msg-id') const representData = require('./represent-data') const { validateDomain, validateData, validate, validateMsgHash, } = require('./validation') const Tangle = require('./tangle') /** * @typedef {import('ppppp-keypair').Keypair} Keypair */ /** * @typedef {Iterator & {values: () => Iterator}} MsgIter * * @typedef {Buffer | Uint8Array} B4A * * @typedef {{ * depth: number; * prev: Array; * }} TangleMetadata * * @typedef {{ * data: any; * metadata: { * dataHash: string | null; * dataSize: number; * identity: string | (typeof IDENTITY_SELF) | null; * identityTips: Array | null; * tangles: Record; * domain: string; * v: 3; * }; * pubkey: string; * sig: string; * }} Msg * * @typedef {{ * data: any; * domain: string; * keypair: Keypair; * identity: string | null; * identityTips: Array | null; * tangles: Record; * }} CreateOpts */ const IDENTITY_SELF = /** @type {const} */ 'self' /** * @param {string} id * @param {string} domain * @returns {string} */ function getFeedRootHash(id, domain) { /** @type {Msg} */ const msg = { data: null, metadata: { dataHash: null, dataSize: 0, identity: stripIdentity(id), identityTips: null, tangles: {}, domain, v: 3, }, pubkey: '', sig: '', } return getMsgHash(msg) } /** * @param {Pick} opts * @returns {B4A} */ function toPlaintextBuffer(opts) { return b4a.from(stringify(opts.data), 'utf8') } /** * @param {CreateOpts} opts * @returns {Msg} */ function create(opts) { let err if ((err = validateDomain(opts.domain))) throw err if (!opts.tangles) throw new Error('opts.tangles is required') const [dataHash, dataSize] = representData(opts.data) const identity = opts.identity ? stripIdentity(opts.identity) : null const identityTips = opts.identityTips ? opts.identityTips.sort() : null const tangles = /** @type {Msg['metadata']['tangles']} */ ({}) if (opts.tangles) { for (const rootId in opts.tangles) { if ((err = validateMsgHash(rootId))) throw err const tangle = opts.tangles[rootId] const depth = tangle.getMaxDepth() + 1 const tips = tangle.getTips() const lipmaaSet = tangle.getLipmaaSet(depth) const prev = [...union(lipmaaSet, tips)].sort() tangles[rootId] = { depth, prev } } } else { // prettier-ignore throw new Error(`cannot create msg without tangles, that's the case for createRoot()`) } /** @type {Msg} */ const msg = { data: opts.data, metadata: { dataHash, dataSize, identity, identityTips, tangles, domain: opts.domain, v: 3, }, pubkey: opts.keypair.public, sig: '', } if ((err = validateData(msg))) throw err // TODO: add a label prefix to the metadata before signing const metadataBuf = b4a.from(stringify(msg.metadata), 'utf8') msg.sig = Keypair.sign(opts.keypair, metadataBuf) return msg } /** * @param {string} id * @param {string} domain * @param {Keypair} keypair * @returns {Msg} */ function createRoot(id, domain, keypair) { let err if ((err = validateDomain(domain))) throw err /** @type {Msg} */ const msg = { data: null, metadata: { dataHash: null, dataSize: 0, identity: id, identityTips: null, tangles: {}, domain, v: 3, }, pubkey: keypair.public, sig: '', } // TODO: add a label prefix to the metadata before signing const metadataBuf = b4a.from(stringify(msg.metadata), 'utf8') msg.sig = Keypair.sign(keypair, metadataBuf) return msg } /** * @param {Keypair} keypair * @param {string} domain * @param {string | (() => string)} nonce * @returns {Msg} */ function createIdentity( keypair, domain, nonce = () => base58.encode(crypto.randomBytes(32)) ) { const actualNonce = typeof nonce === 'function' ? nonce() : nonce return create({ data: { add: keypair.public, nonce: actualNonce }, identity: IDENTITY_SELF, identityTips: null, keypair, tangles: {}, domain, }) } /** * @param {Msg} msg * @returns {Msg} */ function erase(msg) { return { ...msg, data: null } } /** * @param {B4A} plaintextBuf * @param {Msg} msg * @returns {Msg} */ function fromPlaintextBuffer(plaintextBuf, msg) { return { ...msg, data: JSON.parse(plaintextBuf.toString('utf-8')) } } module.exports = { getMsgHash, getMsgId, isFeedRoot, getFeedRootHash, create, createRoot, createIdentity, erase, stripIdentity, toPlaintextBuffer, fromPlaintextBuffer, Tangle, validate, }