npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ssb-profile

v7.0.2

Published

A helper module for reading and writing to "profile threads" in scuttlebutt

Downloads

69

Readme

ssb-profile

An secret-stack plugin for creating, reading, updating profiles in scuttlebutt

Example Usage

 const Stack = require('secret-stack')
 const caps = require('ssb-caps')
 const Config = require('ssb-config/inject')
 
 const config = Config({})
 
 const ssb = Stack({ caps })
   .use(require('ssb-db'))
+  .use(require('ssb-backlinks')) // required
+  .use(require('ssb-query')) //     required
+  .use(require('ssb-profile'))
   .use(require('ssb-tribes')) //    (optonal) support for private messages
   .call(null, config)
 
 const details = {
   preferredName: 'Ben',
   avatarImage: {
     blob: '&CLbw5B9d5+H59oxDNOy4bOkwIaOhfLfqOLm1MGKyTLI=.sha256',
     mimeType: 'image/png'
   }
 }

 ssb.profile.person.public.create(details, (err, profileId) => {
   // ...
 })
// later:
ssb.profile.person.public.get(profileId, (err, profile) => {
  // ...
})

// or:
const update = {
  preferredName: 'Ben Tairea',
}

ssb.profile.person.public.update(profileId, update, (err, updateMsg) => {
  // ...
})

Requirements

secret-stack instance running the following plugins:

  • ssb-db2/core
  • ssb-classic
  • ssb-db2/compat
  • ssb-db2/compat/publish
  • ssb-db2/compat/feedstate
  • ssb-box2
  • ssb-tribes

API

NOTE - all update methods currently auto-resolve any branch conflicts for you (if they exist) The "winner" for conflicting fields is chosen from the tip that was most recently updated.

Profiles for people:

  • ssb.profile.person.source.*
    • has every field
    • recps must be [group]
  • ssb.profile.person.group.*
    • excludes: [phone, address, email]
    • recps must be [group]
  • ssb.profile.person.admin.*
    • same as source, but admins can post updates to it too.
    • recps must be [poBoxId, feedId] (for someone sending something to admins who wants to be part of updates) OR [groupId] (for something that is one-way to admins/ admin-only)
  • ssb.profile.person.public.*
    • only: [preferredName, avatarImage]
    • no recps
graph TB
  subgraph Personal group
    source
  end
  
public(public)
  
  subgraph Family group
    group(group)
    subgraph kaitiaki group
      admin(admin)
    end
    
  end
  
source-..->public
source-..->group
source-..->admin

This graph show how Āhau uses these profiles. Dotted lines show how updates to the source profile are propogate to the others.

Profiles for communities

  • ssb.profile.community.public.* - public community profile
  • ssb.profile.community.group.* - encrypted community profile

Profiles for pataka

  • `ssb.profile.pataka.public*- public pataka profile

Person profile (PUBLIC)

Handles public facing (unencrypted) profiles of type profile/person.

ssb.profile.person.public

  • .create(details, cb)
  • .get(profileId, cb)
  • .update(profileId, details, cb)
  • .tombstone(profileId, details, cb)

Here details is an Object which allows:

{
  authors: {
    add: [Author]    // required on .create
    remove: [Author]
  },

  preferredName: String,
  gender: Gender,
  source: ProfileSource,
  avatarImage: Image,

  tombstone: Tombstone

  // allowPublic: Booelan // if using ssb-recps-guard
}

NOTES:

  • authors is a special field which defines permissions for updates
    • you must set authors.add when creating a record
  • This type is deliberatly quite limited, to avoid accidental sharing of private data.
  • All fields (apart from authors) can also be set to null
  • See below for types.

Person group profile

Handles encrypted profiles of type profile/person.

ssb.profile.person.group

  • .create(details, cb)
  • .get(profileId, cb)
  • .update(profileId, details, cb)
  • .tombstone(profileId, details, cb)
  • .findAdminProfileLinks(groupProfileId, opts, cb) (see below)

Here details is an Object:

{
  recps: [Recp],     // required
  authors: {
    add: [Author]    // required on .create
    remove: [Author]
  },

  preferredName: String,
  legalName: String,
  altNames: {
    add: [String],
    remove: [String]
  },

  avatarImage: Image,
  headerImage: Image,

  description: String, 
  gender: Gender,
  source: ProfileSource,

  aliveInterval: EdtfIntervalString,
  deceased: Boolean,
  placeOfBirth: String,
  placeOfDeath: String,
  buriedLocation: String,
  birthOrder: Int,

  profession: String,
  education: [String], // overwrites last Array of Strings
  school: [String],    // overwrites last Array of Strings

  address: String, 
  city: String,
  country: String,
  postCode: String,
  phone: String,
  email: String,
  
  customFields: [CustomField]

  tombstone: Tombstone
}

NOTES:

  • authors is a special field which defines permissions for updates
    • you must set authors.add when creating a record
  • recps is required when creating, but updates copy the initial recps
  • All fields (apart from authors, altNames) can also be set to null
  • CustomField { [key]: value } - A custom field is a field on a persons profile which can have a value of multiple types. The person profile will use what is defined on the community profiles to provide its own value for that field
    • key UnixTime
    • value String | [String] | Boolean | EdtfDate | Number | Blob | [Blob]
  • See below for Types

Community profile (PUBLIC)

Handles public facing (unencrypted) profiles of type profile/community.

ssb.profile.community.public

  • .create(details, cb)
  • .get(profileId, cb)
  • .update(profileId, details, cb)
  • .tombstone(profileId, details, cb)

Here details is an Object which allows:

{
  authors: {
    add: [Author]    // required on .create
    remove: [Author],
  },

  preferredName: String,
  description: String,

  avatarImage: Image,
  headerImage: Image,

  address: String,
  city: String,
  country: String,
  postCode: String,
  phone: String,
  email: String,

  // these two fields are only on public community profiles
  joiningQuestions: CustomForm, 
  customFields: CustomFields

  tombstone: Tombstone,

  poBoxId: POBoxId // public part of the poBoxId for a subgroup


  // allowPublic: Boolean       // if using ssb-recps-guard
}

NOTES:

  • authors is a special field which defines permissions for updates
    • you must set authors.add when creating a record
  • All fields (apart from authors) can also be set to null
  • POBoxId is a String cipherlink that can be used in recps by anyone, to send messages only those with the secret key can open
  • customFields are defined on the public community profile and then you use those definitions for what you fill in on the person profile
  • See below for Types

Community profile (GROUP)

Handles encrypted profiles of type profile/community and is for use within a group.

ssb.profile.community.group

  • .create(details, cb)
  • .get(profileId, cb)
  • .update(profileId, details, cb)
  • .tombstone(profileId, details, cb)

Here details is an Object of form:

{
  recps: [Recp],     // required
  authors: {
    add: [Author]    // required on .create
    remove: [Author],
  },

  preferredName: String,
  description: String,

  avatarImage: Image,
  headerImage: Image,

  address: String,
  city: String,
  country: String,
  postCode: String,
  phone: String,
  email: String,

  // private settings
  // only on the group community profile
  allowWhakapapaViews: Boolean,
  allowPersonsList: Boolean,
  allowStories: Boolean,

  // public settings
  // only on the public community profile
  acceptsVerifiedCredentials: Boolean,
  issuesVerifiedCredentials: Boolean,

  tombstone: Tombstone,

  poBoxId: POBoxId
}

NOTES:

  • recps is required when creating, but updates copy the initial recps
  • authors is a special field which defines permissions for updates
    • you must set authors.add when creating a record
  • All fields (apart from authors) can also be set to null
  • POBoxId is a String cipherlink that can be used in recps by anyone, to send messages only those with the secret key can open
  • See below for Types

How get methods work

Because there might be multiple offline edits to a profile which didn't know bout one-another, it's possible for divergence to happen:

   A   (the root message)
   |
   B   (an edit after A)
  / \
 C   D (two concurrent edits after B)

profile is an Object which maps the key of a each latest edit to the state it perceives the profile to be in! So for that prior example:

// profile
{
  key: MessageId,          // the root message of the profile tangle, aka profileId
  type: ProfileType,
  recps: [Recp],           // recipients (will be null on public records)
  originalAuthor: FeedId
  ...state,                // the best guess of the current state of each field
  states: [                // (advanced) in depth detail about the state of all tips
    { key: C, ...state },  //
    { key: D, ...state },
  ],
  conflictFields: [String] // a list of any fields which are in conflict
}

where

  • recps is the private recipients who can access the profile
  • states [State] - the one / multiple states in which the profile is in:
    • these are sorted from most to least recent edit (by asserted publishedDate on the last update message)
    • key MessageId is the key of the message which is the most recent edit
    • state is an object which shows what the state of the profile is (from the perspective of a person standing at that particular "head")
    • e.g. for some Public Person profile, it might look like:
      // State
      {
        type: 'person'                      // added to help you out
      
        authors: {
          '@xIP5FV16FwPUiIZ0TmINhoCo4Hdx6c4KQcznEDeWtWg=.ed25519': [
            { start: 203, end: Integer }
          ]
        },
        preferredName: 'Ben Tairea',
        gender: 'male', 
        source: 'ahau',                          
        tombstone: null                     // all profile fields are present, are "null" if unset
      }

Fields which get reduced:

  • authors returns a collection of authors, and "intervals" for which that author was active
    • these are sequence numbers from the authors feed (unless the author is "*" in which case it's a time-stamp)
  • altNames returns an Array of names (ordered is not guarenteed)

ssb.profile.link.create(profileId, opts, cb)

where

  • profileId MessageId is the profile you're creating a link to
  • opts Object (optional) allows you to tune the link:
    • opts.feedId FeedId if provided creates a link/feed-profile with provided feedId instead of current ssb instance's feedId
    • opts.groupId GroupId creates a link/group-profile
    • opts.profileId MsgId creates a link/profile-profile/admin (set profileId to be the group profile, opts.profileId to be the admin profile)
    • opts.allowPublic Boolean (optional) - if you have ssb-recps-guard installed and want to bypass it for a public (unencrypted) link
  • cb Function - callback with signature (err, link) where link is the link message

Note:

  • if you link to a private profile, the link will be encrypted to the same recps as that profile
  • if you provide opts.feedId and opts.groupId you will get an error

Find methods

ssb.profile.find(opts, cb)

Arguments:

  • opts Object - an options object with properties:
    • opts.name String - a name (or fragment of) that could be part of a preferredName or legalName or altNames
    • opts.type String (optional)
      • if set, method will only return profiles of given type
      • Valid types:
        • 'person' 'person/admin' 'person/source' 'community' 'pataka'
        • null - if set to null, will return all types
      • default: 'person'
    • opts.groupId String (optional)
      • only returns results encrypted to a particular group
      • if it's a GroupId, and that group has a poBoxId, profiles encrypted to both are included
      • id it's a POBoxId, then just profiles encrypted to that P.O. Box will be included
    • opts.includeTombstoned Boolean (optional) - whether to include profiles which habe been tombstoned (default: false)
  • cb Function - a callback with signature (err, suggestions) where suggestions is an array of Profiles

ssb.profile.findByFeedId(feedId, cb)

Takes a feedId and calls back with all profiles which that feedId has linked to it. Signature of cb is cb(err, profiles) where profiles is of form:

{
  public: [Profile],
  private: [Profile]
}

NOTE:

  • profiles which have been tombstoned are not included in results
  • profiles are ordered from oldest to newest in terms of when they were linked to the feedId
  • advanced : ssb.profile.findByFeedId(feedId, opts, cb)
    • opts.getProfile - provide your own getter. signature getProfile(profileId, cb)
      • callback with cb(null, null) if you want to exclude a result
      • useful if you want to add a cache to your getter, or only allow certain types of profile
    • opts.groupId GroupId - only return profiles that exist in a particular private group
    • opts.sortPublicPrivate Boolean - whether to sort into { public, private }
      • default: true
      • if false returns an Array of profiles
    • opts.selfLinkOnly Boolean - only include profiles where the link message was authored by the feedId
      • default: true
      • if false, public and private groupings are further split into self and other:
        {
          self: { public: [Profile], private: [Profile] },
          other: { public: [Profile], private: [Profile] }
        }
      • if false you get profiles that anyone has linked to that feedId,
        • WARNING links asserted by others could be malicious
        • if you trust your context this can be a useful fallback

ssb.profile.findByGroupId(groupId, cb)

Takes a groupId and calls back with all profiles which that feedId has linked to it. Signature of cb is cb(err, profiles) where profiles is of form:

{
  public: [Profile],
  private: [Profile]
}

NOTE:

  • profiles which have been tombstoned are not included in results
  • profiles are ordered from oldest to newest in terms of when they were linked to the feedId
  • advanced you can call this with ssb.profile.findByGroupId(feedId, opts, cb)
    • opts.getProfile - provide your own getter. signature getProfile(profileId, cb)
      • callback with cb(null, null) if you want to exclude a result
      • useful if you want to add a cache to your getter, or only allow certain types of profile

ssb.profile.findFeedsByProfileId(profileId, cb)

Takes a profileId and calls back with all the feedIds which that profileId has linked to it. Signature of cb is cb(err, feeds) where feeds is of form:

[FeedId, FeedId, ...]

NOTE:

  • advanced : ssb.profile.findFeedsByProfile(profileId, opts, cb)
    • opts.selfLinkOnly Boolean - only include profiles where the link message was authored by the feedId
      • default: true
      • if false returns results in format:
        {
          self: [FeedId, ...],  // feeds that have link themselves to the profile
          other: [FeedId, ...]  // feeds that another person has linked to the profile
        }
  • alias ssb.profile.findFeedsByProfile

ssb.profile.person.group.findAdminProfileLinks(profileId, opts, cb)

Takes a profileId (person group profileId) and calls back with the parentLinks and childLinks which that profileId has linked to it. Signature of cb is cb(err, links) where links is of form:

{
  parentLinks: [Link],
  childLinks: [Link]
}

and Link is:

{
  key: MsgId,
  type: 'link/profile-profile/admin',
  parent: MsgId,
  child: MsgId,
  states: [{ key: MsgId, tombstone: Tombstone }]
  originalAuthor: FeedId,
  recps: [GroupId]
}

Types

  • Author String a FeedId or "*" (i.e. any user)

    • any updates that arent from a valid author are classed as invalid and will be ignored when using the get method
  • Recp String a "recipient", usually a FeedId or GroupId

    • the record will be encrypted so only that recipient(s) can access the record
    • requires ssb-tribes to be installed as a plugin
  • Image Object:

    {
      blob: Blob,        // the content address for the blob (with prefex &)
      mimeType: String,  // mimetype of the image
      unbox: UnboxKey,   // (optional) a String for unboxing the blob if encrypted
      size: Number,      // (optional) size of the image in bytes
      width: Number,     // (optional) width of image in pixels
      height: Number     // (optional) height of image in pixels
    }
  • Gender String (male|female|other|unknown)

  • ProfileSource String (ahau|webForm). A ProfileSource is an enum explaining where this profile came from e.g. ahau - it was created in ahau. webForm - it was created using a webForm

  • EdtfIntervalString - see edtf module and library of congress spec

  • Tombstone Object

    {
      date: UnixTime,  // an Integer indicating microseconds from 1st Jan 1970, can be negative!
      reason: String   // (optional)
    }
  • UnixTime Integer microseconds since 00:00 1st Jan 1970 (can be negative, read more)

  • CustomForm [FormField] - used generate custom form for people applying to join a community. e.g

    [
      { type: 'input', label: 'Who introduced you?' },
      { type: 'textarea', label: 'Please tell use about yourself' },
    ]
    • FormField Object of shape:

      {
        type: FieldType, // String: input|textarea
        label: String
      }
  • CustomFields { [key]: CustomFieldDef } - defines the custom fields that person profiles within the group will use

    • key UnixTime (see example above)
    • CustomFieldDef Object of shape
      {
        type: String, // text|array|list|checkbox|file
        label: String,
        order: Number,
        required: Boolean,
        visibleBy: String, // members|admin
      
        // NOTE: these fields are used when type=list
        options: [String],
        multiple: Boolean
      
        // NOTE: these fields are used when type=file
        fileTypes: [String] // document|video|audio|photo
        description: String, // a helpful descriptions may be needed when uploading files
        multiple: Boolean
      }
      • Valid types
        • text string value
        • array multiple response value
        • list value containing one or more values from the defined options
        • checkbox boolean value
        • file blob values to store files
  • Blob Object - the blob object for the uploaded media, see ssb-blobs and ssb-hyper-blobs

Record types

graph TB

%% ssb.profile

%% cipherlinks
feedId(feedId)
groupId(groupId)

%% public profiles
personPublic[profile/person]
communityPublic[profile/community]

%% public links
linkPersonPublic([link/feed-profile])
linkCommunityPublic([link/group-profile])

%% pataka[profile/pataka]

subgraph group
  communityGroup[profile/community]
  personGroup[profile/person<br/>]
 
  %% links encrypted to the group
  linkPersonGroup([link/feed-profile])
  linkCommunityGroup([link/group-profile])
  linkPersonPersonAdmin([link/profile-profile/admin])
 
  subgraph admin
    personAdmin[profile/person/admin]
 
    %% links encrypted to the admins
    linkPersonAdmin([link/feed-profile])
  end
end

%% connecting links
feedId -..-> linkPersonPublic -..-> personPublic
feedId -.-> linkPersonGroup -.-> personGroup
feedId -.-> linkPersonAdmin -.-> personAdmin

personAdmin -.-> linkPersonPersonAdmin -.-> personGroup

groupId -..-> linkCommunityPublic -..-> communityPublic
groupId -..-> linkCommunityGroup -..-> communityGroup

%% styling
classDef default fill:#990098, stroke:purple, stroke-width:1, color:white, font-family:sans, font-size:14px;

classDef cluster fill:#1fdbde55, stroke:#1fdbde;
classDef path stroke: blue;

classDef encrypted fill:#ffffffaa, stroke:purple, stroke-width:1, color:black, font-family:sans, font-size:14px;
classDef cipherlink fill:#0000ff33, stroke:purple, stroke-width:0, color:#00f, font-family:sans, font-size: 14px;

class personGroup,personAdmin,communityGroup,linkPersonGroup,linkPersonAdmin,linkCommunityGroup,linkPersonPersonAdmin encrypted;
class feedId,groupId cipherlink

Note - you only have link/profile-profile/admin for "unowned" profile (i.e. no link/feed-profile is present)

FAQ

I want to delete my legalName, how do?

  • first, know that if you previously published a legalName it will always be part of your record (even if it's not currently displayed)
  • if you want to clear a text field, just publish an update with null value: { legalName: null }

How do I clear an image?

  • same as with legalName - set it to null

Multiple editors for a profile?

  • work in progress!
  • currently supports multiple writers, but does not support merging of branched state
    • by default, .update extends the most recent branch

Development

Project layout (made with tree):

.
├── index.js           // ssb-server plugin (collects all methods)
├── method             // user facing methods
├── spec               // describes message + how to reduce them
│   ├── person
│   │   ├── source
│   │   ├── group
│   │   ├── admin
│   │   └── private
│   ├── community
│   │   ├── group
│   │   └── public
│   ├── pataka
│   │   
│   ├── link
│   │   ├── feed-profile
│   │   ├── group-profile
│   │   └── profile-profile-admin
│   └── lib
│
└── test               // tests!

run npm test to run tests