ssb-crut
v6.1.1
Published
easy CRUT methods for secure scuttlebutt
Downloads
282
Readme
ssb-crut
Easily mint CRUD (Create, Read, Update, Delete) methods for scuttlebutt records! Note there is no Delete, so instead we have T for Tombstone (CRUT!)
Example usage
const Crut = require('ssb-crut')
const Overwrite = require('@tangle/overwrite')
const SimpleSet = require('@tangle/simple-set')
const spec = {
type: 'gathering',
props: {
title: Overwrite(),
description: Overwrite(),
attendees: SimpleSet()
}
}
const crut = new Crut(ssb, spec) // ssb = an ssb server
crut.create(
{
title: 'Ahau launch party',
attendees: { add: ['Mix'] },
recps: ['%A9OUzXtv7BhaAfSMqBzOO6JC8kvwmZWGVxHDAlM+/so=.cloaked']
// makes it a private record, can only be set on create
},
(err, gatheringId) => {
//
}
)
// later
crut.update(
gatheringId,
{
description: "Let's celebrate this new phase!",
attendees: { add: ['Cherese'] } // props
},
(err, updateId) => {
crut.read(gatheringId, (err, gathering) => {
//
})
}
)
Requirements
You require an ssb instance with some index plugins installed
db1 plugins:
const stack = require('secret-stack')({ caps })
.use(require('ssb-db'))
.use(require('ssb-backlinks'))
.use(require('ssb-query'))
// ...
const ssb = stack(opts)
db2 plugins:
const stack = require('secret-stack')({ caps })
.use(require('ssb-db2'))
.use(require('ssb-db2/compat/db'))
.use(require('ssb-db2/compat/history-stream'))
.use(require('ssb-db2/compat/feedstate'))
// ...
const ssb = stack(opts)
Optionally, if you want to make private (encrypted) records, you will need to have a plugin installed
which knows how to handle your recps
e.g.
ssb-tribes
(recommended) for records stored in a private group, or direct messagesssb-private1
for records as direct messages
API
new CRUT(ssb, spec, opts?) => crut
Takes ssb
, a scuttlebutt server instance, a spec
and an optional
opts and returns an crut instance with methods for mutable records.
A spec
is an Object with properties:
spec.type
String - directly related to thetype
field that will occur on messagesspec.props
Object- 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
) - reserved keys:
allowPublic, conflictFields, key, originalAuthor, reason, recps, states, tangles, tombstone, type, undo
Optional properties:
spec.tangle
String- where the tangle info will be stored
content.tangles[tangle]
- defaults to
type.split('/')[0]
- where the tangle info will be stored
spec.typePattern
String- a regex pattern string which can be used to over-write the default. Useful if you've used regex characters that need escaping in you
spec.type
(e.g.*
) - defaults to :
"^" + spec.type + "$"
- a regex pattern string which can be used to over-write the default. Useful if you've used regex characters that need escaping in you
spec.staticProps
Object- a list of props which are not mutable, and will be baked into root message
- format is in JSON Schema, for example:
{ source: { type: 'string', required: true }, live: { type: 'boolean' } }
- reserved keys:
allowPublic, conflictFields, key, originalAuthor, reason, recps, states, tangles, tombstone, type, undo
spec.nextStepData
Object- a list of props used only in a transformation, and thus will not show up in final state
- can be accessed in
isValidNextStep
undernode.data
- format is in JSON Schema, for example:
{ source: { type: 'string' }, live: { type: 'boolean' } }
- reserved keys:
allowPublic, conflictFields, key, originalAuthor, reason, recps, states, tangles, tombstone, type, undo
- must not include any already defined in either
props
orstaticProps
spec.isValidNextStep
Functiona function run before writing and during reading to confirm the validity of message extending the tangle
signature:
fn(tangleContext, node, ssb) => Boolean
wheretangleContext = { tips, graph, ssb }
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:{ key: MessageId, previous: ['%sd4kasDD...'], data: { title: { set: 'spec gathering' }, attendees: { mix: 1, luandro: 1 } }, author: '@ye+4das...', sequence: 132 }
ssb
the ssb server
used by:
create
to check the message about to be published is valid. In this caseaccT = I
, the (empty) identity transform.update
to check the message about to be published is valid given the existing tangle state it would extendread
to determine which messages are valid to include in reducing
NOTE - in
create
you don't have access totangleContext.graph
, as there's no graph yetADVANCED - if you want to provide a detailed error, you can attach errors to your validator like so:
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:isRoot
Array a collection of validator functions which each root message must pass to proceedisUpdate
Array a collection of validator functions which each update message must pass to proceed- NOTE these validators are expected to have signature
isValid (content) => true|Error
spec.getTransformation
Function- a function of signature
(msg, d)
used to map a full message to a transformationmsg
is a message in the form{ key, value }
d
is an index of the message within a flattened version of the tangle
- default:
(msg) => msg.value.content
- this is particularly useful for coercing legacy messages into a format your spec can process
- WARNING: create a new object if you plan to mutate the shape of a message, otherwise ssb-crut will break
- a function of signature
spec.arbitraryRoot
Boolean- default:
false
- if
true
, then the tangle root of the record can be any messageId - this allows you to attach (for example) a
settings
record to a group root message - disables
crut.create
(as you already have your root created), so you start withcrut.update
- during tangle reducing, an empty root node with the right shape is fabricated
- default:
opts
can have the following properties:
opts.create(input, cb)
, a custom publish function which is expected where:input
is{ content, allowPublic }
content
is the message contentallowPublic
will be present andtrue
ifallowPublic
was passed intocreate/update
. It's there to be used withssb-recps-guard
opts.feedId
, the feedId to publish as. Defaults to ssb.id.opts.alwaysIncludeStates
Boolean (default:false
)- the current version only includes head state of the tangle IF there are > 1 head to the tangle AND there is a conflict between those heads
- setting this to
true
will ensure thatrecord.states
is always populated
crut.create(input, cb)
Makes a new record, and calls back with the id
of that record.
input
Object where key/value can be- all/some/none declared in
spec.props
- all/some/none declared in
spec.staticProps
- all/some/none declared in
spec.nextStepData
recps
Array (optional) a list of recipients who this record will be encrypted to. Once this is set on create, it cannot be updatedallowPublic
Boolean (optional) for if you havessb-guard-recps
installed and want to explicitly allow a public message through
- all/some/none declared in
Notes:
- if
cb
is not passed, a Promise is returned instead.
crut.read(id, cb)
Takes a record id and calls back with a Record.
A tangle here is a collection of messages linked in a directed acyclic graph. Each of thee messages contains some transformation(s) which are an instuction about how to update the record state.
Transformations are concatenated (added up) while traversing the graph.
For a tangle made up of messages linked like this:
A << root
/ \
B C << concurrent updates
|
D << an update which is also a tip
Then the reduced Record would look like:
{
key: A, // the key of the tangle root message
type,
...staticProps, // any staticProp values
...props, // best guess of state (auto-merged states || most recent state)
conflictFields: ['name'] // IF conflict, names trouble fields
states: [ // IF conflect shows the full state of tips
{
key: D, // key of tangle tip message
name: 'Mix' // reified state of props for this tangle tip
// ...
},
{
key: B,
name: 'mixmix',
// ...
}
},
}
There will be 1 or more "states" depending on whether the tangle is a in a branched / forked state at the moment.
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), because often the transformation format is optimised for mathematical properties, but not very human friendly.
Notes:
states
is sorted "most recent" to "least recent" by the tip messages's declared timestamp.- if
cb
is not passed, a Promise is returned instead.
crut.update(id, input, cb)
Updates record id
.
input
Object containing key/values:- all/some/none declared in
spec.props
- all/some/none declared in
spec.nextStepData
allowPublic
Boolean (optional) for if you havessb-guard-recps
installed and want to explicitly allow a public message through
- all/some/none declared in
The props
provided are used to generate a transformation which is then checked with isValidUpdate
(if provided), they are:
Message contents are also checked against isUpdate
before publishing.
Calls back with the key of the update message published.
Notes:
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 conflictserr.conflictFields
- an Array of fields which had conflicts
by default, updates are accepted from everyone. To change this, specify behaviour in
isValidUpdate
e.g.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 return graph.rootNodes.some(root => { return root.value.author === msg.value.author }) }
- see also
ssb-crut-authors
- see also
crut.tombstone(id, input, cb)
A convenience helper mainly here to put the T in CRUT.
input
Object with properties:reason
String (optional) give a reason for why you're tombstoning the recordundo
Boolean (optional) set totrue
to remove the tombstone- all/some/none keys declared in
spec.nextStepData
allowPublic
Boolean (optional) for if you havessb-guard-recps
installed and want to explicitly allow a public message through
Calls back with the key of the update message which tombstoned.
Notes:
- if
cb
is not passed, a Promise is returned instead.
crut.list(opts, cb)
List all records (or some subset) of this crut type from your database.
opts
Object (optional) where:
opts.limit
Integer - how many records you would like (after all filters have been applied)opts.filter
Function - apply a filter to records being returned (is given a fullrecord
)opts.groupId
GroupId (a String)- a convenience filter which only allows records published to a particular group
- NOTE - not yet available for
ssb-db2
opts.orderBy
String- define what order to pull the records in by, looks at the times recorded on the initial message of the record.
- options:
receiveTime
= order by when the record was first received/ written (default)updateTime
= order by how recently writes (create/update) to the record were receivedcreateTime
= order by when the record was created (as asserted by the author)
opts.descending
Boolean- sets whether the receiveTime/createTime/updateTime is descending throughour the results
- default: true
opts.startFrom
String- start reading from a particular message key onwards (it will exclude the provided key)
opts.tombstoned
Boolean (default: false)- only show tombstoned records
opts.read
Function- provide a custom 'read' function which allows e.g. decorating, or installing a cache
- default:
(id, cb) => { crut.read(id, (err, record) => { if (err) cb(null, null) // prevents one failed read from causing whole like to fail else cb(null, record) }) }
- where
recordId
is the id of the root message of the record tangle
- where
opts.width
Integer- how many
read
functions to run in parallel - default:
5
- how many
Using spec.arbitraryRoot with Private Groups
If you set spec.arbitraryRoot
to true, then you can use a groupId
that you're a part of and ssb-crut
will automatically root your record at the group init message.
To use this feature you must be using ssb-tribes >= 2.7.0
The methods you can do this with are:
crut.updateGroup(groupId, input, cb)
crut.readGroup(groupId, cb)
crut.tombstoneGroup(groupId, input, cb)
Reminder there is no crut.create
when you have an arbitraryRoot