# ssb-profile

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

Latest version **7.0.2** (published 2024-04-10) · AGPL-3.0 license · 0 weekly downloads

## Install

```sh
npm install ssb-profile
pnpm add ssb-profile
yarn add ssb-profile
bun add ssb-profile
```

## Health

**Score 15/100 (F)** — status: abandoned.

Positive: no vulnerabilities.

Warnings: low downloads; no types; no esm support.

Negative: abandoned; low maintenance score.

## Facts

| | |
|---|---|
| Version | 7.0.2 |
| Published | 2024-04-10 |
| First published | 2019-10-21 |
| Weekly downloads | 0 |
| License | AGPL-3.0 |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 15 |
| Unpacked size | 206.9 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | mixmix |
| Maintainers | powersource, arj03, staltz, mixmix, cel, ben-tai, chereseeriepa, luandro |
| Keywords | scuttlebutt, ahau, profile |

## Links

- npm: https://www.npmjs.com/package/ssb-profile
- Repository: https://gitlab.com/ahau/ssb-profile
- Homepage: https://gitlab.com/ahau/ssb-profile#readme
- Issues: https://gitlab.com/ahau/ssb-profile/issues
- npm.io page: https://npm.io/package/ssb-profile

## Dependencies (15)

- [edtf](https://npm.io/package/edtf.md) ^2.8.0
- [ssb-ref](https://npm.io/package/ssb-ref.md) ^2.16.0
- [charwise](https://npm.io/package/charwise.md) ^3.0.1
- [ssb-crut](https://npm.io/package/ssb-crut.md) ^6.0.0
- [pile-sort](https://npm.io/package/pile-sort.md) ^1.0.2
- [lodash.get](https://npm.io/package/lodash.get.md) ^4.4.2
- [pull-stream](https://npm.io/package/pull-stream.md) ^3.7.0
- [pull-paramap](https://npm.io/package/pull-paramap.md) ^1.2.2
- [is-my-json-valid](https://npm.io/package/is-my-json-valid.md) ^2.20.6
- [ssb-crut-authors](https://npm.io/package/ssb-crut-authors.md) ^4.0.0
- [@tangle/overwrite](https://npm.io/package/@tangle/overwrite.md) ^3.0.1
- [@tangle/simple-set](https://npm.io/package/@tangle/simple-set.md) ^3.0.1
- [@tangle/complex-set](https://npm.io/package/@tangle/complex-set.md) ^3.0.3
- [ssb-schema-definitions](https://npm.io/package/ssb-schema-definitions.md) ^3.2.1
- [@tangle/overwrite-fields](https://npm.io/package/@tangle/overwrite-fields.md) ^2.0.3

## Recent versions

- 7.0.2 (latest) — 2024-04-10
- 7.0.1 — 2024-04-09
- 6.0.1 — 2024-03-04
- 5.20.1 — 2024-03-03
- 7.0.0 — 2024-02-01
- 6.0.0 — 2023-12-13
- 5.20.0 — 2023-12-05
- 5.19.0 — 2023-12-01
- 5.18.0 — 2023-07-12
- 5.17.0 — 2023-07-12
- 5.16.0 — 2023-07-04
- 5.15.0 — 2023-01-23
- 5.14.0 — 2022-09-05
- 5.13.0 — 2022-07-07
- 5.12.0 — 2022-06-30
- … 68 more at https://npm.io/package/ssb-profile/versions

## README

# ssb-profile

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

## Example Usage

```js
 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) => {
   // ...
 })
```

```js
// 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


```mermaid
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:

```js
{
  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*:

```js
{
  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:

```js
{
  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:


```js
{
  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:

```js
// 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:
    ```js
    // 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:

```js
{
  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`:
            ```js
            {
              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:

```js
{
  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:

```js
[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:
          ```js
          {
            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:

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

and `Link` is:

```js
{
  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*:

  ```js
  {
    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](https://www.npmjs.com/package/edtf) and [library of congress spec](http://www.loc.gov/standards/datetime/)
- `Tombstone` **Object**

  ```js
  {
    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](https://en.wikipedia.org/wiki/Unix_time))
- `CustomForm` *[FormField]* - used generate custom form for people applying to join a community. e.g
  
  ```js
  [
    { type: 'input', label: 'Who introduced you?' },
    { type: 'textarea', label: 'Please tell use about yourself' },
  ]
  ```

  - `FormField` *Object* of shape:

    ```js
    {
      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
      ```js
      {
        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](https://github.com/ssbc/ssb-blobs) and [ssb-hyper-blobs](https://gitlab.com/ahau/lib/ssb-plugins/ssb-hyper-blobs)

## Record types

```mermaid
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

---
_Source: https://npm.io/package/ssb-profile · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
