From 674e2ba66c7158517e7ea50c69ad4d19a7c005c3 Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Sun, 25 Jun 2023 18:48:24 +0300 Subject: [PATCH] use TypeScript in JSDoc --- .gitignore | 1 + lib/encryption.js | 32 +++-- lib/index.js | 240 +++++++++++++++++++++++++++-------- lib/msg-v3/get-msg-id.js | 4 +- lib/msg-v3/index.js | 17 ++- lib/msg-v3/is-feed-root.js | 12 ++ lib/msg-v3/represent-data.js | 8 +- lib/msg-v3/strip.js | 7 + lib/msg-v3/tangle.js | 37 ++++-- lib/msg-v3/validation.js | 66 +++++++++- lib/utils.js | 3 + package.json | 5 +- tsconfig.json | 16 +++ 13 files changed, 362 insertions(+), 86 deletions(-) create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 4b96477..3a1f1fd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules pnpm-lock.yaml package-lock.json coverage +**/*.d.ts *~ # For misc scripts and experiments: diff --git a/lib/encryption.js b/lib/encryption.js index fbe6e1d..1a0194c 100644 --- a/lib/encryption.js +++ b/lib/encryption.js @@ -3,10 +3,23 @@ const b4a = require('b4a') const MsgV3 = require('./msg-v3') /** - * @typedef {import('./index').Rec} Rec + * @typedef {import('./index').RecPresent} RecPresent * @typedef {import('ppppp-keypair').Keypair} Keypair + * + * @typedef {Buffer | Uint8Array} B4A + * + * @typedef {{ + * name: string; + * setup?: (config: any, cb: any) => void; + * onReady?: (cb: any) => void; + * encrypt: (plaintext: B4A, opts: any) => B4A; + * decrypt: (ciphertext: B4A, opts: any) => B4A | null; + * }} EncryptionFormat */ +/** + * @param {string} str + */ function ciphertextStrToBuffer(str) { const dot = str.indexOf('.') return b4a.from(str.slice(0, dot), 'base64') @@ -17,21 +30,21 @@ function ciphertextStrToBuffer(str) { * @param {Keypair} keypair */ function keypairToSSBKeys(keypair) { - const public = b4a.from(base58.decode(keypair.public)).toString('base64') - const private = b4a.from(base58.decode(keypair.private)).toString('base64') + const _public = b4a.from(base58.decode(keypair.public)).toString('base64') + const _private = b4a.from(base58.decode(keypair.private)).toString('base64') return { - id: `@${public}.ed25519`, + id: `@${_public}.ed25519`, curve: keypair.curve, - public, - private, + public: _public, + private: _private, } } /** - * @param {Rec} rec + * @param {RecPresent} rec * @param {any} peer * @param {any} config - * @returns {Rec} + * @returns {RecPresent} */ function decrypt(rec, peer, config) { const msgEncrypted = rec.msg @@ -63,6 +76,9 @@ function decrypt(rec, peer, config) { } } +/** + * @param {RecPresent} rec + */ function reEncrypt(rec) { return { hash: rec.hash, diff --git a/lib/index.js b/lib/index.js index 12cb8ea..c152e91 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,47 +1,63 @@ const path = require('node:path') +// @ts-ignore const push = require('push-stream') +// @ts-ignore const AAOL = require('async-append-only-log') const promisify = require('promisify-4loc') const b4a = require('b4a') const base58 = require('bs58') +// @ts-ignore const Obz = require('obz') const MsgV2 = require('./msg-v3') const { ReadyGate } = require('./utils') const { decrypt } = require('./encryption') /** + * @typedef {import('ppppp-keypair').Keypair} Keypair * @typedef {import('./msg-v3').Msg} Msg + * @typedef {import('./encryption').EncryptionFormat} EncryptionFormat + * + * @typedef {Buffer | Uint8Array} B4A */ /** - * @typedef {Object} RecDeleted - * @property {never} hash - * @property {never} msg - * @property {never} received - * @property {Object} misc - * @property {number} misc.offset - * @property {number} misc.size - * @property {number} misc.seq - */ - -/** - * @typedef {Object} RecPresent - * @property {string} hash - * @property {Msg} msg - * @property {number} received - * @property {Object} misc - * @property {number} misc.offset - * @property {number} misc.size - * @property {number} misc.seq - * @property {boolean=} misc.private - * @property {Object=} misc.originalData - * @property {string=} misc.encryptionFormat - */ - -/** + * @typedef {{ + * hash?: never; + * msg?: never; + * received?: never; + * misc: { + * offset: number; + * size: number; + * seq: number; + * }; + * }} RecDeleted + * + * @typedef {{ + * hash: string; + * msg: Msg; + * received: number; + * misc: { + * offset: number; + * size: number; + * seq: number; + * private?: boolean; + * originalData?: any; + * encryptionFormat?: string; + * } + * }} RecPresent + * * @typedef {RecPresent | RecDeleted} Rec */ +/** + * @template T + * @typedef {(...args: [Error] | [null, T]) => void} CB + */ + +/** + * @typedef {(...args: [Error] | []) => void} CBVoid + */ + class DBTangle extends MsgV2.Tangle { /** * @param {string} rootHash @@ -55,6 +71,9 @@ class DBTangle extends MsgV2.Tangle { } } + /** + * @param {string} msgHash + */ getDeletablesAndErasables(msgHash) { const erasables = this.shortestPathToRoot(msgHash) const sorted = this.topoSort() @@ -66,28 +85,42 @@ class DBTangle extends MsgV2.Tangle { } } -exports.name = 'db' - -exports.init = function initDB(peer, config) { +/** + * @param {any} peer + * @param {{ path: string; keypair: Keypair; }} config + */ +function initDB(peer, config) { /** @type {Array} */ const recs = [] + + /** @type {Map} */ const encryptionFormats = new Map() + const onRecordAdded = Obz() const log = AAOL(path.join(config.path, 'db.bin'), { cacheSize: 1, blockSize: 64 * 1024, codec: { + /** + * @param {Msg} msg + */ encode(msg) { return b4a.from(JSON.stringify(msg), 'utf8') }, + /** + * @param {B4A} buf + */ decode(buf) { - return JSON.parse(buf.toString('utf8')) + return JSON.parse(b4a.toString(buf, 'utf8')) }, }, + /** + * @param {B4A} buf + */ validateRecord(buf) { try { - JSON.parse(buf.toString('utf8')) + JSON.parse(b4a.toString(buf, 'utf8')) return true } catch { return false @@ -95,8 +128,9 @@ exports.init = function initDB(peer, config) { }, }) - peer.close.hook(function (fn, args) { + peer.close.hook(function (/** @type {any} */ fn, /** @type {any} */ args) { log.close(() => { + // @ts-ignore fn.apply(this, args) }) }) @@ -107,24 +141,27 @@ exports.init = function initDB(peer, config) { let i = -1 log.stream({ offsets: true, values: true, sizes: true }).pipe( push.drain( + // @ts-ignore function drainEach({ offset, value, size }) { i += 1 if (!value) { // deleted record - recs.push({ misc: { offset, size, seq: i } }) + /** @type {RecDeleted} */ + const rec = { misc: { offset, size, seq: i } } + recs.push(rec) return } // TODO: for performance, dont decrypt on startup, instead decrypt on // demand, or decrypt in the background. Or then store the log with // decrypted msgs and only encrypt when moving it to the network. const rec = decrypt(value, peer, config) - rec.misc ??= {} + rec.misc ??= /** @type {Rec['misc']} */ ({}) rec.misc.offset = offset rec.misc.size = size rec.misc.seq = i recs.push(rec) }, - function drainEnd(err) { + function drainEnd(/** @type {any} */ err) { // prettier-ignore if (err) throw new Error('Failed to initially scan the log', { cause: err }); scannedLog.setReady() @@ -133,28 +170,45 @@ exports.init = function initDB(peer, config) { ) }) + /** + * @param {string} hash + * @param {Msg} msg + * @param {CB} cb + */ function logAppend(hash, msg, cb) { + /** @type {RecPresent} */ const rec = { hash, msg, received: Date.now(), + misc: { + offset: 0, + size: 0, + seq: 0, + }, } - log.append(rec, (err, newOffset) => { - if (err) return cb(new Error('logAppend failed', { cause: err })) - const offset = newOffset // latestOffset - const size = b4a.from(JSON.stringify(rec), 'utf8').length - const seq = recs.length - const recExposed = decrypt(rec, peer, config) - rec.misc = recExposed.misc = { offset, size, seq } - recs.push(recExposed) - cb(null, rec) - }) + log.append( + rec, + (/** @type {any} */ err, /** @type {number} */ newOffset) => { + if (err) return cb(new Error('logAppend failed', { cause: err })) + const offset = newOffset // latestOffset + const size = b4a.from(JSON.stringify(rec), 'utf8').length + const seq = recs.length + const recExposed = decrypt(rec, peer, config) + rec.misc = recExposed.misc = { offset, size, seq } + recs.push(recExposed) + cb(null, rec) + } + ) } + /** + * @param {EncryptionFormat} encryptionFormat + */ function installEncryptionFormat(encryptionFormat) { if (encryptionFormat.setup) { const loaded = new ReadyGate() - encryptionFormat.setup(config, (err) => { + encryptionFormat.setup(config, (/** @type {any} */ err) => { // prettier-ignore if (err) throw new Error(`Failed to install encryption format "${encryptionFormat.name}"`, {cause: err}); loaded.setReady() @@ -164,15 +218,27 @@ exports.init = function initDB(peer, config) { encryptionFormats.set(encryptionFormat.name, encryptionFormat) } + /** + * @param {string} ciphertextJS + */ function findEncryptionFormatFor(ciphertextJS) { if (!ciphertextJS) return null if (typeof ciphertextJS !== 'string') return null const suffix = ciphertextJS.split('.').pop() + if (!suffix) { + // prettier-ignore + console.warn('findEncryptionFormatFor() failed to find suffix\n\n' + ciphertextJS) + return null + } const encryptionFormat = encryptionFormats.get(suffix) ?? null return encryptionFormat } + /** + * @param {Array} tangleIds + */ function populateTangles(tangleIds) { + /** @type {Record} */ const tangles = {} for (const tangleId of tangleIds) { tangles[tangleId] ??= new DBTangle(tangleId, records()) @@ -180,11 +246,19 @@ exports.init = function initDB(peer, config) { return tangles } + /** + * @param {CB} cb + */ function loaded(cb) { if (cb === void 0) return promisify(loaded)() scannedLog.onReady(cb) } + /** + * @param {Msg} msg + * @param {string} tangleRootHash + * @param {CB} cb + */ function add(msg, tangleRootHash, cb) { const msgHash = MsgV2.getMsgHash(msg) @@ -223,6 +297,10 @@ exports.init = function initDB(peer, config) { }) } + /** + * @param {{ keypair?: any; identity: string; domain: string; }} opts + * @param {CB} cb + */ function initializeFeed(opts, cb) { const keypair = opts.keypair ?? config.keypair const { identity, domain } = opts @@ -234,10 +312,15 @@ exports.init = function initDB(peer, config) { add(feedRoot, MsgV2.getMsgHash(feedRoot), (err, rec) => { // prettier-ignore if (err) return cb(new Error('initializeFeed() failed to add root', { cause: err })); - cb(null, rec.hash) + const recHash = /** @type {string} */ (rec.hash) + cb(null, recHash) }) } + /** + * @param {{keypair?: Keypair, _nonce?: string} | null} opts + * @param {CB} cb + */ function createIdentity(opts, cb) { const keypair = opts?.keypair ?? config.keypair @@ -257,9 +340,15 @@ exports.init = function initDB(peer, config) { }) } + /** + * @param {{ keypair: Keypair; identity: string; }} opts + * @param {CB} cb + */ function addToIdentity(opts, cb) { - if (!opts?.keypair) return cb(new Error('identity.add() requires a `keypair`')) - if (!opts?.identity) return cb(new Error('identity.add() requires a `identity`')) + if (!opts?.keypair) + return cb(new Error('identity.add() requires a `keypair`')) + if (!opts?.identity) + return cb(new Error('identity.add() requires a `identity`')) const addedKeypair = opts.keypair const signingKeypair = config.keypair @@ -291,20 +380,31 @@ exports.init = function initDB(peer, config) { }) } + /** + * @param {{ + * keypair?: Keypair; + * encryptionFormat?: string; + * data: any; + * domain: string; + * identity: string; + * tangles?: Array; + * }} opts + * @param {CB} cb + */ function publishToFeed(opts, cb) { if (!opts) return cb(new Error('feed.publish() requires an `opts`')) const keypair = opts.keypair ?? config.keypair - const encryptionFormat = encryptionFormats.get(opts.encryptionFormat) if (opts.data.recps) { - if (!encryptionFormat) { + if (!encryptionFormats.has(opts.encryptionFormat ?? '')) { // prettier-ignore return cb(new Error(`feed.publish() does not support encryption format "${opts.encryptionFormat}"`)) } } if (!opts.data) return cb(new Error('feed.publish() requires a `data`')) if (!opts.domain) return cb(new Error('feed.publish() requires a `domain`')) - if (!opts.identity) return cb(new Error('feed.publish() requires a `identity`')) + if (!opts.identity) + return cb(new Error('feed.publish() requires a `identity`')) initializeFeed(opts, (err, feedRootHash) => { // prettier-ignore @@ -330,6 +430,9 @@ exports.init = function initDB(peer, config) { `@${b4a.from(base58.decode(recp)).toString('base64')}.ed25519` ), } + const encryptionFormat = /** @type {EncryptionFormat} */ ( + encryptionFormats.get(opts.encryptionFormat ?? '') + ) let ciphertextBuf try { ciphertextBuf = encryptionFormat.encrypt(plaintext, encryptOpts) @@ -367,31 +470,47 @@ exports.init = function initDB(peer, config) { }) } + /** + * @param {string} id + * @param {string} findDomain + */ function getFeedId(id, findDomain) { const findIdentity = MsgV2.stripIdentity(id) for (const rec of records()) { - if (MsgV2.isFeedRoot(rec.msg, findIdentity, findDomain)) return rec.hash + if (rec.msg && MsgV2.isFeedRoot(rec.msg, findIdentity, findDomain)) { + return rec.hash + } } return null } - // TODO: improve performance of this when getting many messages, the argument - // could be an array of hashes, so we can do a single pass over the records. + /** + * @param {string} msgId + */ function getRecord(msgId) { + // TODO: improve performance of this when getting many messages, the arg + // could be an array of hashes, so we can do a single pass over the records. const isUri = msgId.startsWith('ppppp:') for (let i = 0; i < recs.length; i++) { const rec = recs[i] if (!rec) continue - if (isUri && msgId.endsWith(rec.hash)) return rec + if (isUri && rec.hash && msgId.endsWith(rec.hash)) return rec else if (!isUri && rec.hash === msgId) return rec } return null } + /** + * @param {string} msgId + */ function get(msgId) { return getRecord(msgId)?.msg } + /** + * @param {string} msgId + * @param {CBVoid} cb + */ function del(msgId, cb) { const rec = getRecord(msgId) if (!rec) return cb() @@ -403,6 +522,10 @@ exports.init = function initDB(peer, config) { }) } + /** + * @param {string} msgId + * @param {CBVoid} cb + */ function erase(msgId, cb) { const rec = getRecord(msgId) if (!rec) return cb() @@ -413,6 +536,10 @@ exports.init = function initDB(peer, config) { cb() } + /** + * @param {string} tangleId + * @returns {DBTangle} + */ function getTangle(tangleId) { return new DBTangle(tangleId, records()) } @@ -460,3 +587,6 @@ exports.init = function initDB(peer, config) { _getLog: () => log, } } + +exports.name = 'db' +exports.init = initDB diff --git a/lib/msg-v3/get-msg-id.js b/lib/msg-v3/get-msg-id.js index 2e4dbe2..9a2a667 100644 --- a/lib/msg-v3/get-msg-id.js +++ b/lib/msg-v3/get-msg-id.js @@ -1,6 +1,7 @@ const b4a = require('b4a') const blake3 = require('blake3') const base58 = require('bs58') +// @ts-ignore const stringify = require('json-canon') /** @@ -14,7 +15,8 @@ const stringify = require('json-canon') */ function getMsgHashBuf(msg) { const metadataBuf = b4a.from(stringify(msg.metadata), 'utf8') - return blake3.hash(metadataBuf).subarray(0, 16) + const longHash = b4a.from(blake3.hash(metadataBuf)) + return longHash.subarray(0, 16) } /** diff --git a/lib/msg-v3/index.js b/lib/msg-v3/index.js index 155067a..5a719bc 100644 --- a/lib/msg-v3/index.js +++ b/lib/msg-v3/index.js @@ -1,8 +1,10 @@ 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') @@ -12,7 +14,6 @@ const { validateDomain, validateData, validate, - validateBatch, validateMsgHash, } = require('./validation') const Tangle = require('./tangle') @@ -56,6 +57,11 @@ const Tangle = require('./tangle') * }} CreateOpts */ +/** + * @param {string} id + * @param {string} domain + * @returns {string} + */ function getFeedRootHash(id, domain) { /** @type {Msg} */ const msg = { @@ -76,6 +82,10 @@ function getFeedRootHash(id, domain) { return getMsgHash(msg) } +/** + * @param {Pick} opts + * @returns {B4A} + */ function toPlaintextBuffer(opts) { return b4a.from(stringify(opts.data), 'utf8') } @@ -93,7 +103,7 @@ function create(opts) { const identity = opts.identity ? stripIdentity(opts.identity) : null const identityTips = opts.identityTips ? opts.identityTips.sort() : null - const tangles = {} + const tangles = /** @type {Msg['metadata']['tangles']} */ ({}) if (opts.tangles) { for (const rootId in opts.tangles) { if ((err = validateMsgHash(rootId))) throw err @@ -168,7 +178,7 @@ function createRoot(id, domain, keypair) { /** * @param {Keypair} keypair - * @param {string} nonce + * @param {string | (() => string)} nonce * @returns {Msg} */ function createIdentity( @@ -217,5 +227,4 @@ module.exports = { fromPlaintextBuffer, Tangle, validate, - validateBatch, } diff --git a/lib/msg-v3/is-feed-root.js b/lib/msg-v3/is-feed-root.js index 1e9cfdc..2bf43d0 100644 --- a/lib/msg-v3/is-feed-root.js +++ b/lib/msg-v3/is-feed-root.js @@ -1,5 +1,12 @@ const { stripIdentity } = require('./strip') +/** + * @typedef {import('.').Msg} Msg + */ + +/** + * @param {any} obj + */ function isEmptyObject(obj) { for (const _key in obj) { return false @@ -7,6 +14,11 @@ function isEmptyObject(obj) { return true } +/** + * @param {Msg} msg + * @param {string | 0} id + * @param {string | 0} findDomain + */ function isFeedRoot(msg, id = 0, findDomain = 0) { const { dataHash, dataSize, identity, identityTips, tangles, domain } = msg.metadata if (dataHash !== null) return false diff --git a/lib/msg-v3/represent-data.js b/lib/msg-v3/represent-data.js index afc4472..e8822af 100644 --- a/lib/msg-v3/represent-data.js +++ b/lib/msg-v3/represent-data.js @@ -1,15 +1,21 @@ const blake3 = require('blake3') const b4a = require('b4a') const base58 = require('bs58') +// @ts-ignore const stringify = require('json-canon') +/** + * @typedef {Buffer | Uint8Array} B4A + */ + /** * @param {any} data * @returns {[string, number]} */ function representData(data) { const dataBuf = b4a.from(stringify(data), 'utf8') - const dataHash = base58.encode(blake3.hash(dataBuf).subarray(0, 16)) + const fullHash = /** @type {B4A} */ (blake3.hash(dataBuf)) + const dataHash = base58.encode(fullHash.subarray(0, 16)) const dataSize = dataBuf.length return [dataHash, dataSize] } diff --git a/lib/msg-v3/strip.js b/lib/msg-v3/strip.js index 40947af..ccc7885 100644 --- a/lib/msg-v3/strip.js +++ b/lib/msg-v3/strip.js @@ -1,5 +1,12 @@ const { getMsgHash } = require('./get-msg-id') +/** + * @typedef {import('.').Msg} Msg + */ + +/** + * @param {any} msgKey + */ function stripMsgKey(msgKey) { if (typeof msgKey === 'object') { if (msgKey.key) return stripMsgKey(msgKey.key) diff --git a/lib/msg-v3/tangle.js b/lib/msg-v3/tangle.js index 15c1f41..4c207d7 100644 --- a/lib/msg-v3/tangle.js +++ b/lib/msg-v3/tangle.js @@ -2,6 +2,9 @@ * @typedef {import("./index").Msg} Msg */ +/** + * @param {number} n + */ function lipmaa(n) { let m = 1 let po3 = 3 @@ -46,7 +49,7 @@ class Tangle { #rootHash /** - * @type {Msg} + * @type {Msg | undefined} */ #rootMsg @@ -77,13 +80,16 @@ class Tangle { /** * @param {string} rootHash - * @param {Iterable} msgsIter */ constructor(rootHash) { this.#rootHash = rootHash this.#maxDepth = 0 } + /** + * @param {string} msgHash + * @param {Msg} msg + */ add(msgHash, msg) { if (msgHash === this.#rootHash && !this.#rootMsg) { this.#tips.add(msgHash) @@ -195,10 +201,17 @@ class Tangle { getFeed() { if (!this.isFeed()) return null + if (!this.#rootMsg) { + console.trace('Tangle is missing root message') + return null + } const { identity, domain } = this.#rootMsg.metadata return { identity, domain } } + /** + * @param {string} msgHash + */ shortestPathToRoot(msgHash) { if (!this.#rootMsg) { console.trace('Tangle is missing root message') @@ -209,10 +222,10 @@ class Tangle { while (true) { const prev = this.#prev.get(current) if (!prev) break - let minDepth = this.#depth.get(current) + let minDepth = /** @type {number} */ (this.#depth.get(current)) let min = current for (const p of prev) { - const d = this.#depth.get(p) + const d = /** @type {number} */ (this.#depth.get(p)) if (d < minDepth) { minDepth = d min = p @@ -226,18 +239,22 @@ class Tangle { return path } - precedes(a, b) { + /** + * @param {string} msgHashA + * @param {string} msgHashB + */ + precedes(msgHashA, msgHashB) { if (!this.#rootMsg) { console.trace('Tangle is missing root message') return false } - if (a === b) return false - if (b === this.#rootHash) return false - let toCheck = [b] + if (msgHashA === msgHashB) return false + if (msgHashB === this.#rootHash) return false + let toCheck = [msgHashB] while (toCheck.length > 0) { - const prev = this.#prev.get(toCheck.shift()) + const prev = this.#prev.get(/** @type {string} */ (toCheck.shift())) if (!prev) continue - if (prev.includes(a)) return true + if (prev.includes(msgHashA)) return true toCheck.push(...prev) } return false diff --git a/lib/msg-v3/validation.js b/lib/msg-v3/validation.js index 4bda8a8..6250eba 100644 --- a/lib/msg-v3/validation.js +++ b/lib/msg-v3/validation.js @@ -1,11 +1,20 @@ const b4a = require('b4a') const base58 = require('bs58') const Keypair = require('ppppp-keypair') +// @ts-ignore const stringify = require('json-canon') const Tangle = require('./tangle') const representData = require('./represent-data') const isFeedRoot = require('./is-feed-root') +/** + * @typedef {import('.').Msg} Msg + */ + +/** + * @param {Msg} msg + * @returns {string | undefined} + */ function validateShape(msg) { if (!msg || typeof msg !== 'object') { return 'invalid message: not an object\n' + JSON.stringify(msg) @@ -25,7 +34,9 @@ function validateShape(msg) { return 'invalid message: must have metadata.dataSize\n' + JSON.stringify(msg) } if (!('identity' in msg.metadata)) { - return 'invalid message: must have metadata.identity\n' + JSON.stringify(msg) + return ( + 'invalid message: must have metadata.identity\n' + JSON.stringify(msg) + ) } if (!('identityTips' in msg.metadata)) { // prettier-ignore @@ -45,6 +56,10 @@ function validateShape(msg) { } } +/** + * @param {Msg} msg + * @returns {string | undefined} + */ function validatePubkey(msg) { const { pubkey } = msg if (typeof pubkey !== 'string') { @@ -63,6 +78,12 @@ function validatePubkey(msg) { } } +/** + * + * @param {Msg} msg + * @param {Set} pubkeys + * @returns {string | undefined} + */ function validateIdentityPubkey(msg, pubkeys) { // Unusual case: if the msg is a feed root, ignore the identity and pubkey if (isFeedRoot(msg)) return @@ -73,6 +94,10 @@ function validateIdentityPubkey(msg, pubkeys) { } } +/** + * @param {string} str + * @returns {string | undefined} + */ function validateMsgHash(str) { try { const hashBuf = b4a.from(base58.decode(str)) @@ -85,6 +110,10 @@ function validateMsgHash(str) { } } +/** + * @param {Msg} msg + * @returns {string | undefined} + */ function validateDataSize(msg) { const { dataSize } = msg.metadata if (!Number.isSafeInteger(dataSize) || dataSize < 0) { @@ -93,6 +122,10 @@ function validateDataSize(msg) { } } +/** + * @param {Msg} msg + * @returns {string | undefined} + */ function validateSignature(msg) { const { sig } = msg if (typeof sig !== 'string') { @@ -114,7 +147,7 @@ function validateSignature(msg) { } const signableBuf = b4a.from(stringify(msg.metadata), 'utf8') - const keypair = {curve: 'ed25519', public: msg.pubkey} + const keypair = { curve: 'ed25519', public: msg.pubkey } const verified = Keypair.verify(keypair, signableBuf, sig) if (!verified) { return 'invalid message: sig is invalid\n' + JSON.stringify(msg) @@ -122,10 +155,13 @@ function validateSignature(msg) { } /** - * - * @param {any} msg + * @typedef {NonNullable>} FeedDetails + */ + +/** + * @param {Msg} msg * @param {Tangle} tangle - * @param {*} tangleId + * @param {string} tangleId * @returns */ function validateTangle(msg, tangle, tangleId) { @@ -143,7 +179,7 @@ function validateTangle(msg, tangle, tangleId) { return `invalid message: depth "${depth}" should have been a positive integer\n` + JSON.stringify(msg) } if (tangle.isFeed()) { - const { identity, domain } = tangle.getFeed() + const { identity, domain } = /** @type {FeedDetails} */ (tangle.getFeed()) if (domain !== msg.metadata.domain) { // prettier-ignore return `invalid message: domain "${msg.metadata.domain}" should have been feed domain "${domain}"\n` + JSON.stringify(msg) @@ -202,6 +238,11 @@ function validateTangle(msg, tangle, tangleId) { } } +/** + * @param {Msg} msg + * @param {string} msgHash + * @param {string} tangleId + */ function validateTangleRoot(msg, msgHash, tangleId) { if (msgHash !== tangleId) { // prettier-ignore @@ -213,6 +254,9 @@ function validateTangleRoot(msg, msgHash, tangleId) { } } +/** + * @param {string} domain + */ function validateDomain(domain) { if (!domain || typeof domain !== 'string') { // prettier-ignore @@ -232,6 +276,9 @@ function validateDomain(domain) { } } +/** + * @param {Msg} msg + */ function validateData(msg) { const { data } = msg if (data === null) { @@ -260,6 +307,13 @@ function validateData(msg) { } } +/** + * @param {Msg} msg + * @param {Tangle} tangle + * @param {Set} pubkeys + * @param {string} msgHash + * @param {string} rootHash + */ function validate(msg, tangle, pubkeys, msgHash, rootHash) { let err if ((err = validateShape(msg))) return err diff --git a/lib/utils.js b/lib/utils.js index 73aee3a..9ecb0ec 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -6,6 +6,9 @@ class ReadyGate { this.#ready = false } + /** + * @param {() => void} cb + */ onReady(cb) { if (this.#ready) cb() else this.#waiting.add(cb) diff --git a/package.json b/package.json index 61125c7..a50fbd8 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "files": [ "lib/**/*" ], + "types": "types/index.d.ts", "engines": { "node": ">=16" }, @@ -46,9 +47,11 @@ "pretty-quick": "^3.1.3", "rimraf": "^4.4.0", "secret-stack": "^6.4.2", - "ssb-box": "^1.0.1" + "ssb-box": "^1.0.1", + "typescript": "^5.1.3" }, "scripts": { + "build": "tsc --build --clean && tsc --build", "test": "node --test", "format-code": "prettier --write \"(lib|test)/**/*.js\"", "format-code-staged": "pretty-quick --staged --pattern \"(lib|test)/**/*.js\"", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bd2acd5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["lib/**/*.js"], + "exclude": ["coverage/", "node_modules/", "test/"], + "compilerOptions": { + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2022", "dom"], + "module": "node16", + "skipLibCheck": true, + "strict": true, + "target": "es2021" + } +} \ No newline at end of file