Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Socket
Sign inDemoInstall

ssb-crut

Package Overview
Dependencies
Maintainers
3
Versions
38
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ssb-crut - npm Package Compare versions

Comparing version 3.2.1 to 4.0.0

CHANGELOG.md

2

constants.js

@@ -1,2 +0,2 @@

const nono = ['type', 'recps', 'tangle', 'tombstone', 'allowPublic']
const nono = ['type', 'recps', 'tangle', 'tombstone', 'allowPublic', 'key', 'states', 'conflictFields']

@@ -3,0 +3,0 @@ module.exports = {

@@ -7,6 +7,6 @@ const Strategy = require('@tangle/strategy')

const isValidSpec = require('./lib/is-valid-spec')
const IsValidMsg = require('./lib/is-valid-msg')
const IsValidInput = require('./lib/is-valid-input')
const IsValidMsg = require('./lib/is-valid-msg')
const ResolveTangle = require('./lib/resolve-tangle')
const handlePublish = require('./lib/handle-publish')
const HandlePublish = require('./lib/handle-publish')
const { merge, isObject, pick, defaultGetTransformation } = require('./lib/util')

@@ -17,7 +17,12 @@

module.exports = class CRUT {
constructor (server, spec) {
checkServerDeps(server)
this.server = server
constructor (ssb, spec, opts = {}) {
checkServerDeps(ssb)
checkReservedValues(spec)
checkOpts(opts)
checkReservedValues(spec)
const {
publish = ssb.publish,
feedId = ssb.id
} = opts
this.spec = merge(

@@ -42,8 +47,12 @@ {

if (!isValidSpec(this.spec)) throw isValidSpec.error
this.spec.props = new Strategy(this.spec.props)
this.spec.isRoot = IsValidMsg.root(this.spec)
this.spec.isUpdate = IsValidMsg.update(this.spec)
this.strategy = new Strategy(this.spec.props)
this.isValidProps = IsValidInput('props', this.spec.props._composition)
this.resolveTangle = ResolveTangle(server, this.spec)
this.isRoot = IsValidMsg.root(this)
this.isUpdate = IsValidMsg.update(this)
this.isValidProps = IsValidInput('props', this.spec.props)
this.resolveTangle = ResolveTangle(ssb, this)
this.handlePublish = HandlePublish({ crut: this, publish, feedId, getFeedState: ssb.getFeedState })
if (this.spec.arbitraryRoot && ssb.tribes) this.getTribe = ssb.tribes.get
}

@@ -66,5 +75,5 @@

try {
propsT = this.spec.props.mapFromInput(
this.spec.props.identity(),
props
propsT = this.strategy.mapFromInput(
props,
[this.strategy.identity()]
)

@@ -84,8 +93,13 @@ } catch (e) { return cb(e) }

const context = {
accT: this.spec.props.identity()
// graph: ?? should there be a graph here?
// we mock a an empty initial transformation state so we can check `isValidNextStep`
// on the initial step (root message)
const tangleContext = {
tips: [{
key: null, // DNE
T: this.strategy.identity()
}],
graph: null // DNE
}
handlePublish(this.server, this.spec, context, content, allowPublic, cb)
this.handlePublish(tangleContext, content, allowPublic, cb)
}

@@ -109,7 +123,37 @@

),
states: tangle.tips
.map(({ key, T }) => ({ key, ...this.spec.props.mapToOutput(T) }))
.map(({ key, T }) => ({ key, ...this.strategy.mapToOutput(T) })),
conflictFields: []
}
let bestGuessState
// The transformation props are also spread into result (from states).
// If there is only one state we can just use that (without it's key):
if (result.states.length === 1) bestGuessState = { ...result.states[0] }
// However, if there are multiple states we first try to automatically merge them.
else {
const dummyNode = {
// key: '%dummyMessageId',
previous: tangle.tips.map(({ key, T }) => key),
data: this.strategy.identity()
// author: '%dummyMessageAuthor',
// sequence: (sequence || 0) + 1
}
// If we can automatically merge the tips, then the best guess of state is the merged tips
if (this.strategy.isValidMerge(tangle.graph, dummyNode)) {
const T = this.strategy.merge(tangle.graph, dummyNode)
bestGuessState = this.strategy.mapToOutput(T)
} // eslint-disable-line
// Otherwise, we fall back to the tip with the latest entry
else {
bestGuessState = { ...result.states[0] }
result.conflictFields = this.strategy.isValidMerge.fields
}
}
if ('key' in bestGuessState) delete bestGuessState.key
// Spread the bestGuessState props into result
Object.assign(result, bestGuessState)
cb(null, result)

@@ -129,13 +173,10 @@ })

const tip = tangle.tips[0]
// NOTE <<< need to change this to support merging!
const data = this.strategy.mapFromInput(
props,
tangle.tips.map(tip => tip.T) // currentTips
)
const content = {
type: this.spec.type,
...this.spec.props.mapFromInput(
tip.T,
props
),
...data,
recps: tangle.root.value.content.recps,

@@ -145,3 +186,3 @@ tangles: {

root: id,
previous: [tip.key]
previous: tangle.tips.map(tip => tip.key)
}

@@ -151,7 +192,3 @@ }

const context = {
accT: tip.T,
graph: tangle.graph
}
handlePublish(this.server, this.spec, context, content, allowPublic, cb)
this.handlePublish(tangle, content, allowPublic, cb)
})

@@ -178,3 +215,3 @@ }

this.server.tribes.get(groupId, (err, info) => {
this.getTribe(groupId, (err, info) => {
if (err) return cb(err)

@@ -190,3 +227,3 @@ this.update(info.root, props, cb)

this.server.tribes.get(groupId, (err, info) => {
this.getTribe(groupId, (err, info) => {
if (err) return cb(err)

@@ -202,3 +239,3 @@ this.read(info.root, cb)

this.server.tribes.get(groupId, (err, info) => {
this.getTribe(groupId, (err, info) => {
if (err) return cb(err)

@@ -215,3 +252,3 @@ this.tombstone(info.root, props, cb)

else if (key === 'allowPublic') acc.allowPublic = value
else if (key in props._composition) acc.props[key] = value
else if (key in props) acc.props[key] = value
else if (key in staticProps) acc.staticProps[key] = value

@@ -242,1 +279,8 @@ else acc.unknown[key] = value

}
function checkOpts (opts) {
if (opts.publish) {
if (typeof opts.publish !== 'function') throw new Error('opts.publish must be a function')
if (typeof opts.feedId !== 'string') throw new Error('opts.feedId must be provided if opts.publish is used')
}
}
const stringify = require('fast-json-stable-stringify')
const { createHash } = require('crypto')
const { defaultGetTransformation, getCanonicalContent } = require('./util')
const { defaultGetTransformation, getCanonicalContent, MsgToNode } = require('./util')
module.exports = function handlePublish (ssb, spec, context, content, allowPublic, cb) {
const isRoot = content.tangles[spec.tangle].previous === null
module.exports = function HandlePublish ({ crut, publish, feedId, getFeedState }) {
const { spec, isRoot, isUpdate } = crut
const msgToNode = MsgToNode(spec)
if (isRoot) {
if (!spec.isRoot(content)) return cb(new Error(spec.isRoot.errorsString))
} else {
if (!spec.isUpdate(content)) return cb(new Error(spec.isUpdate.errorsString))
}
return function handlePublish (tangleContext, content, allowPublic, cb) {
const previous = content.tangles[spec.tangle].previous
const isRootContent = previous === null
const dummyMsg = buildDummyMsg(ssb, content)
if (isRootContent) {
if (!isRoot(content)) return cb(new Error(isRoot.errorsString))
} else {
if (!isUpdate(content)) return cb(new Error(isUpdate.errorsString))
}
// ensure results of getTransformation still conform to schema
// so we can guarentee reduce will work
if (spec.getTransformation !== defaultGetTransformation) {
const checkSum = hash(content)
buildDummyMsg(content, (err, dummyMsg) => {
err = err || findGetTransformationError(content, dummyMsg, isRootContent)
// ensure results of getTransformation still conform to schema
// so we can guarentee reduce will work
if (err) return cb(err)
// we map msg into canonical form of content that we can use to validate with isRoot/ isUpdate
const canonicalContent = getCanonicalContent(dummyMsg, spec.getTransformation)
const dummyNode = msgToNode(dummyMsg)
// if this is a merge update, check if it would be a valid merge.
if (previous && previous.length > 1) {
if (!crut.strategy.isValidMerge(tangleContext.graph, dummyNode)) {
const error = crut.strategy.isValidMerge.error
if (!error) return cb(invalidMergeError())
// check to see if getTransformation has mutated the content we're about to publish
if (hash(content) !== checkSum) return cb(new Error('getTransformation mutated content about to be published'))
error.conflictFields = crut.strategy.isValidMerge.fields
return cb(error)
}
}
// check is whether it's a valid next step (according to custom function)
if (!spec.isValidNextStep(tangleContext, dummyNode)) {
return cb(spec.isValidNextStep.error || invalidNextStepError(isRootContent))
}
if (isRoot) {
if (!spec.isRoot(canonicalContent)) return cb(new Error(spec.isRoot.errorsString))
} else {
if (!spec.isUpdate(canonicalContent)) return cb(new Error(spec.isUpdate.errorsString))
}
publish(guard(content, allowPublic), (err, msg) => {
if (err) return cb(err)
cb(null, msg.key)
})
})
}
ssb.getFeedState(ssb.id, (err, { sequence } = {}) => {
if (err) return cb(err)
function buildDummyMsg (content, cb) {
getFeedState(feedId, (err, { sequence } = {}) => {
if (err) return cb(err)
dummyMsg.value.sequence = (sequence || 0) + 1 // NEXT message, so +1
cb(null, {
key: '%dummyMessageId', // trick ssb-msg-content
value: {
sequence: (sequence || 0) + 1,
timestamp: Date.now(),
author: feedId,
content,
// NOTE we trust the programmer not to mutate the content with getTransformation
meta: {
dummyMsg: true
}
}
})
})
}
if (!spec.isValidNextStep(context, dummyMsg)) {
const err = isRoot
? 'Invalid root message, failed isValidNextStep, publish aborted'
: 'Invalid update message, failed isValidNextStep, publish aborted'
return cb(spec.isValidNextStep.error || new Error(err))
}
function findGetTransformationError (content, dummyMsg, isRootContent) {
if (spec.getTransformation === defaultGetTransformation) return
// WIP I think should be able to hand in custom 'publish' method:
// new Crut(ssb, spec, { publish })
// - we do this so that we can customise what exactly comes back if we want (and whether things hit db)
const checkSum = hash(content)
// unsolved problems:
// - extract recps-guard handler as an example
// - what to do about steps which assume ssb.id (for author, and feed you're validating against)?
ssb.publish(guard(content, allowPublic), (err, msg) => {
if (err) return cb(err)
// we map msg into canonical form of content that we can use to validate with isRoot/ isUpdate
const canonicalContent = getCanonicalContent(dummyMsg, spec.getTransformation)
cb(null, msg.key)
})
})
if (isRootContent) {
if (!isRoot(canonicalContent)) return new Error(isRoot.errorsString)
} else {
if (!isUpdate(canonicalContent)) return new Error(isUpdate.errorsString)
}
// check to see if getTransformation has mutated the content we're about to publish
if (hash(content) !== checkSum) return new Error('getTransformation mutated content about to be published')
}
}

@@ -69,16 +96,2 @@

function buildDummyMsg (ssb, content) {
return {
key: '%dummy', // trick ssb-msg-content
value: {
sequence: 1,
timestamp: Date.now(),
author: ssb.id,
content,
// NOTE we trust the programmer not to mutate the content with getTransformation
dummyMsg: true
}
}
}
function guard (content, allowPublic) {

@@ -90,1 +103,12 @@ // this is for ssb-recps-guard

}
function invalidNextStepError (isRoot = true) {
return new Error(isRoot
? 'Invalid root message, failed isValidNextStep, publish aborted'
: 'Invalid update message, failed isValidNextStep, publish aborted'
)
}
function invalidMergeError () {
return new Error('Invalid message, failed merge, publish aborted')
}
const { isObject } = require('./util')
module.exports = function IsValidInput (label, allowed) {
// NOTE allowed here is an object
// label *String*
// allowed *Object*
const isValidInput = function isValidInput (input) {

@@ -6,0 +7,0 @@ isValidInput.error = null

const Validator = require('is-my-ssb-valid')
const definitions = require('ssb-schema-definitions')()
function buildIsRoot ({ type, typePattern, tangle, staticProps, props, hooks }) {
function buildIsRoot ({ spec, strategy }) {
const { typePattern, tangle, staticProps, hooks } = spec
const { properties } = strategy.schema
const schema = {

@@ -12,3 +15,3 @@ type: 'object',

...staticProps,
...props.schema.properties,
...properties,

@@ -34,3 +37,6 @@ tangles: {

function buildIsUpdate ({ type, typePattern, tangle, props, hooks }) {
function buildIsUpdate ({ spec, strategy }) {
const { typePattern, tangle, hooks } = spec
const { properties } = strategy.schema
const schema = {

@@ -42,3 +48,3 @@ type: 'object',

...props.schema.properties,
...properties,

@@ -45,0 +51,0 @@ tangles: {

@@ -19,10 +19,2 @@ const Strategy = require('@tangle/strategy')

// TODO add checks for restricted keys in props, staticProps'
// - recps
// - type
// - attendees ... (props only)
// - staticProps and props cannot overlap
//
// tangle !== group
/* Required */

@@ -57,2 +49,6 @@

if (isProblem) errors.push('spec.staticProps were malformed')
// check staticProps don't collide with props
const duplicates = Object.keys(staticProps).filter(staticProp => staticProp in props)
if (duplicates.length) errors.push(`props and staticProps may not have the same name [${duplicates}]`)
}

@@ -59,0 +55,0 @@

@@ -1,15 +0,17 @@

const pull = require('pull-stream')
const Reduce = require('@tangle/reduce')
const { getCanonicalContent } = require('./util')
module.exports = function ResolveTangle (server, spec) {
// NOTE: we only care if the messages we're ingesting pass validators
// *after* they have been mapped by getTransformation (raw msg could have been quite malformed)
const isRoot = msg => spec.isRoot(getCanonicalContent(msg, spec.getTransformation))
const isUpdate = msg => spec.isUpdate(getCanonicalContent(msg, spec.getTransformation))
const GetRoot = require('./get-root')
const GetUpdates = require('./get-updates')
const { MsgToNode } = require('./util')
const getBacklinks = msg => msg.value.content.tangles[spec.tangle].previous
const isDB2 = Boolean(server.db)
module.exports = function ResolveTangle (ssb, crut) {
const { spec, strategy } = crut
const msgToNode = MsgToNode(spec)
const getRoot = GetRoot(ssb, crut)
const getUpdates = GetUpdates(ssb, crut)
return function resolveTangle (id, cb) {
// First, the root message and it's update messages are fetched from the ssb
getRoot(id, (err, root) => {

@@ -20,19 +22,13 @@ if (err) return cb(err)

if (err) return cb(err)
// First, the root message and it's update messages are fetched from the ssb
const msgs = [root, ...updates]
const msgs = [root, ...updates]
const reduce = new Reduce(spec.props, {
nodes: msgs,
getTransformation: spec.getTransformation,
getBacklinks,
isValid: spec.isValidNextStep
const reduce = new Reduce(strategy, {
nodes: msgs.map(msgToNode),
isValidNextStep: spec.isValidNextStep
})
const timestamps = msgs.reduce((timestamps, m) => {
timestamps[m.key] = m.value.timestamp
return timestamps
}, {})
const tips = Object.entries(reduce.state)
.map(([key, T]) => ({ key, T }))
.sort((A, B) => timestamps[B.key] - timestamps[A.key])
.sort(TimestampComparer(msgs))

@@ -43,135 +39,17 @@ cb(null, { root, tips, graph: reduce.graph })

}
}
function getRoot (id, cb) {
server.get({ id, private: true, meta: true }, (err, root) => {
if (err) return cb(err)
function TimestampComparer (msgs) {
const timestamps = msgs.reduce((timestamps, m) => {
timestamps[m.key] = m.value.timestamp
return timestamps
}, {})
handleArbitraryRoot(root, (err, root) => {
if (err) return cb(err)
let result
return (A, B) => {
result = timestamps[B.key] - timestamps[A.key]
if (result !== 0) return result
if (!isRoot(root)) {
return cb(new Error(`not a valid ${spec.type}, ${spec.isRoot.errorsString}`))
}
// HACK - ensures there's an authors field on the root message
// this is to work around ssb-crut-authors which assumes all root messages must set
// authors. Old ssb-profile records did not do this so resolving them would break
// TODO - remove this hack (e.g. move to opts.getTransformation in ssb-profile)
if (isEmpty(root.value.content.authors)) {
root.value.content.authors = {
[root.value.author]: { [root.value.sequence]: 1 }
}
}
cb(null, root)
})
})
return A.key < B.key ? -1 : 1
}
function handleArbitraryRoot (rootMsg, cb) {
if (!spec.arbitraryRoot) return cb(null, rootMsg)
// build a mock root message with needed details
const mock = {
key: rootMsg.key,
value: {
author: rootMsg.value.author,
content: {
type: spec.type,
tangles: {
[spec.tangle]: { root: null, previous: null }
}
// recps decided below
}
}
}
const { type, recps } = rootMsg.value.content
if (recps) {
mock.value.content.recps = recps
cb(null, mock)
} // eslint-disable-line
// NOTE group/init messages
// - are manually encrypted, and do not have content.recps field
// - historically could not be decrypted by the author! (ammended now)
else if (type === 'group/init' || isLegacyGroupInit(rootMsg.value.content)) {
server.tribes.list({ subtribes: true }, (err, groupIds) => {
if (err) return cb(err)
pull(
pull.values(groupIds),
// TODO modify server.tribes.get to include groupId in returned result by default:
// pull.asyncMap(server.tribes.get),
pull.asyncMap((groupId, cb) => {
server.tribes.get(groupId, (err, info) => {
if (err) cb(err)
else cb(null, { groupId, ...info })
})
}),
pull.filter(groupInfo => groupInfo.root === rootMsg.key),
pull.take(1),
pull.collect((err, groupInfos) => {
if (err) return cb(err)
mock.value.content.recps = [groupInfos[0].groupId]
mock.value.content.tangles.group = { root: null, previous: null }
cb(null, mock)
})
)
})
} // eslint-disable-line
else cb(null, mock)
}
function getUpdates (id, cb) {
// a query which gets all update messages to the root message
let source
if (isDB2) {
const { where, and, slowEqual, type, toPullStream } = server.db.operators
source = server.db.query(
where(
and(
type(spec.type),
slowEqual(`value.content.tangles.${spec.tangle}.root`, id)
)
),
toPullStream()
)
} else {
const query = [{
$filter: {
dest: id,
value: {
content: {
type: spec.type,
tangles: {
[spec.tangle]: { root: id }
}
}
}
}
}]
source = server.backlinks.read({ query })
}
pull(
source,
pull.filter(isUpdate),
pull.collect(cb)
)
}
}
function isEmpty (obj) {
if (obj == null) return true
if (!Object.keys(obj).length) return true
return false
}
function isLegacyGroupInit (content) {
return (
typeof content === 'string' &&
content.endsWith('.box2') &&
content.length <= 209 // max historical size
)
}

@@ -42,11 +42,24 @@ const merge = require('lodash.merge')

tangles: msg.value.content.tangles,
recps: msg.value.content.recps
tangles: msg.value.content.tangles
// recps ?
}
if (msg.value.content.recps) content.recps = msg.value.content.recps
if (!content.recps) delete content.recps
return content
}
function MsgToNode (spec) {
return function msgToNode (msg) {
return {
key: msg.key,
previous: msg.value.content.tangles[spec.tangle].previous,
data: spec.getTransformation(msg),
// Note: getTransformation may include props that are not in the strategy
author: msg.value.author,
sequence: msg.value.sequence
}
}
}
module.exports = {

@@ -57,3 +70,4 @@ isObject,

defaultGetTransformation,
getCanonicalContent
getCanonicalContent,
MsgToNode
}
{
"name": "ssb-crut",
"version": "3.2.1",
"version": "4.0.0",
"description": "easy CRUT methods for secure scuttlebutt",

@@ -10,5 +10,5 @@ "main": "index.js",

"scripts": {
"test": "npm run test:js:db1 && npm run test:js:db2 && npm run test:only && npm run lint",
"test": "npm run test:js:db1 && npm run test:js:db2 && npm run lint && npm run test:only ",
"test:js:db1": "tape 'test/**/*.test.js' | tap-spec",
"test:js:db2": "DB2=1 tape 'test/**/*.test.js' | tap-spec",
"test:js:db2": "cross-env DB2=1 tape 'test/**/*.test.js' | tap-spec",
"test:only": "if grep -r --exclude-dir=node_modules --exclude-dir=.git --color 'test\\.only' ; then exit 1; fi",

@@ -33,5 +33,4 @@ "lint": "standard --fix"

"dependencies": {
"@tangle/overwrite": "^2.0.1",
"@tangle/reduce": "^3.1.0",
"@tangle/strategy": "^2.0.1",
"@tangle/reduce": "^5.0.0",
"@tangle/strategy": "^4.1.0",
"fast-json-stable-stringify": "^2.1.0",

@@ -44,5 +43,7 @@ "is-my-ssb-valid": "^1.1.0",

"devDependencies": {
"@tangle/complex-set": "^2.0.0",
"@tangle/linear-append": "^1.0.0",
"@tangle/simple-set": "^2.0.0",
"@tangle/complex-set": "^3.0.1",
"@tangle/linear-append": "^2.0.0",
"@tangle/overwrite": "^3.0.0",
"@tangle/simple-set": "^3.0.0",
"cross-env": "^7.0.3",
"scuttle-testbot": "^1.8.0",

@@ -49,0 +50,0 @@ "ssb-backlinks": "^2.1.1",

@@ -90,6 +90,6 @@ # ssb-crut

### `new CRUT(ssb, spec) => crut`
### `new CRUT(ssb, spec, opts?) => crut`
Takes `ssb`, a scuttelbutt server instance and a `spec` and returns an crut instance
with methods for mutable records.
Takes `ssb`, a scuttlebutt server instance, a `spec` and an optional
opts and returns an crut instance with methods for mutable records.

@@ -100,3 +100,3 @@ A `spec` is an Object with properties:

- defines how the mutable parts of the record will behaved.
- each property is expected to be an instance of a tangle strategy (e.g. `@tangle/simple-set`)
- each property is expected to be an instance of a tangle strategy (e.g. `@tangle/simple-set`)
- reserved props: `['type', 'recps', 'tangle', 'tombstone']`

@@ -123,10 +123,38 @@

- a function run before writing and during reading to confirm the validity of message extending the tangle
- signature: `fn(context, msg) => Boolean` where
- `context = { accT, graph }` where `accT` is the accumulated transform for the position immediately before this message in the tangle, and `graph` is a `@tangle/graph` instance for the tangle so far.
- `msg` the message being assessed
- signature: `fn(tangleContext, node) => Boolean` where
- `tangleContext = { tips, graph }` where:
- `tips` *Array* represents the accumulated transform for the position immediately before this message in the tangle, `[{ key, T }]`
- `graph` is a `@tangle/graph` instance for the tangle so far.
- `node` the node being assessed is of form:
```js
{
key: MessageId,
previous: ['%sd4kasDD...'],
data: {
title: { set: 'spec gathering' },
attendees: { mix: 1, luandro: 1 }
},
author: '@ye+4das...',
sequence: 132
}
```
- used by:
- `create` to check the message about to be published is valid. In this case `accT = I`, the (empty) identity transform.
- `update` to check the message about to be published is valid given the existing tangle state it would extend
- `read` to determine which messages are valid to include in reducing
- NOTE - in `create` you don't have access to `context.graph`, as there's no graph yet
- `create` to check the message about to be published is valid. In this case `accT = I`, the (empty) identity transform.
- `update` to check the message about to be published is valid given the existing tangle state it would extend
- `read` to determine which messages are valid to include in reducing
- NOTE - in `create` you don't have access to `tangleContext.graph`, as there's no graph yet
- ADVANCED - if you want to provide a detailed error, you can attach errors to your validator like so:
```js
function isValidNextStep (tangleContext, node) {
isValidNextStep.error = null
const isValid = ... // your logic
if (isValid) return true
else {
isValidNextStep.error = new Error('your detailed error message')
return false
}
}
```
- `spec.hooks` *Object* with properties:

@@ -148,2 +176,6 @@ - `isRoot` *Array* a collection of validator functions which each root message must pass to proceed

`opts` can have the following properties:
- `publish(content, cb)`, a custom publish function instead of the default ssb.publish. Could be `publishAs` when using ssb-db2 to publish the content as another feedId
- `feedId`, the feedId to publish as. Defaults to ssb.id.
### `crut.create(allProps, cb)`

@@ -153,3 +185,3 @@

- `allProps` *Object*
- `allProps` *Object*
- none/ some/ all of the properties declared in `spec.props`

@@ -169,4 +201,3 @@ - none/ some/ all of the properties declared in `spec.staticProps`

A tangle here is a collection of messages linked in a directed acyclic graph.
Each of thee messages contains an "operational transform" which is an
instuction about how to update the record state.
Each of thee messages contains some transformation(s) which are an instuction about how to update the record state.

@@ -190,15 +221,17 @@ Transformations are concatenated (added up) while traversing the graph.

{
key: A, // the key of the tangle root message
key: A, // the key of the tangle root message
type,
...staticProps, // any staticProp values
...staticProps, // any staticProp values
...props // best guess of state (auto-merged states or if conflict the most recent state)
states: [
{
key: D, // key of tangle tip message
...props // reified state of props for this tangle tip
key: D, // key of tangle tip message
...props // reified state of props for this tangle tip
},
{
key: B, // key of tangle tip message
...props // reified state of props for this tangle tip
key: B, // key of tangle tip message
...props // reified state of props for this tangle tip
}
}
},
conflictFields: [] // names of trouble fields if there is a conflict
}

@@ -210,2 +243,5 @@ ```

For convenience the states are automatically merged and spread into the result.
If the states are in conflict then the first state is used as a 'best guess'
The state of the props returned are "riefied" (meaning _has been made real_),

@@ -219,2 +255,3 @@ because often the transformation format is optimised for mathematical properties,

### `crut.update(id, props, cb)`

@@ -234,16 +271,20 @@

- if `cb` is not passed, a Promise is returned instead.
- if there is a merge conflict that needs resolving and your update does not resolve it you will get an Error with
- `err.message` - describes in human sentence the fields which has conflicts
- `err.fields` - an Array of fields which had conflicts
- by default, updates are accepted from everyone. To change this, specifiy behaviour in `isValidUpdate` e.g.
```js
spec.isValidUpdate = (context, msg) => {
const { accT, graph } = context
```js
spec.isValidUpdate = (context, msg) => {
const { accT, graph } = context
if (!graph) return true
// crut.read has graph, but crut.update doesn't yet
// this means updates from others can be published but will be ignored
if (!graph) return true
// crut.read has graph, but crut.update doesn't yet
// this means updates from others can be published but will be ignored
return graph.rootNodes.some(root => {
return root.value.author === msg.value.author
})
}
```
return graph.rootNodes.some(root => {
return root.value.author === msg.value.author
})
}
```

@@ -265,3 +306,3 @@

---
---

@@ -275,3 +316,3 @@ ### Using spec.arbitraryRoot with Private Groups

The methods you can do this with are:
The methods you can do this with are:
- `crut.updateGroup(groupId, props, cb)`

@@ -287,6 +328,3 @@ - `crut.readGroup(groupId, cb)`

- `crut.update` does not currently publish merges
- currently extends the tip with most recent activity
- want to change this in the future but @tangle/reduce will needs more work
- Show user error message caused by failed merge

@@ -9,2 +9,3 @@ const test = require('tape')

const fixRecps = require('./db2-compat')
const keys = require('ssb-keys')

@@ -125,2 +126,6 @@ test('create (minimal)', t => {

},
// {
// allProps: ['dogo'],
// expectedErr: 'allProps must be an Object'
// },
{

@@ -249,1 +254,24 @@ allProps: { dogo: 'pup' },

})
test('create, opts = { feedId, publish }', t => {
if (!process.env.DB2) return t.end()
const otherKeys = keys.generate()
const server = Server({ recpsGuard: true })
const spec = Spec()
const crut = new CRUT(server, spec, {
feedId: otherKeys.id,
publish: (content, cb) => server.db.publishAs(otherKeys, content, cb)
})
crut.create({ parent: 'mix', recps: [server.id] }, (err, msgKey) => {
t.error(err, 'creates')
server.get(msgKey, (_, value) => {
t.equal(value.author, otherKeys.id, 'create with proper author')
server.close()
t.end()
})
})
})

@@ -7,2 +7,3 @@ const test = require('tape')

const server = { backlinks: true } // mock server!
let spec

@@ -17,2 +18,3 @@ test('new Crut', t => {

/* protected tangle */
t.throws(

@@ -23,2 +25,7 @@ () => new CRUT(server, Spec({ tangle: 'group' })),

)
t.throws(
() => new CRUT(server, Spec({ type: 'group/something', tangle: undefined })),
/Invalid spec/,
'spec.type = "group/something" throws (when tangle not set)'
)

@@ -31,3 +38,3 @@ /* protected props */

() => new CRUT(server, spec),
/Invalid spec: spec.props.* reserved/,
new RegExp(`Invalid spec: spec.props.${prop} .*reserved`),
`spec with props.${prop} throws`

@@ -37,2 +44,3 @@ )

/* protected props */
PROTECTED_STATIC_PROPS.forEach(prop => {

@@ -48,3 +56,12 @@ const spec = Spec()

spec = Spec()
const propName = Object.keys(spec.props)[0]
spec.staticProps[propName] = { type: 'string' }
t.throws(
() => new CRUT(server, spec),
new RegExp(`props and staticProps may not have the same name \\[${propName}\\]`),
`spec which has ${propName} in both props + staticProps throws`
)
t.throws(
() => new CRUT({}, Spec()), // mock server missing backlinks

@@ -51,0 +68,0 @@ /requires .* ssb-backlinks/,

@@ -53,3 +53,6 @@ const test = require('tape')

tombstone: null
}]
}],
autoFollow: true,
tombstone: null,
conflictFields: []
},

@@ -92,3 +95,6 @@ 'public #read'

tombstone: null
}]
}],
autoFollow: true,
tombstone: null,
conflictFields: []
},

@@ -141,3 +147,6 @@ 'private #read'

tombstone: null
}]
}],
autoFollow: true,
tombstone: null,
conflictFields: []
},

@@ -144,0 +153,0 @@ 'private #readGroup'

const test = require('tape')
const { promisify } = require('util')
const { replicate } = require('scuttle-testbot')

@@ -6,2 +7,3 @@ const CRUT = require('../')

const Spec = require('./spec.mock')
const { expectedState } = require('./util')

@@ -41,3 +43,12 @@ test('read', t => {

if (err) throw err
const states = [{
key: update.key,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
const expected = {

@@ -50,12 +61,5 @@ key: profileId,

recps: null,
states: [{
key: update.key,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
}

@@ -107,2 +111,13 @@

const profile = await crut.read(profileId)
const states = [{
key: update.key,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
t.deepEqual(

@@ -117,12 +132,5 @@ profile,

recps: null,
states: [{
key: update.key,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
},

@@ -140,3 +148,3 @@ 'reads current state'

test('read (multiple states)', t => {
test('read (multiple states - conflicting)', t => {
const server = Server()

@@ -183,2 +191,23 @@ const spec = Spec()

const states = [
{
key: updateB.key,
preferredName: 'Māui B',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
},
{
key: updateA.key,
preferredName: 'Māui A',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
}
]
t.deepEqual(

@@ -193,24 +222,7 @@ profile,

recps: null,
states: [
{
key: updateB.key,
preferredName: 'Māui B',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
},
{
key: updateA.key,
preferredName: 'Māui A',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
}
]
states,
...expectedState(states),
conflictFields: ['preferredName']
},
'reads current states (and orders by timestamp - latest first)'
'reads current states (and orders by timestamp - latest first, cannot automatic merge)'
)

@@ -227,2 +239,74 @@

test('read (multiple states - auto-mergeable)', async t => {
const server = Server()
const spec = Spec()
const crut = new CRUT(server, spec)
const profileId = await crut.create({
parent: 'Taranga',
preferredName: 'Māui',
attendees: {
add: [{ id: server.id, seq: 1 }]
}
})
const publish = promisify(server.publish)
// NOTE these both branch off from the root (see tangle previous)
const updateA = await publish({
type: spec.type,
preferredName: { set: 'Māui A' },
tangles: {
[spec.tangle]: { root: profileId, previous: [profileId] }
}
})
await new Promise(resolve => setTimeout(resolve, 10)) // ensure not published at same time!
const updateB = await publish({
type: spec.type,
legalName: { set: 'Māui B' },
tangles: {
[spec.tangle]: { root: profileId, previous: [profileId] }
}
})
// Now we see what read returns
const profile = await crut.read(profileId)
const expected = {
key: profileId,
type: spec.type,
originalAuthor: server.id,
parent: 'Taranga',
child: null,
recps: null,
states: [
{
key: updateB.key,
preferredName: 'Māui',
legalName: 'Māui B',
attendees: { [server.id]: [{ start: 1, end: null }] },
tombstone: null
},
{
key: updateA.key,
preferredName: 'Māui A',
legalName: null,
attendees: { [server.id]: [{ start: 1, end: null }] },
tombstone: null
}
],
preferredName: 'Māui A',
legalName: 'Māui B',
attendees: { [server.id]: [{ start: 1, end: null }] },
tombstone: null,
conflictFields: []
}
t.deepEqual(
profile,
expected,
'Read automatically merges tips into state prop (if all tips are nonconflicting)'
)
server.close()
t.end()
})
test('read (isValidNextStep ignores a friends update)', t => {

@@ -270,2 +354,12 @@ const me = Server()

const states = [{
key: profileId,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
}]
const expected = {

@@ -278,11 +372,5 @@ key: profileId,

recps: null,
states: [{
key: profileId,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
}

@@ -347,2 +435,13 @@ t.deepEqual(profile, expected, 'current state ignores invalid update')

const states = [{
key: update.key,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
[Hine]: [{ start: 5000, end: null }]
},
tombstone: null
}]
const expected = {

@@ -355,12 +454,5 @@ key: profileId,

recps: null,
states: [{
key: update.key,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
[Hine]: [{ start: 5000, end: null }]
},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
}

@@ -413,3 +505,12 @@ t.deepEqual(profile, expected, 'current state ignores invalid update')

if (err) throw err
const states = [{
key: update.key,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
t.deepEqual(

@@ -424,12 +525,5 @@ profile,

recps: [server.id],
states: [{
key: update.key,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
},

@@ -479,3 +573,11 @@ 'reads current state'

if (err) throw err
const states = [{
key: profileId,
preferredName: { author: Māui, value: 'Māui' },
legalName: 'Ben',
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
}]
t.deepEqual(

@@ -490,11 +592,5 @@ profile,

recps: null,
states: [{
key: profileId,
preferredName: { author: Māui, value: 'Māui' },
legalName: 'Ben',
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
},

@@ -501,0 +597,0 @@ 'reads decorated info'

@@ -17,2 +17,3 @@ # Tests

- TODO
5. merge
- spans read and update scenarios for merging

@@ -17,10 +17,3 @@ const overwrite = require('@tangle/overwrite')()

tangle: 'profile',
isValidNextStep ({ accT, graph }, m) {
if (m.value.content.tangles.profile.root === null) return true
return Object.keys(accT.attendees)
.some(attendee => attendee === m.value.author)
// NOTE this only checks if a given author was ever mentioned
// in the attendees transformations, not if they were added / removed!
},
isValidNextStep,
hooks: {

@@ -43,1 +36,17 @@ isRoot: [

}
function isValidNextStep ({ tips, graph }, m) {
isValidNextStep.error = null
if (m.previous === null) return true
const isValid = tips.every(tip => {
return Object.keys(tip.T.attendees)
.some(attendee => attendee === m.author)
// NOTE this only checks if a given author was ever mentioned
// in the attendees transformations, not if they were added / removed!
})
if (isValid) return true
else {
isValidNextStep.error = new Error(`Invalid update - ${m.author} is not attendee, so can't update`)
}
}
const test = require('tape')
const CRUT = require('../')
const Server = require('./test-bot')
const Spec = require('./spec.mock')
const { expectedState } = require('./util')

@@ -35,2 +37,13 @@ test('tombstone', t => {

const states = [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {},
tombstone: {
// author: server.id,
date: update.content.tombstone.set.date,
reason: 'woops'
}
}]
const expected = {

@@ -43,14 +56,5 @@ key: profileId,

recps: null,
// tombstone?
states: [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {},
tombstone: {
// author: server.id,
date: update.content.tombstone.set.date,
reason: 'woops'
}
}]
states,
...expectedState(states),
conflictFields: []
}

@@ -65,2 +69,9 @@

if (err) throw err
const states = [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {},
tombstone: null
}]

@@ -74,10 +85,5 @@ const expected = {

recps: null,
// tombstone?
states: [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
}

@@ -114,2 +120,13 @@ t.deepEqual(profile, expected, 'read tombstoned state')

const states = [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {},
tombstone: {
// author: server.id,
date: update.content.tombstone.set.date,
reason: 'woops'
}
}]
const expected = {

@@ -122,14 +139,5 @@ key: profileId,

recps: null,
// tombstone?
states: [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {},
tombstone: {
// author: server.id,
date: update.content.tombstone.set.date,
reason: 'woops'
}
}]
states,
...expectedState(states),
conflictFields: []
}

@@ -136,0 +144,0 @@

const test = require('tape')
const { replicate } = require('scuttle-testbot')
const pull = require('pull-stream')
const keys = require('ssb-keys')
const CRUT = require('../')
const Server = require('./test-bot')
const Spec = require('./spec.mock')
const { expectedState } = require('./util')

@@ -39,2 +42,13 @@ test('update', t => {

server.get(updateId, (_, value) => {
const states = [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
const expected = {

@@ -47,12 +61,5 @@ key: profileId,

recps: null,
states: [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
}

@@ -98,2 +105,12 @@

server.get(updateId, (_, value) => {
const states = [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
const expected = {

@@ -106,12 +123,5 @@ key: profileId,

recps: null,
states: [{
key: updateId,
preferredName: 'Māui',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: 41 }],
'@Hine-nui-te-pō': [{ start: 5000, end: null }]
},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
}

@@ -163,95 +173,2 @@ t.deepEqual(profile, expected, 'read returns updated state')

test('update (forked state)', t => {
const server = Server()
const spec = Spec()
const crut = new CRUT(server, spec)
// TODO set up forked state manually
// call update and document current expected behaviour
// V this is just currently a test copied from read
const Māui = server.id
crut.create(
{
parent: 'Taranga',
preferredName: 'Māui',
attendees: {
add: [{ id: Māui, seq: 33 }]
}
},
(err, profileId) => {
t.error(err, 'crut.create a profile')
const manualUpdateA = {
type: spec.type,
preferredName: { set: 'Māui A' },
tangles: {
[spec.tangle]: { root: profileId, previous: [profileId] }
}
}
const manualUpdateB = {
type: spec.type,
preferredName: { set: 'Māui B' },
tangles: {
[spec.tangle]: { root: profileId, previous: [profileId] }
}
}
// NOTE these both branch off from the root (see tangle previous)
server.publish(manualUpdateA, (err, updateA) => {
t.error(err, 'manually publish an update')
server.publish(manualUpdateB, (err, updateB) => {
t.error(err, 'manually publish a forked update')
/* now see what crut.update does */
crut.update(profileId, { preferredName: 'Māui C' }, (err, updateId) => {
t.error(err, 'use crut.update on forked state')
crut.read(profileId, (err, profile) => {
if (err) throw err
const expected = {
key: profileId,
type: spec.type,
originalAuthor: server.id,
parent: 'Taranga',
child: null,
recps: null,
states: [
{
key: updateId,
preferredName: 'Māui C',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
},
{
key: updateA.key,
preferredName: 'Māui A',
legalName: null,
attendees: {
[Māui]: [{ start: 33, end: null }]
},
tombstone: null
}
]
}
t.deepEqual(
profile,
expected,
'reads current states (and orders by timestamp - latest first)'
)
server.close()
t.end()
})
})
})
})
}
)
})
test('update (ssb-recps-guard)', t => {

@@ -356,2 +273,13 @@ if (process.env.DB2) return t.end()

const states = [{
key: updateId,
preferredName: { author: server2.id, value: 'Māui !!!' },
legalName: 'Ben',
attendees: {
[Māui]: [{ start: 33, end: null }],
[server2.id]: [{ start: 1, end: null }]
},
tombstone: null
}]
t.deepEqual(

@@ -366,12 +294,5 @@ profile,

recps: null,
states: [{
key: updateId,
preferredName: { author: server2.id, value: 'Māui !!!' },
legalName: 'Ben',
attendees: {
[Māui]: [{ start: 33, end: null }],
[server2.id]: [{ start: 1, end: null }]
},
tombstone: null
}]
states,
...expectedState(states),
conflictFields: []
},

@@ -396,1 +317,91 @@ 'reads decorated info'

})
test('update (malformed attrs)', t => {
const server = Server()
const spec = Spec()
const crut = new CRUT(server, spec)
const tests = [
{
allProps: 'dogo',
expectedErr: 'allProps must be an Object'
},
// {
// allProps: ['dogo'],
// expectedErr: 'allProps must be an Object'
// },
{
allProps: { dogo: 'pup' },
expectedErr: 'unallowed inputs: dogo'
},
{
allProps: null,
expectedErr: 'allProps must be an Object'
},
{
allProps: undefined,
expectedErr: 'allProps must be an Object'
},
{
allProps: 0,
expectedErr: 'allProps must be an Object'
}
]
crut.create({ parent: 'dave' }, (err, recordId) => {
if (err) throw err
pull(
pull.values(tests),
pull.asyncMap(({ allProps, expectedErr }, cb) => {
crut.create(allProps, (err) => {
if (!err) {
return cb(new Error(`Expected to see error '${expectedErr}'`))
}
t.match(err.message, new RegExp(expectedErr, 'g'), expectedErr)
cb(null, null)
})
}),
pull.collect((err) => {
if (err) throw err
server.close()
t.end()
})
)
})
})
test('update, opts = { feedId, publish }', t => {
if (!process.env.DB2) return t.end()
const otherKeys = keys.generate()
const server = Server()
const spec = Spec()
const crut = new CRUT(server, spec, {
feedId: otherKeys.id,
publish: (content, cb) => server.db.publishAs(otherKeys, content, cb)
})
const input = {
parent: 'mix',
attendees: {
add: [{ id: otherKeys.id, seq: 33 }]
}
}
crut.create(input, (err, msgKey) => {
t.error(err, 'creates')
crut.update(msgKey, { preferredName: 'arj' }, (err, updateId) => {
t.error(err, 'publish an update with crut.update')
server.get(updateId, (_, value) => {
t.equal(value.author, otherKeys.id, 'update with proper author')
server.close()
t.end()
})
})
})
})

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc