@affinidi/wallet-core-sdk
TypeScript icon, indicating that this package has built-in type declarations

7.19.1 • Public • Published

Affinity Core SDK - Affinity network DID solution

WARNING Action required from you
Update your services to use Affinidi SDK v6.0.4 or above.
Note please pay attention to the changelog while upgrading the version of SDK as some methods may be changed or deprecated. If you are using Affinidi SDK below v6, your application doesn’t support Affinidi Vault and hence we cannot migrate you out of the Bloom Vault.
With Affinidi SDK v6.0.4 onwards, we have also introduced automatic trigger of migration to Affinidi Vault and that is why we ask you to upgrade to that version or above.
Otherwise your credentials will never be migrated. The migration will not anyhow impact SDK performance negatively.
Furthermore, if you have more than 100 credentials in Bloom Vault the performance should be increased after migration. Bloom Vault is no longer supported with Affinidi SDK v7.

Table of contents

How to install

npm i --save @affinidi/wallet-core-sdk

Setup Integration Tests

Test credentials should be added to the top level .env file. These contain usernames and passwords of pre-populated accounts on the staging environment. Reach out to a team member for instructions on how to set up this file, or to obtain a copy.

You can also run integration tests against dev:

TEST_AGAINST=dev npm run test:integration

Initialize

Create API-KEY

You should register your entity at Affinity for appropriate environment staging, production or dev, to obtain the apiKey and apiKeyHash values, one of which should be passed via options as a required parameter.

If you want to specify issuer's URL, pass it in the options.

You can also specify the stack environment to be used in env variable. env - (optional) is enum which can be dev | staging | prod (staging is used by default).

const options = {
  env: 'staging',
  apiKey: 'YOUR API KEY'
}

OR

const options = {
  env: 'staging',
  accessApiKey: 'YOUR API KEY HASH VALUE'
}

encryptedSeed- randomly auto generated by Generate seed more detail example material that used as input for a private key generation. Generated as a part of register / signUp process and stored as encrypted value in protected vault. seed itself never exposed to the public and available in encrypted form as a property of wallet. accountNumber - optional parameter to derive custom account keys/did from the root seed(could be used to create several ephemeral DIDs to support not traceability option).

const wallet = AffinidiWallet.openWalletByEncryptedSeed(options, encryptedSeed, password, accountNumber)

Issuer / Holder / Verifier interface examples

Set up SDK options

options is a required parameter for wallet initialization. You can specify optional field didMethod and skipAnchoringForElemMethod in options

const options = {
  env: 'staging',
  apiKey: 'YOUR API KEY',
  didMethod: '...',   // 'elem' (default),  'jolo', 'elem-anchored', 'did:web', 'did:key' and 'did:polygon'
  skipAnchoringForElemMethod: true  
}

If skipAnchoringForElemMethod is set to true and didMethod is elem, DID anchoring will be skipped. By default, didMethod is elem and skipAnchoringForElemMethod is false, so anchoring will not be skipped unless explicitly specified.

Create a new wallet

'elem-anchored' did method returns did:elem, and it is anchored with sidetree in ropsten testnet
note: elem-anchored doesn't support external keys due to Sidetree protocol limitations

IMPORTANT NOTICE: Please be informed that this Ropsten is a Testnet and an environment deployed for testing purposes. Please do not include sensitive or valuable information during your trials on the Testnet. Ropsten does not guaranty the consistency, stability or uninterrupted access to the functionalities provided herein and before as well as during the testing activities the user is informed and understands that any information and connections introduced, uploaded or generated during your interactions with the Testnet can and will be removed at any time at the sole criteria of Ropsten.

const options = {
  env: 'staging',
  apiKey: '....',
  webDomain: 'identity.actor:alice' // required parameter in case `web` is passed as didMethod
  didMethod: '....' // 'elem' (default),  'jolo' or 'elem-anchored', 'web', 'key' and 'polygon'
}

const wallet = await AffinidiWallet.createWallet(options, password)
const { didDocument } = wallet // for web did method didDocument is returned
// In case of web did method for others to be able to resolve this did - need to host it 
// at your domain which was provided as webDomain
// `:` using as path (replacing to the `/` at url), 
// e.g. webDomain: 'identity.actor:alice' should hosted at https://identity.actor/alice/did.json

Sign DID document

const signedDidDocument = await wallet.signDidDocument(didDocument)

Interface

const { AffinidiWallet } = require('@affinidi/wallet-{PLATFORM}-sdk') // where platform is one of 'browser', 'expo', 'node', 'react-native'

Create DID

const wallet = await AffinidiWallet.createWallet(options, password)
const { did, encryptedSeed } = wallet

Intiate SDK

const wallet = AffinidiWallet.openWalletByEncryptedSeed(options, password, encryptedSeed)

Get DID

const wallet = AffinidiWallet.openWalletByEncryptedSeed(options, password, encryptedSeed)

const did = wallet.did

did - user's DID

Passwordless sign in or sign up if user does not exist + DID creation

NOTE: This passwordless method was designed for a specific usecase, and its
simple user interface achieved by making extra calls to AWS Cognito.
If this method is chosen for authentication you may see failed requests
to Cognito in the browser's console - that is expected behaviour,
exceptions are caught and handled properly.
const token = await AffinidiWallet.initiateSignInPasswordless(options, username, messageParameters)

username - email or phoneNumber, of existing Cognito user or if it does not exist, a new one will be created.

IMPORTANT: Username is case sensitive, so 2 separate accounts will be created
on sign up for `Test@gmail.com` and `test@gmail.com`.

In case you want to have a case-agnostic behaviour, please resolve this
on the application layer by normalizing the input before passing it to the SDK
(e.g. email.toLowerCase()).

messageParameters - (optional) used to specify message, htmlMessage, subject, see signup method.

Confirm sign in

const { isNew, wallet } = await AffinidiWallet.completeSignInPasswordless(token, confirmationCode, options)

token - from previous step.

confirmationCode - 6 digits code, generated and sent by AWS Cognito/SES.

Returns isNew flag, identifying whether new account was created, and initialized instance of SDK - wallet.

Sign up

We STRONGLY recommend using a password at least 8 characters, but it's allowed to be 6 min (in this case - salt as username hash and special character will be added on signup, and the same rule will be applied on login cases)

const wallet = await AffinidiWallet.signUpWithUsername(options, username, password, messageParameters)

or

const token = await AffinidiWallet.initiateSignUpByEmail(options, email, password, messageParameters)

or

const token = await AffinidiWallet.initiateSignUpByPhone(options, phone, password, messageParameters)

Password recovery is not possible unless user have registered with email or phone number or have added email or phone number later.

IMPORTANT: Username is case sensitive, so 2 separate accounts will be created
on sign up for `Test@gmail.com` and `test@gmail.com`.

In case you want to have a case-agnostic behaviour, please resolve this
on the application layer by normalizing the input before passing it to the SDK
(e.g. email.toLowerCase()).

password - optional. Requirements: min length 8, require number, upper and lowercase letter.

NOTE: password is optional if username is email or phone number only. If not provided, user will be able to login with passwordless flow only (initiateLogInPasswordless + completeLogInPasswordless, with OTP submit). When username is arbitrary username, password must be provided.

messageParameters - optional

const htmlMessage = `
  <table align="center" border="1" cellpadding="0" cellspacing="0" width="600">
    <tr>
     <td bgcolor="#70bbd9">
       here is your {{CODE}}.
     </td>
    </tr>
  </table>
`
const messageParameters = {
  message: 'Welcome to Affinity, your OTP: {{CODE}}'
  subject?: 'Your verification Code'
  htmlMessage?
}

{{CODE}} - will be replaced at the message by OTP

If htmlMessage not provided, meesage parameter will be used

To finish registration:

NOTE: email/SMS with verification code (OTP) will be sent to the provided email/phoneNumber, unless username is an arbitrary username.

const wallet = await AffinidiWallet.completeSignUp(options, token, confirmationCode)

token - token from the previous step (value returned from the initiateSignUp).

confirmationCode - OTP sent by AWS Cognito/SES. Parameter is required if email/phoneNumber was given as a username, and is ignored in case when username is an arbitrary username.

To re-send sign up confirmation code (in case when username is email/phoneNumber):

await AffinidiWallet.resendSignUpConfirmationCode(options, token, messageParameters)

token - token returned by initiateSignUp.

messageParameters - (optional) used to specify message, htmlMessage, subject, see signup method.

Sign up with email/phoneNumber (example)
const email = 'great_user@email.com'
const password = 'Password123'
const options = { env: 'dev' }

const token = await AffinidiWallet.initiateSignUpByEmail(options, email, password)

// OTP is sent out by Cognito
const confirmationCode = '123456' // OTP from email/SMS

const wallet = await AffinidiWallet.completeSignUp(options, token, confirmationCode)

Now user can login

Sign up with arbitrary username (example)
const username = 'great_user'
const password = 'Password123'
const options = { env: 'dev' }

const wallet = await AffinidiWallet.signUpWithUsername(options, username, password)

Now user can login with username and pasword

Sign up to Affinity Wallet with already created DID/keys. (Create User at Affinity Wallet and store there user keys)

User already have created keys in advance, e.g.

const { did, encryptedSeed } = await AffinidiWallet.createWallet(options, password)

Sign up with already created keys:

const keyParams = { encryptedSeed, password }
const email = 'example@affinity-project.org'
const userPassword = 'Password123'
const options = { env: 'dev' }
const messageParameters = { message: 'Welcome to Affinity, here is your OTP: {{CODE}}' } //  (optional)

const token = await AffinidiWallet.initiateSignUpByEmail(options, email, userPassword, messageParameters)
const confirmationCode = '123456'
const wallet = await AffinidiWallet.completeSignUp(options, token, confirmationCode, keyParams)

Or, alternatively, if an arbitrary username is used

const wallet = await AffinidiWallet.signUpWithUsername(options, username, password, keyParams)

Update Did Document (supported only for jolo method):

init SDK

const wallet = await AffinidiWallet.openWalletByEncryptedSeed(options, encryptedSeed, password)
// OR
const wallet = await AffinidiWallet.logInWithPassword(options, userName, userPassword)

Then

await wallet.updateDidDocument(didDocument)

where didDocument - its valid signed didDocument

Initiate instance of SDK with login and pasword

To initiate instance of wallet using just login and password (when user already signed up at Affinity, and if stored his keys at Affinity Guardian Wallet).

const wallet = await AffinidiWallet.logInWithPassword(options, username, password)

options - (optional) used to specify environment stack (dev | staging | prod).

Initiate instance of SDK with refreshToken

To initiate instance of wallet using refreshToken.
To get refreshToken use wallet.serializeSession() (for wallets with cognito, when logged in)

Take care about refresh token to save it in right place, its lifetime is 30 days. To invalidate tokens please use wallet.logOut() method. The best way to use only access token and re-login user each hour (lifetime of accessToken)

const wallet = await AffinidiWallet.logInWithRefreshToken(options, refreshToken)

options - (optional) used to specify environment stack (dev | staging | prod).

Passwordless login

Login to the network by username, registered in AWS Cognito.

const token = await AffinidiWallet.initiateLogInPasswordless(options, login, messageParameters: MessageParameters )

login - email or phone number, at which confirmation code will be sent.

messageParameters - (optional) used to specify message, htmlMessage, subject, see signup method.

Complete login challenge and initiate instance of SDK:

const wallet = await AffinidiWallet.completeLoginPasswordless(options, token, confirmationCode)

token - token from the previous step.

confirmationCode - 6 digits code, generated and sent by AWS Cognito/SES.

completeLoginPasswordless could return next errors:

  • COR-5 Invalid confirmation OTP code was uses. As part of error payload new session token is passed that should be used for continuation of completing a session with old OTP
import retry from "async-retry";

let newToken;
try {
  const wallet = await AffinidiWallet.completeLoginPasswordless(options, token, invalidConfirmationCode)
} catch (sdkError) {
  if (sdkError.code === 'COR-5') {
    newToken = sdkError.context.newToken
  }
}
const wallet = await AffinidiWallet.completeLoginPasswordless(options, newToken, confirmationCode)

CAUTION for serverside SDK usage you should always use a token returned with error to continue process

Up to 3 retries are possible. After 3 times new session should be used

  • COR-13 Invalid confirmation OTP code was used 3 times or more. Use a initiateLogInPasswordless call to initiate a new session.
  • COR-17 Confirmation code is expired. Lifetime of confirmation code is around 3 minutes. Use a initiateLogInPasswordless call to initiate a new session

Password recovery

NOTE: Password recovery is not possible with arbitrary username.

const token = await AffinidiWallet.initiateForgotPassword(options, login, messageParameters)

username - email or phone number, at which confirmation code will be sent.

messageParameters - (optional) used to specify message, htmlMessage, subject, see signup method.

Complete change password challenge:

const wallet = await AffinidiWallet.completeForgotPassword(options, token, confirmationCode)

token - token returned by initiatePassword

confirmationCode - 6 digits code, generated and sent by AWS Cognito/SES.

newPassword - new password for Cognito user.

Change password

User have to be logged in to change password. Otherwise use password recovery.

await wallet.changePassword(oldPassword, newPassword)

oldPassword - old password.

newPassword - new password.

Change username

const token = await wallet.initiateChangeEmail(newEmail)

or

const token = await wallet.initiateChangePhone(newPhone)

newEmail or newPhone - email address or phone number to be later used to log into cognito

Complete change username challenge:

await wallet.completeChangeEmailOrPhone(options, token, confirmationCode)

token - token returned on the previous step.

confirmationCode - 6 digits code, generated and sent by AWS Cognito/SES.

Sign Out

Signs out current user from all devices.
It also invalidates all refresh tokens issued to a user.
The user's current access and Id tokens remain valid until their expiry.
Access and Id tokens expire one hour after they are issued.

await wallet.logOut()

Serialize session

Returns all active cognito tokens as json string
{ "accessToken": "eyJraWQiOiJHiLCJlxKU...", "idToken": 'eyJraWQiOiJQYTVjZFwvKzVyb...", "refreshToken": 'eyJjdHkiOiJKV1QiLCJ...", "expiresIn": 1655544042964 }

await wallet.serializeSession()

Issuer

Initiate credential offer request

const jwtOptions = { audienceDid, expiresAt, nonce, callbackUrl }

const credentialOfferToken = await issuerWallet.generateCredentialOfferRequestToken(offeredCredentials, jwtOptions)

audienceDid (string) - audience of genreated token.

expiresAt (isoString) - expires of genreated token.

nonce (string) - nonce/jti of genreated token.

callbackUrl (string)

Initiate DID auth

const jwtOptions = { audienceDid, expiresAt, nonce, callbackUrl }

const authDidRequestToken = await issuerWallet.generateDidAuthRequest(jwtOptions)

audienceDid (string) - audience of genreated token.

expiresAt (isoString) - expires of genreated token.

nonce (string) - nonce/jti of genreated token.

callbackUrl (string)

const offeredCredentials = [
  {
    type: 'IssuerCustomCredential',
    renderInfo,
  },
]

renderInfo (optional) where issuer can define how that VC can be represented/shown.

Example:

const renderInfo = {
  logo: {
    url: 'https://miro.medium.com/fit/c/240/240/1*jbb5WdcAvaY1uVdCjX1XVg.png',
  },
  background: {
    url: 'https://i.imgur.com/0Mrldei.png',
  },
  text: {
    color: '#05050d',
  },
}

credentialOfferToken can be passed to the wallet side, and let Wallet/Holder option to response to this offer if usser want take offered credentials.

Validate Holder Response on Offer Request

const { isValid, did, nonce, selectedCredentials } = await issuer.verifyCredentialOfferResponseToken(
  credentialOfferResponseToken,
  credentialOfferRequestToken,
)

credentialOfferRequestToken (optional) - using when need check response against request (nonce, audience).

Validates response token and verify signature, if verification not passed response { isValid: false } if response is valid returns also { issuer, nonce, selectedCredentials }.

did - it's DID which signed that response.

Sign multiple credentials

import { VCV1Unsigned } from '@affinidi/vc-common'
import { VCSPhonePersonV1, getVCPhonePersonV1Context } from '@affinidi/vc-data'
import { buildVCV1Unsigned, buildVCV1Skeleton } from '@affinidi/vc-common'

const unsignedCredentials: VCV1Unsigned[] = [
  buildVCV1Unsigned({
    skeleton: buildVCV1Skeleton<VCSPhonePersonV1>({
      id: 'urn:urn-5:...',
      credentialSubject: {
        data: {
          '@type': ['Person', 'PersonE', 'PhonePerson'],
          telephone: '+1 555 555 5555',
        },
      },
      holder: { id: 'did:...:...' },
      type: 'PhoneCredentialPersonV1',
      context: getVCPhonePersonV1Context(),
    }),
    issuanceDate: new Date().toISOString(),
    expirationDate: new Date(new Date().getTime() + 10 * 60 * 1000).toISOString(),
  }),
]

const credentials = await signCredentials(credentialOfferResponseToken, unsignedCredentials)

credentialOfferResponseToken - credential offer response JWT.

credentialParams - array of params for credentials, where expiresAt is optional.

Generate signed credential

import { VCSPhonePersonV1, getVCPhonePersonV1Context } from '@affinidi/vc-data'

const credentialSubject: VCSPhonePersonV1 = {
  data: {
    '@type': ['Person', 'PersonE', 'PhonePerson'],
    telephone: '+1 555 555 5555',
  },
}

const credentialMetadata = {
  context: [getVCPhonePersonV1Context()],
  name: 'Phone Number',
  type: ['PhoneCredentialPersonV1'],
}

const credential = await issuer.signCredential(
  credentialSubject,
  credentialMetadata,
  { credentialOfferResponseToken, requesterDid },
  expiresAt,
)

credentialSubject - data which should be present in VC according to VC schema, must be a valid VCV1Subject.

credentialMetadata - schema of credential (should be defined and Issuer and Verifier use the same, so verifier will be able to understand what kind of credential was created by Issuer).

Revocation

SDK Support Issuing Revocable Credential based on Revocation List 2020 W3C standard

Revocation Flow

Revocation Flow

issuance of Revocable credential

   const unsignedCredential = buildVCV1Unsigned({
      skeleton: buildVCV1Skeleton<VCSPhonePersonV1>({
        id: `credId:${credId}`,
        credentialSubject: {
          data: {
            '@type': ['Person', 'PersonE', 'PhonePerson'],
            telephone: '+1 555 555 5555',
          },
        },
        holder: { id: holderDid },
        type: 'PhoneCredentialPersonV1',
        context: getVCPhonePersonV1Context(),
      }),
      issuanceDate: new Date().toISOString(),
      expirationDate: new Date(new Date().getTime() + 10 * 60 * 1000).toISOString(),
    })

    const revokableUnsignedCredential = await wallet.buildRevocationListStatus(
      unsignedCredential,
      accessToken,
    )

buildRevocationListStatus will add to unsigned credential special credentialStatus field with revocationlist2020status data.

Revocation of Revocable credential

 await wallet.revokeCredential(credentialId, 'Status changed', accessToken)

credentialId - id of credential for revoke

revocation reason - free text reason of revocation

accessToken

Verifier

Initiate Verifiable Presentation request (credential share request)

Verifiable Presentation according to w3c spec structure flow:
const jwtOptions = { audienceDid, expiresAt, nonce, callbackUrl }

const presentationChallenge = await verifier.generatePresentationChallenge(
  credentialRequirements,
  issuerDid,
  jwtOptions,
)

audienceDid (string) - audience of genreated token.

expiresAt (isoString) - expires of genreated token.

nonce (number) - nonce/jti of genreated token.

callbackUrl (string)

Generates JWT with info of which VC credentialRequirements to be provided from Wallet/Holder.

const credentialRequirements = [{ type: ['Credential', 'ProofOfNameCredential'] }]

callbackUrl - (optional) Holder/Wallet will be able send response on this request to this URL.

issuerDid - (optional) its contrain, that define required isser of VC.

credentialShareRequestToken can be send to Wallet/Holder to anwser on this with response with requested VC inside.

Verifiable Presentation as JWT method:
const credentialShareRequestToken = await verifier.generateCredentialShareRequestToken(
  credentialRequirements,
  issuerDid,
  options,
)

see parameters description at Verifiable Presentation according to w3c spec section

Validate Verifiable Presentation (Holder Response on Share Request)

Verifiable Presentation according to w3c spec structure flow:
const { isValid, did, challenge, suppliedPresentation } = await verifier.verifyPresentation(vp)
Verifiable Presentation as JWT method:
const { isValid, did, nonce, suppliedCredentials } = await verifier.verifyCredentialShareResponseToken(
  credentialShareResponseToken,
  credentialShareRequestToken,
  shouldOwn,
)

credentialShareResponseToken - (optional) using when need check response against request (when request have constrains).

shouldOwn - (optional) Verify that subject is holder of VC. Default true as per W3C spec.

Its validate response token and verify signature on provided VC inside, if verification not passed response { isValid: false }. If response is valid it returns also { did, nonce, suppliedCredentials }.

Validate Holder Response on Did auth Request

const { isValid, did, nonce } = await verifier.verifyDidAuthResponse(authDidResponseToken, authDidRequestToken)

Its validate response token, if verification not passed response { isValid: false } if response is valid returns also { did, nonce }

Wallet

Initialize region for storing credentials

You can specify AWS region where user credentials will be stored using optional storageRegion parameter (region should be a 3 character string correlating to an Alpha-3 country code).

const options = {
  storageRegion: 'SGP'
}

Create Verifiable Presentation (Response on credential share request)

Verifiable Presentation according to w3c spec structure flow:
const vp = await wallet.createPresentationFromChallenge(
  presentationChallenge,
  credentials,
  domain,
)

credentials - credentials which Holder providing for Verifier.

callbackURL - (optional)

domain - (could be empty string)

Verifiable Presentation as JWT method:
const responseToken = await wallet.createCredentialShareResponseToken(
  credentialShareRequestToken,
  suppliedCredentials,
  expiresAt,
)

credentialShareRequestToken (jwt) - previously generated request token.

suppliedCredentials - credentials which Holder providing for Verifier.

expiresAt (isoString) - (optional) expires of created token.

Create Response on credential offer request

const responseToken = await wallet.createCredentialOfferResponseToken(credentialOfferRequestToken)

Agree to recieve proposed credentials by the Issuer.

Create Response on DID auth request

const authDidResponseToken = await wallet.createDidAuthResponse(authDidRequestToken)

Claim Credential from credential offer request

Exchange credentialOfferResponseToken to a credentials from a callback url specified in request for offer.

const credentials = await wallet.claimCredentials(credentialOfferRequestToken)

Requirements to a callback endpoint

  • accept credentialOfferResponseToken in a post body
{
  "credentialOfferResponseToken": "xxxxx.xxxxx.xxx"
}
  • return an array of credentials in a payload
{
  "vcs": []
}

Expected errors

  • COR-26 & COR-19 - credentialOfferRequestToken validation errors
  • COR-27 - failed to invoke callback url
  • COR-28 - unsuccessful callback invocation.
  • COR-29 - invalid response format. No credentials

Encrypted messages

Create encrypted message

const encryptedMessage = await wallet.createEncryptedMessage(toDid, object)

toDid - DID, string value of document to be resolved.

object - value to be encrypted by public key.

Read encrypted message

const message = await wallet.readEncryptedMessage(encryptedMessage)

encryptedMessage - message to be decrypted.

Credentials vault

const credentials = [ signedCredential ]
const storageRegion = 'SGP'

await wallet.saveCredentials(credentials, storageRegion)

credentials - array of credentials to store in the vault.

storageRegion - (optional) AWS region where user's credentials will be stored. Region should be a 3 character string correlating to an Alpha-3 country code.

Get all credentials matching the shareRequestToken

const credentials = await wallet.getCredentials(shareRequestToken)

shareRequestToken - optional parameter (if passed - returns VC, which match the request, if not - then returns all VCs).

Get all credentials

const credentials = await wallet.getCredentials(null)

Get a single credential

const credential = await wallet.getCredentialById(credentialId)

Delete credential by ID

await wallet.deleteCredential(credentialId)

Affinidi Infra dependencies

This SDK using next Affinidi services:

  • affinidi registry (to anchor when applicable, resolve and update did/didDocument)
  • affinidi verifier (to build credential request)
  • affinidi issuer (to build credential offer and verify credential offer response)
  • affinidi wallet backend (to store endrypted seed and encrypted VC optioanlly as backup)
  • affinidi user management (using only when backup option for encrypted seed used)

Readme

Keywords

Package Sidebar

Install

npm i @affinidi/wallet-core-sdk

Weekly Downloads

44

Version

7.19.1

License

ISC

Unpacked Size

2.84 MB

Total Files

309

Last publish

Collaborators

  • rohitjjw
  • maratsh-affinidi
  • robert-affinidi
  • standemchuk