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'))
+ .use(require('ssb-query'))
+ .use(require('ssb-profile'))
.use(require('ssb-tribes'))
.call(null, config)
const details = {
preferredName: 'Ben',
avatarImage: {
blob: '&CLbw5B9d5+H59oxDNOy4bOkwIaOhfLfqOLm1MGKyTLI=.sha256',
mimeType: 'image/png'
}
}
ssb.profile.person.public.create(details, (err, profileId) => {
})
ssb.profile.person.public.get(profileId, (err, profile) => {
})
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 profilessb.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]
remove: [Author]
},
preferredName: String,
gender: Gender,
source: ProfileSource,
avatarImage: Image,
tombstone: Tombstone
}
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],
authors: {
add: [Author]
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],
school: [String],
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
UnixTimevalue
String | [String] | Boolean | EdtfDate | Number | Blob | [Blob]
- See below for Types
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]
remove: [Author],
},
preferredName: String,
description: String,
avatarImage: Image,
headerImage: Image,
address: String,
city: String,
country: String,
postCode: String,
phone: String,
email: String,
joiningQuestions: CustomForm,
customFields: CustomFields
tombstone: Tombstone,
poBoxId: POBoxId
}
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 opencustomFields
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
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],
authors: {
add: [Author]
remove: [Author],
},
preferredName: String,
description: String,
avatarImage: Image,
headerImage: Image,
address: String,
city: String,
country: String,
postCode: String,
phone: String,
email: String,
allowWhakapapaViews: Boolean,
allowPersonsList: Boolean,
allowStories: Boolean,
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:
{
key: MessageId,
type: ProfileType,
recps: [Recp],
originalAuthor: FeedId
...state,
states: [
{ key: C, ...state },
{ key: D, ...state },
],
conflictFields: [String]
}
where
recps
is the private recipients who can access the profilestates
[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 editstate
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:
{
type: 'person'
authors: {
'@xIP5FV16FwPUiIZ0TmINhoCo4Hdx6c4KQcznEDeWtWg=.ed25519': [
{ start: 203, end: Integer }
]
},
preferredName: 'Ben Tairea',
gender: 'male',
source: 'ahau',
tombstone: null
}
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 toopts
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 feedIdopts.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 groupopts.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
- 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,
mimeType: String,
unbox: UnboxKey,
size: Number,
width: Number,
height: Number
}
-
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,
reason: String
}
-
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' },
]
-
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,
label: String,
order: Number,
required: Boolean,
visibleBy: String,
options: [String],
multiple: Boolean
fileTypes: [String]
description: String,
multiple: Boolean
}
- Valid types
text
string valuearray
multiple response valuelist
value containing one or more values from the defined optionscheckbox
boolean valuefile
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