mirror of https://codeberg.org/pzp/pzp-db.git
use TypeScript in JSDoc
This commit is contained in:
parent
aab707f5da
commit
674e2ba66c
|
@ -3,6 +3,7 @@ node_modules
|
|||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
coverage
|
||||
**/*.d.ts
|
||||
*~
|
||||
|
||||
# For misc scripts and experiments:
|
||||
|
|
|
@ -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,
|
||||
|
|
224
lib/index.js
224
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<Rec>} */
|
||||
const recs = []
|
||||
|
||||
/** @type {Map<string, EncryptionFormat>} */
|
||||
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,13 +170,26 @@ exports.init = function initDB(peer, config) {
|
|||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} hash
|
||||
* @param {Msg} msg
|
||||
* @param {CB<Rec>} 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) => {
|
||||
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
|
||||
|
@ -148,13 +198,17 @@ exports.init = function initDB(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<string>} tangleIds
|
||||
*/
|
||||
function populateTangles(tangleIds) {
|
||||
/** @type {Record<string, DBTangle>} */
|
||||
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<void>} cb
|
||||
*/
|
||||
function loaded(cb) {
|
||||
if (cb === void 0) return promisify(loaded)()
|
||||
scannedLog.onReady(cb)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Msg} msg
|
||||
* @param {string} tangleRootHash
|
||||
* @param {CB<Rec>} 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<string>} 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<Rec>} 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<Rec>} 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<string>;
|
||||
* }} opts
|
||||
* @param {CB<Rec>} 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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<CreateOpts, 'data'>} 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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<Msg>} 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
|
||||
|
|
|
@ -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<string>} 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') {
|
||||
|
@ -122,10 +155,13 @@ function validateSignature(msg) {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} msg
|
||||
* @typedef {NonNullable<ReturnType<Tangle['getFeed']>>} 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<string>} pubkeys
|
||||
* @param {string} msgHash
|
||||
* @param {string} rootHash
|
||||
*/
|
||||
function validate(msg, tangle, pubkeys, msgHash, rootHash) {
|
||||
let err
|
||||
if ((err = validateShape(msg))) return err
|
||||
|
|
|
@ -6,6 +6,9 @@ class ReadyGate {
|
|||
this.#ready = false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {() => void} cb
|
||||
*/
|
||||
onReady(cb) {
|
||||
if (this.#ready) cb()
|
||||
else this.#waiting.add(cb)
|
||||
|
|
|
@ -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\"",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue