ssb-profile v7.0.2
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]
 
- excludes: 
- 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)
 
- same as 
- ssb.profile.person.public.*- only: [preferredName, avatarImage]
- no recps
 
- only: 
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-..->adminThis 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:
- authorsis a special field which defines permissions for updates- you must set authors.addwhen creating a record
 
- you must set 
- This type is deliberatly quite limited, to avoid accidental sharing of private data.
- All fields (apart from authors) can also be set tonull
- 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:
- authorsis a special field which defines permissions for updates- you must set authors.addwhen creating a record
 
- you must set 
- recpsis required when creating, but updates copy the initial- recps
- All fields (apart from authors,altNames) can also be set tonull
- 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
- 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:
- authorsis a special field which defines permissions for updates- you must set authors.addwhen creating a record
 
- you must set 
- All fields (apart from authors) can also be set tonull
- POBoxIdis a- Stringcipherlink that can be used in recps by anyone, to send messages only those with the secret key can open
- customFieldsare 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:
- recpsis required when creating, but updates copy the initial- recps
- authorsis a special field which defines permissions for updates- you must set authors.addwhen creating a record
 
- you must set 
- All fields (apart from authors) can also be set tonull
- POBoxIdis a- Stringcipherlink 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
- recpsis the private recipients who can access the profile
- statesState - 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)
- keyMessageId is the key of the message which is the most recent edit
- stateis 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:
- authorsreturns 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)
 
- these are sequence numbers from the authors feed (unless the author is 
- altNamesreturns an Array of names (ordered is not guarenteed)
ssb.profile.link.create(profileId, opts, cb)
where
- profileIdMessageId is the profile you're creating a link to
- optsObject (optional) allows you to tune the link:- opts.feedIdFeedId if provided creates a- link/feed-profilewith provided feedId instead of current ssb instance's feedId
- opts.groupIdGroupId creates a- link/group-profile
- opts.profileIdMsgId creates a- link/profile-profile/admin(set- profileIdto be the group profile,- opts.profileIdto be the admin profile)
- opts.allowPublicBoolean (optional) - if you have- ssb-recps-guardinstalled and want to bypass it for a public (unencrypted) link
 
- cbFunction - callback with signature- (err, link)where- linkis the link message
Note:
- if you link to a private profile, the link will be encrypted to the same recpsas that profile
- if you provide opts.feedIdandopts.groupIdyou will get an error
Find methods
ssb.profile.find(opts, cb)
Arguments:
- optsObject - an options object with properties:- opts.nameString - a name (or fragment of) that could be part of a- preferredNameor- legalNameor- altNames
- opts.typeString (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.includeTombstonedBoolean (optional) - whether to include profiles which habe been tombstoned (default:- false)
 
- cbFunction - a callback with signature- (err, suggestions)where- suggestionsis 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
 
- callback with 
- opts.groupIdGroupId - only return profiles that exist in a particular private group
- opts.sortPublicPrivateBoolean - whether to sort into- { public, private }- default: true
- if falsereturns an Array of profiles
 
- default: 
- opts.selfLinkOnlyBoolean - only include profiles where the- linkmessage was authored by the- feedId- default: true
- if false, public and private groupings are further split intoselfandother:{ self: { public: [Profile], private: [Profile] }, other: { public: [Profile], private: [Profile] } }
- if falseyou 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
 
 
- default: 
 
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
 
- callback with 
 
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.selfLinkOnlyBoolean - only include profiles where the- linkmessage was authored by the- feedId- default: true
- if falsereturns results in format:{ self: [FeedId, ...], // feeds that have link themselves to the profile other: [FeedId, ...] // feeds that another person has linked to the profile }
 
- default: 
 
- 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
- AuthorString a- FeedIdor- "*"(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
 
- RecpString a "recipient", usually a- FeedIdor- GroupId- the record will be encrypted so only that recipient(s) can access the record
- requires ssb-tribesto be installed as a plugin
 
- ImageObject:- { 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 }
- GenderString (male|female|other|unknown)
- ProfileSourceString (ahau|webForm). A- ProfileSourceis 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
- TombstoneObject- { date: UnixTime, // an Integer indicating microseconds from 1st Jan 1970, can be negative! reason: String // (optional) }
- UnixTimeInteger microseconds since 00:00 1st Jan 1970 (can be negative, read more)
- CustomFormFormField - 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' }, ]- FormFieldObject of shape:- { type: FieldType, // String: input|textarea label: String }
 
- CustomFields{ key: CustomFieldDef } - defines the custom fields that person profiles within the group will use- keyUnixTime (see example above)
- CustomFieldDefObject 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- textstring value
- arraymultiple response value
- listvalue containing one or more values from the defined options
- checkboxboolean value
- fileblob values to store files
 
 
- Valid types
 
- BlobObject - 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 cipherlinkNote - 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, .updateextends the most recent branch
 
- by default, 
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
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago