Sharing
Share vault records with other accounts through invites, and manage who has access.
Preview
This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.
The owner of a record shares it by creating an invite. The recipient previews the invite, accepts
it with the invite's secret and, when the invite asks for one, a passcode. From then on the record
appears in the recipient's vault with permission View. The owner can list and revoke invites
and take access away again.
Sharing runs through the same store as the rest of the vault: dispatch VaultAction.Share with a
SharingRequest on VaultStore and read the result from the sharing field of its
state. Sharing needs a signed-in session: in a transient session every sharing request fails with
TransientNotSupported.
How an invite travels
- The owner dispatches
CreateInvitefor one of their records. The result,sharing.created, holds the invite and itssecret. - Your app delivers the invite's
linkIdand thesecretto the recipient, and the passcode, if the invite has one, through a separate channel. The SDK does not build or send the message. - The recipient dispatches
PreviewInvitewith thelinkId. The result,sharing.preview, shows the invite, including itsid,statusand whether it asks for a passcode. - The recipient dispatches
AcceptInvitewith the inviteid, thesecretand the passcode. The SDK unwraps the record key with the secret, stores the record in the recipient's vault and setssharing.acceptedRecordId.
Only the holder of the secret can open the record: the invite carries the record key sealed under a key derived from the secret, and the secret itself never reaches Folio in the clear.
Requests
| Request | Fields | Result in sharing |
|---|---|---|
LoadInvites | none | owned and incoming are replaced. |
LoadAccess | recordId: String | access is replaced with the grants of that record. |
CreateInvite | recordId: String, invite: InviteRequest | created is set to the new invite and its secret. |
PreviewInvite | linkId: String | preview is set to the invite. |
AcceptInvite | inviteId: String, secret: String, passcode: String (optional) | preview is cleared and acceptedRecordId is set. |
RejectInvite | inviteId: String | The invite is rejected; owned and incoming are reloaded. |
RevokeInvite | recordId: String, inviteId: String | The invite is revoked; owned and incoming are reloaded. |
RevokeAccess | recordId: String, accountId: String | The account loses access; access is reloaded for the record. |
On iOS the requests are lowerCamelCase cases with labeled fields, such as
.acceptInvite(inviteId:secret:passcode:); on Android they are classes of SharingRequest, such as
SharingRequest.AcceptInvite(inviteId, secret, passcode), and SharingRequest.LoadInvites is an
object; on the web they are functions of SharingRequest, such as
SharingRequest.acceptInvite(inviteId, secret, passcode), and SharingRequest.loadInvites is a
value. Wrap each request in VaultAction.Share before you dispatch it.
LoadInvites and LoadAccess return up to 100 entries each. A failed request leaves sharing
as it was and sets error in the vault state; a successful one clears error. busy is true
while a request runs.
let invite = InviteRequest(audience: .open(multiUse: false), expiresAt: nil, passcode: "4821")
try vault.dispatch(action: .share(value: .createInvite(recordId: record.header.id, invite: invite)))
try vault.dispatch(action: .share(value: .previewInvite(linkId: linkId)))
try vault.dispatch(
action: .share(value: .acceptInvite(inviteId: preview.id, secret: secret, passcode: "4821"))
)InviteRequest
| Field | Type | Meaning |
|---|---|---|
audience | InviteAudience | Who can accept the invite. |
expiresAt | i64 (optional) | When the invite expires, seconds since the Unix epoch. Without a value the invite does not expire. |
passcode | String (optional) | A passcode the recipient must give to accept. The invite carries only a hash of it. |
InviteAudience
| Case | Fields | Meaning |
|---|---|---|
Open | multiUse: Bool | Anyone with the link and the secret can accept. multiUse asks for an invite that more than one account can accept. |
Account | accountId: String | Only the named account can accept. |
On iOS the cases are .open(multiUse:) and .account(accountId:), on Android
InviteAudience.Open(multiUse) and InviteAudience.Account(accountId), and on the web
InviteAudience.open(multiUse) and InviteAudience.account(accountId), which build
{ type: 'OPEN', value: { multiUse } } and { type: 'ACCOUNT', value: { accountId } }.
An account id is the userId of the account's session identity. FolioSDK has no
directory that finds an account by email address or name: the recipient reads their own userId
from SessionStore and gives it to the inviter through your app or your backend. The account ids
the SDK already shows you are the recipients in AccessGrant and the owner of a
shared record in header.ownerId.
expiresAt is an Int64 on iOS, a Long on Android and a bigint on the web. It counts seconds,
not milliseconds: an invite that expires in a day is BigInt(Math.floor(Date.now() / 1000) + 86_400)
on the web. The recipient sends the passcode with AcceptInvite, and Folio checks it against the
hash.
SharingView
The sharing field of the vault state.
| Field | Type | Meaning |
|---|---|---|
owned | [InviteSummary] | Invites the account created. Filled by LoadInvites. |
incoming | [InviteSummary] | Invites addressed to the account. Filled by LoadInvites. |
access | [AccessGrant] | Accounts with access to the record of the last LoadAccess. |
created | CreatedInviteLink (optional) | The invite of the last successful CreateInvite. |
preview | InviteSummary (optional) | The invite of the last successful PreviewInvite. |
acceptedRecordId | String (optional) | The id of the record the last successful AcceptInvite added to the vault. |
created keeps the last created invite until the next CreateInvite replaces it. Keep the secret
only as long as you need it to deliver the invite.
CreatedInviteLink
| Field | Type | Meaning |
|---|---|---|
invite | InviteSummary | The new invite. |
secret | String | The invite's secret, base64-encoded. The recipient needs it to accept. |
InviteSummary
| Field | Type | Meaning |
|---|---|---|
id | String | The invite id. Use it in AcceptInvite, RejectInvite and RevokeInvite. |
recordId | String | The shared record. |
audience | InviteAudience | Open with multiUse, or Account with the invited accountId. |
status | InviteStatus | The invite's status. See below. |
challenges | [InviteChallengeType] | What the recipient must provide. Passcode means the invite asks for a passcode. |
linkId | String | The public link id. Use it in PreviewInvite. |
ownerId | String | The account that created the invite. |
expiresAt | i64 (optional) | Expiry, seconds since the Unix epoch. Absent when the invite does not expire. |
createdAt | i64 (optional) | Creation time, milliseconds since the Unix epoch. Absent when unknown. |
updatedAt | i64 (optional) | Time of the last change, milliseconds since the Unix epoch. Absent when unknown. |
expiresAt keeps the seconds of the InviteRequest, while createdAt and updatedAt are in
milliseconds. Multiply expiresAt by 1000 before you compare it with the other two.
InviteStatus
| Status | Meaning |
|---|---|
Pending | The invite can be accepted. |
Consumed | The invite was accepted. A multi-use open invite stays Pending after an accept. |
Rejected | The recipient rejected the invite. |
Revoked | The owner revoked the invite. |
InviteStatus and InviteChallengeType have no payloads. On iOS the cases are .pending,
.consumed, .rejected, .revoked and .passcode; on Android InviteStatus.PENDING,
InviteStatus.CONSUMED, InviteStatus.REJECTED, InviteStatus.REVOKED and
InviteChallengeType.PASSCODE; on the web the strings 'PENDING', 'CONSUMED', 'REJECTED',
'REVOKED' and 'PASSCODE', also available as InviteStatus.PENDING and so on.
let canAccept = preview.status == .pending
let needsPasscode = preview.challenges.contains(.passcode)AccessGrant
| Field | Type | Meaning |
|---|---|---|
recordId | String | The shared record. |
accountId | String | The account that has access. |
grantedAt | i64 (optional) | When access was granted, milliseconds since the Unix epoch. Absent when unknown. |
Shared records in the recipient's vault
An accepted record appears in the recipient's records like any other record, with the owner's
account in header.ownerId and header.permission set to View. The recipient can read it and
write its own settings for it, but cannot update, delete or restore
it. A conflict of RemoteRevoked means the account lost access to the record; see
Conflicts.
Issued documents
An organization delivers a document it issues to the person as an Account invite. The SDK accepts
these invites on its own, once Folio confirms that an invite carries an issued document, and the
document appears in FolioDocumentListStore. Your app does not dispatch AcceptInvite for them.
See Automatic reception.
The SDK never accepts any other invite on its own. An invite another account sends stays in
incoming until your app accepts or rejects it.
Errors
Sharing requests fail with an error in error of the vault state, a VaultErrorView whose
details is the VaultError. The cases specific to sharing:
| Error | When |
|---|---|
ChallengeFailed | The passcode check failed. |
InviteExpired | The invite has expired. |
InviteNotPending | The invite was already consumed, rejected or revoked. |
InviteNotFound | No invite has the given id or link id. |
InvalidWrappedKey | The invite carries no key to accept it with. |
NotFound | CreateInvite names a record that is not in the vault. |
TransientNotSupported | The session is transient; sharing needs a signed-in session. |
See Vault errors for the full list and the spelling of the cases on each
platform, and Types on each platform for i64, Bool and the
list types in the tables above.