An secret-stack
plugin for creating, reading, updating profiles in scuttlebutt
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) => {
// ...
})
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
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-..->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
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
- 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.
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
- you must set
-
recps
is required when creating, but updates copy the initialrecps
- 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-
key
UnixTime -
value
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] // 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
- you must set
- All fields (apart from
authors
) can also be set tonull
-
POBoxId
is aString
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
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 initialrecps
-
authors
is a special field which defines permissions for updates- you must set
authors.add
when creating a record
- you must set
- All fields (apart from
authors
) can also be set tonull
-
POBoxId
is aString
cipherlink that can be used in recps by anyone, to send messages only those with the secret key can open - See below for Types
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)
- these are sequence numbers from the authors feed (unless the author is
-
altNames
returns an Array of names (ordered is not guarenteed)
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 alink/feed-profile
with provided feedId instead of current ssb instance's feedId -
opts.groupId
GroupId creates alink/group-profile
-
opts.profileId
MsgId creates alink/profile-profile/admin
(setprofileId
to be the group profile,opts.profileId
to be the admin profile) -
opts.allowPublic
Boolean (optional) - if you havessb-recps-guard
installed and want to bypass it for a public (unencrypted) link
-
-
cb
Function - callback with signature(err, link)
wherelink
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
andopts.groupId
you will get an error
Arguments:
-
opts
Object - an options object with properties:-
opts.name
String - a name (or fragment of) that could be part of apreferredName
orlegalName
oraltNames
-
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 tonull
, 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)
wheresuggestions
is an array of Profiles
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. signaturegetProfile(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.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
- default:
-
opts.selfLinkOnly
Boolean - only include profiles where thelink
message was authored by thefeedId
- default:
true
- if
false
, public and private groupings are further split intoself
andother
:{ 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
- default:
-
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. signaturegetProfile(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
-
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 thelink
message was authored by thefeedId
- 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 }
- default:
-
- alias
ssb.profile.findFeedsByProfile
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]
}
-
Author
String aFeedId
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 aFeedId
orGroupId
- 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). AProfileSource
is an enum explaining where this profile came from e.g.ahau
- it was created inahau
.webForm
- it was created using awebForm
-
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
-
- Valid types
-
-
Blob
Object - the blob object for the uploaded media, see ssb-blobs and ssb-hyper-blobs
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)
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
- by default,
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