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

easy CRUT methods for secure scuttlebutt


Version published
Maintainers
3
Created
Source

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 messages
  • ssb-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 the type field that will occur on messages
  • spec.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]
  • 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 + "$"
  • 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 under node.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 or staticProps
  • spec.isValidNextStep Function
    • a function run before writing and during reading to confirm the validity of message extending the tangle

    • signature: fn(tangleContext, node, ssb) => Boolean where

      • tangleContext = { 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 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:

      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 proceed
    • isUpdate 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 which is used to map a full message ({ key, value }) to a transformation
    • default: m => m.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
  • 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 with crut.update
    • during tangle reducing, an empty root node with the right shape is fabricated

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(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 updated
    • allowPublic Boolean (optional) for if you have ssb-guard-recps installed and want to explicitly allow a public message through

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 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: 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
}

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 have ssb-guard-recps installed and want to explicitly allow a public message through

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 conflicts
    • err.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

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 record
    • undo Boolean (optional) set to true to remove the tombstone
    • all/some/none keys declared in spec.nextStepData
    • allowPublic Boolean (optional) for if you have ssb-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 full record)
  • 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|createTime
    • default: receiveTime
  • 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:
      (msg, cb) => {
        crut.read(msg.key, (err, record) => {
          if (err) cb(null, null) // prevents one failed read from causing whole like to fail
          else cb(null, record)
        })
      }
      
  • opts.width Integer
    • how many read functions to run in parallel
    • default: 5

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

Keywords

FAQs

Package last updated on 16 Apr 2023

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

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