all tests pass

This commit is contained in:
Andre Staltz 2023-04-02 23:53:20 +03:00
parent ddbb3f367e
commit 83d941e2dc
25 changed files with 993 additions and 1324 deletions

View File

@ -1,39 +1,44 @@
const FeedV1 = require('./feed-v1')
/**
* @typedef {import('./plugin').Rec} Rec
*/
function ciphertextStrToBuffer(str) {
const dot = str.indexOf('.')
return Buffer.from(str.slice(0, dot), 'base64')
}
function decrypt(msg, ssb, config) {
const { author, previous, content } = msg.value
if (typeof content !== 'string') return msg
/**
* @param {Rec} rec
* @param {any} peer
* @param {any} config
* @returns {Rec}
*/
function decrypt(rec, peer, config) {
const msgEncrypted = rec.msg
const { metadata, content } = msgEncrypted
const { who, prev } = metadata
if (typeof content !== 'string') return rec
const encryptionFormat = ssb.db.findEncryptionFormatFor(content)
if (!encryptionFormat) return msg
const feedFormat = ssb.db.findFeedFormatForAuthor(author)
if (!feedFormat) return msg
const encryptionFormat = peer.db.findEncryptionFormatFor(content)
if (!encryptionFormat) return rec
// Decrypt
const ciphertextBuf = ciphertextStrToBuffer(content)
const opts = { keys: config.keys, author, previous }
const opts = { keys: config.keys, author: who, previous: prev }
const plaintextBuf = encryptionFormat.decrypt(ciphertextBuf, opts)
if (!plaintextBuf) return msg
if (!plaintextBuf) return rec
// Reconstruct KVT in JS encoding
const nativeMsg = feedFormat.toNativeMsg(msg.value, 'js')
// TODO: feedFormat.fromDecryptedNativeMsg() should NOT mutate nativeMsg
// but in the case of ssb-classic, it is
const msgVal = feedFormat.fromDecryptedNativeMsg(
plaintextBuf,
{ ...nativeMsg, value: { ...nativeMsg.value } }, // TODO revert this
'js'
)
const msgDecrypted = FeedV1.fromPlaintextBuffer(plaintextBuf, msgEncrypted)
return {
key: msg.key,
value: msgVal,
timestamp: msg.timestamp,
meta: {
id: rec.id,
msg: msgDecrypted,
received: rec.received,
misc: {
...rec.misc,
private: true,
originalContent: content,
encryptionFormat: encryptionFormat.name,
@ -41,16 +46,16 @@ function decrypt(msg, ssb, config) {
}
}
function reEncrypt(msg) {
function reEncrypt(rec) {
return {
key: msg.key,
value: { ...msg.value, content: msg.meta.originalContent },
timestamp: msg.timestamp,
...(msg.meta.size
id: rec.id,
msg: { ...rec.msg, content: rec.misc.originalContent },
received: rec.received,
...(rec.misc.size
? {
meta: {
offset: msg.meta.offset,
size: msg.meta.size,
misc: {
offset: rec.misc.offset,
size: rec.misc.size,
},
}
: null),

View File

@ -2,25 +2,54 @@ const blake3 = require('blake3')
const base58 = require('bs58')
const stringify = require('fast-json-stable-stringify')
function getMsgHashBuf(nativeMsg) {
const { metadata, signature } = nativeMsg
/**
* @typedef {import('./index').Msg} Msg
*/
/**
* @param {Msg} msg
* @returns {Buffer}
*/
function getMsgHashBuf(msg) {
const { metadata, sig } = msg
const metadataBuf = Buffer.from(stringify(metadata), 'utf8')
const sigBuf = base58.decode(signature)
return blake3
.hash(Buffer.concat([metadataBuf, sigBuf]))
.subarray(0, 16)
const sigBuf = base58.decode(sig)
return blake3.hash(Buffer.concat([metadataBuf, sigBuf])).subarray(0, 16)
}
function getMsgHash(nativeMsg) {
const msgHashBuf = getMsgHashBuf(nativeMsg)
/**
* @param {Msg | string} x
* @returns {string}
*/
function getMsgHash(x) {
if (typeof x === 'string') {
if (x.startsWith('ppppp:message/v1/')) {
const msgUri = x
const parts = msgUri.split('/')
return parts[parts.length - 1]
} else {
const msgHash = x
return msgHash
}
} else {
const msg = x
const msgHashBuf = getMsgHashBuf(msg)
return base58.encode(msgHashBuf)
}
}
function getMsgId(nativeMsg) {
const author = nativeMsg.metadata.author
const type = nativeMsg.metadata.type
const msgHash = getMsgHash(nativeMsg)
return `ssb:message/dag/${author}/${type}/${msgHash}`
/**
* @param {Msg} msg
* @returns {string}
*/
function getMsgId(msg) {
const { who, type } = msg.metadata
const msgHash = getMsgHash(msg)
if (type) {
return `ppppp:message/v1/${who}/${type}/${msgHash}`
} else {
return `ppppp:message/v1/${who}/${msgHash}`
}
}
module.exports = { getMsgId, getMsgHash }

View File

@ -5,12 +5,7 @@
const stringify = require('fast-json-stable-stringify')
const ed25519 = require('ssb-keys/sodium')
const base58 = require('bs58')
const {
stripAuthor,
stripMsgKey,
unstripMsgKey,
unstripAuthor,
} = require('./strip')
const { stripAuthor, stripMsgKey } = require('./strip')
const { getMsgId, getMsgHash } = require('./get-msg-id')
const representContent = require('./represent-content')
const {
@ -22,18 +17,32 @@ const {
validateOOOBatch,
} = require('./validation')
const name = 'dag'
const encodings = ['js']
/**
* @typedef {Object} Msg
* @property {*} content
* @property {Object} metadata
* @property {number} metadata.depth
* @property {Array<string>} metadata.prev
* @property {string} metadata.proof
* @property {number} metadata.size
* @property {string=} metadata.type
* @property {string} metadata.who
* @property {number=} metadata.when
* @property {string} sig
*/
function getFeedId(nativeMsg) {
return nativeMsg.metadata.author + nativeMsg.metadata.type
/**
* @param {Msg} msg
*/
function getFeedId(msg) {
if (msg.metadata.type) {
return `ppppp:feed/v1/${msg.metadata.who}/${msg.metadata.type}`
} else {
return `ppppp:feed/v1/${msg.metadata.who}`
}
}
function getSequence(nativeMsg) {
throw new Error('getSequence not supported for dagfeed')
}
function isNativeMsg(x) {
function isMsg(x) {
return (
typeof x === 'object' &&
!!x &&
@ -44,119 +53,92 @@ function isNativeMsg(x) {
)
}
function isAuthor(author) {
function isFeedId(author) {
if (typeof author !== 'string') return false
return author.startsWith('ssb:feed/dag/')
return author.startsWith('ppppp:feed/v1/')
}
function toPlaintextBuffer(opts) {
return Buffer.from(stringify(opts.content), 'utf8')
}
function newNativeMsg(opts) {
function calculateDepth(prev) {
let max = -1;
for (const p of prev) {
if (p.metadata.depth > max) {
max = p.metadata.depth;
}
}
return max + 1
}
function summarizePrev(prev) {
return Array.from(prev).map(getMsgHash)
}
function prevalidatePrev(prev) {
if (prev && !prev[Symbol.iterator]) {
// prettier-ignore
throw new Error('opts.prev must be an iterator, but got ' + typeof prev)
}
for (const p of prev) {
if (!p.metadata) {
throw new Error('opts.prev must contain messages, but got ' + typeof p)
}
}
}
/**
* @param {*} opts
* @returns {Msg}
*/
function create(opts) {
let err
if ((err = validateType(opts.type))) throw err
if (opts.previous && !Array.isArray(opts.previous)) {
// prettier-ignore
throw new Error('opts.previous must be an array, but got ' + typeof opts.previous)
}
prevalidatePrev(opts.prev)
const [contentHash, contentSize] = representContent(opts.content)
const nativeMsg = {
metadata: {
author: stripAuthor(opts.keys.id),
type: opts.type,
previous: (opts.previous ?? []).map(stripMsgKey),
timestamp: +opts.timestamp,
contentHash,
contentSize,
},
const [proof, size] = representContent(opts.content)
const depth = calculateDepth(opts.prev)
const msg = {
content: opts.content,
signature: '',
}
if ((err = validateContent(nativeMsg))) throw err
const metadataBuf = Buffer.from(stringify(nativeMsg.metadata), 'utf8')
// FIXME: this should allow using hmacKey
const privateKey = Buffer.from(opts.keys.private, 'base64')
const signature = ed25519.sign(privateKey, metadataBuf)
nativeMsg.signature = base58.encode(signature)
return nativeMsg
}
function fromNativeMsg(nativeMsg, encoding = 'js') {
if (encoding === 'js') {
const msgVal = {
// traditional:
previous: nativeMsg.metadata.previous.map((id) =>
unstripMsgKey(nativeMsg, id)
),
sequence: 0,
author: unstripAuthor(nativeMsg),
timestamp: nativeMsg.metadata.timestamp,
content: nativeMsg.content,
signature: nativeMsg.signature,
// unusual:
contentHash: nativeMsg.metadata.contentHash,
contentSize: nativeMsg.metadata.contentSize,
type: nativeMsg.metadata.type,
}
if (typeof msgVal.content === 'object') {
msgVal.content.type = nativeMsg.metadata.type
}
return msgVal
} else {
// prettier-ignore
throw new Error(`Feed format "${name}" does not support encoding "${encoding}"`)
}
}
function fromDecryptedNativeMsg(plaintextBuf, nativeMsg, encoding = 'js') {
if (encoding === 'js') {
const msgVal = fromNativeMsg(nativeMsg, 'js')
const content = JSON.parse(plaintextBuf.toString('utf8'))
msgVal.content = content
msgVal.content.type = nativeMsg.metadata.type
return msgVal
} else {
// prettier-ignore
throw new Error(`Feed format "${name}" does not support encoding "${encoding}"`)
}
}
function toNativeMsg(msgVal, encoding = 'js') {
if (encoding === 'js') {
return {
metadata: {
author: stripAuthor(msgVal.author),
type: msgVal.type ?? '',
previous: (msgVal.previous ?? []).map(stripMsgKey),
timestamp: msgVal.timestamp,
contentHash: msgVal.contentHash,
contentSize: msgVal.contentSize,
depth,
prev: summarizePrev(opts.prev),
proof,
size,
type: opts.type,
who: stripAuthor(opts.keys.id),
when: +opts.when,
},
content: msgVal.content,
signature: msgVal.signature,
}
} else {
// prettier-ignore
throw new Error(`Feed format "${name}" does not support encoding "${encoding}"`)
sig: '',
}
if ((err = validateContent(msg))) throw err
const privateKey = Buffer.from(opts.keys.private, 'base64')
const metadataBuf = Buffer.from(stringify(msg.metadata), 'utf8')
// TODO: when signing, what's the point of a customizable hmac?
const sigBuf = ed25519.sign(privateKey, metadataBuf)
msg.sig = base58.encode(sigBuf)
return msg
}
/**
* @param {Buffer} plaintextBuf
* @param {Msg} msg
* @returns {Msg}
*/
function fromPlaintextBuffer(plaintextBuf, msg) {
return { ...msg, content: JSON.parse(plaintextBuf.toString('utf-8')) }
}
module.exports = {
name,
encodings,
getMsgId,
getFeedId,
getSequence,
isAuthor,
isNativeMsg,
isFeedId,
isMsg,
create,
toPlaintextBuffer,
newNativeMsg,
fromNativeMsg,
fromDecryptedNativeMsg,
toNativeMsg,
fromPlaintextBuffer,
validate,
validateOOO,
validateBatch,

View File

@ -2,6 +2,10 @@ const blake3 = require('blake3')
const base58 = require('bs58')
const stringify = require('fast-json-stable-stringify')
/**
* @param {any} content
* @returns {[string, number]}
*/
function representContent(content) {
const contentBuf = Buffer.from(stringify(content), 'utf8')
const hash = base58.encode(blake3.hash(contentBuf).subarray(0, 16))

View File

@ -1,6 +1,11 @@
const { getMsgHash } = require('./get-msg-id')
function stripMsgKey(msgKey) {
if (typeof msgKey === 'object') return stripMsgKey(msgKey.key)
if (msgKey.startsWith('ssb:message/dag/')) {
if (typeof msgKey === 'object') {
if (msgKey.key) return stripMsgKey(msgKey.key)
else return getMsgHash(msgKey)
}
if (msgKey.startsWith('ppppp:message/v1/')) {
const parts = msgKey.split('/')
return parts[parts.length - 1]
} else {
@ -8,24 +13,12 @@ function stripMsgKey(msgKey) {
}
}
function unstripMsgKey(nativeMsg, msgId) {
const { author, type } = nativeMsg.metadata
return `ssb:message/dag/${author}/${type}/${msgId}`
}
function stripAuthor(id) {
const withoutPrefix = id.replace('ssb:feed/dag/', '')
const withoutPrefix = id.replace('ppppp:feed/v1/', '')
return withoutPrefix.split('/')[0]
}
function unstripAuthor(nativeMsg) {
const { author, type } = nativeMsg.metadata
return `ssb:feed/dag/${author}/${type}`
}
module.exports = {
stripMsgKey,
unstripMsgKey,
stripAuthor,
unstripAuthor,
}

View File

@ -1,132 +1,122 @@
const base58 = require('bs58')
const ed25519 = require('ssb-keys/sodium')
const stringify = require('fast-json-stable-stringify')
const { stripMsgKey } = require('./strip')
const { getMsgHash } = require('./get-msg-id')
function validateShape(nativeMsg) {
if (!nativeMsg || typeof nativeMsg !== 'object') {
return new Error('invalid message: not a dag msg')
function validateShape(msg) {
if (!msg || typeof msg !== 'object') {
return new Error('invalid message: not an object')
}
if (!nativeMsg.metadata || typeof nativeMsg.metadata !== 'object') {
if (!msg.metadata || typeof msg.metadata !== 'object') {
return new Error('invalid message: must have metadata')
}
if (typeof nativeMsg.metadata.author === 'undefined') {
return new Error('invalid message: must have metadata.author')
if (typeof msg.metadata.who === 'undefined') {
return new Error('invalid message: must have metadata.who')
}
if (typeof nativeMsg.metadata.type === 'undefined') {
return new Error('invalid message: must have metadata.sequence')
if (typeof msg.metadata.depth === 'undefined') {
return new Error('invalid message: must have metadata.depth')
}
if (typeof nativeMsg.metadata.previous === 'undefined') {
return new Error('invalid message: must have metadata.previous')
if (typeof msg.metadata.prev === 'undefined') {
return new Error('invalid message: must have metadata.prev')
}
if (typeof nativeMsg.metadata.timestamp === 'undefined') {
return new Error('invalid message: must have metadata.timestamp')
if (typeof msg.metadata.proof === 'undefined') {
return new Error('invalid message: must have metadata.proof')
}
if (typeof nativeMsg.metadata.contentHash === 'undefined') {
return new Error('invalid message: must have metadata.contentHash')
if (typeof msg.metadata.size === 'undefined') {
return new Error('invalid message: must have metadata.size')
}
if (typeof nativeMsg.metadata.contentSize === 'undefined') {
return new Error('invalid message: must have metadata.contentSize')
}
if (typeof nativeMsg.content === 'undefined') {
if (typeof msg.content === 'undefined') {
return new Error('invalid message: must have content')
}
if (typeof nativeMsg.signature === 'undefined') {
return new Error('invalid message: must have signature')
if (typeof msg.sig === 'undefined') {
return new Error('invalid message: must have sig')
}
}
function validateAuthor(nativeMsg) {
function validateWho(msg) {
try {
base58.decode(nativeMsg.metadata.author)
base58.decode(msg.metadata.who)
} catch (err) {
return new Error('invalid message: must have author as base58 string')
return new Error('invalid message: must have "who" as base58 string')
}
// FIXME: if there are prev, then `who` must match
}
function validateSignature(nativeMsg, hmacKey) {
const { signature } = nativeMsg
if (typeof signature !== 'string') {
return new Error('invalid message: must have signature as a string')
function validateSignature(msg) {
const { sig } = msg
if (typeof sig !== 'string') {
return new Error('invalid message: must have sig as a string')
}
try {
base58.decode(signature)
base58.decode(sig)
} catch (err) {
return new Error('invalid message: signature must be a base58 string')
return new Error('invalid message: sig must be a base58 string')
}
const signatureBuf = Buffer.from(base58.decode(signature))
if (signatureBuf.length !== 64) {
const sigBuf = Buffer.from(base58.decode(sig))
if (sigBuf.length !== 64) {
// prettier-ignore
return new Error('invalid message: signature should be 64 bytes but was ' + signatureBuf.length + ', on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: sig should be 64 bytes but was ' + sigBuf.length + ', on feed: ' + msg.metadata.who);
}
const publicKeyBuf = Buffer.from(base58.decode(nativeMsg.metadata.author))
const signableBuf = Buffer.from(stringify(nativeMsg.metadata), 'utf8')
const verified = ed25519.verify(publicKeyBuf, signatureBuf, signableBuf)
const publicKeyBuf = Buffer.from(base58.decode(msg.metadata.who))
const signableBuf = Buffer.from(stringify(msg.metadata), 'utf8')
const verified = ed25519.verify(publicKeyBuf, sigBuf, signableBuf)
if (!verified) {
// prettier-ignore
return new Error('invalid message: signature does not match, on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: sig does not match, on feed: ' + msg.metadata.who);
}
}
function validatePrevious(nativeMsg, existingNativeMsgs) {
if (!Array.isArray(nativeMsg.metadata.previous)) {
function validatePrev(msg, existingMsgs) {
if (!msg.metadata.prev || !msg.metadata.prev[Symbol.iterator]) {
// prettier-ignore
return new Error('invalid message: previous must be an array, on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: prev must be an iterator, on feed: ' + msg.metadata.who);
}
for (const prevId of nativeMsg.metadata.previous) {
if (typeof prevId !== 'string') {
for (const p of msg.metadata.prev) {
if (typeof p !== 'string') {
// prettier-ignore
return new Error('invalid message: previous must contain strings but found ' + prevId + ', on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: prev must contain strings but found ' + p + ', on feed: ' + msg.metadata.who);
}
if (prevId.startsWith('ssb:')) {
if (p.startsWith('ppppp:')) {
// prettier-ignore
return new Error('invalid message: previous must not contain SSB URIs, on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: prev must not contain URIs, on feed: ' + msg.metadata.who);
}
if (existingNativeMsgs instanceof Set) {
if (!existingNativeMsgs.has(prevId)) {
if (!existingMsgs.has(p)) {
// prettier-ignore
return new Error('invalid message: previous ' + prevId + ' is not a known message ID, on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: prev ' + p + ' is not locally known, on feed: ' + msg.metadata.who);
}
continue
} else {
let found = false
for (const nmsg of existingNativeMsgs) {
const existingId = nmsg.key
? stripMsgKey(nmsg.key)
: typeof nmsg === 'string'
? stripMsgKey(nmsg)
: getMsgHash(nmsg)
if (existingId === prevId) {
found = true
break
}
}
if (!found) {
const existingMsg = existingMsgs.get(p)
if (existingMsg.metadata.who !== msg.metadata.who) {
// prettier-ignore
return new Error('invalid message: previous ' + prevId + ' is not a known message ID, on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: prev ' + p + ' is not from the same who, on feed: ' + msg.metadata.who);
}
if (existingMsg.metadata.type !== msg.metadata.type) {
// prettier-ignore
return new Error('invalid message: prev ' + p + ' is not from the same type, on feed: ' + msg.metadata.who);
}
if (existingMsg.metadata.depth >= msg.metadata.depth) {
// prettier-ignore
return new Error('invalid message: depth of prev ' + p + ' is not lower, on feed: ' + msg.metadata.who);
}
}
}
function validateFirstPrevious(nativeMsg) {
if (!Array.isArray(nativeMsg.metadata.previous)) {
function validateFirstPrev(msg) {
if (!Array.isArray(msg.metadata.prev)) {
// prettier-ignore
return new Error('invalid message: previous must be an array, on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: prev must be an array, on feed: ' + msg.metadata.who);
}
if (nativeMsg.metadata.previous.length !== 0) {
if (msg.metadata.prev.length !== 0) {
// prettier-ignore
return new Error('initial message: previous must be an empty array, on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: prev of 1st msg must be an empty array, on feed: ' + msg.metadata.who);
}
}
function validateTimestamp(nativeMsg) {
if (typeof nativeMsg.metadata.timestamp !== 'number') {
function validateWhen(msg) {
if (msg.metadata.when && typeof msg.metadata.when !== 'number') {
// prettier-ignore
return new Error('initial message must have timestamp, on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: `when` is not a number, on feed: ' + msg.metadata.who);
}
}
@ -149,8 +139,10 @@ function validateType(type) {
}
}
function validateContent(nativeMsg) {
const { content } = nativeMsg
function validateContent(msg) {
// FIXME: if content exists, check it against `proof` and `size`
// FIXME: if content does not exist, do nothing
const { content } = msg
if (!content) {
return new Error('invalid message: must have content')
}
@ -159,51 +151,24 @@ function validateContent(nativeMsg) {
}
if (typeof content !== 'object' && typeof content !== 'string') {
// prettier-ignore
return new Error('invalid message: content must be an object or string, on feed: ' + nativeMsg.metadata.author);
return new Error('invalid message: content must be an object or string, on feed: ' + msg.metadata.who);
}
}
function validateHmac(hmacKey) {
if (!hmacKey) return
if (typeof hmacKey !== 'string' && !Buffer.isBuffer(hmacKey)) {
return new Error('invalid hmac key: must be a string or buffer')
}
const bytes = Buffer.isBuffer(hmacKey)
? hmacKey
: Buffer.from(hmacKey, 'base64')
// FIXME: validateDepth should be +1 of the max of prev depth
if (typeof hmacKey === 'string' && bytes.toString('base64') !== hmacKey) {
return new Error('invalid hmac')
}
if (bytes.length !== 32) {
return new Error('invalid hmac, it should have 32 bytes')
}
}
function emptyExisting(existingNativeMsgs) {
if (existingNativeMsgs instanceof Set) {
return existingNativeMsgs.size === 0
} else if (Array.isArray(existingNativeMsgs)) {
return existingNativeMsgs.length === 0
} else {
return !existingNativeMsgs
}
}
function validateSync(nativeMsg, existingNativeMsgs, hmacKey) {
function validateSync(msg, existingMsgs) {
let err
if ((err = validateShape(nativeMsg))) return err
if ((err = validateHmac(hmacKey))) return err
if ((err = validateAuthor(nativeMsg))) return err
if ((err = validateTimestamp(nativeMsg))) return err
if (emptyExisting(existingNativeMsgs)) {
if ((err = validateFirstPrevious(nativeMsg))) return err
if ((err = validateShape(msg))) return err
if ((err = validateWho(msg))) return err
if ((err = validateWhen(msg))) return err
if (msg.metadata.depth === 0) {
if ((err = validateFirstPrev(msg))) return err
} else {
if ((err = validatePrevious(nativeMsg, existingNativeMsgs))) return err
if ((err = validatePrev(msg, existingMsgs))) return err
}
if ((err = validateContent(nativeMsg))) return err
if ((err = validateSignature(nativeMsg, hmacKey))) return err
if ((err = validateContent(msg))) return err
if ((err = validateSignature(msg))) return err
}
// function validateOOOSync(nativeMsg, hmacKey) {
@ -212,16 +177,15 @@ function validateSync(nativeMsg, existingNativeMsgs, hmacKey) {
// if ((err = validateHmac(hmacKey))) return err
// if ((err = validateAuthor(nativeMsg))) return err
// if ((err = validateHash(nativeMsg))) return err
// if ((err = validateTimestamp(nativeMsg))) return err
// if ((err = validateOrder(nativeMsg))) return err
// if ((err = validateContent(nativeMsg))) return err
// if ((err = validateAsJSON(nativeMsg))) return err
// if ((err = validateSignature(nativeMsg, hmacKey))) return err
// }
function validate(nativeMsg, prevNativeMsg, hmacKey, cb) {
function validate(msg, existingMsgs, cb) {
let err
if ((err = validateSync(nativeMsg, prevNativeMsg, hmacKey))) {
if ((err = validateSync(msg, existingMsgs))) {
return cb(err)
}
cb()

View File

@ -3,80 +3,88 @@ const push = require('push-stream')
const AAOL = require('async-append-only-log')
const promisify = require('promisify-4loc')
const Obz = require('obz')
const FeedV1 = require('./feed-v1')
const { ReadyGate } = require('./utils')
const { decrypt, reEncrypt } = require('./encryption')
const { decrypt } = require('./encryption')
/**
* @typedef {import('./feed-v1').Msg} Msg
*/
/**
* @typedef {Object} RecDeleted
* @property {never} id
* @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} id
* @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.originalContent
* @property {string=} misc.encryptionFormat
*/
/**
* @typedef {RecPresent | RecDeleted} Rec
*/
exports.name = 'db'
exports.init = function initMemDB(ssb, config) {
const hmacKey = null
const msgs = []
const feedFormats = new Map()
exports.init = function initDB(peer, config) {
/** @type {Array<Rec>} */
const recs = []
const encryptionFormats = new Map()
const onMsgAdded = Obz()
const onRecordAdded = Obz()
const latestMsgPerFeed = {
_map: new Map(), // feedId => nativeMsg
preupdateFromKVT(kvtf, i) {
const feedId = kvtf.feed ?? kvtf.value.author
this._map.set(feedId, i)
},
commitAllPreupdates() {
for (const i of this._map.values()) {
if (typeof i === 'number') {
this.updateFromKVT(msgs[i])
}
const msgsPerFeed = {
_mapAll: new Map(), // who => Set<Msg>
_mapTips: new Map(), // who => Set<Msg>
_byHash: new Map(), // msgId => Msg // TODO: optimize space usage of this??
update(msg, msgId) {
const msgHash = FeedV1.getMsgHash(msgId ?? msg)
const feedId = FeedV1.getFeedId(msg)
const setAll = this._mapAll.get(feedId) ?? new Set()
const setTips = this._mapTips.get(feedId) ?? new Set()
for (const p of msg.metadata.prev) {
const prevMsg = this._byHash.get(p)
setTips.delete(prevMsg)
}
setAll.add(msg)
setTips.add(msg)
this._mapTips.set(feedId, setTips)
this._mapAll.set(feedId, setAll)
this._byHash.set(msgHash, msg)
},
updateFromKVT(kvtf) {
const feedId = kvtf.feed ?? kvtf.value.author
const feedFormat = findFeedFormatForAuthor(feedId)
if (!feedFormat) {
console.warn('No feed format installed understands ' + feedId)
return
}
const msg = reEncrypt(kvtf)
const nativeMsg = feedFormat.toNativeMsg(msg.value, 'js')
this._map.set(feedId, nativeMsg)
getAll() {
return this._byHash
},
update(feedId, nativeMsg) {
this._map.set(feedId, nativeMsg)
getTips(feedId) {
return this._mapTips.get(feedId) ?? []
},
get(feedId) {
return this._map.get(feedId) ?? null
},
has(feedId) {
return this._map.has(feedId)
},
getAsKV(feedId, feedFormat) {
const nativeMsg = this._map.get(feedId)
if (!nativeMsg) return null
const feedFormat2 = feedFormat ?? findFeedFormatForAuthor(feedId)
if (!feedFormat2) {
throw new Error('No feed format installed understands ' + feedId)
}
const key = feedFormat2.getMsgId(nativeMsg, 'js')
const value = feedFormat2.fromNativeMsg(nativeMsg, 'js')
return { key, value }
},
deleteKVT(kvtf) {
const feedId = kvtf.feed ?? kvtf.value.author
const nativeMsg = this._map.get(feedId)
if (!nativeMsg) return
const feedFormat = findFeedFormatForAuthor(feedId)
if (!feedFormat) {
console.warn('No feed format installed understands ' + feedId)
return
}
const msgId = feedFormat.getMsgId(nativeMsg, 'js')
if (msgId === kvtf.key) this._map.delete(feedId)
},
delete(feedId) {
this._map.delete(feedId)
deleteMsg(msg) {
const feedId = FeedV1.getFeedId(msg)
const msgHash = FeedV1.getMsgHash(msg)
const setAll = this._mapAll.get(feedId)
setAll.delete(msg)
const setTips = this._mapTips.get(feedId)
setTips.delete(msg)
this._byHash.delete(msgHash)
},
}
const log = AAOL(path.join(config.path, 'memdb-log.bin'), {
const log = AAOL(path.join(config.path, 'db.bin'), {
cacheSize: 1,
blockSize: 64 * 1024,
codec: {
@ -97,14 +105,14 @@ exports.init = function initMemDB(ssb, config) {
},
})
ssb.close.hook(function (fn, args) {
peer.close.hook(function (fn, args) {
log.close(() => {
fn.apply(this, args)
})
})
const scannedLog = new ReadyGate()
// setTimeout to let ssb.db.* secret-stack become available
// setTimeout to let peer.db.* secret-stack become available
setTimeout(() => {
let i = -1
log.stream({ offsets: true, values: true, sizes: true }).pipe(
@ -113,59 +121,49 @@ exports.init = function initMemDB(ssb, config) {
i += 1
if (!value) {
// deleted record
msgs.push(null)
recs.push({ misc: { offset, size, seq: i } })
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 msg = decrypt(value, ssb, config)
msg.meta ??= {}
msg.meta.offset = offset
msg.meta.size = size
msg.meta.seq = i
msgs.push(msg)
const rec = decrypt(value, peer, config)
rec.misc ??= {}
rec.misc.offset = offset
rec.misc.size = size
rec.misc.seq = i
recs.push(rec)
latestMsgPerFeed.preupdateFromKVT(msg, i)
msgsPerFeed.update(rec.msg)
},
function drainEnd(err) {
// prettier-ignore
if (err) throw new Error('Failed to initially scan the log', { cause: err });
latestMsgPerFeed.commitAllPreupdates()
scannedLog.setReady()
}
)
)
})
function logAppend(key, value, feedId, isOOO, cb) {
const kvt = {
key,
value,
timestamp: Date.now(),
function logAppend(id, msg, feedId, isOOO, cb) {
const rec = {
id,
msg,
received: Date.now(),
}
if (feedId !== value.author) kvt.feed = feedId
if (isOOO) kvt.ooo = isOOO
log.append(kvt, (err, newOffset) => {
if (isOOO) rec.ooo = isOOO
log.append(rec, (err, newOffset) => {
if (err) return cb(new Error('logAppend failed', { cause: err }))
const offset = newOffset // latestOffset
const size = Buffer.from(JSON.stringify(kvt), 'utf8').length
const seq = msgs.length
const kvtExposed = decrypt(kvt, ssb, config)
kvt.meta = kvtExposed.meta = { offset, size, seq }
msgs.push(kvtExposed)
cb(null, kvt)
const size = Buffer.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)
})
}
function installFeedFormat(feedFormat) {
if (!feedFormat.encodings.includes('js')) {
// prettier-ignore
throw new Error(`Failed to install feed format "${feedFormat.name}" because it must support JS encoding`)
}
feedFormats.set(feedFormat.name, feedFormat)
}
function installEncryptionFormat(encryptionFormat) {
if (encryptionFormat.setup) {
const loaded = new ReadyGate()
@ -179,20 +177,6 @@ exports.init = function initMemDB(ssb, config) {
encryptionFormats.set(encryptionFormat.name, encryptionFormat)
}
function findFeedFormatForAuthor(author) {
for (const feedFormat of feedFormats.values()) {
if (feedFormat.isAuthor(author)) return feedFormat
}
return null
}
function findFeedFormatForNativeMsg(nativeMsg) {
for (const feedFormat of feedFormats.values()) {
if (feedFormat.isNativeMsg(nativeMsg)) return feedFormat
}
return null
}
function findEncryptionFormatFor(ciphertextJS) {
if (!ciphertextJS) return null
if (typeof ciphertextJS !== 'string') return null
@ -201,90 +185,57 @@ exports.init = function initMemDB(ssb, config) {
return encryptionFormat
}
function add(nativeMsg, cb) {
const feedFormat = findFeedFormatForNativeMsg(nativeMsg)
if (!feedFormat) {
// prettier-ignore
return cb(new Error('add() failed because no installed feed format understands the native message'))
}
const feedId = feedFormat.getFeedId(nativeMsg)
const prevNativeMsg = latestMsgPerFeed.get(feedId)
function add(msg, cb) {
const feedId = FeedV1.getFeedId(msg)
const existingMsgs = msgsPerFeed.getAll(feedId)
if (prevNativeMsg) {
feedFormat.validate(nativeMsg, prevNativeMsg, hmacKey, validationCB)
} else {
feedFormat.validateOOO(nativeMsg, hmacKey, validationCB)
}
FeedV1.validate(msg, existingMsgs, validationCB)
function validationCB(err) {
// prettier-ignore
if (err) return cb(new Error('add() failed validation for feed format ' + feedFormat.name, {cause: err}))
const msgId = feedFormat.getMsgId(nativeMsg)
const msgVal = feedFormat.fromNativeMsg(nativeMsg)
latestMsgPerFeed.update(feedId, nativeMsg)
if (err) return cb(new Error('add() failed validation for feed format v1', {cause: err}))
const msgId = FeedV1.getMsgId(msg)
msgsPerFeed.update(msg, msgId)
logAppend(msgId, msgVal, feedId, false, (err, kvt) => {
logAppend(msgId, msg, feedId, false, logAppendCB)
}
function logAppendCB(err, rec) {
if (err) return cb(new Error('add() failed in the log', { cause: err }))
onMsgAdded.set({
kvt,
nativeMsg,
feedFormat: feedFormat.name,
})
cb(null, kvt)
})
onRecordAdded.set(rec)
cb(null, rec)
}
}
function create(opts, cb) {
const keys = opts.keys ?? config.keys
const feedFormat = feedFormats.get(opts.feedFormat)
const encryptionFormat = encryptionFormats.get(opts.encryptionFormat)
// prettier-ignore
if (!feedFormat) return cb(new Error(`create() does not support feed format "${opts.feedFormat}"`))
// prettier-ignore
if (!feedFormat.isAuthor(keys.id)) return cb(new Error(`create() failed because keys.id ${keys.id} is not a valid author for feed format "${feedFormat.name}"`))
// prettier-ignore
if (opts.content.recps) {
if (!encryptionFormat) {
return cb(new Error(`create() does not support encryption format "${opts.encryptionFormat}"`))
}
}
if (!opts.content) return cb(new Error('create() requires a `content`'))
if (!opts.type) return cb(new Error('create() requires a `type`'))
// Create full opts:
let provisionalNativeMsg
let tempMsg
try {
provisionalNativeMsg = feedFormat.newNativeMsg({
timestamp: Date.now(),
...opts,
previous: null,
keys,
})
tempMsg = FeedV1.create({ when: Date.now(), ...opts, prev: [], keys })
} catch (err) {
return cb(new Error('create() failed', { cause: err }))
}
const feedId = feedFormat.getFeedId(provisionalNativeMsg)
const previous = latestMsgPerFeed.getAsKV(feedId, feedFormat)
const fullOpts = {
timestamp: Date.now(),
...opts,
previous,
keys,
hmacKey,
}
const feedId = FeedV1.getFeedId(tempMsg)
const prev = msgsPerFeed.getTips(feedId)
const fullOpts = { when: Date.now(), ...opts, prev, keys }
// If opts ask for encryption, encrypt and put ciphertext in opts.content
const recps = fullOpts.content.recps
if (Array.isArray(recps) && recps.length > 0) {
const plaintext = feedFormat.toPlaintextBuffer(fullOpts)
const encryptOpts = {
...fullOpts,
keys,
recps,
previous: previous ? previous.key : null,
}
const plaintext = FeedV1.toPlaintextBuffer(fullOpts)
const encryptOpts = { ...fullOpts, keys, recps, prev }
let ciphertextBuf
try {
ciphertextBuf = encryptionFormat.encrypt(plaintext, encryptOpts)
@ -300,80 +251,59 @@ exports.init = function initMemDB(ssb, config) {
fullOpts.content = ciphertextBase64 + '.' + encryptionFormat.name
}
// Create the native message:
let nativeMsg
// Create the actual message:
let msg
try {
nativeMsg = feedFormat.newNativeMsg(fullOpts)
msg = FeedV1.create(fullOpts)
} catch (err) {
return cb(new Error('create() failed', { cause: err }))
}
const msgId = feedFormat.getMsgId(nativeMsg)
const msgVal = feedFormat.fromNativeMsg(nativeMsg, 'js')
latestMsgPerFeed.update(feedId, nativeMsg)
const msgId = FeedV1.getMsgId(msg)
msgsPerFeed.update(msg, msgId)
// Encode the native message and append it to the log:
logAppend(msgId, msgVal, feedId, false, (err, kvt) => {
logAppend(msgId, msg, feedId, false, (err, rec) => {
// prettier-ignore
if (err) return cb(new Error('create() failed to append the log', { cause: err }))
onMsgAdded.set({
kvt,
nativeMsg,
feedFormat: feedFormat.name,
})
cb(null, kvt)
onRecordAdded.set(rec)
cb(null, rec)
})
}
function del(msgId, cb) {
const kvt = getKVT(msgId)
latestMsgPerFeed.deleteKVT(kvt)
msgs[kvt.meta.seq] = null
const rec = getRecord(msgId)
msgsPerFeed.deleteMsg(rec.msg)
const { offset, size, seq } = rec.misc
recs[rec.misc.seq] = { misc: { offset, size, seq } }
log.onDrain(() => {
log.del(kvt.meta.offset, cb)
log.del(offset, cb)
})
}
function filterAsPullStream(fn) {
let i = 0
return function source(end, cb) {
if (end) return cb(end)
if (i >= msgs.length) return cb(true)
for (; i < msgs.length; i++) {
const msg = msgs[i]
if (msg && fn(msg, i, msgs)) {
i += 1
return cb(null, msg)
}
}
return cb(true)
function* msgs() {
for (let i = 0; i < recs.length; i++) {
const rec = recs[i]
if (rec.msg) yield rec.msg
}
}
function* filterAsIterator(fn) {
for (let i = 0; i < msgs.length; i++) {
const msg = msgs[i]
if (msg && fn(msg, i, msgs)) yield msg
function* records() {
for (let i = 0; i < recs.length; i++) {
const rec = recs[i]
if (rec) yield rec
}
}
function filterAsArray(fn) {
return msgs.filter(fn)
}
function forEach(fn) {
for (let i = 0; i < msgs.length; i++) if (msgs[i]) fn(msgs[i], i, msgs)
}
function getKVT(msgKey) {
for (let i = 0; i < msgs.length; i++) {
const msg = msgs[i]
if (msg && msg.key === msgKey) return msg
function getRecord(msgId) {
for (let i = 0; i < recs.length; i++) {
const rec = recs[i]
if (rec && rec.id === msgId) return rec
}
return null
}
function get(msgKey) {
return getKVT(msgKey)?.value
function get(msgId) {
return getRecord(msgId)?.msg
}
function loaded(cb) {
@ -383,22 +313,18 @@ exports.init = function initMemDB(ssb, config) {
return {
// public
installFeedFormat,
installEncryptionFormat,
loaded,
add,
create,
del,
onMsgAdded,
filterAsPullStream,
filterAsIterator,
filterAsArray,
forEach,
getKVT,
onRecordAdded,
msgs,
records,
getRecord,
get,
// internal
findEncryptionFormatFor,
findFeedFormatForAuthor,
}
}

View File

@ -25,7 +25,8 @@
"fast-json-stable-stringify": "^2.1.0",
"obz": "^1.1.0",
"promisify-4loc": "^1.0.0",
"push-stream": "^11.2.0"
"push-stream": "^11.2.0",
"ssb-uri2": "^2.4.1"
},
"devDependencies": {
"c8": "^7.11.0",

View File

@ -1,112 +1,35 @@
const test = require('tape')
const ssbKeys = require('ssb-keys')
const path = require('path')
const os = require('os')
const rimraf = require('rimraf')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const classic = require('ssb-classic/format')
const FeedV1 = require('../lib/feed-v1')
const p = require('util').promisify
const { generateKeypair } = require('./util')
const DIR = path.join(os.tmpdir(), 'ppppp-db-add')
rimraf.sync(DIR)
test('add() classic', async (t) => {
const ssb = SecretStack({ appKey: caps.shs })
test('add()', async (t) => {
const keys = generateKeypair('alice')
const peer = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-classic'))
.use(require('ssb-box'))
.call(null, {
keys: ssbKeys.generate('ed25519', 'alice'),
path: DIR,
.call(null, { keys, path: DIR })
await peer.db.loaded()
const inputMsg = FeedV1.create({
keys,
when: 1514517067954,
type: 'post',
content: { text: 'This is the first post!' },
prev: [],
})
await ssb.db.loaded()
const rec = await p(peer.db.add)(inputMsg)
t.equal(rec.msg.content.text, 'This is the first post!')
const nativeMsg = classic.toNativeMsg(
{
previous: null,
author: '@FCX/tsDLpubCPKKfIrw4gc+SQkHcaD17s7GI6i/ziWY=.ed25519',
sequence: 1,
timestamp: 1514517067954,
hash: 'sha256',
content: {
type: 'post',
text: 'This is the first post!',
},
signature:
'QYOR/zU9dxE1aKBaxc3C0DJ4gRyZtlMfPLt+CGJcY73sv5abKKKxr1SqhOvnm8TY784VHE8kZHCD8RdzFl1tBA==.sig.ed25519',
},
'js'
)
const msg = await p(ssb.db.add)(nativeMsg)
t.equal(msg.value.content.text, 'This is the first post!')
await p(ssb.close)(true)
})
test('add() some classic message starting from non-first', async (t) => {
const ssb = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-classic'))
.use(require('ssb-box'))
.call(null, {
keys: ssbKeys.generate('ed25519', 'alice'),
path: DIR,
})
await ssb.db.loaded()
const nativeMsg1 = classic.toNativeMsg({
previous: '%6jh0kDakv0EIu5v9QwDhz9Lz2jEVRTCwyh5sWWzSvSo=.sha256',
sequence: 1711,
author: '@qeVe7SSpEZxL2Q0sE2jX+TXtMuAgcS889oBZYFDc5WU=.ed25519',
timestamp: 1457240385000,
hash: 'sha256',
content: {
type: 'post',
text: 'Nulla ullamco laboris proident eu sint cillum. Est proident veniam deserunt quis enim sint reprehenderit voluptate consectetur adipisicing.',
root: '%uH8IpYmw6uV1M4uhezcHq1v0xyeJ8J8bQqR/FVm0csM=.sha256',
branch: '%SiM9aUnQSk01m0EStBHXD4HLf773OJm998IReSLO1So=.sha256',
mentions: [
{
link: '&bGFib3J1bWRvbG9yYWxpcXVhY29tbW9kb2N1bHBhcGE=.sha256',
type: 'image/jpeg',
size: 1367352,
name: 'commodo cillum',
},
{
link: '@zRr3265aLU/T1/DfB8+Rm+IPDZJnuuRgfurOztIYBi4=.ed25519',
name: 'laborum aliquip',
},
],
},
signature:
'ypQ+4ubHo/zcUakMzN4dHqd9qmx06VEADAZPjK0OXbseaEg9s0AWccKgn+WFI0XSO1y7TIphFOA6Dyn6kDzXAg==.sig.ed25519',
})
const nativeMsg2 = classic.toNativeMsg({
previous: '%l8drxQMuxpOjUb3RK9rGJl6oPKF4QPHchGvRyqL+IZ4=.sha256',
sequence: 1712,
author: '@qeVe7SSpEZxL2Q0sE2jX+TXtMuAgcS889oBZYFDc5WU=.ed25519',
timestamp: 1457253345000,
hash: 'sha256',
content: {
type: 'post',
text: 'Commodo duis eiusmod est tempor eu fugiat commodo sint excepteur non est mollit est exercitation. Sit velit eu quis aute reprehenderit id sit labore quis mollit fugiat magna. Proident eu et proident duis labore irure laboris dolor. Cupidatat aute occaecat proident ut cillum sunt ullamco laborum labore cillum eu ut excepteur laborum aliqua. Magna adipisicing in occaecat adipisicing duis mollit esse. Reprehenderit excepteur labore excepteur qui elit labore velit officia non consectetur id labore ullamco excepteur. Laborum cillum anim ex irure ex proident consequat aute ipsum quis id esse. Exercitation mollit deserunt labore ut eu ea eu consectetur ullamco ex.\nEiusmod qui in proident irure consequat enim duis elit culpa minim dolore nisi aute. Qui anim Lorem consectetur ad do dolore laborum enim aute ex velit eu dolor et incididunt. Nisi nulla aliquip anim irure proident deserunt nostrud in anim elit veniam exercitation aliquip sint. Culpa excepteur sit et eu quis reprehenderit sunt. Id velit reprehenderit nostrud incididunt dolore sint consequat officia pariatur dolore ipsum. Nisi incididunt tempor voluptate fugiat esse. Amet ut elit eu nulla adipisicing non veniam nulla ut culpa.\nDolor adipisicing anim id anim eiusmod laboris aliquip. Anim sint deserunt exercitation nostrud adipisicing amet enim adipisicing Lorem voluptate anim. Sunt pariatur cupidatat culpa dolore ullamco anim. Minim laborum excepteur commodo et aliqua duis reprehenderit exercitation.',
root: '%0AwZP5C5aFwzCV5OCxG/2D6Qx70N6ZVIoZ0ZgIu0pPw=.sha256',
branch: '%oZF1M4cKj6t2LHloUiegWD1qZ2IIvcLvOPIiVHbQudI=.sha256',
},
signature:
'uWYwWtG2zTmdfpaSTmOghW3QsNCgYNGh5d3VKOFtp2MNQopSCAxjDDER/yfj3k8Bu+NKEnAy5eJ2ylWuxeuEDQ==.sig.ed25519',
})
const msg1 = await p(ssb.db.add)(nativeMsg1)
t.equal(msg1.value.sequence, 1711)
const msg2 = await p(ssb.db.add)(nativeMsg2)
t.equal(msg2.value.sequence, 1712)
await p(ssb.close)(true)
await p(peer.close)(true)
})

View File

@ -1,57 +1,92 @@
const test = require('tape')
const ssbKeys = require('ssb-keys')
const path = require('path')
const os = require('os')
const rimraf = require('rimraf')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const p = require('util').promisify
const FeedV1 = require('../lib/feed-v1')
const { generateKeypair } = require('./util')
const DIR = path.join(os.tmpdir(), 'ppppp-db-create');
const DIR = path.join(os.tmpdir(), 'ppppp-db-create')
rimraf.sync(DIR)
let ssb
const keys = generateKeypair('alice')
let peer
test('setup', async (t) => {
ssb = SecretStack({ appKey: caps.shs })
peer = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-classic'))
.use(require('ssb-box'))
.call(null, {
keys: ssbKeys.generate('ed25519', 'alice'),
path: DIR,
})
.call(null, { keys, path: DIR })
await ssb.db.loaded()
await peer.db.loaded()
})
test('create() classic', async (t) => {
const msg1 = await p(ssb.db.create)({
feedFormat: 'classic',
content: { type: 'post', text: 'I am hungry' },
let msgHash1
let rec1
let msgHash2
test('create()', async (t) => {
rec1 = await p(peer.db.create)({
type: 'post',
content: { text: 'I am 1st post' },
})
t.equal(msg1.value.content.text, 'I am hungry', 'msg1 text correct')
t.equal(rec1.msg.content.text, 'I am 1st post', 'msg1 text correct')
msgHash1 = FeedV1.getMsgHash(rec1.msg)
const msg2 = await p(ssb.db.create)({
content: { type: 'post', text: 'I am hungry 2' },
feedFormat: 'classic',
const rec2 = await p(peer.db.create)({
type: 'post',
content: { text: 'I am 2nd post' },
})
t.equal(msg2.value.content.text, 'I am hungry 2', 'msg2 text correct')
t.equal(msg2.value.previous, msg1.key, 'msg2 previous correct')
t.equal(rec2.msg.content.text, 'I am 2nd post', 'msg2 text correct')
t.deepEquals(rec2.msg.metadata.prev, [msgHash1], 'msg2 prev correct')
msgHash2 = FeedV1.getMsgHash(rec2.msg)
})
test('create() classic box', async (t) => {
const msgBoxed = await p(ssb.db.create)({
feedFormat: 'classic',
content: { type: 'post', text: 'I am chewing food', recps: [ssb.id] },
test('add() forked then create() merged', async (t) => {
const msg3 = FeedV1.create({
keys,
when: Date.now(),
type: 'post',
content: { text: '3rd post forked from 1st' },
prev: [rec1.msg],
})
const rec3 = await p(peer.db.add)(msg3)
const msgHash3 = FeedV1.getMsgHash(rec3.msg)
const rec4 = await p(peer.db.create)({
type: 'post',
content: { text: 'I am 4th post' },
})
t.ok(rec4, '4th post created')
t.deepEquals(
rec4.msg.metadata.prev,
[msgHash2, msgHash3],
'msg4 prev is msg2 and msg3'
)
const msgHash4 = FeedV1.getMsgHash(rec4.msg)
const rec5 = await p(peer.db.create)({
type: 'post',
content: { text: 'I am 5th post' },
})
t.ok(rec5, '5th post created')
t.deepEquals(rec5.msg.metadata.prev, [msgHash4], 'msg5 prev is msg4')
})
test('create() encrypted with box', async (t) => {
const recEncrypted = await p(peer.db.create)({
type: 'post',
content: { text: 'I am chewing food', recps: [peer.id] },
encryptionFormat: 'box',
})
t.equal(typeof msgBoxed.value.content, 'string')
t.true(msgBoxed.value.content.endsWith('.box'), '.box')
t.equal(typeof recEncrypted.msg.content, 'string')
t.true(recEncrypted.msg.content.endsWith('.box'), '.box')
const msgVal = ssb.db.get(msgBoxed.key)
t.equals(msgVal.content.text, 'I am chewing food')
const msgDecrypted = peer.db.get(recEncrypted.id)
t.equals(msgDecrypted.content.text, 'I am chewing food')
})
test('teardown', (t) => {
ssb.close(t.end)
peer.close(t.end)
})

View File

@ -1,5 +1,4 @@
const test = require('tape')
const ssbKeys = require('ssb-keys')
const path = require('path')
const os = require('os')
const rimraf = require('rimraf')
@ -8,47 +7,47 @@ const AAOL = require('async-append-only-log')
const push = require('push-stream')
const caps = require('ssb-caps')
const p = require('util').promisify
const { generateKeypair } = require('./util')
const DIR = path.join(os.tmpdir(), 'ppppp-db-del')
rimraf.sync(DIR)
test('del', async (t) => {
const ssb = SecretStack({ appKey: caps.shs })
const keys = generateKeypair('alice')
const peer = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-classic'))
.call(null, {
keys: ssbKeys.generate('ed25519', 'alice'),
path: DIR,
})
.call(null, { keys, path: DIR })
await ssb.db.loaded()
await peer.db.loaded()
const msgIDs = []
for (let i = 0; i < 5; i++) {
const msg = await p(ssb.db.create)({
feedFormat: 'classic',
content: { type: 'post', text: 'm' + i },
const rec = await p(peer.db.create)({
type: 'post',
content: { text: 'm' + i },
})
msgIDs.push(msg.key)
msgIDs.push(rec.id)
}
const before = ssb.db
.filterAsArray(() => true)
.map((msg) => msg.value.content.text)
const before = []
for (const msg of peer.db.msgs()) {
before.push(msg.content.text)
}
t.deepEqual(before, ['m0', 'm1', 'm2', 'm3', 'm4'], 'msgs before the delete')
await p(ssb.db.del)(msgIDs[2])
await p(peer.db.del)(msgIDs[2])
const after = ssb.db
.filterAsArray(() => true)
.map((msg) => msg?.value.content.text ?? null)
const after = []
for (const msg of peer.db.msgs()) {
after.push(msg.content.text)
}
t.deepEqual(after, ['m0', 'm1', null, 'm3', 'm4'], 'msgs after the delete')
t.deepEqual(after, ['m0', 'm1', 'm3', 'm4'], 'msgs after the delete')
await p(ssb.close)(true)
await p(peer.close)(true)
const log = AAOL(path.join(DIR, 'memdb-log.bin'), {
const log = AAOL(path.join(DIR, 'db.bin'), {
cacheSize: 1,
blockSize: 64 * 1024,
codec: {
@ -66,10 +65,8 @@ test('del', async (t) => {
log.stream({ offsets: true, values: true, sizes: true }).pipe(
push.drain(
function drainEach({ offset, value, size }) {
if (!value) {
persistedMsgs.push(null)
} else {
persistedMsgs.push(value)
if (value) {
persistedMsgs.push(value.msg)
}
},
function drainEnd(err) {
@ -81,8 +78,8 @@ test('del', async (t) => {
})
t.deepEqual(
persistedMsgs.map((msg) => msg?.value.content.text ?? null),
['m0', 'm1', null, 'm3', 'm4'],
persistedMsgs.map((msg) => msg.content.text),
['m0', 'm1', 'm3', 'm4'],
'msgs in disk after the delete'
)
})

View File

@ -1,91 +1,82 @@
const tape = require('tape')
const dagfeed = require('../lib/feed-v1')
const FeedV1 = require('../lib/feed-v1')
const { generateKeypair } = require('./util')
tape('encode/decode works', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const content = { text: 'Hello world!' }
const timestamp = 1652037377204
const when = 1652037377204
const nmsg1 = dagfeed.newNativeMsg({
const msg1 = FeedV1.create({
keys,
content,
type: 'post',
previous: [],
timestamp,
hmacKey,
prev: [],
when,
})
t.deepEquals(
Object.keys(msg1.metadata),
['depth', 'prev', 'proof', 'size', 'type', 'who', 'when'],
'metadata fields'
)
t.equals(
nmsg1.metadata.author,
msg1.metadata.who,
'4mjQ5aJu378cEu6TksRG3uXAiKFiwGjYQtWAjfVjDAJW',
'metadata.author is correct'
'metadata.who'
)
t.equals(nmsg1.metadata.type, 'post', 'metadata.type is correct')
t.deepEquals(nmsg1.metadata.previous, [], 'metadata.previous is correct')
console.log(nmsg1)
t.equals(msg1.metadata.type, 'post', 'metadata.type')
t.equals(msg1.metadata.depth, 0, 'metadata.depth')
t.deepEquals(msg1.metadata.prev, [], 'metadata.prev')
t.deepEquals(msg1.metadata.proof, '9R7XmBhHF5ooPg34j9TQcz', 'metadata.proof')
t.deepEquals(msg1.metadata.size, 23, 'metadata.size')
t.equals(typeof msg1.metadata.when, 'number', 'metadata.when')
t.deepEqual(msg1.content, content, 'content is correct')
const jsonMsg = {
key: dagfeed.getMsgId(nmsg1),
value: dagfeed.fromNativeMsg(nmsg1),
timestamp: Date.now(),
}
console.log(msg1)
const msgHash1 = 'HEzse89DSDWUXVPyav35GC'
const msgKey1 =
'ssb:message/dag/4mjQ5aJu378cEu6TksRG3uXAiKFiwGjYQtWAjfVjDAJW/post/' +
msgHash1
const msgHash = '9cYegpVpddoMSdvSf53dTH'
t.deepEqual(jsonMsg.key, msgKey1, 'key is correct')
t.deepEqual(
jsonMsg.value.author,
'ssb:feed/dag/4mjQ5aJu378cEu6TksRG3uXAiKFiwGjYQtWAjfVjDAJW/post',
'author is correct'
t.equals(
FeedV1.getMsgId(msg1),
'ppppp:message/v1/4mjQ5aJu378cEu6TksRG3uXAiKFiwGjYQtWAjfVjDAJW/post/' +
msgHash,
'getMsgId'
)
t.deepEqual(jsonMsg.value.type, 'post', 'correct type')
t.equals(typeof jsonMsg.value.timestamp, 'number', 'has timestamp')
t.deepEqual(jsonMsg.value.previous, [], 'correct previous')
t.deepEqual(jsonMsg.value.content, content, 'content is the same')
const reconstructedNMsg1 = dagfeed.toNativeMsg(jsonMsg.value)
t.deepEqual(reconstructedNMsg1, nmsg1, 'can reconstruct')
const content2 = { text: 'Ola mundo!' }
const content2 = { text: 'Hello butty world!' }
const nmsg2 = dagfeed.newNativeMsg({
const msg2 = FeedV1.create({
keys,
content: content2,
type: 'post',
previous: [msgHash1],
timestamp: timestamp + 1,
hmacKey,
prev: [msg1],
when: when + 1,
})
console.log(nmsg2)
t.deepEquals(
Object.keys(msg2.metadata),
['depth', 'prev', 'proof', 'size', 'type', 'who', 'when'],
'metadata keys'
)
t.equals(
msg2.metadata.who,
'4mjQ5aJu378cEu6TksRG3uXAiKFiwGjYQtWAjfVjDAJW',
'metadata.who'
)
t.equals(msg2.metadata.type, 'post', 'metadata.type')
t.equals(msg2.metadata.depth, 1, 'metadata.depth')
t.deepEquals(msg2.metadata.prev, [msgHash], 'metadata.prev')
t.deepEquals(msg2.metadata.proof, 'XuZEzH1Dhy1yuRMcviBBcN', 'metadata.proof')
t.deepEquals(msg2.metadata.size, 21, 'metadata.size')
t.equals(typeof msg2.metadata.when, 'number', 'metadata.when')
t.deepEqual(msg2.content, content2, 'content is correct')
const jsonMsg2 = {
key: dagfeed.getMsgId(nmsg2),
value: dagfeed.fromNativeMsg(nmsg2),
timestamp: Date.now(),
}
console.log(msg2)
t.deepEqual(
jsonMsg2.key,
'ssb:message/dag/4mjQ5aJu378cEu6TksRG3uXAiKFiwGjYQtWAjfVjDAJW/post/U5n4v1m7gFzrtrdK84gGsV',
'key is correct'
FeedV1.getMsgId(msg2),
'ppppp:message/v1/4mjQ5aJu378cEu6TksRG3uXAiKFiwGjYQtWAjfVjDAJW/post/LEH1JVENvJgSpBBrVUwJx6',
'getMsgId'
)
t.deepEqual(
jsonMsg2.value.author,
'ssb:feed/dag/4mjQ5aJu378cEu6TksRG3uXAiKFiwGjYQtWAjfVjDAJW/post',
'author is correct'
)
t.deepEqual(jsonMsg2.value.type, 'post', 'correct type')
t.equals(typeof jsonMsg2.value.timestamp, 'number', 'has timestamp')
t.deepEqual(jsonMsg2.value.previous, [msgKey1], 'correct previous')
t.deepEqual(jsonMsg2.value.content, content2, 'content is the same')
// test slow version as well
const reconstructedNMsg2 = dagfeed.toNativeMsg(jsonMsg2.value)
t.deepEqual(reconstructedNMsg2, nmsg2, 'can reconstruct')
t.end()
})

View File

@ -0,0 +1,224 @@
const tape = require('tape')
const base58 = require('bs58')
const FeedV1 = require('../lib/feed-v1')
const { generateKeypair } = require('./util')
tape('invalid 1st msg with non-empty prev', (t) => {
const keys = generateKeypair('alice')
const msg = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [{ metadata: { depth: 10 }, sig: 'fake' }],
when: 1652030001000,
})
FeedV1.validate(msg, new Map(), (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/prev .+ is not locally known/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid 1st msg with non-array prev', (t) => {
const keys = generateKeypair('alice')
const msg = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [],
when: 1652030001000,
})
msg.metadata.prev = null
FeedV1.validate(msg, new Map(), (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(err.message, /prev must be an array/, 'invalid 2nd msg description')
t.end()
})
})
tape('invalid msg with non-array prev', (t) => {
const keys = generateKeypair('alice')
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [],
when: 1652030001000,
})
const msgHash1 = FeedV1.getMsgHash(msg1)
const msg2 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [{ metadata: { depth: 10 }, sig: 'fake' }],
when: 1652030002000,
})
msg2.metadata.prev = null
const existing = new Map()
existing.set(msgHash1, msg1)
FeedV1.validate(msg2, existing, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/prev must be an iterator/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid msg with bad prev', (t) => {
const keys = generateKeypair('alice')
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [],
when: 1652030001000,
})
const msgHash1 = FeedV1.getMsgHash(msg1)
const msg2 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [{ metadata: { depth: 10 }, sig: 'fake' }],
when: 1652030002000,
})
msg2.metadata.prev = [1234]
const existing = new Map()
existing.set(msgHash1, msg1)
FeedV1.validate(msg2, existing, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/prev must contain strings/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid msg with URI in prev', (t) => {
const keys = generateKeypair('alice')
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [],
when: 1652030001000,
})
const msgHash1 = FeedV1.getMsgHash(msg1)
const msg2 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [{ metadata: { depth: 10 }, sig: 'fake' }],
when: 1652030002000,
})
const randBuf = Buffer.alloc(16).fill(16)
const fakeMsgKey1 = `ppppp:message/v1/${base58.encode(randBuf)}`
msg2.metadata.prev = [fakeMsgKey1]
const existing = new Map()
existing.set(msgHash1, msg1)
FeedV1.validate(msg2, existing, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/prev must not contain URIs/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid msg with unknown prev', (t) => {
const keys = generateKeypair('alice')
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [],
when: 1652030001000,
})
const msgHash1 = FeedV1.getMsgHash(msg1)
const unknownMsg = FeedV1.create({
keys,
content: { text: 'Alien' },
type: 'post',
prev: [],
when: 1652030001000,
})
const msg2 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [unknownMsg],
when: 1652030002000,
})
const existing = new Map()
existing.set(msgHash1, msg1)
FeedV1.validate(msg2, existing, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/prev .+ is not locally known/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid msg with unknown prev in a Set', (t) => {
const keys = generateKeypair('alice')
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [],
when: 1652030001000,
})
const msg2 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
prev: [{ metadata: { depth: 10 }, sig: 'fake' }],
when: 1652030002000,
})
const fakeMsgKey1 = base58.encode(Buffer.alloc(16).fill(42))
msg2.metadata.prev = [fakeMsgKey1]
const existing = new Set([msg1])
FeedV1.validate(msg2, existing, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/prev .+ is not locally known/,
'invalid 2nd msg description'
)
t.end()
})
})

View File

@ -1,241 +0,0 @@
const tape = require('tape')
const base58 = require('bs58')
const dagfeed = require('../lib/feed-v1')
const { generateKeypair } = require('./util')
tape('invalid 1st msg with non-empty previous', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const fakeMsgKey0 = base58.encode(Buffer.alloc(16).fill(42))
const nmsg1 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [fakeMsgKey0],
timestamp: 1652030001000,
hmacKey,
})
dagfeed.validate(nmsg1, [], null, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/previous must be an empty array/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid 1st msg with non-array previous', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
})
nmsg1.metadata.previous = null
dagfeed.validate(nmsg1, [], null, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/previous must be an array/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid msg with non-array previous', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
})
const fakeMsgKey1 = `ssb:message/dag/${base58.encode(
Buffer.alloc(16).fill(42)
)}`
const nmsg2 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [fakeMsgKey1],
timestamp: 1652030002000,
hmacKey,
})
nmsg2.metadata.previous = null
dagfeed.validate(nmsg2, [nmsg1], null, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/previous must be an array/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid msg with bad previous', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
})
const fakeMsgKey1 = `ssb:message/dag/${base58.encode(
Buffer.alloc(16).fill(42)
)}`
const nmsg2 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [fakeMsgKey1],
timestamp: 1652030002000,
hmacKey,
})
nmsg2.metadata.previous = [1234]
dagfeed.validate(nmsg2, [nmsg1], null, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/previous must contain strings/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid msg with SSB URI previous', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
})
const fakeMsgKey1 = `ssb:message/dag/${base58.encode(
Buffer.alloc(16).fill(42)
)}`
const nmsg2 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [fakeMsgKey1],
timestamp: 1652030002000,
hmacKey,
})
nmsg2.metadata.previous = [fakeMsgKey1]
dagfeed.validate(nmsg2, [nmsg1], null, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/previous must not contain SSB URIs/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid msg with unknown previous', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
})
const fakeMsgKey1 = base58.encode(Buffer.alloc(16).fill(42))
const nmsg2 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [fakeMsgKey1],
timestamp: 1652030002000,
hmacKey,
})
dagfeed.validate(nmsg2, [nmsg1], null, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/previous .+ is not a known message ID/,
'invalid 2nd msg description'
)
t.end()
})
})
tape('invalid msg with unknown previous in a Set', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
})
const fakeMsgKey1 = base58.encode(Buffer.alloc(16).fill(42))
const nmsg2 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [fakeMsgKey1],
timestamp: 1652030002000,
hmacKey,
})
const existing = new Set([nmsg1])
dagfeed.validate(nmsg2, existing, null, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/previous .+ is not a known message ID/,
'invalid 2nd msg description'
)
t.end()
})
})

View File

@ -1,20 +1,18 @@
const tape = require('tape')
const dagfeed = require('../lib/feed-v1')
const FeedV1 = require('../lib/feed-v1')
const { generateKeypair } = require('./util')
tape('invalid type not a string', function (t) {
tape('invalid type not a string', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
t.throws(
() => {
dagfeed.newNativeMsg({
FeedV1.create({
keys,
content: { text: 'Hello world!' },
timestamp: 1652037377204,
when: 1652037377204,
type: 123,
previous: [],
hmacKey,
prev: [],
})
},
/type is not a string/,
@ -23,19 +21,17 @@ tape('invalid type not a string', function (t) {
t.end()
})
tape('invalid type with "/" character', function (t) {
tape('invalid type with "/" character', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
t.throws(
() => {
dagfeed.newNativeMsg({
FeedV1.create({
keys,
content: { text: 'Hello world!' },
timestamp: 1652037377204,
when: 1652037377204,
type: 'group/init',
previous: [],
hmacKey,
prev: [],
})
},
/invalid type/,
@ -44,19 +40,17 @@ tape('invalid type with "/" character', function (t) {
t.end()
})
tape('invalid type with "*" character', function (t) {
tape('invalid type with "*" character', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
t.throws(
() => {
dagfeed.newNativeMsg({
FeedV1.create({
keys,
content: { text: 'Hello world!' },
timestamp: 1652037377204,
when: 1652037377204,
type: 'star*',
previous: [],
hmacKey,
prev: [],
})
},
/invalid type/,
@ -65,19 +59,17 @@ tape('invalid type with "*" character', function (t) {
t.end()
})
tape('invalid type too short', function (t) {
tape('invalid type too short', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
t.throws(
() => {
dagfeed.newNativeMsg({
FeedV1.create({
keys,
content: { text: 'Hello world!' },
timestamp: 1652037377204,
when: 1652037377204,
type: 'xy',
previous: [],
hmacKey,
prev: [],
})
},
/shorter than 3/,
@ -86,19 +78,17 @@ tape('invalid type too short', function (t) {
t.end()
})
tape('invalid type too long', function (t) {
tape('invalid type too long', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
t.throws(
() => {
dagfeed.newNativeMsg({
FeedV1.create({
keys,
content: { text: 'Hello world!' },
timestamp: 1652037377204,
when: 1652037377204,
type: 'a'.repeat(120),
previous: [],
hmacKey,
prev: [],
})
},
/100\+ characters long/,

View File

@ -1,22 +1,20 @@
const tape = require('tape')
const base58 = require('bs58')
const dagfeed = require('../lib/feed-v1')
const FeedV1 = require('../lib/feed-v1')
const { generateKeypair } = require('./util')
tape('validate 1st msg', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
const msg = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
prev: [],
when: 1652030001000,
})
dagfeed.validate(nmsg1, null, null, (err) => {
FeedV1.validate(msg, [], (err) => {
if (err) console.log(err)
t.error(err, 'valid 1st msg')
t.end()
@ -25,28 +23,27 @@ tape('validate 1st msg', (t) => {
tape('validate 2nd msg with existing nativeMsg', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
prev: [],
when: 1652030001000,
})
const msgKey1 = dagfeed.getMsgId(nmsg1)
const msgHash1 = FeedV1.getMsgHash(msg1)
const nmsg2 = dagfeed.newNativeMsg({
const msg2 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [msgKey1],
timestamp: 1652030002000,
hmacKey,
prev: [msg1],
when: 1652030002000,
})
dagfeed.validate(nmsg2, [nmsg1], null, (err) => {
const existing = new Map()
existing.set(msgHash1, msg1)
FeedV1.validate(msg2, existing, (err) => {
if (err) console.log(err)
t.error(err, 'valid 2nd msg')
t.end()
@ -55,60 +52,28 @@ tape('validate 2nd msg with existing nativeMsg', (t) => {
tape('validate 2nd msg with existing msgId', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
prev: [],
when: 1652030001000,
})
const msgKey1 = dagfeed.getMsgId(nmsg1)
const msgKey1 = FeedV1.getMsgId(msg1)
const msgHash1 = FeedV1.getMsgHash(msg1)
const nmsg2 = dagfeed.newNativeMsg({
const msg2 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [msgKey1],
timestamp: 1652030002000,
hmacKey,
prev: [msg1],
when: 1652030002000,
})
dagfeed.validate(nmsg2, [msgKey1], null, (err) => {
if (err) console.log(err)
t.error(err, 'valid 2nd msg')
t.end()
})
})
tape('validate 2nd msg with existing msgId in a Set', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
})
const msgId1 = dagfeed.getMsgHash(nmsg1)
const nmsg2 = dagfeed.newNativeMsg({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [msgId1],
timestamp: 1652030002000,
hmacKey,
})
const existing = new Set([msgId1])
dagfeed.validate(nmsg2, existing, null, (err) => {
const existing = new Map()
existing.set(msgHash1, msg1)
FeedV1.validate(msg2, existing, (err) => {
if (err) console.log(err)
t.error(err, 'valid 2nd msg')
t.end()
@ -117,32 +82,27 @@ tape('validate 2nd msg with existing msgId in a Set', (t) => {
tape('validate 2nd msg with existing KVT', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
prev: [],
when: 1652030001000,
})
const kvt1 = {
key: dagfeed.getMsgId(nmsg1),
value: dagfeed.fromNativeMsg(nmsg1),
timestamp: Date.now(),
}
const msgHash1 = FeedV1.getMsgHash(msg1)
const nmsg2 = dagfeed.newNativeMsg({
const msg2 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [kvt1.key],
timestamp: 1652030002000,
hmacKey,
prev: [msg1],
when: 1652030002000,
})
dagfeed.validate(nmsg2, [kvt1], null, (err) => {
const existing = new Map()
existing.set(msgHash1, msg1)
FeedV1.validate(msg2,existing, (err) => {
if (err) console.log(err)
t.error(err, 'valid 2nd msg')
t.end()
@ -151,37 +111,37 @@ tape('validate 2nd msg with existing KVT', (t) => {
tape('validate 2nd forked msg', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
prev: [],
when: 1652030001000,
})
const msgKey1 = dagfeed.getMsgId(nmsg1)
const msgHash1 = FeedV1.getMsgHash(msg1)
const nmsg2A = dagfeed.newNativeMsg({
const msg2A = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [msgKey1],
timestamp: 1652030002000,
hmacKey,
prev: [msg1],
when: 1652030002000,
})
const msgHash2A = FeedV1.getMsgHash(msg2A)
const nmsg2B = dagfeed.newNativeMsg({
const msg2B = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [msgKey1],
timestamp: 1652030003000,
hmacKey,
prev: [msg1],
when: 1652030003000,
})
dagfeed.validate(nmsg2B, [nmsg1, nmsg2A], null, (err) => {
const existing = new Map()
existing.set(msgHash1, msg1)
existing.set(msgHash2A, msg2A)
FeedV1.validate(msg2B, existing, (err) => {
if (err) console.log(err)
t.error(err, 'valid 2nd forked msg')
t.end()
@ -190,33 +150,34 @@ tape('validate 2nd forked msg', (t) => {
tape('invalid msg with unknown previous', (t) => {
const keys = generateKeypair('alice')
const hmacKey = null
const nmsg1 = dagfeed.newNativeMsg({
const msg1 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [],
timestamp: 1652030001000,
hmacKey,
prev: [],
when: 1652030001000,
})
const msgHash1 = FeedV1.getMsgHash(msg1)
const fakeMsgKey1 = base58.encode(Buffer.alloc(16).fill(42))
const nmsg2 = dagfeed.newNativeMsg({
const msg2 = FeedV1.create({
keys,
content: { text: 'Hello world!' },
type: 'post',
previous: [fakeMsgKey1],
timestamp: 1652030002000,
hmacKey,
prev: [msg1],
when: 1652030002000,
})
msg2.metadata.prev = [fakeMsgKey1]
dagfeed.validate(nmsg2, [nmsg1], null, (err) => {
const existing = new Map()
existing.set(msgHash1, msg1)
FeedV1.validate(msg2, existing, (err) => {
t.ok(err, 'invalid 2nd msg throws')
t.match(
err.message,
/previous .+ is not a known message ID/,
/prev .+ is not locally known/,
'invalid 2nd msg description'
)
t.end()

View File

@ -1,45 +0,0 @@
const test = require('tape')
const ssbKeys = require('ssb-keys')
const path = require('path')
const os = require('os')
const rimraf = require('rimraf')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const p = require('util').promisify
const DIR = path.join(os.tmpdir(), 'ppppp-db-filter-as-array')
rimraf.sync(DIR)
test('filterAsArray', async (t) => {
const ssb = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-classic'))
.call(null, {
keys: ssbKeys.generate('ed25519', 'alice'),
path: DIR,
})
await ssb.db.loaded()
for (let i = 0; i < 10; i++) {
await p(ssb.db.create)({
feedFormat: 'classic',
content:
i % 2 === 0
? { type: 'post', text: 'hello ' + i }
: { type: 'about', about: ssb.id, name: 'Mr. #' + i },
})
}
const results = ssb.db
.filterAsArray((msg) => msg.value.content.type === 'post')
.map((msg) => msg.value.content.text)
t.deepEqual(
results,
['hello 0', 'hello 2', 'hello 4', 'hello 6', 'hello 8'],
'queried posts'
)
await p(ssb.close)(true)
})

View File

@ -1,49 +0,0 @@
const test = require('tape')
const ssbKeys = require('ssb-keys')
const path = require('path')
const os = require('os')
const rimraf = require('rimraf')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const p = require('util').promisify
const DIR = path.join(os.tmpdir(), 'ppppp-db-filter-as-iterator')
rimraf.sync(DIR)
test('filterAsIterator', async (t) => {
const ssb = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-classic'))
.call(null, {
keys: ssbKeys.generate('ed25519', 'alice'),
path: DIR,
})
await ssb.db.loaded()
for (let i = 0; i < 10; i++) {
await p(ssb.db.create)({
feedFormat: 'classic',
content:
i % 2 === 0
? { type: 'post', text: 'hello ' + i }
: { type: 'about', about: ssb.id, name: 'Mr. #' + i },
})
}
const iterator = ssb.db.filterAsIterator(
(msg) => msg.value.content.type === 'post'
)
const results = []
for (const msg of iterator) {
results.push(msg.value.content.text)
}
t.deepEqual(
results,
['hello 0', 'hello 2', 'hello 4', 'hello 6', 'hello 8'],
'queried posts'
)
await p(ssb.close)(true)
})

View File

@ -1,48 +0,0 @@
const test = require('tape')
const ssbKeys = require('ssb-keys')
const path = require('path')
const rimraf = require('rimraf')
const os = require('os')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const pull = require('pull-stream')
const p = require('util').promisify
const DIR = path.join(os.tmpdir(), 'ppppp-db-filter-as-pull-stream')
rimraf.sync(DIR)
test('filterAsPullStream', async (t) => {
const ssb = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-classic'))
.call(null, {
keys: ssbKeys.generate('ed25519', 'alice'),
path: DIR,
})
await ssb.db.loaded()
for (let i = 0; i < 10; i++) {
await p(ssb.db.create)({
feedFormat: 'classic',
content:
i % 2 === 0
? { type: 'post', text: 'hello ' + i }
: { type: 'about', about: ssb.id, name: 'Mr. #' + i },
})
}
const results = await pull(
ssb.db.filterAsPullStream((msg) => msg.value.content.type === 'post'),
pull.map((msg) => msg.value.content.text),
pull.collectAsPromise()
)
t.deepEqual(
results,
['hello 0', 'hello 2', 'hello 4', 'hello 6', 'hello 8'],
'queried posts'
)
await p(ssb.close)(true)
})

View File

@ -1,48 +0,0 @@
const test = require('tape')
const ssbKeys = require('ssb-keys')
const path = require('path')
const rimraf = require('rimraf')
const os = require('os')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const p = require('util').promisify
const DIR = path.join(os.tmpdir(), 'ppppp-db-for-each')
rimraf.sync(DIR)
test('forEach', async (t) => {
const ssb = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-classic'))
.call(null, {
keys: ssbKeys.generate('ed25519', 'alice'),
path: DIR,
})
await ssb.db.loaded()
for (let i = 0; i < 10; i++) {
await p(ssb.db.create)({
feedFormat: 'classic',
content:
i % 2 === 0
? { type: 'post', text: 'hello ' + i }
: { type: 'about', about: ssb.id, name: 'Mr. #' + i },
})
}
const results = []
ssb.db.forEach((msg) => {
if (msg.value.content.type === 'post') {
results.push(msg.value.content.text)
}
})
t.deepEqual(
results,
['hello 0', 'hello 2', 'hello 4', 'hello 6', 'hello 8'],
'queried posts'
)
await p(ssb.close)(true)
})

View File

@ -0,0 +1,42 @@
const test = require('tape')
const path = require('path')
const os = require('os')
const rimraf = require('rimraf')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const p = require('util').promisify
const { generateKeypair } = require('./util')
const DIR = path.join(os.tmpdir(), 'ppppp-db-msgs-iter')
rimraf.sync(DIR)
test('msgs() iterator', async (t) => {
const keys = generateKeypair('alice')
const peer = SecretStack({ appKey: caps.shs })
.use(require('../'))
.call(null, { keys, path: DIR })
await peer.db.loaded()
for (let i = 0; i < 6; i++) {
await p(peer.db.create)({
type: i % 2 === 0 ? 'post' : 'about',
content:
i % 2 === 0
? { text: 'hello ' + i }
: { about: peer.id, name: 'Mr. #' + i },
})
}
const posts = []
const abouts = []
for (const msg of peer.db.msgs()) {
if (msg.metadata.type === 'post') posts.push(msg.content.text)
else if (msg.metadata.type === 'about') abouts.push(msg.content.name)
}
t.deepEqual(posts, ['hello 0', 'hello 2', 'hello 4'], 'queried posts')
t.deepEqual(abouts, ['Mr. #1', 'Mr. #3', 'Mr. #5'], 'queried abouts')
await p(peer.close)(true)
})

View File

@ -1,45 +0,0 @@
const test = require('tape')
const ssbKeys = require('ssb-keys')
const path = require('path')
const rimraf = require('rimraf')
const os = require('os')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const p = require('util').promisify
const DIR = path.join(os.tmpdir(), 'ppppp-db-on-msg-added')
rimraf.sync(DIR)
test('onMsgAdded', async (t) => {
const ssb = SecretStack({ appKey: caps.shs })
.use(require('../'))
.use(require('ssb-classic'))
.call(null, {
keys: ssbKeys.generate('ed25519', 'alice'),
path: DIR,
})
await ssb.db.loaded()
const listened = []
var remove = ssb.db.onMsgAdded((ev) => {
listened.push(ev)
})
const msg1 = await p(ssb.db.create)({
feedFormat: 'classic',
content: { type: 'post', text: 'I am hungry' },
})
t.equal(msg1.value.content.text, 'I am hungry', 'msg1 text correct')
await p(setTimeout)(500)
t.equal(listened.length, 1)
t.deepEquals(Object.keys(listened[0]), ['kvt', 'nativeMsg', 'feedFormat'])
t.deepEquals(listened[0].kvt, msg1)
t.deepEquals(listened[0].nativeMsg, msg1.value)
t.equals(listened[0].feedFormat, 'classic')
remove()
await p(ssb.close)(true)
})

View File

@ -0,0 +1,39 @@
const test = require('tape')
const path = require('path')
const rimraf = require('rimraf')
const os = require('os')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const p = require('util').promisify
const { generateKeypair } = require('./util')
const DIR = path.join(os.tmpdir(), 'ppppp-db-on-msg-added')
rimraf.sync(DIR)
test('onRecordAdded', async (t) => {
const keys = generateKeypair('alice')
const peer = SecretStack({ appKey: caps.shs })
.use(require('../'))
.call(null, { keys, path: DIR })
await peer.db.loaded()
const listened = []
var remove = peer.db.onRecordAdded((ev) => {
listened.push(ev)
})
const rec1 = await p(peer.db.create)({
type: 'post',
content: { text: 'I am hungry' },
})
t.equal(rec1.msg.content.text, 'I am hungry', 'msg1 text correct')
await p(setTimeout)(500)
t.equal(listened.length, 1)
t.deepEquals(listened, [rec1])
remove()
await p(peer.close)(true)
})

View File

@ -0,0 +1,39 @@
const test = require('tape')
const path = require('path')
const os = require('os')
const rimraf = require('rimraf')
const SecretStack = require('secret-stack')
const caps = require('ssb-caps')
const p = require('util').promisify
const { generateKeypair } = require('./util')
const DIR = path.join(os.tmpdir(), 'ppppp-db-records-iter')
rimraf.sync(DIR)
test('records() iterator', async (t) => {
const keys = generateKeypair('alice')
const peer = SecretStack({ appKey: caps.shs })
.use(require('../'))
.call(null, { keys, path: DIR })
await peer.db.loaded()
for (let i = 0; i < 6; i++) {
await p(peer.db.create)({
type: i % 2 === 0 ? 'post' : 'about',
content:
i % 2 === 0
? { text: 'hello ' + i }
: { about: peer.id, name: 'Mr. #' + i },
})
}
let count = 0
for (const rec of peer.db.records()) {
t.true(rec.misc.size > rec.msg.metadata.size)
count++
}
t.equals(count, 6)
await p(peer.close)(true)
})

View File

@ -5,7 +5,7 @@ const base58 = require('bs58')
function generateKeypair(seed) {
const keys = ssbKeys.generate('ed25519', seed, 'buttwoo-v1')
const { data } = SSBURI.decompose(keys.id)
keys.id = `ssb:feed/dag/${base58.encode(Buffer.from(data, 'base64'))}`
keys.id = `ppppp:feed/v1/${base58.encode(Buffer.from(data, 'base64'))}`
return keys
}