diff --git a/.eslintrc.js b/.eslintrc.js index a2d2189863..86ce362612 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,7 +31,9 @@ module.exports = { 'no-console': 'error', '@typescript-eslint/ban-ts-comment': 'warn', '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-floating-promises': 'error', 'import/no-cycle': 'error', + 'import/newline-after-import': ['error', { count: 1 }], 'import/order': [ 'error', { @@ -42,14 +44,38 @@ module.exports = { 'newlines-between': 'always', }, ], + '@typescript-eslint/no-non-null-assertion': 'error', 'import/no-extraneous-dependencies': [ 'error', { devDependencies: false, }, ], + 'no-restricted-imports': [ + 'error', + { + patterns: ['packages/*'], + }, + ], }, overrides: [ + { + files: ['packages/core/**'], + rules: { + 'no-restricted-globals': [ + 'error', + { + name: 'Buffer', + message: 'Global buffer is not supported on all platforms. Import buffer from `src/utils/buffer`', + }, + { + name: 'AbortController', + message: + "Global AbortController is not supported on all platforms. Use `import { AbortController } from 'abort-controller'`", + }, + ], + }, + }, { files: ['jest.config.ts', '.eslintrc.js'], env: { @@ -57,7 +83,13 @@ module.exports = { }, }, { - files: ['*.test.ts', '**/__tests__/**', '**/tests/**', 'jest.*.ts', 'samples/**'], + files: ['demo/**'], + rules: { + 'no-console': 'off', + }, + }, + { + files: ['*.test.ts', '**/__tests__/**', '**/tests/**', 'jest.*.ts', 'samples/**', 'demo/**'], env: { jest: true, node: false, diff --git a/.github/actions/setup-postgres-wallet-plugin/action.yml b/.github/actions/setup-postgres-wallet-plugin/action.yml new file mode 100644 index 0000000000..7ac41af866 --- /dev/null +++ b/.github/actions/setup-postgres-wallet-plugin/action.yml @@ -0,0 +1,17 @@ +name: Setup Postgres wallet plugin +description: Setup Postgres wallet plugin +author: 'sairanjit.tummalapalli@ayanworks.com' + +runs: + using: composite + steps: + - name: Setup Postgres wallet plugin + run: | + sudo apt-get install -y libzmq3-dev libsodium-dev pkg-config libssl-dev + curl https://sh.rustup.rs -sSf | bash -s -- -y + export PATH="/root/.cargo/bin:${PATH}" + cd ../ + git clone https://github.com/hyperledger/indy-sdk.git + cd indy-sdk/experimental/plugins/postgres_storage/ + cargo build --release + shell: bash diff --git a/.github/actions/setup-postgres/action.yml b/.github/actions/setup-postgres/action.yml new file mode 100644 index 0000000000..6e69e6574f --- /dev/null +++ b/.github/actions/setup-postgres/action.yml @@ -0,0 +1,12 @@ +name: Setup Postgres +description: Setup Postgres +author: 'sairanjit.tummalapalli@ayanworks.com' + +runs: + using: composite + steps: + - name: Setup Postgres + run: | + docker pull postgres + docker run --name postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres + shell: bash diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml new file mode 100644 index 0000000000..02e72050c8 --- /dev/null +++ b/.github/workflows/continuous-deployment.yml @@ -0,0 +1,104 @@ +name: Continuous Deployment + +on: + push: + branches: + - main + +jobs: + release-canary: + runs-on: ubuntu-20.04 + name: Release Canary + if: "!startsWith(github.event.head_commit.message, 'chore(release): v')" + steps: + - name: Checkout aries-framework-javascript + uses: actions/checkout@v2 + with: + # pulls all commits (needed for lerna to correctly version) + fetch-depth: 0 + + # setup dependencies + - name: Setup Libindy + uses: ./.github/actions/setup-libindy + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: 16 + + - name: Install dependencies + run: yarn install --frozen-lockfile + + # On push to main, release unstable version + - name: Release Unstable + run: yarn lerna publish --loglevel=verbose --canary minor --exact --force-publish --yes --no-verify-access --dist-tag alpha + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Get version number + id: get-version + run: | + LAST_RELEASED_VERSION=$(npm view @aries-framework/core@alpha version) + + echo "::set-output name=version::$LAST_RELEASED_VERSION" + + - name: Setup git user + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Set git tag + run: | + git tag v${{ steps.get-version.outputs.version }} + git push origin v${{ steps.get-version.outputs.version }} --no-verify + + release-stable: + runs-on: ubuntu-20.04 + name: Create Stable Release + # Only run if the last pushed commit is a release commit + if: "startsWith(github.event.head_commit.message, 'chore(release): v')" + steps: + - name: Checkout aries-framework-javascript + uses: actions/checkout@v2 + + # setup dependencies + - name: Setup Libindy + uses: ./.github/actions/setup-libindy + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: 16 + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Get updated version + id: new-version + run: | + NEW_VERSION=$(node -p "require('./lerna.json').version") + echo $NEW_VERSION + + echo "::set-output name=version::$NEW_VERSION" + + - name: Create Tag + uses: mathieudutour/github-tag-action@v6.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + custom_tag: ${{ steps.new-version.outputs.version }} + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ steps.new-version.outputs.version }} + body: | + Release v${{ steps.new-version.outputs.version }} + + You can find the changelog in the [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) file. + + - name: Release to NPM + run: yarn lerna publish from-package --loglevel=verbose --yes --no-verify-access + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 4fba317dfe..4f4cfed1e3 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -3,12 +3,15 @@ name: Continuous Integration on: pull_request: branches: [main] + types: [opened, synchronize, reopened, labeled] push: branches: [main] + workflow_dispatch: env: TEST_AGENT_PUBLIC_DID_SEED: 000000000000000000000000Trustee9 GENESIS_TXN_PATH: network/genesis/local-genesis.txn + LIB_INDY_STRG_POSTGRES: /home/runner/work/aries-framework-javascript/indy-sdk/experimental/plugins/postgres_storage/target/release # for Linux # Make sure we're not running multiple release steps at the same time as this can give issues with determining the next npm version to release. # Ideally we only add this to the 'release' job so it doesn't limit PR runs, but github can't guarantee the job order in that case: @@ -18,6 +21,28 @@ concurrency: cancel-in-progress: true jobs: + # PRs created by github actions won't trigger CI. Before we can merge a PR we need to run the tests and + # validation scripts. To still be able to run the CI we can manually trigger it by adding the 'ci-test' + # label to the pull request + ci-trigger: + runs-on: ubuntu-20.04 + outputs: + triggered: ${{ steps.check.outputs.triggered }} + steps: + - name: Determine if CI should run + id: check + run: | + if [[ "${{ github.event.action }}" == "labeled" && "${{ github.event.label.name }}" == "ci-test" ]]; then + export SHOULD_RUN='true' + elif [[ "${{ github.event.action }}" == "labeled" && "${{ github.event.label.name }}" != "ci-test" ]]; then + export SHOULD_RUN='false' + else + export SHOULD_RUN='true' + fi + + echo "SHOULD_RUN: ${SHOULD_RUN}" + echo "::set-output name=triggered::${SHOULD_RUN}" + validate: runs-on: ubuntu-20.04 name: Validate @@ -43,16 +68,19 @@ jobs: - name: Prettier run: yarn check-format - - name: Compile + - name: Check Types run: yarn check-types + - name: Compile + run: yarn build + integration-test: runs-on: ubuntu-20.04 name: Integration Tests strategy: matrix: - node-version: [12.x, 14.x, 16.x, 17.x] + node-version: [12.x, 14.x, 16.x, 17.x, 18.x] steps: - name: Checkout aries-framework-javascript @@ -66,11 +94,16 @@ jobs: with: seed: ${TEST_AGENT_PUBLIC_DID_SEED} + - name: Setup Postgres + uses: ./.github/actions/setup-postgres + + - name: Setup Postgres wallet plugin + uses: ./.github/actions/setup-postgres-wallet-plugin + - name: Setup NodeJS uses: ./.github/actions/setup-node with: node-version: ${{ matrix.node-version }} - - name: Install dependencies run: yarn install @@ -80,11 +113,11 @@ jobs: - uses: codecov/codecov-action@v1 if: always() - release-canary: + version-stable: runs-on: ubuntu-20.04 - name: Release Canary + name: Release stable needs: [integration-test, validate] - if: github.ref == 'refs/heads/main' && github.repository == 'hyperledger/aries-framework-javascript' && github.event_name == 'push' + if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' steps: - name: Checkout aries-framework-javascript uses: actions/checkout@v2 @@ -104,26 +137,35 @@ jobs: - name: Install dependencies run: yarn install --frozen-lockfile - # On push to main, release unstable version - - name: Release Unstable - run: yarn lerna publish --loglevel=verbose --canary minor --exact --force-publish --yes --no-verify-access --dist-tag latest - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Get version number - id: get-version + - name: Git config run: | - COMMIT_NUMBER=$(git rev-list --count ${{ github.ref }}) - PACKAGE_NUMBER=$((COMMIT_NUMBER-1)) + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - echo alpha.$PACKAGE_NUMBER + - name: Update version + run: yarn lerna version --conventional-commits --no-git-tag-version --no-push --yes --exact + + - name: Format lerna changes + run: yarn format + + - name: Get updated version + id: new-version + run: | + NEW_VERSION=$(node -p "require('./lerna.json').version") + echo $NEW_VERSION - echo "::set-output name=version::$PACKAGE_NUMBER" + echo "::set-output name=version::$NEW_VERSION" - - name: Create Tag - uses: mathieudutour/github-tag-action@v6.0 + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - custom_tag: ${{ steps.get-version.outputs.version }} - tag_prefix: 'alpha.' + commit-message: | + chore(release): v${{ steps.new-version.outputs.version }} + author: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>' + committer: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>' + branch: lerna-release + signoff: true + title: | + chore(release): v${{ steps.new-version.outputs.version }} + body: | + Release version ${{ steps.new-version.outputs.version }} diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml new file mode 100644 index 0000000000..d5af138f96 --- /dev/null +++ b/.github/workflows/lint-pr.yml @@ -0,0 +1,21 @@ +name: 'Lint PR' + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + # Please look up the latest version from + # https://github.com/amannn/action-semantic-pull-request/releases + - uses: amannn/action-semantic-pull-request@v3.4.6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + validateSingleCommit: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..35326e2773 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,171 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.1.0 (2021-12-23) + +### Bug Fixes + +- add details to connection signing error ([#484](https://github.com/hyperledger/aries-framework-javascript/issues/484)) ([e24eafd](https://github.com/hyperledger/aries-framework-javascript/commit/e24eafd83f53a9833b95bc3a4587cf825ee5d975)) +- add error message to log ([#342](https://github.com/hyperledger/aries-framework-javascript/issues/342)) ([a79e4f4](https://github.com/hyperledger/aries-framework-javascript/commit/a79e4f4556a9a9b59203cf529343c97cd418658b)) +- add option check to attribute constructor ([#450](https://github.com/hyperledger/aries-framework-javascript/issues/450)) ([8aad3e9](https://github.com/hyperledger/aries-framework-javascript/commit/8aad3e9f16c249e9f9291388ec8efc9bf27213c8)) +- Add samples folder as test root ([#215](https://github.com/hyperledger/aries-framework-javascript/issues/215)) ([b6a3c1c](https://github.com/hyperledger/aries-framework-javascript/commit/b6a3c1c47f00768e8b7ec1be8cca4c00a05fcf70)) +- added ariesframeworkerror to httpoutboundtransport ([#438](https://github.com/hyperledger/aries-framework-javascript/issues/438)) ([ee1a229](https://github.com/hyperledger/aries-framework-javascript/commit/ee1a229f8fc21739bca05c516a7b561f53726b91)) +- alter mediation recipient websocket transport priority ([#434](https://github.com/hyperledger/aries-framework-javascript/issues/434)) ([52c7897](https://github.com/hyperledger/aries-framework-javascript/commit/52c789724c731340daa8528b7d7b4b7fdcb40032)) +- check instance types of record properties ([#163](https://github.com/hyperledger/aries-framework-javascript/issues/163)) ([cc61c80](https://github.com/hyperledger/aries-framework-javascript/commit/cc61c8023bb5adbff599a6e0d563897ddb5e00dc)) +- connection record type was BaseRecord ([#278](https://github.com/hyperledger/aries-framework-javascript/issues/278)) ([515395d](https://github.com/hyperledger/aries-framework-javascript/commit/515395d847c492dd3b55cc44c94715de94a12bb8)) +- convert from buffer now also accepts uint8Array ([#283](https://github.com/hyperledger/aries-framework-javascript/issues/283)) ([dae123b](https://github.com/hyperledger/aries-framework-javascript/commit/dae123bc18f62f01c0962d099c88eed723dba972)) +- **core:** convert legacy prefix for inner msgs ([#479](https://github.com/hyperledger/aries-framework-javascript/issues/479)) ([a2b655a](https://github.com/hyperledger/aries-framework-javascript/commit/a2b655ac79bf0c7460671c8d31e92828e6f5ccf0)) +- **core:** do not throw error on timeout in http ([#512](https://github.com/hyperledger/aries-framework-javascript/issues/512)) ([4e73a7b](https://github.com/hyperledger/aries-framework-javascript/commit/4e73a7b0d9224bc102b396d821a8ea502a9a509d)) +- **core:** do not use did-communication service ([#402](https://github.com/hyperledger/aries-framework-javascript/issues/402)) ([cdf2edd](https://github.com/hyperledger/aries-framework-javascript/commit/cdf2eddc61e12f7ecd5a29e260eef82394d2e467)) +- **core:** export AgentMessage ([#480](https://github.com/hyperledger/aries-framework-javascript/issues/480)) ([af39ad5](https://github.com/hyperledger/aries-framework-javascript/commit/af39ad535320133ee38fc592309f42670a8517a1)) +- **core:** expose record metadata types ([#556](https://github.com/hyperledger/aries-framework-javascript/issues/556)) ([68995d7](https://github.com/hyperledger/aries-framework-javascript/commit/68995d7e2b049ff6496723d8a895e07b72fe72fb)) +- **core:** fix empty error log in console logger ([#524](https://github.com/hyperledger/aries-framework-javascript/issues/524)) ([7d9c541](https://github.com/hyperledger/aries-framework-javascript/commit/7d9c541de22fb2644455cf1949184abf3d8e528c)) +- **core:** improve wallet not initialized error ([#513](https://github.com/hyperledger/aries-framework-javascript/issues/513)) ([b948d4c](https://github.com/hyperledger/aries-framework-javascript/commit/b948d4c83b4eb0ab0594ae2117c0bb05b0955b21)) +- **core:** improved present-proof tests ([#482](https://github.com/hyperledger/aries-framework-javascript/issues/482)) ([41d9282](https://github.com/hyperledger/aries-framework-javascript/commit/41d9282ca561ca823b28f179d409c70a22d95e9b)) +- **core:** log errors if message is undeliverable ([#528](https://github.com/hyperledger/aries-framework-javascript/issues/528)) ([20b586d](https://github.com/hyperledger/aries-framework-javascript/commit/20b586db6eb9f92cce16d87d0dcfa4919f27ffa8)) +- **core:** remove isPositive validation decorators ([#477](https://github.com/hyperledger/aries-framework-javascript/issues/477)) ([e316e04](https://github.com/hyperledger/aries-framework-javascript/commit/e316e047b3e5aeefb929a5c47ad65d8edd4caba5)) +- **core:** remove unused url import ([#466](https://github.com/hyperledger/aries-framework-javascript/issues/466)) ([0f1323f](https://github.com/hyperledger/aries-framework-javascript/commit/0f1323f5bccc2dc3b67426525b161d7e578bb961)) +- **core:** requested predicates transform type ([#393](https://github.com/hyperledger/aries-framework-javascript/issues/393)) ([69684bc](https://github.com/hyperledger/aries-framework-javascript/commit/69684bc48a4002483662a211ec1ddd289dbaf59b)) +- **core:** send messages now takes a connection id ([#491](https://github.com/hyperledger/aries-framework-javascript/issues/491)) ([ed9db11](https://github.com/hyperledger/aries-framework-javascript/commit/ed9db11592b4948a1d313dbeb074e15d59503d82)) +- **core:** using query-string to parse URLs ([#457](https://github.com/hyperledger/aries-framework-javascript/issues/457)) ([78e5057](https://github.com/hyperledger/aries-framework-javascript/commit/78e505750557f296cc72ef19c0edd8db8e1eaa7d)) +- Correctly persist createdAt attribute ([#119](https://github.com/hyperledger/aries-framework-javascript/issues/119)) ([797a112](https://github.com/hyperledger/aries-framework-javascript/commit/797a112270dd67b75d9fe39dcf6753c64b049a39)), closes [#118](https://github.com/hyperledger/aries-framework-javascript/issues/118) +- date parsing ([#426](https://github.com/hyperledger/aries-framework-javascript/issues/426)) ([2d31b87](https://github.com/hyperledger/aries-framework-javascript/commit/2d31b87e99d04136f57cb457e2c67397ad65cc62)) +- export indy pool config ([#504](https://github.com/hyperledger/aries-framework-javascript/issues/504)) ([b1e2b8c](https://github.com/hyperledger/aries-framework-javascript/commit/b1e2b8c54e909927e5afa8b8212e0c8e156b97f7)) +- export module classes from framework root ([#315](https://github.com/hyperledger/aries-framework-javascript/issues/315)) ([a41cc75](https://github.com/hyperledger/aries-framework-javascript/commit/a41cc755f29887bc8ea7690791284ea9e375f5ce)) +- export ProofsModule to public API ([#325](https://github.com/hyperledger/aries-framework-javascript/issues/325)) ([f2e3a06](https://github.com/hyperledger/aries-framework-javascript/commit/f2e3a06d84bd40b5dcfa59f7b07bd77876fda861)) +- handle receive message promise rejection ([#318](https://github.com/hyperledger/aries-framework-javascript/issues/318)) ([ca6fb13](https://github.com/hyperledger/aries-framework-javascript/commit/ca6fb13eb9bf6c6218e3b042670fd1d41ff3dfd2)) +- include error when message cannot be handled ([#533](https://github.com/hyperledger/aries-framework-javascript/issues/533)) ([febfb05](https://github.com/hyperledger/aries-framework-javascript/commit/febfb05330c097aa918087ec3853a247d6a31b7c)) +- incorrect recip key with multi routing keys ([#446](https://github.com/hyperledger/aries-framework-javascript/issues/446)) ([db76823](https://github.com/hyperledger/aries-framework-javascript/commit/db76823400cfecc531575584ef7210af0c3b3e5c)) +- legacy did:sov prefix on invitation ([#216](https://github.com/hyperledger/aries-framework-javascript/issues/216)) ([dce3081](https://github.com/hyperledger/aries-framework-javascript/commit/dce308120045bb155d24b3b675621856937c0d2b)) +- make presentation proposal optional ([#197](https://github.com/hyperledger/aries-framework-javascript/issues/197)) ([1c5bfbd](https://github.com/hyperledger/aries-framework-javascript/commit/1c5bfbdf262323a5741b68c047161fd8af882839)) +- make records serializable ([#448](https://github.com/hyperledger/aries-framework-javascript/issues/448)) ([7e2946e](https://github.com/hyperledger/aries-framework-javascript/commit/7e2946eaa9e35083f3aa70c26c732a972f6eb12f)) +- mediator transports ([#419](https://github.com/hyperledger/aries-framework-javascript/issues/419)) ([87bc589](https://github.com/hyperledger/aries-framework-javascript/commit/87bc589695505de21294a1373afcf874fe8d22f6)) +- mediator updates ([#432](https://github.com/hyperledger/aries-framework-javascript/issues/432)) ([163cda1](https://github.com/hyperledger/aries-framework-javascript/commit/163cda19ba8437894a48c9bc948528ea0486ccdf)) +- monorepo release issues ([#386](https://github.com/hyperledger/aries-framework-javascript/issues/386)) ([89a628f](https://github.com/hyperledger/aries-framework-javascript/commit/89a628f7c3ea9e5730d2ba5720819ac6283ee404)) +- **node:** node v12 support for is-indy-installed ([#542](https://github.com/hyperledger/aries-framework-javascript/issues/542)) ([17e9157](https://github.com/hyperledger/aries-framework-javascript/commit/17e9157479d6bba90c2a94bce64697d7f65fac96)) +- proof configurable on proofRecord ([#397](https://github.com/hyperledger/aries-framework-javascript/issues/397)) ([8e83c03](https://github.com/hyperledger/aries-framework-javascript/commit/8e83c037e1d59c670cfd4a8a575d4459999a64f8)) +- **redux-store:** add reducers to initializeStore ([#413](https://github.com/hyperledger/aries-framework-javascript/issues/413)) ([d9aeabf](https://github.com/hyperledger/aries-framework-javascript/commit/d9aeabff3b8eec08aa86c005959ae4fafd7e948b)) +- **redux-store:** credential and proof selector by id ([#407](https://github.com/hyperledger/aries-framework-javascript/issues/407)) ([fd8933d](https://github.com/hyperledger/aries-framework-javascript/commit/fd8933dbda953177044c6ac737102c9608b4a2c6)) +- Remove apostrophe from connection request message type ([#364](https://github.com/hyperledger/aries-framework-javascript/issues/364)) ([ee81d01](https://github.com/hyperledger/aries-framework-javascript/commit/ee81d0115f2365fd33156105ba69a80e265d5846)) +- remove dependency on global types ([#327](https://github.com/hyperledger/aries-framework-javascript/issues/327)) ([fb28935](https://github.com/hyperledger/aries-framework-javascript/commit/fb28935a0658ef29ee6dc3bcf7cd064f15ac471b)) +- removed check for senderkey for connectionless exchange ([#555](https://github.com/hyperledger/aries-framework-javascript/issues/555)) ([ba3f17e](https://github.com/hyperledger/aries-framework-javascript/commit/ba3f17e073b28ee5f16031f0346de0b71119e6f3)) +- return valid schema in create schema method ([#193](https://github.com/hyperledger/aries-framework-javascript/issues/193)) ([4ca020b](https://github.com/hyperledger/aries-framework-javascript/commit/4ca020bd1ec0f3284064d4a52f5e81fee88e81c9)) +- revert target back to es2017 ([#319](https://github.com/hyperledger/aries-framework-javascript/issues/319)) ([9859db1](https://github.com/hyperledger/aries-framework-javascript/commit/9859db1d04b8e13e54a00e645e9837134d176154)) +- revert to ES2017 to fix function generator issues in react native ([#226](https://github.com/hyperledger/aries-framework-javascript/issues/226)) ([6078324](https://github.com/hyperledger/aries-framework-javascript/commit/60783247c7cf753c731b9a152b994dcf23285805)) +- support mediation for connectionless exchange ([#577](https://github.com/hyperledger/aries-framework-javascript/issues/577)) ([3dadfc7](https://github.com/hyperledger/aries-framework-javascript/commit/3dadfc7a202b3642e93e39cd79c9fd98a3dc4de2)) +- test failing because of moved import ([#282](https://github.com/hyperledger/aries-framework-javascript/issues/282)) ([e5efce0](https://github.com/hyperledger/aries-framework-javascript/commit/e5efce0b92d6eb10ab8fe0d1caa3a6b1d17b7f99)) +- their did doc not ours ([#436](https://github.com/hyperledger/aries-framework-javascript/issues/436)) ([0226609](https://github.com/hyperledger/aries-framework-javascript/commit/0226609a279303f5e8d09a2c01e54ff97cf61839)) +- use both thread id and connection id ([#299](https://github.com/hyperledger/aries-framework-javascript/issues/299)) ([3366a55](https://github.com/hyperledger/aries-framework-javascript/commit/3366a552959b63662809b612ae1162612dc6a50a)) +- Use custom make-error with cause that works in RN ([#285](https://github.com/hyperledger/aries-framework-javascript/issues/285)) ([799b6c8](https://github.com/hyperledger/aries-framework-javascript/commit/799b6c8e44933b03acce25636a8bf8dfbbd234d5)) +- websocket and fetch fix for browser ([#291](https://github.com/hyperledger/aries-framework-javascript/issues/291)) ([84e570d](https://github.com/hyperledger/aries-framework-javascript/commit/84e570dc1ffff9ff60792b43ce6bc19241ae2886)) + +### Code Refactoring + +- make a connection with mediator asynchronously ([#231](https://github.com/hyperledger/aries-framework-javascript/issues/231)) ([bafa839](https://github.com/hyperledger/aries-framework-javascript/commit/bafa8399b32b0f814c90a2406a00a74036df96c8)) + +- fix(core)!: Improved typing on metadata api (#585) ([4ab8d73](https://github.com/hyperledger/aries-framework-javascript/commit/4ab8d73e5fc866a91085f95f973022846ed431fb)), closes [#585](https://github.com/hyperledger/aries-framework-javascript/issues/585) +- fix(core)!: update class transformer library (#547) ([dee03e3](https://github.com/hyperledger/aries-framework-javascript/commit/dee03e38d2732ba0bd38eeacca6ad58b191e87f8)), closes [#547](https://github.com/hyperledger/aries-framework-javascript/issues/547) +- fix(core)!: prefixed internal metadata with \_internal/ (#535) ([aa1b320](https://github.com/hyperledger/aries-framework-javascript/commit/aa1b3206027fdb71e6aaa4c6491f8ba84dca7b9a)), closes [#535](https://github.com/hyperledger/aries-framework-javascript/issues/535) +- feat(core)!: metadata on records (#505) ([c92393a](https://github.com/hyperledger/aries-framework-javascript/commit/c92393a8b5d8abd38d274c605cd5c3f97f96cee9)), closes [#505](https://github.com/hyperledger/aries-framework-javascript/issues/505) +- fix(core)!: do not request ping res for connection (#527) ([3db5519](https://github.com/hyperledger/aries-framework-javascript/commit/3db5519f0d9f49b71b647ca86be3b336399459cb)), closes [#527](https://github.com/hyperledger/aries-framework-javascript/issues/527) +- refactor(core)!: simplify get creds for proof api (#523) ([ba9698d](https://github.com/hyperledger/aries-framework-javascript/commit/ba9698de2606e5c78f018dc5e5253aeb1f5fc616)), closes [#523](https://github.com/hyperledger/aries-framework-javascript/issues/523) +- fix(core)!: improve proof request validation (#525) ([1b4d8d6](https://github.com/hyperledger/aries-framework-javascript/commit/1b4d8d6b6c06821a2a981fffb6c47f728cac803e)), closes [#525](https://github.com/hyperledger/aries-framework-javascript/issues/525) +- feat(core)!: added basic message sent event (#507) ([d2c04c3](https://github.com/hyperledger/aries-framework-javascript/commit/d2c04c36c00d772943530bd599dbe56f3e1fb17d)), closes [#507](https://github.com/hyperledger/aries-framework-javascript/issues/507) + +### Features + +- Add assertions for credential state transitions ([#130](https://github.com/hyperledger/aries-framework-javascript/issues/130)) ([00d2b1f](https://github.com/hyperledger/aries-framework-javascript/commit/00d2b1f2ea42ff70bfc70c54da9f2341a27aa479)), closes [#123](https://github.com/hyperledger/aries-framework-javascript/issues/123) +- add credential info to access attributes ([#254](https://github.com/hyperledger/aries-framework-javascript/issues/254)) ([2fef3aa](https://github.com/hyperledger/aries-framework-javascript/commit/2fef3aafd954df93911579f82d0945d04b086750)) +- add delete methods to services and modules ([#447](https://github.com/hyperledger/aries-framework-javascript/issues/447)) ([e7ed602](https://github.com/hyperledger/aries-framework-javascript/commit/e7ed6027d2aa9be7f64d5968c4338e63e56657fb)) +- add dependency injection ([#257](https://github.com/hyperledger/aries-framework-javascript/issues/257)) ([1965bfe](https://github.com/hyperledger/aries-framework-javascript/commit/1965bfe660d7fd335a5988056bdea7335c88021b)) +- add from record method to cred preview ([#428](https://github.com/hyperledger/aries-framework-javascript/issues/428)) ([895f7d0](https://github.com/hyperledger/aries-framework-javascript/commit/895f7d084287f99221c9492a25fed58191868edd)) +- add inbound message queue ([#339](https://github.com/hyperledger/aries-framework-javascript/issues/339)) ([93893b7](https://github.com/hyperledger/aries-framework-javascript/commit/93893b7ab6afd1b4d4f3be4c6b807bab970dd63a)) +- add internal http outbound transporter ([#255](https://github.com/hyperledger/aries-framework-javascript/issues/255)) ([4dd950e](https://github.com/hyperledger/aries-framework-javascript/commit/4dd950eab6390fa08bf4c59c9efe69b5f4541640)) +- add internal polling inbound transporter ([#323](https://github.com/hyperledger/aries-framework-javascript/issues/323)) ([6dd273b](https://github.com/hyperledger/aries-framework-javascript/commit/6dd273b266fdfb336592bcd2a4834d4b508c0425)) +- add internal ws outbound transporter ([#267](https://github.com/hyperledger/aries-framework-javascript/issues/267)) ([2933207](https://github.com/hyperledger/aries-framework-javascript/commit/29332072f49e645bfe0fa394bb4c6f66b0bc0600)) +- add isInitialized agent property ([#293](https://github.com/hyperledger/aries-framework-javascript/issues/293)) ([deb5554](https://github.com/hyperledger/aries-framework-javascript/commit/deb5554d912587a1298eb86e42b64df6700907f9)) +- add multiple inbound transports ([#433](https://github.com/hyperledger/aries-framework-javascript/issues/433)) ([56cb9f2](https://github.com/hyperledger/aries-framework-javascript/commit/56cb9f2202deb83b3c133905f21651bfefcb63f7)) +- add problem report protocol ([#560](https://github.com/hyperledger/aries-framework-javascript/issues/560)) ([baee5db](https://github.com/hyperledger/aries-framework-javascript/commit/baee5db29f3d545c16a651c80392ddcbbca6bf0e)) +- add support for Multibase, Multihash and Hashlinks ([#263](https://github.com/hyperledger/aries-framework-javascript/issues/263)) ([36ceaea](https://github.com/hyperledger/aries-framework-javascript/commit/36ceaea4c500da90babd8d54bb88b2d9e7846e4e)) +- add support for RFC 0211 mediator coordination ([2465d4d](https://github.com/hyperledger/aries-framework-javascript/commit/2465d4d88771b0d415492585ee60d3dc78163786)) +- Add support for WebSocket transports ([#256](https://github.com/hyperledger/aries-framework-javascript/issues/256)) ([07b479f](https://github.com/hyperledger/aries-framework-javascript/commit/07b479fbff87bfc914a2b933f1216969a29cf790)) +- add toJson method to BaseRecord ([#455](https://github.com/hyperledger/aries-framework-javascript/issues/455)) ([f3790c9](https://github.com/hyperledger/aries-framework-javascript/commit/f3790c97c4d9a0aaec9abdce417ecd5429c6026f)) +- Added attachment extension ([#266](https://github.com/hyperledger/aries-framework-javascript/issues/266)) ([e8ab5fa](https://github.com/hyperledger/aries-framework-javascript/commit/e8ab5fa5c13c9633febfbdf3d5fdf2b352947322)) +- added decline credential offer method ([#416](https://github.com/hyperledger/aries-framework-javascript/issues/416)) ([d9ac141](https://github.com/hyperledger/aries-framework-javascript/commit/d9ac141122f1d4902f91f9537e6526796fef1e01)) +- added declined proof state and decline method for presentations ([e5aedd0](https://github.com/hyperledger/aries-framework-javascript/commit/e5aedd02737d3764871c6b5d4ae61a3a33ed8398)) +- added their label to the connection record ([#370](https://github.com/hyperledger/aries-framework-javascript/issues/370)) ([353e1d8](https://github.com/hyperledger/aries-framework-javascript/commit/353e1d8733cb2ea217dcf7c815a70eb89527cffc)) +- adds support for linked attachments ([#320](https://github.com/hyperledger/aries-framework-javascript/issues/320)) ([ea91559](https://github.com/hyperledger/aries-framework-javascript/commit/ea915590217b1bf4a560cd2931b9891374b03188)) +- allow for lazy wallet initialization ([#331](https://github.com/hyperledger/aries-framework-javascript/issues/331)) ([46918a1](https://github.com/hyperledger/aries-framework-javascript/commit/46918a1d971bc93a1b6e2ad5ef5f7b3a8e8f2bdc)) +- allow to use legacy did sov prefix ([#442](https://github.com/hyperledger/aries-framework-javascript/issues/442)) ([c41526f](https://github.com/hyperledger/aries-framework-javascript/commit/c41526fb57a7e2e89e923b95ede43f890a6cbcbb)) +- auto accept proofs ([#367](https://github.com/hyperledger/aries-framework-javascript/issues/367)) ([735d578](https://github.com/hyperledger/aries-framework-javascript/commit/735d578f72fc5f3bfcbcf40d27394bd013e7cf4f)) +- automatic transformation of record classes ([#253](https://github.com/hyperledger/aries-framework-javascript/issues/253)) ([e07b90e](https://github.com/hyperledger/aries-framework-javascript/commit/e07b90e264c4bb29ff0d7246ceec7c664782c546)) +- break out indy wallet, better indy handling ([#396](https://github.com/hyperledger/aries-framework-javascript/issues/396)) ([9f1a4a7](https://github.com/hyperledger/aries-framework-javascript/commit/9f1a4a754a61573ce3fee78d52615363c7e25d58)) +- **core:** add discover features protocol ([#390](https://github.com/hyperledger/aries-framework-javascript/issues/390)) ([3347424](https://github.com/hyperledger/aries-framework-javascript/commit/3347424326cd15e8bf2544a8af53b2fa57b1dbb8)) +- **core:** add support for multi use inviations ([#460](https://github.com/hyperledger/aries-framework-javascript/issues/460)) ([540ad7b](https://github.com/hyperledger/aries-framework-javascript/commit/540ad7be2133ee6609c2336b22b726270db98d6c)) +- **core:** connection-less issuance and verification ([#359](https://github.com/hyperledger/aries-framework-javascript/issues/359)) ([fb46ade](https://github.com/hyperledger/aries-framework-javascript/commit/fb46ade4bc2dd4f3b63d4194bb170d2f329562b7)) +- **core:** d_m invitation parameter and invitation image ([#456](https://github.com/hyperledger/aries-framework-javascript/issues/456)) ([f92c322](https://github.com/hyperledger/aries-framework-javascript/commit/f92c322b97be4a4867a82c3a35159d6068951f0b)) +- **core:** ledger module registerPublicDid implementation ([#398](https://github.com/hyperledger/aries-framework-javascript/issues/398)) ([5f2d512](https://github.com/hyperledger/aries-framework-javascript/commit/5f2d5126baed2ff58268c38755c2dbe75a654947)) +- **core:** store mediator id in connection record ([#503](https://github.com/hyperledger/aries-framework-javascript/issues/503)) ([da51f2e](https://github.com/hyperledger/aries-framework-javascript/commit/da51f2e8337f5774d23e9aeae0459bd7355a3760)) +- **core:** support image url in invitations ([#463](https://github.com/hyperledger/aries-framework-javascript/issues/463)) ([9fda24e](https://github.com/hyperledger/aries-framework-javascript/commit/9fda24ecf55fdfeba74211447e9fadfdcbf57385)) +- **core:** support multiple indy ledgers ([#474](https://github.com/hyperledger/aries-framework-javascript/issues/474)) ([47149bc](https://github.com/hyperledger/aries-framework-javascript/commit/47149bc5742456f4f0b75e0944ce276972e645b8)) +- **core:** update agent label and imageUrl plus per connection label and imageUrl ([#516](https://github.com/hyperledger/aries-framework-javascript/issues/516)) ([5e9a641](https://github.com/hyperledger/aries-framework-javascript/commit/5e9a64130c02c8a5fdf11f0e25d0c23929a33a4f)) +- **core:** validate outbound messages ([#526](https://github.com/hyperledger/aries-framework-javascript/issues/526)) ([9c3910f](https://github.com/hyperledger/aries-framework-javascript/commit/9c3910f1e67200b71bb4888c6fee62942afaff20)) +- expose wallet API ([#566](https://github.com/hyperledger/aries-framework-javascript/issues/566)) ([4027fc9](https://github.com/hyperledger/aries-framework-javascript/commit/4027fc975d7e4118892f43cb8c6a0eea412eaad4)) +- generic attachment handler ([#578](https://github.com/hyperledger/aries-framework-javascript/issues/578)) ([4d7d3c1](https://github.com/hyperledger/aries-framework-javascript/commit/4d7d3c1502d5eafa2b884a4a84934e072fe70ea6)) +- method to retrieve credentials for proof request ([#329](https://github.com/hyperledger/aries-framework-javascript/issues/329)) ([012afa6](https://github.com/hyperledger/aries-framework-javascript/commit/012afa6e455ebef1df024b5ba67b63ec66d1d8d5)) +- negotiation and auto accept credentials ([#336](https://github.com/hyperledger/aries-framework-javascript/issues/336)) ([55e8697](https://github.com/hyperledger/aries-framework-javascript/commit/55e86973e52e55235308696f4a7e0477b0dc01c6)) +- **node:** add http and ws inbound transport ([#392](https://github.com/hyperledger/aries-framework-javascript/issues/392)) ([34a6ff2](https://github.com/hyperledger/aries-framework-javascript/commit/34a6ff2699197b9d525422a0a405e241582a476c)) +- **node:** add is-indy-installed command ([#510](https://github.com/hyperledger/aries-framework-javascript/issues/510)) ([e50b821](https://github.com/hyperledger/aries-framework-javascript/commit/e50b821343970d299a4cacdcba3a051893524ed6)) +- only connect to ledger when needed ([#273](https://github.com/hyperledger/aries-framework-javascript/issues/273)) ([a9c261e](https://github.com/hyperledger/aries-framework-javascript/commit/a9c261eb22c86ad5d804e7a1bc792bb74cce5015)) +- Pack and send a message based on DidDoc services ([#304](https://github.com/hyperledger/aries-framework-javascript/issues/304)) ([6a26337](https://github.com/hyperledger/aries-framework-javascript/commit/6a26337fe1f52d661bd33208354a85e15512aec4)) +- **redux-store:** add mediation store ([#424](https://github.com/hyperledger/aries-framework-javascript/issues/424)) ([03e4341](https://github.com/hyperledger/aries-framework-javascript/commit/03e43418fb45cfa4d52e36fc04b98cd59a8eb21e)) +- **redux-store:** move from mobile agent repo ([#388](https://github.com/hyperledger/aries-framework-javascript/issues/388)) ([d84acc7](https://github.com/hyperledger/aries-framework-javascript/commit/d84acc75e24de4cd1cae99256df293276cc69c18)) +- **redux:** delete credentialRecord and proofRecord ([#421](https://github.com/hyperledger/aries-framework-javascript/issues/421)) ([9fa6c6d](https://github.com/hyperledger/aries-framework-javascript/commit/9fa6c6daf77ac56b9bc83ae3bfdae72cd919bc6c)) +- support newer did-communication service type ([#233](https://github.com/hyperledger/aries-framework-javascript/issues/233)) ([cf29d8f](https://github.com/hyperledger/aries-framework-javascript/commit/cf29d8fa3b4b6e098b9c7db87e73e84143a71c48)) +- support node v12+ ([#294](https://github.com/hyperledger/aries-framework-javascript/issues/294)) ([6ec201b](https://github.com/hyperledger/aries-framework-javascript/commit/6ec201bacb618bb08612dac832681e56a099bdde)) +- use computed tags for records ([#313](https://github.com/hyperledger/aries-framework-javascript/issues/313)) ([4e9a48b](https://github.com/hyperledger/aries-framework-javascript/commit/4e9a48b077dddd000e1c9826c653ec31d4b7897f)) +- Use session to send outbound message ([#362](https://github.com/hyperledger/aries-framework-javascript/issues/362)) ([7366ca7](https://github.com/hyperledger/aries-framework-javascript/commit/7366ca7b6ba2925a28020d5d063272505d53b0d5)) + +### BREAKING CHANGES + +- removed the getAll() function. +- The agent’s `shutdown` method does not delete the wallet anymore. If you want to delete the wallet, you can do it via exposed wallet API. +- class-transformer released a breaking change in a patch version, causing AFJ to break. I updated to the newer version and pinned the version exactly as this is the second time this has happened now. + +Signed-off-by: Timo Glastra + +- internal metadata is now prefixed with \_internal to avoid clashing and accidental overwriting of internal data. + +- fix(core): added \_internal/ prefix on metadata + +Signed-off-by: Berend Sliedrecht + +- credentialRecord.credentialMetadata has been replaced by credentialRecord.metadata. + +Signed-off-by: Berend Sliedrecht + +- a trust ping response will not be requested anymore after completing a connection. This is not required, and also non-standard behaviour. It was also causing some tests to be flaky as response messages were stil being sent after one of the agents had already shut down. + +Signed-off-by: Timo Glastra + +- The `ProofsModule.getRequestedCredentialsForProofRequest` expected some low level message objects as input. This is not in line with the public API of the rest of the framework and has been simplified to only require a proof record id and optionally a boolean whether the retrieved credentials should be filtered based on the proof proposal (if available). + +Signed-off-by: Timo Glastra + +- Proof request requestedAttributes and requestedPredicates are now a map instead of record. This is needed to have proper validation using class-validator. + +Signed-off-by: Timo Glastra + +- `BasicMessageReceivedEvent` has been replaced by the more general `BasicMessageStateChanged` event which triggers when a basic message is received or sent. + +Signed-off-by: NeilSMyers + +- Tags on a record can now be accessed using the `getTags()` method. Records should be updated with this method and return the properties from the record to include in the tags. + +Signed-off-by: Timo Glastra + +- extracts outbound transporter from Agent's constructor. + +Signed-off-by: Jakub Koci diff --git a/DEVREADME.md b/DEVREADME.md index 99ef9b7040..e470d61ad7 100644 --- a/DEVREADME.md +++ b/DEVREADME.md @@ -2,6 +2,11 @@ This file is intended for developers working on the internals of the framework. If you're just looking how to get started with the framework, see the [docs](./docs) +## Installing dependencies + +Right now, as a patch that will later be changed, some platforms will have an "error" when installing the dependencies with yarn. This is because the BBS signatures library that we use is built for Linux x86 and MacOS x86 (and not Windows and MacOS arm). This means that it will show that it could not download the binary. +This is not an error for developers, the library that fails is `node-bbs-signaturs` and is an optional dependency for perfomance improvements. It will fallback to a, slower, wasm build. + ## Running tests Test are executed using jest. Some test require either the **mediator agents** or the **ledger** to be running. When running tests that require a connection to the ledger pool, you need to set the `TEST_AGENT_PUBLIC_DID_SEED` and `GENESIS_TXN_PATH` environment variables. @@ -26,6 +31,9 @@ For testing we've added a setup to this repo that allows you to quickly setup an # Build indy pool docker build -f network/indy-pool.dockerfile -t indy-pool . --platform linux/amd64 +# NOTE: If you are on an ARM (M1) mac use the `network/indy-pool-arm.dockerfile` instead +# docker build -f network/indy-pool-arm.dockerfile -t indy-pool . --platform linux/arm64/v8 + # Start indy pool docker run -d --rm --name indy-pool -p 9701-9708:9701-9708 indy-pool diff --git a/Dockerfile b/Dockerfile index 36d396a3d8..d1c75fecd6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,9 @@ RUN apt-get update -y && apt-get install -y \ apt-transport-https \ curl \ # Only needed to build indy-sdk - build-essential + build-essential \ + git \ + libzmq3-dev libsodium-dev pkg-config libssl-dev # libindy RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 @@ -28,6 +30,19 @@ RUN apt-get update -y && apt-get install -y --allow-unauthenticated \ # Install yarn seperately due to `no-install-recommends` to skip nodejs install RUN apt-get install -y --no-install-recommends yarn +# postgres plugin setup +# install rust and set up rustup +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# clone indy-sdk and build postgres plugin +RUN git clone https://github.com/hyperledger/indy-sdk.git +WORKDIR /indy-sdk/experimental/plugins/postgres_storage/ +RUN cargo build --release + +# set up library path for postgres plugin +ENV LIB_INDY_STRG_POSTGRES="/indy-sdk/experimental/plugins/postgres_storage/target/release" + FROM base as final # AFJ specifc setup diff --git a/README.md b/README.md index 5eaed2452a..e99cce1b80 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,11 @@ Some features are not yet supported, but are on our roadmap. Check [the roadmap] - ✅ React Native - ✅ Node.JS +- ✅ Report Problem Protocol ([RFC 0035](https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md)) - ✅ Issue Credential Protocol ([RFC 0036](https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md)) - ✅ Present Proof Protocol ([RFC 0037](https://github.com/hyperledger/aries-rfcs/tree/master/features/0037-present-proof/README.md)) -- ✅ Connection Protocol ([RFC 0160](https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md)) - ✅ Basic Message Protocol ([RFC 0095](https://github.com/hyperledger/aries-rfcs/blob/master/features/0095-basic-message/README.md)) +- ✅ Connection Protocol ([RFC 0160](https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md)) - ✅ Mediator Coordination Protocol ([RFC 0211](https://github.com/hyperledger/aries-rfcs/blob/master/features/0211-route-coordination/README.md)) - ✅ Indy Credentials (with `did:sov` support) - ✅ HTTP & WebSocket Transport @@ -110,6 +111,12 @@ In order to use Aries Framework JavaScript some platform specific dependencies a - [NodeJS](/docs/setup-nodejs.md) - [Electron](/docs/setup-electron.md) +### Demo + +To get to know the AFJ flow, we built a demo to walk through it yourself together with agents Alice and Faber. + +- [Demo](/demo) + ### Usage Now that your project is setup and everything seems to be working, it is time to start building! Follow these guides below to get started! @@ -130,9 +137,10 @@ Also check out [Aries Framework JavaScript Extensions](https://github.com/hyperl Although Aries Framework JavaScript tries to follow the standards as described in the Aries RFCs as much as possible, some features in AFJ slightly diverge from the written spec. Below is an overview of the features that diverge from the spec, their impact and the reasons for diverging. -| Feature | Impact | Reason | -| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Support for `imageUrl` attribute in connection invitation and connection request | Properties that are not recognized should be ignored, meaning this shouldn't limit interoperability between agents. As the image url is self-attested it could give a false sense of trust. Better, credential based, method for visually identifying an entity are not present yet. | Even though not documented, almost all agents support this feature. Not including this feature means AFJ is lacking in features in comparison to other implementations. | +| Feature | Impact | Reason | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Support for `imageUrl` attribute in connection invitation and connection request | Properties that are not recognized should be ignored, meaning this shouldn't limit interoperability between agents. As the image url is self-attested it could give a false sense of trust. Better, credential based, method for visually identifying an entity are not present yet. | Even though not documented, almost all agents support this feature. Not including this feature means AFJ is lacking in features in comparison to other implementations. | +| Revocation Notification v1 uses a different `thread_id` format ( `indy::::`) than specified in the Aries RFC | Any agents adhering to the [revocation notification v1 RFC](https://github.com/hyperledger/aries-rfcs/tree/main/features/0183-revocation-notification) will not be interoperable with Aries Framework Javascript. However, revocation notification is considered an optional portion of revocation, therefore this will not break core revocation behavior. Ideally agents should use and implement revocation notification v2. | Actual implementations (ACA-Py) of revocation notification v1 so far have implemented this different format, so this format change was made to remain interoperable. | ## Contributing diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000000..5d2cd5a29c --- /dev/null +++ b/demo/README.md @@ -0,0 +1,89 @@ +

DEMO

+ +This is the Aries Framework Javascript demo. Walk through the AFJ flow yourself together with agents Alice and Faber. + +Alice, a former student of Faber College, connects with the College, is issued a credential about her degree and then is asked by the College for a proof. + +## Features + +- ✅ Creating a connection +- ✅ Offering a credential +- ✅ Requesting a proof +- ✅ Sending basic messages + +## Getting Started + +### Platform Specific Setup + +In order to use Aries Framework JavaScript some platform specific dependencies and setup is required. See our guides below to quickly set up you project with Aries Framework JavaScript for NodeJS, React Native and Electron. + +- [NodeJS](/docs/setup-nodejs.md) + +### Run the demo + +These are the steps for running the AFJ demo: + +Clone the AFJ git repository: + +```sh +git clone https://github.com/hyperledger/aries-framework-javascript.git +``` + +Open two different terminals next to each other and in both, go to the demo folder: + +```sh +cd aries-framework-javascript/demo +``` + +Install the project in one of the terminals: + +```sh +yarn install +``` + +In the left terminal run Alice: + +```sh +yarn alice +``` + +In the right terminal run Faber: + +```sh +yarn faber +``` + +### Usage + +To set up a connection: + +- Select 'setup connection' in both Agents +- Alice will print a invitation link which you then copy and paste to Faber +- You have now set up a connection! + +To offer a credential: + +- Select 'offer credential' in Faber +- Faber will start with registering a schema and the credential definition accordingly +- You have now send a credential offer to Alice! +- Go to Alice to accept the incoming credential offer + +To request a proof: + +- Select 'request proof' in Faber +- Faber will create a new proof attribute and will then send a proof request to Alice! +- Go to Alice to accept the incoming proof request + +To send a basic message: + +- Select 'send message' in either one of the Agents +- Type your message and press enter +- Message sent! + +Exit: + +- Select 'exit' to shutdown the agent. + +Restart: + +- Select 'restart', to shutdown the current agent and start a new one diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 0000000000..54bafd504b --- /dev/null +++ b/demo/package.json @@ -0,0 +1,26 @@ +{ + "name": "afj-demo", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "demo/" + }, + "license": "Apache-2.0", + "scripts": { + "alice": "ts-node src/AliceInquirer.ts", + "faber": "ts-node src/FaberInquirer.ts", + "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" + }, + "devDependencies": { + "@aries-framework/core": "^0.1.0", + "@aries-framework/node": "^0.1.0", + "@types/figlet": "^1.5.4", + "@types/inquirer": "^8.1.3", + "clear": "^0.1.0", + "commander": "^8.3.0", + "figlet": "^1.5.2", + "ts-node": "^10.4.0" + } +} diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts new file mode 100644 index 0000000000..c46db6988b --- /dev/null +++ b/demo/src/Alice.ts @@ -0,0 +1,94 @@ +/*eslint import/no-cycle: [2, { maxDepth: 1 }]*/ +import type { CredentialExchangeRecord, ProofRecord } from '@aries-framework/core' + +import { BaseAgent } from './BaseAgent' +import { greenText, Output, redText } from './OutputClass' + +export class Alice extends BaseAgent { + public connectionRecordFaberId?: string + public connected: boolean + + public constructor(port: number, name: string) { + super(port, name) + this.connected = false + } + + public static async build(): Promise { + const alice = new Alice(9000, 'alice') + await alice.initializeAgent() + return alice + } + + private async getConnectionRecord() { + if (!this.connectionRecordFaberId) { + throw Error(redText(Output.MissingConnectionRecord)) + } + return await this.agent.connections.getById(this.connectionRecordFaberId) + } + + private async printConnectionInvite() { + const outOfBand = await this.agent.oob.createInvitation() + // FIXME: this won't work as oob doesn't create a connection immediately + const [connectionRecord] = await this.agent.connections.findAllByOutOfBandId(outOfBand.id) + if (!connectionRecord) { + throw new Error(redText(Output.NoConnectionRecordFromOutOfBand)) + } + this.connectionRecordFaberId = connectionRecord.id + + console.log( + Output.ConnectionLink, + outOfBand.outOfBandInvitation.toUrl({ domain: `http://localhost:${this.port}` }), + '\n' + ) + return connectionRecord + } + + private async waitForConnection() { + const connectionRecord = await this.getConnectionRecord() + + console.log('Waiting for Faber to finish connection...') + try { + await this.agent.connections.returnWhenIsConnected(connectionRecord.id) + } catch (e) { + console.log(redText(`\nTimeout of 20 seconds reached.. Returning to home screen.\n`)) + return + } + console.log(greenText(Output.ConnectionEstablished)) + this.connected = true + } + + public async setupConnection() { + await this.printConnectionInvite() + await this.waitForConnection() + } + + public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) { + await this.agent.credentials.acceptOffer({ + credentialRecordId: credentialRecord.id, + }) + } + + public async acceptProofRequest(proofRecord: ProofRecord) { + const retrievedCredentials = await this.agent.proofs.getRequestedCredentialsForProofRequest(proofRecord.id, { + filterByPresentationPreview: true, + }) + const requestedCredentials = this.agent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) + await this.agent.proofs.acceptRequest(proofRecord.id, requestedCredentials) + console.log(greenText('\nProof request accepted!\n')) + } + + public async sendMessage(message: string) { + const connectionRecord = await this.getConnectionRecord() + await this.agent.basicMessages.sendMessage(connectionRecord.id, message) + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo/src/AliceInquirer.ts b/demo/src/AliceInquirer.ts new file mode 100644 index 0000000000..44ae7432c4 --- /dev/null +++ b/demo/src/AliceInquirer.ts @@ -0,0 +1,126 @@ +import type { CredentialExchangeRecord, ProofRecord } from '@aries-framework/core' + +import { clear } from 'console' +import { textSync } from 'figlet' +import inquirer from 'inquirer' + +import { Alice } from './Alice' +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Listener } from './Listener' +import { Title } from './OutputClass' + +export const runAlice = async () => { + clear() + console.log(textSync('Alice', { horizontalLayout: 'full' })) + const alice = await AliceInquirer.build() + await alice.processAnswer() +} + +enum PromptOptions { + CreateConnection = 'Create connection invitation', + SendMessage = 'Send message', + Exit = 'Exit', + Restart = 'Restart', +} + +export class AliceInquirer extends BaseInquirer { + public alice: Alice + public promptOptionsString: string[] + public listener: Listener + + public constructor(alice: Alice) { + super() + this.alice = alice + this.listener = new Listener() + this.promptOptionsString = Object.values(PromptOptions) + this.listener.messageListener(this.alice.agent, this.alice.name) + } + + public static async build(): Promise { + const alice = await Alice.build() + return new AliceInquirer(alice) + } + + private async getPromptChoice() { + if (this.alice.connectionRecordFaberId) return inquirer.prompt([this.inquireOptions(this.promptOptionsString)]) + + const reducedOption = [PromptOptions.CreateConnection, PromptOptions.Exit, PromptOptions.Restart] + return inquirer.prompt([this.inquireOptions(reducedOption)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + if (this.listener.on) return + + switch (choice.options) { + case PromptOptions.CreateConnection: + await this.connection() + break + case PromptOptions.SendMessage: + await this.message() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) { + const confirm = await inquirer.prompt([this.inquireConfirmation(Title.CredentialOfferTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.alice.agent.credentials.declineOffer(credentialRecord.id) + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.acceptCredentialOffer(credentialRecord) + } + } + + public async acceptProofRequest(proofRecord: ProofRecord) { + const confirm = await inquirer.prompt([this.inquireConfirmation(Title.ProofRequestTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.alice.agent.proofs.declineRequest(proofRecord.id) + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.acceptProofRequest(proofRecord) + } + } + + public async connection() { + await this.alice.setupConnection() + if (!this.alice.connected) return + + this.listener.credentialOfferListener(this.alice, this) + this.listener.proofRequestListener(this.alice, this) + } + + public async message() { + const message = await this.inquireMessage() + if (!message) return + + await this.alice.sendMessage(message) + } + + public async exit() { + const confirm = await inquirer.prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.exit() + } + } + + public async restart() { + const confirm = await inquirer.prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.alice.restart() + await runAlice() + } + } +} + +void runAlice() diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts new file mode 100644 index 0000000000..3332af9ec4 --- /dev/null +++ b/demo/src/BaseAgent.ts @@ -0,0 +1,54 @@ +import type { InitConfig } from '@aries-framework/core' + +import { Agent, AutoAcceptCredential, AutoAcceptProof, HttpOutboundTransport } from '@aries-framework/core' +import { agentDependencies, HttpInboundTransport } from '@aries-framework/node' + +import { greenText } from './OutputClass' + +const bcovrin = `{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"138.197.138.255","client_port":9702,"node_ip":"138.197.138.255","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"138.197.138.255","client_port":9704,"node_ip":"138.197.138.255","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"138.197.138.255","client_port":9706,"node_ip":"138.197.138.255","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"} +{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"138.197.138.255","client_port":9708,"node_ip":"138.197.138.255","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}` + +export class BaseAgent { + public port: number + public name: string + public config: InitConfig + public agent: Agent + + public constructor(port: number, name: string) { + this.name = name + this.port = port + + const config: InitConfig = { + label: name, + walletConfig: { + id: name, + key: name, + }, + publicDidSeed: '6b8b882e2618fa5d45ee7229ca880083', + indyLedgers: [ + { + genesisTransactions: bcovrin, + id: 'greenlights' + name, + isProduction: false, + }, + ], + endpoints: [`http://localhost:${this.port}`], + autoAcceptConnections: true, + autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + autoAcceptProofs: AutoAcceptProof.ContentApproved, + } + + this.config = config + + this.agent = new Agent(config, agentDependencies) + this.agent.registerInboundTransport(new HttpInboundTransport({ port })) + this.agent.registerOutboundTransport(new HttpOutboundTransport()) + } + + public async initializeAgent() { + await this.agent.initialize() + console.log(greenText(`\nAgent ${this.name} created!\n`)) + } +} diff --git a/demo/src/BaseInquirer.ts b/demo/src/BaseInquirer.ts new file mode 100644 index 0000000000..6c43a5eb11 --- /dev/null +++ b/demo/src/BaseInquirer.ts @@ -0,0 +1,55 @@ +import inquirer from 'inquirer' + +import { Title } from './OutputClass' + +export enum ConfirmOptions { + Yes = 'yes', + No = 'no', +} + +export class BaseInquirer { + public optionsInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] } + public inputInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] } + + public constructor() { + this.optionsInquirer = { + type: 'list', + prefix: '', + name: 'options', + message: '', + choices: [], + } + + this.inputInquirer = { + type: 'input', + prefix: '', + name: 'input', + message: '', + choices: [], + } + } + + public inquireOptions(promptOptions: string[]) { + this.optionsInquirer.message = Title.OptionsTitle + this.optionsInquirer.choices = promptOptions + return this.optionsInquirer + } + + public inquireInput(title: string) { + this.inputInquirer.message = title + return this.inputInquirer + } + + public inquireConfirmation(title: string) { + this.optionsInquirer.message = title + this.optionsInquirer.choices = [ConfirmOptions.Yes, ConfirmOptions.No] + return this.optionsInquirer + } + + public async inquireMessage() { + this.inputInquirer.message = Title.MessageTitle + const message = await inquirer.prompt([this.inputInquirer]) + + return message.input[0] === 'q' ? null : message.input + } +} diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts new file mode 100644 index 0000000000..a99b02cac8 --- /dev/null +++ b/demo/src/Faber.ts @@ -0,0 +1,168 @@ +import type { ConnectionRecord } from '@aries-framework/core' +import type { CredDef, Schema } from 'indy-sdk' +import type BottomBar from 'inquirer/lib/ui/bottom-bar' + +import { + CredentialProtocolVersion, + V1CredentialPreview, + AttributeFilter, + ProofAttributeInfo, + utils, +} from '@aries-framework/core' +import { ui } from 'inquirer' + +import { BaseAgent } from './BaseAgent' +import { Color, greenText, Output, purpleText, redText } from './OutputClass' + +export class Faber extends BaseAgent { + public connectionRecordAliceId?: string + public credentialDefinition?: CredDef + public ui: BottomBar + + public constructor(port: number, name: string) { + super(port, name) + this.ui = new ui.BottomBar() + } + + public static async build(): Promise { + const faber = new Faber(9001, 'faber') + await faber.initializeAgent() + return faber + } + + private async getConnectionRecord() { + if (!this.connectionRecordAliceId) { + throw Error(redText(Output.MissingConnectionRecord)) + } + return await this.agent.connections.getById(this.connectionRecordAliceId) + } + + private async receiveConnectionRequest(invitationUrl: string) { + const { connectionRecord } = await this.agent.oob.receiveInvitationFromUrl(invitationUrl) + if (!connectionRecord) { + throw new Error(redText(Output.NoConnectionRecordFromOutOfBand)) + } + return connectionRecord + } + + private async waitForConnection(connectionRecord: ConnectionRecord) { + connectionRecord = await this.agent.connections.returnWhenIsConnected(connectionRecord.id) + console.log(greenText(Output.ConnectionEstablished)) + return connectionRecord.id + } + + public async acceptConnection(invitation_url: string) { + const connectionRecord = await this.receiveConnectionRequest(invitation_url) + this.connectionRecordAliceId = await this.waitForConnection(connectionRecord) + } + + private printSchema(name: string, version: string, attributes: string[]) { + console.log(`\n\nThe credential definition will look like this:\n`) + console.log(purpleText(`Name: ${Color.Reset}${name}`)) + console.log(purpleText(`Version: ${Color.Reset}${version}`)) + console.log(purpleText(`Attributes: ${Color.Reset}${attributes[0]}, ${attributes[1]}, ${attributes[2]}\n`)) + } + + private async registerSchema() { + const schemaTemplate = { + name: 'Faber College' + utils.uuid(), + version: '1.0.0', + attributes: ['name', 'degree', 'date'], + } + this.printSchema(schemaTemplate.name, schemaTemplate.version, schemaTemplate.attributes) + this.ui.updateBottomBar(greenText('\nRegistering schema...\n', false)) + const schema = await this.agent.ledger.registerSchema(schemaTemplate) + this.ui.updateBottomBar('\nSchema registered!\n') + return schema + } + + private async registerCredentialDefinition(schema: Schema) { + this.ui.updateBottomBar('\nRegistering credential definition...\n') + this.credentialDefinition = await this.agent.ledger.registerCredentialDefinition({ + schema, + tag: 'latest', + supportRevocation: false, + }) + this.ui.updateBottomBar('\nCredential definition registered!!\n') + return this.credentialDefinition + } + + private getCredentialPreview() { + const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'Alice Smith', + degree: 'Computer Science', + date: '01/01/2022', + }) + return credentialPreview + } + + public async issueCredential() { + const schema = await this.registerSchema() + const credDef = await this.registerCredentialDefinition(schema) + const credentialPreview = this.getCredentialPreview() + const connectionRecord = await this.getConnectionRecord() + + this.ui.updateBottomBar('\nSending credential offer...\n') + + await this.agent.credentials.offerCredential({ + connectionId: connectionRecord.id, + protocolVersion: CredentialProtocolVersion.V1, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDef.id, + }, + }, + }) + this.ui.updateBottomBar( + `\nCredential offer sent!\n\nGo to the Alice agent to accept the credential offer\n\n${Color.Reset}` + ) + } + + private async printProofFlow(print: string) { + this.ui.updateBottomBar(print) + await new Promise((f) => setTimeout(f, 2000)) + } + + private async newProofAttribute() { + await this.printProofFlow(greenText(`Creating new proof attribute for 'name' ...\n`)) + const proofAttribute = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: this.credentialDefinition?.id, + }), + ], + }), + } + return proofAttribute + } + + public async sendProofRequest() { + const connectionRecord = await this.getConnectionRecord() + const proofAttribute = await this.newProofAttribute() + await this.printProofFlow(greenText('\nRequesting proof...\n', false)) + await this.agent.proofs.requestProof(connectionRecord.id, { + requestedAttributes: proofAttribute, + }) + this.ui.updateBottomBar( + `\nProof request sent!\n\nGo to the Alice agent to accept the proof request\n\n${Color.Reset}` + ) + } + + public async sendMessage(message: string) { + const connectionRecord = await this.getConnectionRecord() + await this.agent.basicMessages.sendMessage(connectionRecord.id, message) + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo/src/FaberInquirer.ts b/demo/src/FaberInquirer.ts new file mode 100644 index 0000000000..a61ec60175 --- /dev/null +++ b/demo/src/FaberInquirer.ts @@ -0,0 +1,133 @@ +import { clear } from 'console' +import { textSync } from 'figlet' +import inquirer from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Faber } from './Faber' +import { Listener } from './Listener' +import { Title } from './OutputClass' + +export const runFaber = async () => { + clear() + console.log(textSync('Faber', { horizontalLayout: 'full' })) + const faber = await FaberInquirer.build() + await faber.processAnswer() +} + +enum PromptOptions { + ReceiveConnectionUrl = 'Receive connection invitation', + OfferCredential = 'Offer credential', + RequestProof = 'Request proof', + SendMessage = 'Send message', + Exit = 'Exit', + Restart = 'Restart', +} + +export class FaberInquirer extends BaseInquirer { + public faber: Faber + public promptOptionsString: string[] + public listener: Listener + + public constructor(faber: Faber) { + super() + this.faber = faber + this.listener = new Listener() + this.promptOptionsString = Object.values(PromptOptions) + this.listener.messageListener(this.faber.agent, this.faber.name) + } + + public static async build(): Promise { + const faber = await Faber.build() + return new FaberInquirer(faber) + } + + private async getPromptChoice() { + if (this.faber.connectionRecordAliceId) return inquirer.prompt([this.inquireOptions(this.promptOptionsString)]) + + const reducedOption = [PromptOptions.ReceiveConnectionUrl, PromptOptions.Exit, PromptOptions.Restart] + return inquirer.prompt([this.inquireOptions(reducedOption)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + if (this.listener.on) return + + switch (choice.options) { + case PromptOptions.ReceiveConnectionUrl: + await this.connection() + break + case PromptOptions.OfferCredential: + await this.credential() + return + case PromptOptions.RequestProof: + await this.proof() + return + case PromptOptions.SendMessage: + await this.message() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async connection() { + const title = Title.InvitationTitle + const getUrl = await inquirer.prompt([this.inquireInput(title)]) + await this.faber.acceptConnection(getUrl.input) + } + + public async exitUseCase(title: string) { + const confirm = await inquirer.prompt([this.inquireConfirmation(title)]) + if (confirm.options === ConfirmOptions.No) { + return false + } else if (confirm.options === ConfirmOptions.Yes) { + return true + } + } + + public async credential() { + await this.faber.issueCredential() + const title = 'Is the credential offer accepted?' + await this.listener.newAcceptedPrompt(title, this) + } + + public async proof() { + await this.faber.sendProofRequest() + const title = 'Is the proof request accepted?' + await this.listener.newAcceptedPrompt(title, this) + } + + public async message() { + const message = await this.inquireMessage() + if (message) return + + await this.faber.sendMessage(message) + } + + public async exit() { + const confirm = await inquirer.prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.faber.exit() + } + } + + public async restart() { + const confirm = await inquirer.prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.faber.restart() + await runFaber() + } + } +} + +void runFaber() diff --git a/demo/src/Listener.ts b/demo/src/Listener.ts new file mode 100644 index 0000000000..97f98c741a --- /dev/null +++ b/demo/src/Listener.ts @@ -0,0 +1,110 @@ +import type { Alice } from './Alice' +import type { AliceInquirer } from './AliceInquirer' +import type { Faber } from './Faber' +import type { FaberInquirer } from './FaberInquirer' +import type { + Agent, + BasicMessageStateChangedEvent, + CredentialExchangeRecord, + CredentialStateChangedEvent, + ProofRecord, + ProofStateChangedEvent, +} from '@aries-framework/core' +import type BottomBar from 'inquirer/lib/ui/bottom-bar' + +import { + BasicMessageEventTypes, + BasicMessageRole, + CredentialEventTypes, + CredentialState, + ProofEventTypes, + ProofState, +} from '@aries-framework/core' +import { ui } from 'inquirer' + +import { Color, purpleText } from './OutputClass' + +export class Listener { + public on: boolean + private ui: BottomBar + + public constructor() { + this.on = false + this.ui = new ui.BottomBar() + } + + private turnListenerOn() { + this.on = true + } + + private turnListenerOff() { + this.on = false + } + + private printCredentialAttributes(credentialRecord: CredentialExchangeRecord) { + if (credentialRecord.credentialAttributes) { + const attribute = credentialRecord.credentialAttributes + console.log('\n\nCredential preview:') + attribute.forEach((element) => { + console.log(purpleText(`${element.name} ${Color.Reset}${element.value}`)) + }) + } + } + + private async newCredentialPrompt(credentialRecord: CredentialExchangeRecord, aliceInquirer: AliceInquirer) { + this.printCredentialAttributes(credentialRecord) + this.turnListenerOn() + await aliceInquirer.acceptCredentialOffer(credentialRecord) + this.turnListenerOff() + await aliceInquirer.processAnswer() + } + + public credentialOfferListener(alice: Alice, aliceInquirer: AliceInquirer) { + alice.agent.events.on( + CredentialEventTypes.CredentialStateChanged, + async ({ payload }: CredentialStateChangedEvent) => { + if (payload.credentialRecord.state === CredentialState.OfferReceived) { + await this.newCredentialPrompt(payload.credentialRecord, aliceInquirer) + } + } + ) + } + + public messageListener(agent: Agent, name: string) { + agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, async (event: BasicMessageStateChangedEvent) => { + if (event.payload.basicMessageRecord.role === BasicMessageRole.Receiver) { + this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${event.payload.message.content}\n`)) + } + }) + } + + private async newProofRequestPrompt(proofRecord: ProofRecord, aliceInquirer: AliceInquirer) { + this.turnListenerOn() + await aliceInquirer.acceptProofRequest(proofRecord) + this.turnListenerOff() + await aliceInquirer.processAnswer() + } + + public proofRequestListener(alice: Alice, aliceInquirer: AliceInquirer) { + alice.agent.events.on(ProofEventTypes.ProofStateChanged, async ({ payload }: ProofStateChangedEvent) => { + if (payload.proofRecord.state === ProofState.RequestReceived) { + await this.newProofRequestPrompt(payload.proofRecord, aliceInquirer) + } + }) + } + + public proofAcceptedListener(faber: Faber, faberInquirer: FaberInquirer) { + faber.agent.events.on(ProofEventTypes.ProofStateChanged, async ({ payload }: ProofStateChangedEvent) => { + if (payload.proofRecord.state === ProofState.Done) { + await faberInquirer.processAnswer() + } + }) + } + + public async newAcceptedPrompt(title: string, faberInquirer: FaberInquirer) { + this.turnListenerOn() + await faberInquirer.exitUseCase(title) + this.turnListenerOff() + await faberInquirer.processAnswer() + } +} diff --git a/demo/src/OutputClass.ts b/demo/src/OutputClass.ts new file mode 100644 index 0000000000..3d7b9ebff3 --- /dev/null +++ b/demo/src/OutputClass.ts @@ -0,0 +1,40 @@ +export enum Color { + Green = `\x1b[32m`, + Red = `\x1b[31m`, + Purple = `\x1b[35m`, + Reset = `\x1b[0m`, +} + +export enum Output { + NoConnectionRecordFromOutOfBand = `\nNo connectionRecord has been created from invitation\n`, + ConnectionEstablished = `\nConnection established!`, + MissingConnectionRecord = `\nNo connectionRecord ID has been set yet\n`, + ConnectionLink = `\nRun 'Receive connection invitation' in Faber and paste this invitation link:\n\n`, + Exit = 'Shutting down agent...\nExiting...', +} + +export enum Title { + OptionsTitle = '\nOptions:', + InvitationTitle = '\n\nPaste the invitation url here:', + MessageTitle = '\n\nWrite your message here:\n(Press enter to send or press q to exit)\n', + ConfirmTitle = '\n\nAre you sure?', + CredentialOfferTitle = '\n\nCredential offer received, do you want to accept it?', + ProofRequestTitle = '\n\nProof request received, do you want to accept it?', +} + +export const greenText = (text: string, reset?: boolean) => { + if (reset) return Color.Green + text + Color.Reset + + return Color.Green + text +} + +export const purpleText = (text: string, reset?: boolean) => { + if (reset) return Color.Purple + text + Color.Reset + return Color.Purple + text +} + +export const redText = (text: string, reset?: boolean) => { + if (reset) return Color.Red + text + Color.Reset + + return Color.Red + text +} diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 0000000000..df890c6054 --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node"] + } +} diff --git a/docs/getting-started/0-agent.md b/docs/getting-started/0-agent.md index 07ca2edaa8..007b30e8ad 100644 --- a/docs/getting-started/0-agent.md +++ b/docs/getting-started/0-agent.md @@ -137,7 +137,7 @@ const agentConfig: InitConfig = { isProduction: false, }, ], - logger: new ConsoleLogger(LogLevel.debug), + logger: new ConsoleLogger(LogLevel.info), } const agent = new Agent(agentConfig, agentDependencies) diff --git a/docs/getting-started/6-proofs.md b/docs/getting-started/6-proofs.md index e1d82dab66..82422ea69f 100644 --- a/docs/getting-started/6-proofs.md +++ b/docs/getting-started/6-proofs.md @@ -1,3 +1,147 @@ # Proofs -> TODO +As mentioned in the previous documentation ([Credentials](5-credentials.md)), after receiving a credential and saving it to your wallet, you will need to show it to a verifier who will verify the authenticity of this credential and that the credential assertions are not tampered with. + +In VC proofs, we have two involved parties: + +- Holder (prover) +- Verifier + +The process for proving your VC starts by a verifier to request a presentation from a prover, and for the prover to respond by presenting a proof to the verifier or the prover to send a presentation proposal to the verifier. + +## Method 1 - Prover (holder) responds to presentation request from the verifier + +> Note: This setup is assumed for a react native mobile agent + +> Note: This process assumes there is an established connection between the prover and the verifier + +## Full Example Code + +```ts +const handleProofStateChange = async (event: ProofStateChangedEvent) => { + const proofRecord = event.payload.proofRecord + // previous state -> presentation-sent new state: done + if (event.payload.previousState === ProofState.PresentationSent && proofRecord.state === ProofState.Done) { + Alert.alert('Credential Proved!') + return + } + if (proofRecord.state === ProofState.RequestReceived) { + const proofRequest = proofRecord.requestMessage?.indyProofRequest + + //Retrieve credentials + const retrievedCredentials = await agent.proofs.getRequestedCredentialsForProofRequest(proofRecord.id, { + filterByPresentationPreview: true, + }) + + const requestedCredentials = agent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) + agent.proofs.acceptRequest(event.payload.proofRecord.id, requestedCredentials) + } +} +``` + +### 1. Configure agent + +Please make sure you reviewed the [agent setup overview](0-agent.md). + +### 2. Configure proof events handler + +This handler will be triggered whenever there is a Proof state change. + +```ts +const handleProofStateChange = async (agent: Agent, event: ProofStateChangedEvent) => { + console.log( + `>> Proof state changed: ${event.payload.proofRecord.id}, previous state -> ${event.payload.previousState} new state: ${event.payload.proofRecord.state}` + ) + + if (event.payload.proofRecord.state === ProofState.RequestReceived) { + const retrievedCredentials = await agent.proofs.getRequestedCredentialsForProofRequest( + event.payload.proofRecord.id, + { + filterByPresentationPreview: true, + } + ) + + const requestedCredentials = agent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) + + agent.proofs.acceptRequest(event.payload.proofRecord.id, requestedCredentials) + } +} +``` + +- `filterByPresentationPreview`: Whether to filter the retrieved credentials using the presentation preview. This configuration will only have effect if a presentation proposal message is available containing a presentation preview.. + +Make sure to add the event listener to the agent after initializing the wallet + +```ts +agent.events.on(ProofEventTypes.ProofStateChanged, handleProofStateChange) +``` + +## Manually accepting proof request + +```ts +const handleProofStateChange = async (event: ProofStateChangedEvent) => { + .. + + //Construct pop up message + var message = '>> Proof Request Recieved <<\n'; + message += `To prove:${proofRequest?.name}\n`; + message += 'Attributes to prove:\n'; + + //Loop through requested attributes + Object.values(proofRequest.requestedAttributes).forEach(attr => { + message += `${attr.name}\n`; + }); + + message += `Accept proof request?`; + Alert.alert('Attention!', message, [ + { + text: 'Accept', + onPress: () => { + agent.proofs.acceptRequest(event.payload.proofRecord.id, + requestedCredentials, + ); + }, + }, + { + text: 'Reject', + onPress: () => { + //User rejected + }, + }, + ]); + } + }; +``` + +By sending the response to the verifier, the verifier will go through the process of verifying the VC and respond with an ack message. + +To give some context to the user you can add the following code to the Proof event handler + +```ts +const handleProofStateChange = async (agent: Agent, event: ProofStateChangedEvent) => { + ... + if ( + event.payload.previousState === ProofState.PresentationSent && + event.payload.proofRecord.state === ProofState.Done + ) { + console.log('Done proving credentials'); + Alert.alert('Credential Proved!'); + return; + } + .... + }; +``` + +## Method 2 - Prover sends a presentation proposal to verifier + +> To do + +## Connectionless Proof Request + +> To do + +## References + +- [Verifiable credentials model](https://www.w3.org/TR/vc-data-model/). +- [Present Proof Protocol 1.0](https://github.com/hyperledger/aries-rfcs/blob/main/features/0037-present-proof/README.md). +- [Present Proof Protocol 2.0](https://github.com/hyperledger/aries-rfcs/blob/main/features/0454-present-proof-v2/README.md). diff --git a/docs/getting-started/7-logging.md b/docs/getting-started/7-logging.md index 0026681def..508f634700 100644 --- a/docs/getting-started/7-logging.md +++ b/docs/getting-started/7-logging.md @@ -9,7 +9,7 @@ import { ConsoleLogger, LogLevel } from '@aries-framework/core' const agentConfig = { // ... other config properties ... - logger: new ConsoleLogger(LogLevel.debug), + logger: new ConsoleLogger(LogLevel.info), } ``` diff --git a/docs/libindy/macos-apple.md b/docs/libindy/macos-apple.md index 144721313f..468b605c6e 100644 --- a/docs/libindy/macos-apple.md +++ b/docs/libindy/macos-apple.md @@ -12,22 +12,22 @@ The first thing we'll do is install OpenSSL. Since Apple replaced OpenSSL with their own version of LibreSSL, we'll need to install it. Also, we need to install a specific version of OpenSSL that is compatible with Apples architecture. After the installation, we need to link it, so that it overrides the default openssl command (we have not noticed any issues with overriding this command, but be cautious). ```sh -curl https://raw.githubusercontent.com/rbenv/homebrew-tap/e472b7861b49cc082d1db0f66f265368da107589/Formula/openssl%401.0.rb -o openssl@1.0.rb +brew install openssl@1.1 # possibly already installed on your system -brew install ./openssl@1.0.rb +brew link openssl@1.1 --force -rm -rf ./openssl@1.0.rb +echo 'export PATH="/opt/homebrew/opt/openssl@1.1/bin:$PATH"' >> ~/.zshrc + +source ~/.zshrc -brew link openssl@1.0 --force ``` -This script downloads a file and names it `openssl@1.0.rb`. After the download, we're installing it via Brew. After the installation, the file will be deleted and the correct version of OpenSSL is installed! To double-check if the correct version is installed, you need to restart your terminal session and run the following command: ```sh openssl version -# OUTPUT: OpenSSL 1.0.2u 20 Dec 2019 +# OUTPUT: OpenSSL 1.1.1m 14 Dec 2021 ``` ## Step 2: Installing other dependencies diff --git a/docs/migration/0.1-to-0.2.md b/docs/migration/0.1-to-0.2.md new file mode 100644 index 0000000000..ea8fc6d893 --- /dev/null +++ b/docs/migration/0.1-to-0.2.md @@ -0,0 +1,75 @@ +# Migrating from AFJ 0.1.0 to 0.2.x + +## Breaking Code Changes + +> TODO + +## Breaking Storage Changes + +The 0.2.0 release is heavy on breaking changes to the storage format. This is not what we intend to do with every release. But as there's not that many people yet using the framework in production, and there were a lot of changes needed to keep the API straightforward, we decided to bundle a lot of breaking changes in this one release. + +Below all breaking storage changes are explained in as much detail as possible. The update assistant provides all tools to migrate without a hassle, but it is important to know what has changed. + +See [Updating](./updating.md) for a guide on how to use the update assistant. + +The following config can be provided to the update assistant to migrate from 0.1.0 to 0.2.0: + +```json +{ + "v0_1ToV0_2": { + "mediationRoleUpdateStrategy": "" + } +} +``` + +### Credential Metadata + +The credential record had a custom `metadata` property in pre-0.1.0 storage that contained the `requestMetadata`, `schemaId` and `credentialDefinition` properties. Later a generic metadata API was added that only allows objects to be stored. Therefore the properties were moved into a different structure. + +The following pre-0.1.0 structure: + +```json +{ + "requestMetadata": , + "schemaId": "", + "credentialDefinitionId": "" +} +``` + +Will be transformed into the following 0.2.0 structure: + +```json +{ + "_internal/indyRequest": , + "_internal/indyCredential": { + "schemaId": "", + "credentialDefinitionId": "" + } +} +``` + +Accessing the `credentialDefinitionId` and `schemaId` properties will now be done by retrieving the `CredentialMetadataKeys.IndyCredential` metadata key. + +```ts +const indyCredential = credentialRecord.metadata.get(CredentialMetadataKeys.IndyCredential) + +// both properties are optional +indyCredential?.credentialDefinitionId +indyCredential?.schemaId +``` + +### Mediation Record Role + +The role in the mediation record was always being set to `MediationRole.Mediator` for both mediators and recipients. This didn't cause any issues, but would return the wrong role for recipients. + +In 0.2 a check is added to make sure the role of a mediation record matches with actions (e.g. a recipient can't grant mediation), which means it will throw an error if the role is not set correctly. + +Because it's not always possible detect whether the role should actually be mediator or recipient, a number of configuration options are provided on how the role should be updated using the `v0_1ToV0_2.mediationRoleUpdateStrategy` option: + +- `allMediator`: The role is set to `MediationRole.Mediator` for both mediators and recipients +- `allRecipient`: The role is set to `MediationRole.Recipient` for both mediators and recipients +- `recipientIfEndpoint` (**default**): The role is set to `MediationRole.Recipient` if their is an `endpoint` configured on the record. The endpoint is not set when running as a mediator. There is one case where this could be problematic when the role should be recipient, if the mediation grant hasn't actually occurred (meaning the endpoint is not set). This is probably the best approach + otherwise it is set to `MediationRole.Mediator` +- `doNotChange`: The role is not changed + +Most agents only act as either the role of mediator or recipient, in which case the `allMediator` or `allRecipient` configuration is the most appropriate. If your agent acts as both a recipient and mediator, the `recipientIfEndpoint` configuration is the most appropriate. The `doNotChange` options is not recommended and can lead to errors if the role is not set correctly. diff --git a/docs/migration/updating.md b/docs/migration/updating.md new file mode 100644 index 0000000000..b0089d4772 --- /dev/null +++ b/docs/migration/updating.md @@ -0,0 +1,121 @@ +# Updating + +- [Update Strategies](#update-strategies) +- [Backups](#backups) + +## Update Strategies + +There are three options on how to leverage the update assistant on agent startup: + +1. Manually instantiating the update assistant on agent startup +2. Storing the agent storage version outside of the agent storage +3. Automatically update on agent startup + +### Manually instantiating the update assistant on agent startup + +When the version of the storage is stored inside the agent storage, it means we need to check if the agent needs to be updated on every agent startup. We'll initialize the update assistant and check whether the storage is up to date. The advantage of this approach is that you don't have to store anything yourself, and have full control over the workflow. + +```ts +import { UpdateAssistant, Agent } from '@aries-framework/core' + +// or @aries-framework/node +import { agentDependencies } from '@aries-framework/react-native' + +// First create the agent +const agent = new Agent(config, agentDependencies) + +// Then initialize the update assistant with the update config +const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'allMediator', + }, +}) + +// Initialize the update assistant so we can read the current storage version +// from the wallet. If you manually initialize the wallet you should do this _before_ +// calling initialize on the update assistant +// await agent.wallet.initialize(walletConfig) +await updateAssistant.initialize() + +// Check if the agent is up to date, if not call update +if (!(await updateAssistant.isUpToDate())) { + await updateAssistant.update() +} + +// Once finished initialize the agent. You should do this on every launch of the agent +await agent.initialize() +``` + +### Storing the agent storage version outside of the agent storage + +When the version of the storage is stored outside of the agent storage, you don't have to initialize the `UpdateAssistant` on every agent agent startup. You can just check if the storage version is up to date and instantiate the `UpdateAssistant` if not. The advantage of this approach is that you don't have to instantiate the `UpdateAssistant` on every agent startup, but this does mean that you have to store the storage version yourself. + +When a wallet has been exported and later imported you don't always have the latest version available. If this is the case you can always rely on Method 1 or 2 for updating the wallet, and storing the version yourself afterwards. You can also get the current version by calling `await updateAssistant.getCurrentAgentStorageVersion()`. Do note the `UpdateAssistant` needs to be initialized before calling this method. + +```ts +import { UpdateAssistant, Agent } from '@aries-framework/core' + +// or @aries-framework/node +import { agentDependencies } from '@aries-framework/react-native' + +// The storage version will normally be stored in e.g. persistent storage on a mobile device +let currentStorageVersion: VersionString = '0.1' + +// First create the agent +const agent = new Agent(config, agentDependencies) + +// We only initialize the update assistant if our stored version is not equal +// to the frameworkStorageVersion of the UpdateAssistant. The advantage of this +// is that we don't have to initialize the UpdateAssistant to retrieve the current +// storage version. +if (currentStorageVersion !== UpdateAssistant.frameworkStorageVersion) { + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'recipientIfEndpoint', + }, + }) + + // Same as with the previous strategy, if you normally call agent.wallet.initialize() manually + // you need to call this before calling updateAssistant.initialize() + await updateAssistant.initialize() + + await updateAssistant.update() + + // Store the version so we can leverage it during the next agent startup and don't have + // to initialize the update assistant again until a new version is released + currentStorageVersion = UpdateAssistant.frameworkStorageVersion +} + +// Once finished initialize the agent. You should do this on every launch of the agent +await agent.initialize() +``` + +### Automatically update on agent startup + +This is by far the easiest way to update the agent, but has the least amount of flexibility and is not configurable. This means you will have to use the default update options to update the agent storage. You can find the default update config in the respective version migration guides (e.g. in [0.1-to-0.2](/docs/migration/0.1-to-0.2.md)). + +```ts +import { UpdateAssistant, Agent } from '@aries-framework/core' + +// or @aries-framework/node +import { agentDependencies } from '@aries-framework/react-native' + +// First create the agent, setting the autoUpdateStorageOnStartup option to true +const agent = new Agent({ ...config, autoUpdateStorageOnStartup: true }, agentDependencies) + +// Then we call initialize, which under the hood will call the update assistant if the storage is not update to date. +await agent.initialize() +``` + +## Backups + +Before starting the update, the update assistant will automatically create a backup of the wallet. If the migration succeeds the backup won't be used. If the backup fails, another backup will be made of the migrated wallet, after which the backup will be restored. + +The backups can be found at the following locations. The `backupIdentifier` is generated at the start of the update process and generated based on the current timestamp. + +- Backup path: `${agent.config.fileSystem.basePath}/afj/migration/backup/${backupIdentifier}` +- Migration backup: `${agent.config.fileSystem.basePath}/afj/migration/backup/${backupIdentifier}-error` + +> In the future the backup assistant will make a number of improvements to the recovery process. Namely: +> +> - Do not throw an error if the update fails, but rather return an object that contains the status, and include the backup paths and backup identifiers. diff --git a/docs/setup-electron.md b/docs/setup-electron.md index 9e10a6e6ae..1e43b7c724 100644 --- a/docs/setup-electron.md +++ b/docs/setup-electron.md @@ -10,6 +10,8 @@ To start using Electron, the prerequisites of NodeJS are required. Please follow To add the aries framework and indy to your project execute the following: +## Installing dependencies + ```sh yarn add @aries-framework/core @aries-framework/node indy-sdk @@ -17,6 +19,9 @@ yarn add @aries-framework/core @aries-framework/node indy-sdk yarn add --dev @types/indy-sdk ``` +Right now, as a patch that will later be changed, some platforms will have an "error" when installing the dependencies. This is because the BBS signatures library that we use is built for Linux x86 and MacOS x86 (and not Windows and MacOS arm). This means that it will show that it could not download the binary. +This is not an error, as the library that fails is `node-bbs-signaturs` and is an optional dependency for perfomance improvements. It will fallback to a, slower, wasm build. + Because Electron is like a browser-environment, some additional work has to be done to get it working. The indy-sdk is used to make calls to `libindy`. Since `libindy` is not build for browser environments, a binding for the indy-sdk has to be created from the browser to the NodeJS environment in the `public/preload.js` file. ```ts diff --git a/docs/setup-nodejs.md b/docs/setup-nodejs.md index f8714ffe9f..44b3d13186 100644 --- a/docs/setup-nodejs.md +++ b/docs/setup-nodejs.md @@ -12,10 +12,15 @@ To start using Aries Framework JavaScript in NodeJS some platform specific depen - [Windows](../docs/libindy/windows.md) 3. Add `@aries-framework/core` and `@aries-framework/node` to your project. +## Installing dependencies + ```bash yarn add @aries-framework/core @aries-framework/node ``` +Right now, as a patch that will later be changed, some platforms will have an "error" when installing the dependencies. This is because the BBS signatures library that we use is built for Linux x86 and MacOS x86 (and not Windows and MacOS arm). This means that it will show that it could not download the binary. +This is not an error, as the library that fails is `node-bbs-signaturs` and is an optional dependency for perfomance improvements. It will fallback to a, slower, wasm build. + ## Agent Setup Initializing the Agent also requires some NodeJS specific setup, mainly for the Indy SDK and File System. Below is a sample config, see the [README](../README.md#getting-started) for an overview of getting started guides. If you want to jump right in, check the [Getting Started: Agent](./getting-started/0-agent.md) guide. diff --git a/docs/setup-react-native.md b/docs/setup-react-native.md index 596cb70b7f..495f823808 100644 --- a/docs/setup-react-native.md +++ b/docs/setup-react-native.md @@ -7,10 +7,15 @@ To start using Aries Framework JavaScript in React Native some platform specific 1. Follow the [React Native Setup](https://reactnative.dev/docs/environment-setup) guide to set up your environment. 2. Add `@aries-framework/core`, `@aries-framework/react-native`, `react-native-fs`, and `react-native-get-random-values` to your project. +## Installing dependencies + ```bash yarn add @aries-framework/core @aries-framework/react-native react-native-fs react-native-get-random-values ``` +Right now, as a patch that will later be changed, some platforms will have an "error" when installing the dependencies. This is because the BBS signatures library that we use is built for Linux x86 and MacOS x86 (and not Windows and MacOS arm). This means that it will show that it could not download the binary. +This is not an error, as the library that fails is `node-bbs-signaturs` and is an optional dependency for perfomance improvements. It will fallback to a, slower, wasm build. + 3. Install [Libindy](https://github.com/hyperledger/indy-sdk) for iOS and Android: - [iOS](../docs/libindy/ios.md) @@ -81,3 +86,39 @@ try { console.log(error) } ``` + +## Using BBS Signatures + +When using AFJ inside the React Native environment, temporarily, a dependency for creating keys, sigining and verifying +with bbs keys must be swapped. Inside your package.json the following must be added: + +#### yarn + +```diff ++ "resolutions": { ++ "@mattrglobal/bbs-signatures": "@animo-id/react-native-bbs-signatures@0.1.0", ++ }, + "dependencies": { + ... ++ "@animo-id/react-native-bbs-signatures": "0.1.0", + } +``` + +#### npm + +```diff ++ "overrides": { ++ "@mattrglobal/bbs-signatures": "@animo-id/react-native-bbs-signatures@0.1.0", ++ }, + "dependencies": { + ... ++ "@animo-id/react-native-bbs-signatures": "0.1.0", + } +``` + +The resolution field says that any instance of `@mattrglobal/bbs-signatures` in any child dependency must be swapped +with `react-native-bbs-signatures`. + +The added dependency is required for autolinking and should be the same as the one used in the resolution. + +[React Native Bbs Signature](https://github.com/animo/react-native-bbs-signatures) has some quirks with setting it up correctly. If any errors occur while using this library, please refer to their README for the installation guide. diff --git a/lerna.json b/lerna.json index 065ed05541..22c0c4f3aa 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "packages": ["packages/*"], - "version": "0.0.0", + "version": "0.1.0", "useWorkspaces": true, "npmClient": "yarn", "command": { diff --git a/network/indy-pool-arm.dockerfile b/network/indy-pool-arm.dockerfile new file mode 100644 index 0000000000..31412840e0 --- /dev/null +++ b/network/indy-pool-arm.dockerfile @@ -0,0 +1,67 @@ +FROM snel/von-image:node-1.12-4-arm64 + +USER root + +# Install environment +RUN apt-get update -y && apt-get install -y supervisor + +# It is imporatnt the the lines are not indented. Some autformatters +# Indent the supervisord parameters. THIS WILL BREAK THE SETUP +RUN echo "[supervisord]\n\ +logfile = /tmp/supervisord.log\n\ +logfile_maxbytes = 50MB\n\ +logfile_backups=10\n\ +logLevel = error\n\ +pidfile = /tmp/supervisord.pid\n\ +nodaemon = true\n\ +minfds = 1024\n\ +minprocs = 200\n\ +umask = 022\n\ +user = indy\n\ +identifier = supervisor\n\ +directory = /tmp\n\ +nocleanup = true\n\ +childlogdir = /tmp\n\ +strip_ansi = false\n\ +\n\ +[program:node1]\n\ +command=start_indy_node Node1 0.0.0.0 9701 0.0.0.0 9702\n\ +directory=/home/indy\n\ +stdout_logfile=/tmp/node1.log\n\ +stderr_logfile=/tmp/node1.log\n\ +\n\ +[program:node2]\n\ +command=start_indy_node Node2 0.0.0.0 9703 0.0.0.0 9704\n\ +directory=/home/indy\n\ +stdout_logfile=/tmp/node2.log\n\ +stderr_logfile=/tmp/node2.log\n\ +\n\ +[program:node3]\n\ +command=start_indy_node Node3 0.0.0.0 9705 0.0.0.0 9706\n\ +directory=/home/indy\n\ +stdout_logfile=/tmp/node3.log\n\ +stderr_logfile=/tmp/node3.log\n\ +\n\ +[program:node4]\n\ +command=start_indy_node Node4 0.0.0.0 9707 0.0.0.0 9708\n\ +directory=/home/indy\n\ +stdout_logfile=/tmp/node4.log\n\ +stderr_logfile=/tmp/node4.log\n"\ +>> /etc/supervisord.conf + +USER indy + +COPY --chown=indy:indy network/indy_config.py /etc/indy/indy_config.py + +ARG pool_ip=127.0.0.1 +RUN generate_indy_pool_transactions --nodes 4 --clients 5 --nodeNum 1 2 3 4 --ips="$pool_ip,$pool_ip,$pool_ip,$pool_ip" + +COPY network/add-did.sh /usr/bin/add-did +COPY network/indy-cli-setup.sh /usr/bin/indy-cli-setup +COPY network/add-did-from-seed.sh /usr/bin/add-did-from-seed +COPY network/genesis/local-genesis.txn /etc/indy/genesis.txn +COPY network/indy-cli-config.json /etc/indy/indy-cli-config.json + +EXPOSE 9701 9702 9703 9704 9705 9706 9707 9708 + +CMD ["/usr/bin/supervisord"] \ No newline at end of file diff --git a/network/indy_config.py b/network/indy_config.py new file mode 100644 index 0000000000..2ba64c1cb2 --- /dev/null +++ b/network/indy_config.py @@ -0,0 +1,12 @@ +NETWORK_NAME = 'sandbox' + +LEDGER_DIR = '/home/indy/ledger' +LOG_DIR = '/home/indy/log' +KEYS_DIR = LEDGER_DIR +GENESIS_DIR = LEDGER_DIR +BACKUP_DIR = '/home/indy/backup' +PLUGINS_DIR = '/home/indy/plugins' +NODE_INFO_DIR = LEDGER_DIR + +CLI_BASE_DIR = '/home/indy/.indy-cli/' +CLI_NETWORK_DIR = '/home/indy/.indy-cli/networks' diff --git a/package.json b/package.json index 0379608df7..6adc64771c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "private": true, "license": "Apache-2.0", "workspaces": [ - "packages/*" + "packages/*", + "demo", + "samples/*" ], "repository": { "url": "https://github.com/hyperledger/aries-framework-javascript", @@ -31,6 +33,7 @@ "@types/jest": "^26.0.23", "@types/node": "^15.14.4", "@types/uuid": "^8.3.1", + "@types/varint": "^6.0.0", "@types/ws": "^7.4.6", "@typescript-eslint/eslint-plugin": "^4.26.1", "@typescript-eslint/parser": "^4.26.1", @@ -51,7 +54,8 @@ "ts-jest": "^27.0.3", "ts-node": "^10.0.0", "tsconfig-paths": "^3.9.0", - "typescript": "^4.3.0", + "tsyringe": "^4.6.0", + "typescript": "~4.3.0", "ws": "^7.4.6" }, "resolutions": { diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md new file mode 100644 index 0000000000..dd78e16484 --- /dev/null +++ b/packages/core/CHANGELOG.md @@ -0,0 +1,107 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.1.0 (2021-12-23) + +### Bug Fixes + +- add details to connection signing error ([#484](https://github.com/hyperledger/aries-framework-javascript/issues/484)) ([e24eafd](https://github.com/hyperledger/aries-framework-javascript/commit/e24eafd83f53a9833b95bc3a4587cf825ee5d975)) +- add option check to attribute constructor ([#450](https://github.com/hyperledger/aries-framework-javascript/issues/450)) ([8aad3e9](https://github.com/hyperledger/aries-framework-javascript/commit/8aad3e9f16c249e9f9291388ec8efc9bf27213c8)) +- added ariesframeworkerror to httpoutboundtransport ([#438](https://github.com/hyperledger/aries-framework-javascript/issues/438)) ([ee1a229](https://github.com/hyperledger/aries-framework-javascript/commit/ee1a229f8fc21739bca05c516a7b561f53726b91)) +- alter mediation recipient websocket transport priority ([#434](https://github.com/hyperledger/aries-framework-javascript/issues/434)) ([52c7897](https://github.com/hyperledger/aries-framework-javascript/commit/52c789724c731340daa8528b7d7b4b7fdcb40032)) +- **core:** convert legacy prefix for inner msgs ([#479](https://github.com/hyperledger/aries-framework-javascript/issues/479)) ([a2b655a](https://github.com/hyperledger/aries-framework-javascript/commit/a2b655ac79bf0c7460671c8d31e92828e6f5ccf0)) +- **core:** do not throw error on timeout in http ([#512](https://github.com/hyperledger/aries-framework-javascript/issues/512)) ([4e73a7b](https://github.com/hyperledger/aries-framework-javascript/commit/4e73a7b0d9224bc102b396d821a8ea502a9a509d)) +- **core:** do not use did-communication service ([#402](https://github.com/hyperledger/aries-framework-javascript/issues/402)) ([cdf2edd](https://github.com/hyperledger/aries-framework-javascript/commit/cdf2eddc61e12f7ecd5a29e260eef82394d2e467)) +- **core:** export AgentMessage ([#480](https://github.com/hyperledger/aries-framework-javascript/issues/480)) ([af39ad5](https://github.com/hyperledger/aries-framework-javascript/commit/af39ad535320133ee38fc592309f42670a8517a1)) +- **core:** expose record metadata types ([#556](https://github.com/hyperledger/aries-framework-javascript/issues/556)) ([68995d7](https://github.com/hyperledger/aries-framework-javascript/commit/68995d7e2b049ff6496723d8a895e07b72fe72fb)) +- **core:** fix empty error log in console logger ([#524](https://github.com/hyperledger/aries-framework-javascript/issues/524)) ([7d9c541](https://github.com/hyperledger/aries-framework-javascript/commit/7d9c541de22fb2644455cf1949184abf3d8e528c)) +- **core:** improve wallet not initialized error ([#513](https://github.com/hyperledger/aries-framework-javascript/issues/513)) ([b948d4c](https://github.com/hyperledger/aries-framework-javascript/commit/b948d4c83b4eb0ab0594ae2117c0bb05b0955b21)) +- **core:** improved present-proof tests ([#482](https://github.com/hyperledger/aries-framework-javascript/issues/482)) ([41d9282](https://github.com/hyperledger/aries-framework-javascript/commit/41d9282ca561ca823b28f179d409c70a22d95e9b)) +- **core:** log errors if message is undeliverable ([#528](https://github.com/hyperledger/aries-framework-javascript/issues/528)) ([20b586d](https://github.com/hyperledger/aries-framework-javascript/commit/20b586db6eb9f92cce16d87d0dcfa4919f27ffa8)) +- **core:** remove isPositive validation decorators ([#477](https://github.com/hyperledger/aries-framework-javascript/issues/477)) ([e316e04](https://github.com/hyperledger/aries-framework-javascript/commit/e316e047b3e5aeefb929a5c47ad65d8edd4caba5)) +- **core:** remove unused url import ([#466](https://github.com/hyperledger/aries-framework-javascript/issues/466)) ([0f1323f](https://github.com/hyperledger/aries-framework-javascript/commit/0f1323f5bccc2dc3b67426525b161d7e578bb961)) +- **core:** requested predicates transform type ([#393](https://github.com/hyperledger/aries-framework-javascript/issues/393)) ([69684bc](https://github.com/hyperledger/aries-framework-javascript/commit/69684bc48a4002483662a211ec1ddd289dbaf59b)) +- **core:** send messages now takes a connection id ([#491](https://github.com/hyperledger/aries-framework-javascript/issues/491)) ([ed9db11](https://github.com/hyperledger/aries-framework-javascript/commit/ed9db11592b4948a1d313dbeb074e15d59503d82)) +- **core:** using query-string to parse URLs ([#457](https://github.com/hyperledger/aries-framework-javascript/issues/457)) ([78e5057](https://github.com/hyperledger/aries-framework-javascript/commit/78e505750557f296cc72ef19c0edd8db8e1eaa7d)) +- date parsing ([#426](https://github.com/hyperledger/aries-framework-javascript/issues/426)) ([2d31b87](https://github.com/hyperledger/aries-framework-javascript/commit/2d31b87e99d04136f57cb457e2c67397ad65cc62)) +- export indy pool config ([#504](https://github.com/hyperledger/aries-framework-javascript/issues/504)) ([b1e2b8c](https://github.com/hyperledger/aries-framework-javascript/commit/b1e2b8c54e909927e5afa8b8212e0c8e156b97f7)) +- include error when message cannot be handled ([#533](https://github.com/hyperledger/aries-framework-javascript/issues/533)) ([febfb05](https://github.com/hyperledger/aries-framework-javascript/commit/febfb05330c097aa918087ec3853a247d6a31b7c)) +- incorrect recip key with multi routing keys ([#446](https://github.com/hyperledger/aries-framework-javascript/issues/446)) ([db76823](https://github.com/hyperledger/aries-framework-javascript/commit/db76823400cfecc531575584ef7210af0c3b3e5c)) +- make records serializable ([#448](https://github.com/hyperledger/aries-framework-javascript/issues/448)) ([7e2946e](https://github.com/hyperledger/aries-framework-javascript/commit/7e2946eaa9e35083f3aa70c26c732a972f6eb12f)) +- mediator transports ([#419](https://github.com/hyperledger/aries-framework-javascript/issues/419)) ([87bc589](https://github.com/hyperledger/aries-framework-javascript/commit/87bc589695505de21294a1373afcf874fe8d22f6)) +- mediator updates ([#432](https://github.com/hyperledger/aries-framework-javascript/issues/432)) ([163cda1](https://github.com/hyperledger/aries-framework-javascript/commit/163cda19ba8437894a48c9bc948528ea0486ccdf)) +- proof configurable on proofRecord ([#397](https://github.com/hyperledger/aries-framework-javascript/issues/397)) ([8e83c03](https://github.com/hyperledger/aries-framework-javascript/commit/8e83c037e1d59c670cfd4a8a575d4459999a64f8)) +- removed check for senderkey for connectionless exchange ([#555](https://github.com/hyperledger/aries-framework-javascript/issues/555)) ([ba3f17e](https://github.com/hyperledger/aries-framework-javascript/commit/ba3f17e073b28ee5f16031f0346de0b71119e6f3)) +- support mediation for connectionless exchange ([#577](https://github.com/hyperledger/aries-framework-javascript/issues/577)) ([3dadfc7](https://github.com/hyperledger/aries-framework-javascript/commit/3dadfc7a202b3642e93e39cd79c9fd98a3dc4de2)) +- their did doc not ours ([#436](https://github.com/hyperledger/aries-framework-javascript/issues/436)) ([0226609](https://github.com/hyperledger/aries-framework-javascript/commit/0226609a279303f5e8d09a2c01e54ff97cf61839)) + +- fix(core)!: Improved typing on metadata api (#585) ([4ab8d73](https://github.com/hyperledger/aries-framework-javascript/commit/4ab8d73e5fc866a91085f95f973022846ed431fb)), closes [#585](https://github.com/hyperledger/aries-framework-javascript/issues/585) +- fix(core)!: update class transformer library (#547) ([dee03e3](https://github.com/hyperledger/aries-framework-javascript/commit/dee03e38d2732ba0bd38eeacca6ad58b191e87f8)), closes [#547](https://github.com/hyperledger/aries-framework-javascript/issues/547) +- fix(core)!: prefixed internal metadata with \_internal/ (#535) ([aa1b320](https://github.com/hyperledger/aries-framework-javascript/commit/aa1b3206027fdb71e6aaa4c6491f8ba84dca7b9a)), closes [#535](https://github.com/hyperledger/aries-framework-javascript/issues/535) +- feat(core)!: metadata on records (#505) ([c92393a](https://github.com/hyperledger/aries-framework-javascript/commit/c92393a8b5d8abd38d274c605cd5c3f97f96cee9)), closes [#505](https://github.com/hyperledger/aries-framework-javascript/issues/505) +- fix(core)!: do not request ping res for connection (#527) ([3db5519](https://github.com/hyperledger/aries-framework-javascript/commit/3db5519f0d9f49b71b647ca86be3b336399459cb)), closes [#527](https://github.com/hyperledger/aries-framework-javascript/issues/527) +- refactor(core)!: simplify get creds for proof api (#523) ([ba9698d](https://github.com/hyperledger/aries-framework-javascript/commit/ba9698de2606e5c78f018dc5e5253aeb1f5fc616)), closes [#523](https://github.com/hyperledger/aries-framework-javascript/issues/523) +- fix(core)!: improve proof request validation (#525) ([1b4d8d6](https://github.com/hyperledger/aries-framework-javascript/commit/1b4d8d6b6c06821a2a981fffb6c47f728cac803e)), closes [#525](https://github.com/hyperledger/aries-framework-javascript/issues/525) +- feat(core)!: added basic message sent event (#507) ([d2c04c3](https://github.com/hyperledger/aries-framework-javascript/commit/d2c04c36c00d772943530bd599dbe56f3e1fb17d)), closes [#507](https://github.com/hyperledger/aries-framework-javascript/issues/507) + +### Features + +- add delete methods to services and modules ([#447](https://github.com/hyperledger/aries-framework-javascript/issues/447)) ([e7ed602](https://github.com/hyperledger/aries-framework-javascript/commit/e7ed6027d2aa9be7f64d5968c4338e63e56657fb)) +- add from record method to cred preview ([#428](https://github.com/hyperledger/aries-framework-javascript/issues/428)) ([895f7d0](https://github.com/hyperledger/aries-framework-javascript/commit/895f7d084287f99221c9492a25fed58191868edd)) +- add multiple inbound transports ([#433](https://github.com/hyperledger/aries-framework-javascript/issues/433)) ([56cb9f2](https://github.com/hyperledger/aries-framework-javascript/commit/56cb9f2202deb83b3c133905f21651bfefcb63f7)) +- add problem report protocol ([#560](https://github.com/hyperledger/aries-framework-javascript/issues/560)) ([baee5db](https://github.com/hyperledger/aries-framework-javascript/commit/baee5db29f3d545c16a651c80392ddcbbca6bf0e)) +- add toJson method to BaseRecord ([#455](https://github.com/hyperledger/aries-framework-javascript/issues/455)) ([f3790c9](https://github.com/hyperledger/aries-framework-javascript/commit/f3790c97c4d9a0aaec9abdce417ecd5429c6026f)) +- added decline credential offer method ([#416](https://github.com/hyperledger/aries-framework-javascript/issues/416)) ([d9ac141](https://github.com/hyperledger/aries-framework-javascript/commit/d9ac141122f1d4902f91f9537e6526796fef1e01)) +- added declined proof state and decline method for presentations ([e5aedd0](https://github.com/hyperledger/aries-framework-javascript/commit/e5aedd02737d3764871c6b5d4ae61a3a33ed8398)) +- allow to use legacy did sov prefix ([#442](https://github.com/hyperledger/aries-framework-javascript/issues/442)) ([c41526f](https://github.com/hyperledger/aries-framework-javascript/commit/c41526fb57a7e2e89e923b95ede43f890a6cbcbb)) +- auto accept proofs ([#367](https://github.com/hyperledger/aries-framework-javascript/issues/367)) ([735d578](https://github.com/hyperledger/aries-framework-javascript/commit/735d578f72fc5f3bfcbcf40d27394bd013e7cf4f)) +- break out indy wallet, better indy handling ([#396](https://github.com/hyperledger/aries-framework-javascript/issues/396)) ([9f1a4a7](https://github.com/hyperledger/aries-framework-javascript/commit/9f1a4a754a61573ce3fee78d52615363c7e25d58)) +- **core:** add discover features protocol ([#390](https://github.com/hyperledger/aries-framework-javascript/issues/390)) ([3347424](https://github.com/hyperledger/aries-framework-javascript/commit/3347424326cd15e8bf2544a8af53b2fa57b1dbb8)) +- **core:** add support for multi use inviations ([#460](https://github.com/hyperledger/aries-framework-javascript/issues/460)) ([540ad7b](https://github.com/hyperledger/aries-framework-javascript/commit/540ad7be2133ee6609c2336b22b726270db98d6c)) +- **core:** connection-less issuance and verification ([#359](https://github.com/hyperledger/aries-framework-javascript/issues/359)) ([fb46ade](https://github.com/hyperledger/aries-framework-javascript/commit/fb46ade4bc2dd4f3b63d4194bb170d2f329562b7)) +- **core:** d_m invitation parameter and invitation image ([#456](https://github.com/hyperledger/aries-framework-javascript/issues/456)) ([f92c322](https://github.com/hyperledger/aries-framework-javascript/commit/f92c322b97be4a4867a82c3a35159d6068951f0b)) +- **core:** ledger module registerPublicDid implementation ([#398](https://github.com/hyperledger/aries-framework-javascript/issues/398)) ([5f2d512](https://github.com/hyperledger/aries-framework-javascript/commit/5f2d5126baed2ff58268c38755c2dbe75a654947)) +- **core:** store mediator id in connection record ([#503](https://github.com/hyperledger/aries-framework-javascript/issues/503)) ([da51f2e](https://github.com/hyperledger/aries-framework-javascript/commit/da51f2e8337f5774d23e9aeae0459bd7355a3760)) +- **core:** support image url in invitations ([#463](https://github.com/hyperledger/aries-framework-javascript/issues/463)) ([9fda24e](https://github.com/hyperledger/aries-framework-javascript/commit/9fda24ecf55fdfeba74211447e9fadfdcbf57385)) +- **core:** support multiple indy ledgers ([#474](https://github.com/hyperledger/aries-framework-javascript/issues/474)) ([47149bc](https://github.com/hyperledger/aries-framework-javascript/commit/47149bc5742456f4f0b75e0944ce276972e645b8)) +- **core:** update agent label and imageUrl plus per connection label and imageUrl ([#516](https://github.com/hyperledger/aries-framework-javascript/issues/516)) ([5e9a641](https://github.com/hyperledger/aries-framework-javascript/commit/5e9a64130c02c8a5fdf11f0e25d0c23929a33a4f)) +- **core:** validate outbound messages ([#526](https://github.com/hyperledger/aries-framework-javascript/issues/526)) ([9c3910f](https://github.com/hyperledger/aries-framework-javascript/commit/9c3910f1e67200b71bb4888c6fee62942afaff20)) +- expose wallet API ([#566](https://github.com/hyperledger/aries-framework-javascript/issues/566)) ([4027fc9](https://github.com/hyperledger/aries-framework-javascript/commit/4027fc975d7e4118892f43cb8c6a0eea412eaad4)) +- generic attachment handler ([#578](https://github.com/hyperledger/aries-framework-javascript/issues/578)) ([4d7d3c1](https://github.com/hyperledger/aries-framework-javascript/commit/4d7d3c1502d5eafa2b884a4a84934e072fe70ea6)) +- **node:** add http and ws inbound transport ([#392](https://github.com/hyperledger/aries-framework-javascript/issues/392)) ([34a6ff2](https://github.com/hyperledger/aries-framework-javascript/commit/34a6ff2699197b9d525422a0a405e241582a476c)) + +### BREAKING CHANGES + +- removed the getAll() function. +- The agent’s `shutdown` method does not delete the wallet anymore. If you want to delete the wallet, you can do it via exposed wallet API. +- class-transformer released a breaking change in a patch version, causing AFJ to break. I updated to the newer version and pinned the version exactly as this is the second time this has happened now. + +Signed-off-by: Timo Glastra + +- internal metadata is now prefixed with \_internal to avoid clashing and accidental overwriting of internal data. + +- fix(core): added \_internal/ prefix on metadata + +Signed-off-by: Berend Sliedrecht + +- credentialRecord.credentialMetadata has been replaced by credentialRecord.metadata. + +Signed-off-by: Berend Sliedrecht + +- a trust ping response will not be requested anymore after completing a connection. This is not required, and also non-standard behaviour. It was also causing some tests to be flaky as response messages were stil being sent after one of the agents had already shut down. + +Signed-off-by: Timo Glastra + +- The `ProofsModule.getRequestedCredentialsForProofRequest` expected some low level message objects as input. This is not in line with the public API of the rest of the framework and has been simplified to only require a proof record id and optionally a boolean whether the retrieved credentials should be filtered based on the proof proposal (if available). + +Signed-off-by: Timo Glastra + +- Proof request requestedAttributes and requestedPredicates are now a map instead of record. This is needed to have proper validation using class-validator. + +Signed-off-by: Timo Glastra + +- `BasicMessageReceivedEvent` has been replaced by the more general `BasicMessageStateChanged` event which triggers when a basic message is received or sent. + +Signed-off-by: NeilSMyers diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000000..69301788ec --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,31 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript - Core

+

+ License + typescript + @aries-framework/core version + +

+
+ +Aries Framework JavaScript Core provides the core functionality of Aries Framework JavaScript. See the [Getting Started Guide](https://github.com/hyperledger/aries-framework-javascript#getting-started) for installation instructions. diff --git a/packages/core/package.json b/packages/core/package.json index cffa53569b..dbf18d9db7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,7 +2,7 @@ "name": "@aries-framework/core", "main": "build/index", "types": "build/index", - "version": "0.0.0", + "version": "0.1.0", "files": [ "build" ], @@ -23,28 +23,37 @@ "prepublishOnly": "yarn run build" }, "dependencies": { + "@digitalcredentials/jsonld": "^5.2.1", + "@digitalcredentials/jsonld-signatures": "^9.3.1", + "@digitalcredentials/vc": "^1.1.2", + "@mattrglobal/bbs-signatures": "^1.0.0", + "@mattrglobal/bls12381-key-pair": "^1.0.0", "@multiformats/base-x": "^4.0.1", - "@types/indy-sdk": "^1.16.6", + "@stablelib/ed25519": "^1.0.2", + "@stablelib/random": "^1.0.1", + "@stablelib/sha256": "^1.0.1", + "@types/indy-sdk": "^1.16.16", "@types/node-fetch": "^2.5.10", - "@types/ws": "^7.4.4", + "@types/ws": "^7.4.6", "abort-controller": "^3.0.0", "bn.js": "^5.2.0", "borc": "^3.0.0", "buffer": "^6.0.3", "class-transformer": "0.5.1", "class-validator": "0.13.1", - "js-sha256": "^0.9.0", + "did-resolver": "^3.1.3", + "jsonld": "^5.2.0", "lru_map": "^0.4.1", "luxon": "^1.27.0", "make-error": "^1.3.6", - "multibase": "^4.0.4", - "multihashes": "^4.0.2", "object-inspect": "^1.10.3", "query-string": "^7.0.1", "reflect-metadata": "^0.1.13", - "rxjs": "^7.1.0", + "rxjs": "^7.2.0", "tsyringe": "^4.5.0", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "varint": "^6.0.0", + "web-did-resolver": "^2.0.8" }, "devDependencies": { "@types/bn.js": "^5.1.0", @@ -52,6 +61,7 @@ "@types/luxon": "^1.27.0", "@types/object-inspect": "^1.8.0", "@types/uuid": "^8.3.0", + "@types/varint": "^6.0.0", "rimraf": "~3.0.2", "tslog": "^3.2.0", "typescript": "~4.3.0" diff --git a/packages/core/src/agent/Agent.ts b/packages/core/src/agent/Agent.ts index 8868ed35ae..6dcdb9d3f2 100644 --- a/packages/core/src/agent/Agent.ts +++ b/packages/core/src/agent/Agent.ts @@ -17,14 +17,21 @@ import { AriesFrameworkError } from '../error' import { BasicMessagesModule } from '../modules/basic-messages/BasicMessagesModule' import { ConnectionsModule } from '../modules/connections/ConnectionsModule' import { CredentialsModule } from '../modules/credentials/CredentialsModule' +import { DidsModule } from '../modules/dids/DidsModule' import { DiscoverFeaturesModule } from '../modules/discover-features' +import { GenericRecordsModule } from '../modules/generic-records/GenericRecordsModule' import { LedgerModule } from '../modules/ledger/LedgerModule' +import { OutOfBandModule } from '../modules/oob/OutOfBandModule' import { ProofsModule } from '../modules/proofs/ProofsModule' import { MediatorModule } from '../modules/routing/MediatorModule' import { RecipientModule } from '../modules/routing/RecipientModule' +import { StorageUpdateService } from '../storage' import { InMemoryMessageRepository } from '../storage/InMemoryMessageRepository' import { IndyStorageService } from '../storage/IndyStorageService' +import { UpdateAssistant } from '../storage/migration/UpdateAssistant' +import { DEFAULT_UPDATE_CONFIG } from '../storage/migration/updates' import { IndyWallet } from '../wallet/IndyWallet' +import { WalletModule } from '../wallet/WalletModule' import { WalletError } from '../wallet/error' import { AgentConfig } from './AgentConfig' @@ -39,25 +46,33 @@ export class Agent { protected logger: Logger protected container: DependencyContainer protected eventEmitter: EventEmitter - protected wallet: Wallet protected messageReceiver: MessageReceiver protected transportService: TransportService protected messageSender: MessageSender private _isInitialized = false public messageSubscription: Subscription - - public readonly connections!: ConnectionsModule - public readonly proofs!: ProofsModule - public readonly basicMessages!: BasicMessagesModule - public readonly ledger!: LedgerModule - public readonly credentials!: CredentialsModule - public readonly mediationRecipient!: RecipientModule - public readonly mediator!: MediatorModule - public readonly discovery!: DiscoverFeaturesModule - - public constructor(initialConfig: InitConfig, dependencies: AgentDependencies) { - // Create child container so we don't interfere with anything outside of this agent - this.container = baseContainer.createChildContainer() + private walletService: Wallet + + public readonly connections: ConnectionsModule + public readonly proofs: ProofsModule + public readonly basicMessages: BasicMessagesModule + public readonly genericRecords: GenericRecordsModule + public readonly ledger: LedgerModule + public readonly credentials: CredentialsModule + public readonly mediationRecipient: RecipientModule + public readonly mediator: MediatorModule + public readonly discovery: DiscoverFeaturesModule + public readonly dids: DidsModule + public readonly wallet: WalletModule + public readonly oob!: OutOfBandModule + + public constructor( + initialConfig: InitConfig, + dependencies: AgentDependencies, + injectionContainer?: DependencyContainer + ) { + // Take input container or child container so we don't interfere with anything outside of this agent + this.container = injectionContainer ?? baseContainer.createChildContainer() this.agentConfig = new AgentConfig(initialConfig, dependencies) this.logger = this.agentConfig.logger @@ -66,10 +81,18 @@ export class Agent { this.container.registerInstance(AgentConfig, this.agentConfig) // Based on interfaces. Need to register which class to use - this.container.registerInstance(InjectionSymbols.Logger, this.logger) - this.container.register(InjectionSymbols.Wallet, { useToken: IndyWallet }) - this.container.registerSingleton(InjectionSymbols.StorageService, IndyStorageService) - this.container.registerSingleton(InjectionSymbols.MessageRepository, InMemoryMessageRepository) + if (!this.container.isRegistered(InjectionSymbols.Wallet)) { + this.container.register(InjectionSymbols.Wallet, { useToken: IndyWallet }) + } + if (!this.container.isRegistered(InjectionSymbols.Logger)) { + this.container.registerInstance(InjectionSymbols.Logger, this.logger) + } + if (!this.container.isRegistered(InjectionSymbols.StorageService)) { + this.container.registerSingleton(InjectionSymbols.StorageService, IndyStorageService) + } + if (!this.container.isRegistered(InjectionSymbols.MessageRepository)) { + this.container.registerSingleton(InjectionSymbols.MessageRepository, InMemoryMessageRepository) + } this.logger.info('Creating agent with config', { ...initialConfig, @@ -91,7 +114,7 @@ export class Agent { this.messageSender = this.container.resolve(MessageSender) this.messageReceiver = this.container.resolve(MessageReceiver) this.transportService = this.container.resolve(TransportService) - this.wallet = this.container.resolve(InjectionSymbols.Wallet) + this.walletService = this.container.resolve(InjectionSymbols.Wallet) // We set the modules in the constructor because that allows to set them as read-only this.connections = this.container.resolve(ConnectionsModule) @@ -100,15 +123,19 @@ export class Agent { this.mediator = this.container.resolve(MediatorModule) this.mediationRecipient = this.container.resolve(RecipientModule) this.basicMessages = this.container.resolve(BasicMessagesModule) + this.genericRecords = this.container.resolve(GenericRecordsModule) this.ledger = this.container.resolve(LedgerModule) this.discovery = this.container.resolve(DiscoverFeaturesModule) + this.dids = this.container.resolve(DidsModule) + this.wallet = this.container.resolve(WalletModule) + this.oob = this.container.resolve(OutOfBandModule) // Listen for new messages (either from transports or somewhere else in the framework / extensions) this.messageSubscription = this.eventEmitter .observable(AgentEventTypes.AgentMessageReceived) .pipe( takeUntil(this.agentConfig.stop$), - concatMap((e) => this.messageReceiver.receiveMessage(e.payload.message)) + concatMap((e) => this.messageReceiver.receiveMessage(e.payload.message, { connection: e.payload.connection })) ) .subscribe() } @@ -138,7 +165,7 @@ export class Agent { } public async initialize() { - const { publicDidSeed, walletConfig, mediatorConnectionsInvite } = this.agentConfig + const { connectToIndyLedgersOnStartup, publicDidSeed, walletConfig, mediatorConnectionsInvite } = this.agentConfig if (this._isInitialized) { throw new AriesFrameworkError( @@ -156,24 +183,56 @@ export class Agent { ) } + // Make sure the storage is up to date + const storageUpdateService = this.container.resolve(StorageUpdateService) + const isStorageUpToDate = await storageUpdateService.isUpToDate() + this.logger.info(`Agent storage is ${isStorageUpToDate ? '' : 'not '}up to date.`) + + if (!isStorageUpToDate && this.agentConfig.autoUpdateStorageOnStartup) { + const updateAssistant = new UpdateAssistant(this, DEFAULT_UPDATE_CONFIG) + + await updateAssistant.initialize() + await updateAssistant.update() + } else if (!isStorageUpToDate) { + const currentVersion = await storageUpdateService.getCurrentStorageVersion() + // Close wallet to prevent un-initialized agent with initialized wallet + await this.wallet.close() + throw new AriesFrameworkError( + // TODO: add link to where documentation on how to update can be found. + `Current agent storage is not up to date. ` + + `To prevent the framework state from getting corrupted the agent initialization is aborted. ` + + `Make sure to update the agent storage (currently at ${currentVersion}) to the latest version (${UpdateAssistant.frameworkStorageVersion}). ` + + `You can also downgrade your version of Aries Framework JavaScript.` + ) + } + if (publicDidSeed) { // If an agent has publicDid it will be used as routing key. - await this.wallet.initPublicDid({ seed: publicDidSeed }) + await this.walletService.initPublicDid({ seed: publicDidSeed }) + } + + // As long as value isn't false we will async connect to all genesis pools on startup + if (connectToIndyLedgersOnStartup) { + this.ledger.connectToPools().catch((error) => { + this.logger.warn('Error connecting to ledger, will try to reconnect when needed.', { error }) + }) } for (const transport of this.inboundTransports) { - transport.start(this) + await transport.start(this) } for (const transport of this.outboundTransports) { - transport.start(this) + await transport.start(this) } // Connect to mediator through provided invitation if provided in config // Also requests mediation ans sets as default mediator // Because this requires the connections module, we do this in the agent constructor if (mediatorConnectionsInvite) { - await this.mediationRecipient.provision(mediatorConnectionsInvite) + this.logger.debug('Provision mediation with invitation', { mediatorConnectionsInvite }) + const mediatonConnection = await this.getMediationConnection(mediatorConnectionsInvite) + await this.mediationRecipient.provision(mediatonConnection) } await this.mediationRecipient.initialize() @@ -181,35 +240,29 @@ export class Agent { this._isInitialized = true } - public async shutdown({ deleteWallet = false }: { deleteWallet?: boolean } = {}) { + public async shutdown() { // All observables use takeUntil with the stop$ observable // this means all observables will stop running if a value is emitted on this observable this.agentConfig.stop$.next(true) // Stop transports - for (const transport of this.outboundTransports) { - transport.stop() - } - for (const transport of this.inboundTransports) { - transport.stop() - } + const allTransports = [...this.inboundTransports, ...this.outboundTransports] + const transportPromises = allTransports.map((transport) => transport.stop()) + await Promise.all(transportPromises) - // close/delete wallet if still initialized + // close wallet if still initialized if (this.wallet.isInitialized) { - if (deleteWallet) { - await this.wallet.delete() - } else { - await this.wallet.close() - } + await this.wallet.close() } + this._isInitialized = false } public get publicDid() { - return this.wallet.publicDid + return this.walletService.publicDid } - public async receiveMessage(inboundPackedMessage: unknown, session?: TransportSession) { - return await this.messageReceiver.receiveMessage(inboundPackedMessage, session) + public async receiveMessage(inboundMessage: unknown, session?: TransportSession) { + return await this.messageReceiver.receiveMessage(inboundMessage, { session }) } public get injectionContainer() { @@ -219,4 +272,33 @@ export class Agent { public get config() { return this.agentConfig } + + private async getMediationConnection(mediatorInvitationUrl: string) { + const outOfBandInvitation = await this.oob.parseInvitation(mediatorInvitationUrl) + const outOfBandRecord = await this.oob.findByInvitationId(outOfBandInvitation.id) + const [connection] = outOfBandRecord ? await this.connections.findAllByOutOfBandId(outOfBandRecord.id) : [] + + if (!connection) { + this.logger.debug('Mediation connection does not exist, creating connection') + // We don't want to use the current default mediator when connecting to another mediator + const routing = await this.mediationRecipient.getRouting({ useDefaultMediator: false }) + + this.logger.debug('Routing created', routing) + const { connectionRecord: newConnection } = await this.oob.receiveInvitation(outOfBandInvitation, { + routing, + }) + this.logger.debug(`Mediation invitation processed`, { outOfBandInvitation }) + + if (!newConnection) { + throw new AriesFrameworkError('No connection record to provision mediation.') + } + + return this.connections.returnWhenIsConnected(newConnection.id) + } + + if (!connection.isReady) { + return this.connections.returnWhenIsConnected(connection.id) + } + return connection + } } diff --git a/packages/core/src/agent/AgentConfig.ts b/packages/core/src/agent/AgentConfig.ts index e93d73c3da..682e6a9685 100644 --- a/packages/core/src/agent/AgentConfig.ts +++ b/packages/core/src/agent/AgentConfig.ts @@ -10,7 +10,6 @@ import { AriesFrameworkError } from '../error' import { ConsoleLogger, LogLevel } from '../logger' import { AutoAcceptCredential } from '../modules/credentials/CredentialAutoAcceptType' import { AutoAcceptProof } from '../modules/proofs/ProofAutoAcceptType' -import { MediatorPickupStrategy } from '../modules/routing/MediatorPickupStrategy' import { DidCommMimeType } from '../types' export class AgentConfig { @@ -40,6 +39,10 @@ export class AgentConfig { } } + public get connectToIndyLedgersOnStartup() { + return this.initConfig.connectToIndyLedgersOnStartup ?? true + } + public get publicDidSeed() { return this.initConfig.publicDidSeed } @@ -73,7 +76,11 @@ export class AgentConfig { } public get mediatorPickupStrategy() { - return this.initConfig.mediatorPickupStrategy ?? MediatorPickupStrategy.Explicit + return this.initConfig.mediatorPickupStrategy + } + + public get maximumMessagePickup() { + return this.initConfig.maximumMessagePickup ?? 10 } public get endpoints(): [string, ...string[]] { @@ -109,4 +116,8 @@ export class AgentConfig { public get connectionImageUrl() { return this.initConfig.connectionImageUrl } + + public get autoUpdateStorageOnStartup() { + return this.initConfig.autoUpdateStorageOnStartup ?? false + } } diff --git a/packages/core/src/agent/AgentMessage.ts b/packages/core/src/agent/AgentMessage.ts index fa4e1743f5..07cf8ee9db 100644 --- a/packages/core/src/agent/AgentMessage.ts +++ b/packages/core/src/agent/AgentMessage.ts @@ -1,3 +1,6 @@ +import type { ParsedMessageType } from '../utils/messageType' +import type { Constructor } from '../utils/mixins' + import { AckDecorated } from '../decorators/ack/AckDecoratorExtension' import { AttachmentDecorated } from '../decorators/attachment/AttachmentExtension' import { L10nDecorated } from '../decorators/l10n/L10nDecoratorExtension' @@ -7,21 +10,16 @@ import { TimingDecorated } from '../decorators/timing/TimingDecoratorExtension' import { TransportDecorated } from '../decorators/transport/TransportDecoratorExtension' import { JsonTransformer } from '../utils/JsonTransformer' import { replaceNewDidCommPrefixWithLegacyDidSovOnMessage } from '../utils/messageType' -import { Compose } from '../utils/mixins' import { BaseMessage } from './BaseMessage' -const DefaultDecorators = [ - ThreadDecorated, - L10nDecorated, - TransportDecorated, - TimingDecorated, - AckDecorated, - AttachmentDecorated, - ServiceDecorated, -] - -export class AgentMessage extends Compose(BaseMessage, DefaultDecorators) { +export type ConstructableAgentMessage = Constructor & { type: ParsedMessageType } + +const Decorated = ThreadDecorated( + L10nDecorated(TransportDecorated(TimingDecorated(AckDecorated(AttachmentDecorated(ServiceDecorated(BaseMessage)))))) +) + +export class AgentMessage extends Decorated { public toJSON({ useLegacyDidSovPrefix = false }: { useLegacyDidSovPrefix?: boolean } = {}): Record { const json = JsonTransformer.toJSON(this) @@ -33,6 +31,6 @@ export class AgentMessage extends Compose(BaseMessage, DefaultDecorators) { } public is(Class: C): this is InstanceType { - return this.type === Class.type + return this.type === Class.type.messageTypeUri } } diff --git a/packages/core/src/agent/BaseMessage.ts b/packages/core/src/agent/BaseMessage.ts index 3fbd4e5f54..9ff18ccaf5 100644 --- a/packages/core/src/agent/BaseMessage.ts +++ b/packages/core/src/agent/BaseMessage.ts @@ -1,3 +1,4 @@ +import type { ParsedMessageType } from '../utils/messageType' import type { Constructor } from '../utils/mixins' import { Expose } from 'class-transformer' @@ -18,7 +19,7 @@ export class BaseMessage { @Expose({ name: '@type' }) @Matches(MessageTypeRegExp) public readonly type!: string - public static readonly type: string + public static readonly type: ParsedMessageType public generateId() { return uuid() diff --git a/packages/core/src/agent/Dispatcher.ts b/packages/core/src/agent/Dispatcher.ts index 070b2b42ea..16e5fae609 100644 --- a/packages/core/src/agent/Dispatcher.ts +++ b/packages/core/src/agent/Dispatcher.ts @@ -9,7 +9,9 @@ import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../agent/AgentConfig' import { AriesFrameworkError } from '../error/AriesFrameworkError' +import { canHandleMessageType, parseMessageType } from '../utils/messageType' +import { ProblemReportMessage } from './../modules/problem-reports/messages/ProblemReportMessage' import { EventEmitter } from './EventEmitter' import { AgentEventTypes } from './Events' import { MessageSender } from './MessageSender' @@ -45,15 +47,27 @@ class Dispatcher { try { outboundMessage = await handler.handle(messageContext) } catch (error) { - this.logger.error(`Error handling message with type ${message.type}`, { - message: message.toJSON(), - error, - senderVerkey: messageContext.senderVerkey, - recipientVerkey: messageContext.recipientVerkey, - connectionId: messageContext.connection?.id, - }) - - throw error + const problemReportMessage = error.problemReport + + if (problemReportMessage instanceof ProblemReportMessage && messageContext.connection) { + problemReportMessage.setThread({ + threadId: messageContext.message.threadId, + }) + outboundMessage = { + payload: problemReportMessage, + connection: messageContext.connection, + } + } else { + this.logger.error(`Error handling message with type ${message.type}`, { + message: message.toJSON(), + error, + senderKey: messageContext.senderKey?.fingerprint, + recipientKey: messageContext.recipientKey?.fingerprint, + connectionId: messageContext.connection?.id, + }) + + throw error + } } if (outboundMessage && isOutboundServiceMessage(outboundMessage)) { @@ -64,6 +78,7 @@ class Dispatcher { returnRoute: true, }) } else if (outboundMessage) { + outboundMessage.sessionId = messageContext.sessionId await this.messageSender.sendMessage(outboundMessage) } @@ -78,17 +93,21 @@ class Dispatcher { } private getHandlerForType(messageType: string): Handler | undefined { + const incomingMessageType = parseMessageType(messageType) + for (const handler of this.handlers) { for (const MessageClass of handler.supportedMessages) { - if (MessageClass.type === messageType) return handler + if (canHandleMessageType(MessageClass, incomingMessageType)) return handler } } } public getMessageClassForType(messageType: string): typeof AgentMessage | undefined { + const incomingMessageType = parseMessageType(messageType) + for (const handler of this.handlers) { for (const MessageClass of handler.supportedMessages) { - if (MessageClass.type === messageType) return MessageClass + if (canHandleMessageType(MessageClass, incomingMessageType)) return MessageClass } } } @@ -108,7 +127,7 @@ class Dispatcher { * Protocol ID format is PIURI specified at https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0003-protocols/README.md#piuri. */ public get supportedProtocols() { - return Array.from(new Set(this.supportedMessageTypes.map((m) => m.substring(0, m.lastIndexOf('/'))))) + return Array.from(new Set(this.supportedMessageTypes.map((m) => m.protocolUri))) } public filterSupportedProtocolsByMessageFamilies(messageFamilies: string[]) { diff --git a/packages/core/src/agent/EnvelopeService.ts b/packages/core/src/agent/EnvelopeService.ts index 9a025ab88e..303731e69b 100644 --- a/packages/core/src/agent/EnvelopeService.ts +++ b/packages/core/src/agent/EnvelopeService.ts @@ -1,23 +1,24 @@ import type { Logger } from '../logger' -import type { UnpackedMessageContext, WireMessage } from '../types' +import type { EncryptedMessage, PlaintextMessage } from '../types' import type { AgentMessage } from './AgentMessage' import { inject, scoped, Lifecycle } from 'tsyringe' import { InjectionSymbols } from '../constants' +import { Key, KeyType } from '../crypto' import { ForwardMessage } from '../modules/routing/messages' import { Wallet } from '../wallet/Wallet' import { AgentConfig } from './AgentConfig' export interface EnvelopeKeys { - recipientKeys: string[] - routingKeys: string[] - senderKey: string | null + recipientKeys: Key[] + routingKeys: Key[] + senderKey: Key | null } @scoped(Lifecycle.ContainerScoped) -class EnvelopeService { +export class EnvelopeService { private wallet: Wallet private logger: Logger private config: AgentConfig @@ -28,39 +29,51 @@ class EnvelopeService { this.config = agentConfig } - public async packMessage(payload: AgentMessage, keys: EnvelopeKeys): Promise { - const { routingKeys, senderKey } = keys - let recipientKeys = keys.recipientKeys + public async packMessage(payload: AgentMessage, keys: EnvelopeKeys): Promise { + const { recipientKeys, routingKeys, senderKey } = keys + let recipientKeysBase58 = recipientKeys.map((key) => key.publicKeyBase58) + const routingKeysBase58 = routingKeys.map((key) => key.publicKeyBase58) + const senderKeyBase58 = senderKey && senderKey.publicKeyBase58 // pass whether we want to use legacy did sov prefix const message = payload.toJSON({ useLegacyDidSovPrefix: this.config.useLegacyDidSovPrefix }) this.logger.debug(`Pack outbound message ${message['@type']}`) - let wireMessage = await this.wallet.pack(message, recipientKeys, senderKey ?? undefined) + let encryptedMessage = await this.wallet.pack(message, recipientKeysBase58, senderKeyBase58 ?? undefined) // If the message has routing keys (mediator) pack for each mediator - for (const routingKey of routingKeys) { + for (const routingKeyBase58 of routingKeysBase58) { const forwardMessage = new ForwardMessage({ // Forward to first recipient key - to: recipientKeys[0], - message: wireMessage, + to: recipientKeysBase58[0], + message: encryptedMessage, }) - recipientKeys = [routingKey] + recipientKeysBase58 = [routingKeyBase58] this.logger.debug('Forward message created', forwardMessage) const forwardJson = forwardMessage.toJSON({ useLegacyDidSovPrefix: this.config.useLegacyDidSovPrefix }) // Forward messages are anon packed - wireMessage = await this.wallet.pack(forwardJson, [routingKey], undefined) + encryptedMessage = await this.wallet.pack(forwardJson, [routingKeyBase58], undefined) } - return wireMessage + return encryptedMessage } - public async unpackMessage(packedMessage: WireMessage): Promise { - return this.wallet.unpack(packedMessage) + public async unpackMessage(encryptedMessage: EncryptedMessage): Promise { + const decryptedMessage = await this.wallet.unpack(encryptedMessage) + const { recipientKey, senderKey, plaintextMessage } = decryptedMessage + return { + recipientKey: recipientKey ? Key.fromPublicKeyBase58(recipientKey, KeyType.Ed25519) : undefined, + senderKey: senderKey ? Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519) : undefined, + plaintextMessage, + } } } -export { EnvelopeService } +export interface DecryptedMessageContext { + plaintextMessage: PlaintextMessage + senderKey?: Key + recipientKey?: Key +} diff --git a/packages/core/src/agent/Events.ts b/packages/core/src/agent/Events.ts index cb700d57de..f6bc64a7bb 100644 --- a/packages/core/src/agent/Events.ts +++ b/packages/core/src/agent/Events.ts @@ -15,6 +15,7 @@ export interface AgentMessageReceivedEvent extends BaseEvent { type: typeof AgentEventTypes.AgentMessageReceived payload: { message: unknown + connection?: ConnectionRecord } } diff --git a/packages/core/src/agent/Handler.ts b/packages/core/src/agent/Handler.ts index b0db8965b6..7a43bbbbbe 100644 --- a/packages/core/src/agent/Handler.ts +++ b/packages/core/src/agent/Handler.ts @@ -1,9 +1,9 @@ import type { OutboundMessage, OutboundServiceMessage } from '../types' -import type { AgentMessage } from './AgentMessage' +import type { ConstructableAgentMessage } from './AgentMessage' import type { InboundMessageContext } from './models/InboundMessageContext' -export interface Handler { - readonly supportedMessages: readonly T[] +export interface Handler { + readonly supportedMessages: readonly ConstructableAgentMessage[] handle(messageContext: InboundMessageContext): Promise } diff --git a/packages/core/src/agent/MessageReceiver.ts b/packages/core/src/agent/MessageReceiver.ts index 30ec17290e..31afd2eefa 100644 --- a/packages/core/src/agent/MessageReceiver.ts +++ b/packages/core/src/agent/MessageReceiver.ts @@ -1,22 +1,28 @@ import type { Logger } from '../logger' import type { ConnectionRecord } from '../modules/connections' import type { InboundTransport } from '../transport' -import type { UnpackedMessageContext, UnpackedMessage, WireMessage } from '../types' +import type { PlaintextMessage, EncryptedMessage } from '../types' import type { AgentMessage } from './AgentMessage' +import type { DecryptedMessageContext } from './EnvelopeService' import type { TransportSession } from './TransportService' import { Lifecycle, scoped } from 'tsyringe' import { AriesFrameworkError } from '../error' -import { ConnectionService } from '../modules/connections/services/ConnectionService' +import { ConnectionsModule } from '../modules/connections' +import { OutOfBandService } from '../modules/oob/OutOfBandService' +import { ProblemReportError, ProblemReportMessage, ProblemReportReason } from '../modules/problem-reports' +import { isValidJweStructure } from '../utils/JWE' import { JsonTransformer } from '../utils/JsonTransformer' import { MessageValidator } from '../utils/MessageValidator' -import { replaceLegacyDidSovPrefixOnMessage } from '../utils/messageType' +import { canHandleMessageType, parseMessageType, replaceLegacyDidSovPrefixOnMessage } from '../utils/messageType' import { AgentConfig } from './AgentConfig' import { Dispatcher } from './Dispatcher' import { EnvelopeService } from './EnvelopeService' +import { MessageSender } from './MessageSender' import { TransportService } from './TransportService' +import { createOutboundMessage } from './helpers' import { InboundMessageContext } from './models/InboundMessageContext' @scoped(Lifecycle.ContainerScoped) @@ -24,22 +30,28 @@ export class MessageReceiver { private config: AgentConfig private envelopeService: EnvelopeService private transportService: TransportService - private connectionService: ConnectionService + private messageSender: MessageSender private dispatcher: Dispatcher private logger: Logger + private connectionsModule: ConnectionsModule public readonly inboundTransports: InboundTransport[] = [] + private outOfBandService: OutOfBandService public constructor( config: AgentConfig, envelopeService: EnvelopeService, transportService: TransportService, - connectionService: ConnectionService, + messageSender: MessageSender, + connectionsModule: ConnectionsModule, + outOfBandService: OutOfBandService, dispatcher: Dispatcher ) { this.config = config this.envelopeService = envelopeService this.transportService = transportService - this.connectionService = connectionService + this.messageSender = messageSender + this.connectionsModule = connectionsModule + this.outOfBandService = outOfBandService this.dispatcher = dispatcher this.logger = this.config.logger } @@ -49,60 +61,52 @@ export class MessageReceiver { } /** - * Receive and handle an inbound DIDComm message. It will unpack the message, transform it + * Receive and handle an inbound DIDComm message. It will decrypt the message, transform it * to it's corresponding message class and finally dispatch it to the dispatcher. * - * @param inboundPackedMessage the message to receive and handle + * @param inboundMessage the message to receive and handle */ - public async receiveMessage(inboundPackedMessage: unknown, session?: TransportSession) { - if (typeof inboundPackedMessage !== 'object' || inboundPackedMessage == null) { - throw new AriesFrameworkError('Invalid message received. Message should be object') - } - + public async receiveMessage( + inboundMessage: unknown, + { session, connection }: { session?: TransportSession; connection?: ConnectionRecord } + ) { this.logger.debug(`Agent ${this.config.label} received message`) + if (this.isEncryptedMessage(inboundMessage)) { + await this.receiveEncryptedMessage(inboundMessage as EncryptedMessage, session) + } else if (this.isPlaintextMessage(inboundMessage)) { + await this.receivePlaintextMessage(inboundMessage, connection) + } else { + throw new AriesFrameworkError('Unable to parse incoming message: unrecognized format') + } + } - const unpackedMessage = await this.unpackMessage(inboundPackedMessage as WireMessage) - const senderKey = unpackedMessage.senderVerkey - const recipientKey = unpackedMessage.recipientVerkey - - let connection: ConnectionRecord | null = null - - // Only fetch connection if recipientKey and senderKey are present (AuthCrypt) - if (senderKey && recipientKey) { - connection = await this.connectionService.findByVerkey(recipientKey) + private async receivePlaintextMessage(plaintextMessage: PlaintextMessage, connection?: ConnectionRecord) { + const message = await this.transformAndValidate(plaintextMessage) + const messageContext = new InboundMessageContext(message, { connection }) + await this.dispatcher.dispatch(messageContext) + } - // Throw error if the recipient key (ourKey) does not match the key of the connection record - if (connection && connection.theirKey !== null && connection.theirKey !== senderKey) { - throw new AriesFrameworkError( - `Inbound message senderKey '${senderKey}' is different from connection.theirKey '${connection.theirKey}'` - ) - } - } + private async receiveEncryptedMessage(encryptedMessage: EncryptedMessage, session?: TransportSession) { + const decryptedMessage = await this.decryptMessage(encryptedMessage) + const { plaintextMessage, senderKey, recipientKey } = decryptedMessage this.logger.info( - `Received message with type '${unpackedMessage.message['@type']}' from connection ${connection?.id} (${connection?.theirLabel})`, - unpackedMessage.message + `Received message with type '${plaintextMessage['@type']}', recipient key ${recipientKey?.fingerprint} and sender key ${senderKey?.fingerprint}`, + plaintextMessage ) - const message = await this.transformMessage(unpackedMessage) - try { - await MessageValidator.validate(message) - } catch (error) { - this.logger.error(`Error validating message ${message.type}`, { - errors: error, - message: message.toJSON(), - }) + const connection = await this.findConnectionByMessageKeys(decryptedMessage) + const outOfBand = (recipientKey && (await this.outOfBandService.findByRecipientKey(recipientKey))) || undefined - throw error - } + const message = await this.transformAndValidate(plaintextMessage, connection) const messageContext = new InboundMessageContext(message, { // Only make the connection available in message context if the connection is ready // To prevent unwanted usage of unready connections. Connections can still be retrieved from // Storage if the specific protocol allows an unready connection to be used. connection: connection?.isReady ? connection : undefined, - senderVerkey: senderKey, - recipientVerkey: recipientKey, + senderKey, + recipientKey, }) // We want to save a session if there is a chance of returning outbound message via inbound transport. @@ -121,65 +125,144 @@ export class MessageReceiver { // use return routing to make connections. This is especially useful for creating connections // with mediators when you don't have a public endpoint yet. session.connection = connection ?? undefined + messageContext.sessionId = session.id + session.outOfBand = outOfBand this.transportService.saveSession(session) + } else if (session) { + // No need to wait for session to stay open if we're not actually going to respond to the message. + await session.close() } await this.dispatcher.dispatch(messageContext) } /** - * Unpack a message using the envelope service. - * If message is not packed, it will be returned as is, but in the unpacked message structure + * Decrypt a message using the envelope service. * - * @param packedMessage the received, probably packed, message to unpack + * @param message the received inbound message to decrypt */ - private async unpackMessage(packedMessage: WireMessage): Promise { - // If the inbound message has no @type field we assume - // the message is packed and must be unpacked first - if (!this.isUnpackedMessage(packedMessage)) { - try { - return await this.envelopeService.unpackMessage(packedMessage) - } catch (error) { - this.logger.error('error while unpacking message', { - error, - packedMessage, - errorMessage: error instanceof Error ? error.message : error, - }) - throw error - } + private async decryptMessage(message: EncryptedMessage): Promise { + try { + return await this.envelopeService.unpackMessage(message) + } catch (error) { + this.logger.error('Error while decrypting message', { + error, + encryptedMessage: message, + errorMessage: error instanceof Error ? error.message : error, + }) + throw error } + } - // If the message does have an @type field we assume - // the message is already unpacked an use it directly - else { - const unpackedMessage: UnpackedMessageContext = { message: packedMessage } - return unpackedMessage + private isPlaintextMessage(message: unknown): message is PlaintextMessage { + if (typeof message !== 'object' || message == null) { + return false } + // If the message has a @type field we assume the message is in plaintext and it is not encrypted. + return '@type' in message } - private isUnpackedMessage(message: Record): message is UnpackedMessage { - return '@type' in message + private isEncryptedMessage(message: unknown): message is EncryptedMessage { + // If the message does has valid JWE structure, we can assume the message is encrypted. + return isValidJweStructure(message) + } + + private async transformAndValidate( + plaintextMessage: PlaintextMessage, + connection?: ConnectionRecord | null + ): Promise { + let message: AgentMessage + try { + message = await this.transformMessage(plaintextMessage) + await this.validateMessage(message) + } catch (error) { + if (connection) await this.sendProblemReportMessage(error.message, connection, plaintextMessage) + throw error + } + return message + } + + private async findConnectionByMessageKeys({ + recipientKey, + senderKey, + }: DecryptedMessageContext): Promise { + // We only fetch connections that are sent in AuthCrypt mode + if (!recipientKey || !senderKey) return null + + // Try to find the did records that holds the sender and recipient keys + return this.connectionsModule.findByKeys({ + senderKey, + recipientKey, + }) } /** - * Transform an unpacked DIDComm message into it's corresponding message class. Will look at all message types in the registered handlers. + * Transform an plaintext DIDComm message into it's corresponding message class. Will look at all message types in the registered handlers. * - * @param unpackedMessage the unpacked message for which to transform the message in to a class instance + * @param message the plaintext message for which to transform the message in to a class instance */ - private async transformMessage(unpackedMessage: UnpackedMessageContext): Promise { + private async transformMessage(message: PlaintextMessage): Promise { // replace did:sov:BzCbsNYhMrjHiqZDTUASHg;spec prefix for message type with https://didcomm.org - replaceLegacyDidSovPrefixOnMessage(unpackedMessage.message) + replaceLegacyDidSovPrefixOnMessage(message) - const messageType = unpackedMessage.message['@type'] + const messageType = message['@type'] const MessageClass = this.dispatcher.getMessageClassForType(messageType) if (!MessageClass) { - throw new AriesFrameworkError(`No message class found for message type "${messageType}"`) + throw new ProblemReportError(`No message class found for message type "${messageType}"`, { + problemCode: ProblemReportReason.MessageParseFailure, + }) } // Cast the plain JSON object to specific instance of Message extended from AgentMessage - const message = JsonTransformer.fromJSON(unpackedMessage.message, MessageClass) + return JsonTransformer.fromJSON(message, MessageClass) + } - return message + /** + * Validate an AgentMessage instance. + * @param message agent message to validate + */ + private async validateMessage(message: AgentMessage) { + try { + await MessageValidator.validate(message) + } catch (error) { + this.logger.error(`Error validating message ${message.type}`, { + errors: error, + message: message.toJSON(), + }) + throw new ProblemReportError(`Error validating message ${message.type}`, { + problemCode: ProblemReportReason.MessageParseFailure, + }) + } + } + + /** + * Send the problem report message (https://didcomm.org/notification/1.0/problem-report) to the recipient. + * @param message error message to send + * @param connection connection to send the message to + * @param plaintextMessage received inbound message + */ + private async sendProblemReportMessage( + message: string, + connection: ConnectionRecord, + plaintextMessage: PlaintextMessage + ) { + const messageType = parseMessageType(plaintextMessage['@type']) + if (canHandleMessageType(ProblemReportMessage, messageType)) { + throw new AriesFrameworkError(`Not sending problem report in response to problem report: {message}`) + } + const problemReportMessage = new ProblemReportMessage({ + description: { + en: message, + code: ProblemReportReason.MessageParseFailure, + }, + }) + problemReportMessage.setThread({ + threadId: plaintextMessage['@id'], + }) + const outboundMessage = createOutboundMessage(connection, problemReportMessage) + if (outboundMessage) { + await this.messageSender.sendMessage(outboundMessage) + } } } diff --git a/packages/core/src/agent/MessageSender.ts b/packages/core/src/agent/MessageSender.ts index da39dd9fe4..c6920a6f19 100644 --- a/packages/core/src/agent/MessageSender.ts +++ b/packages/core/src/agent/MessageSender.ts @@ -1,6 +1,9 @@ -import type { DidCommService, ConnectionRecord } from '../modules/connections' +import type { Key } from '../crypto' +import type { ConnectionRecord } from '../modules/connections' +import type { DidDocument } from '../modules/dids' +import type { OutOfBandRecord } from '../modules/oob/repository' import type { OutboundTransport } from '../transport/OutboundTransport' -import type { OutboundMessage, OutboundPackage, WireMessage } from '../types' +import type { OutboundMessage, OutboundPackage, EncryptedMessage } from '../types' import type { AgentMessage } from './AgentMessage' import type { EnvelopeKeys } from './EnvelopeService' import type { TransportSession } from './TransportService' @@ -11,12 +14,25 @@ import { DID_COMM_TRANSPORT_QUEUE, InjectionSymbols } from '../constants' import { ReturnRouteTypes } from '../decorators/transport/TransportDecorator' import { AriesFrameworkError } from '../error' import { Logger } from '../logger' +import { keyReferenceToKey } from '../modules/dids' +import { getKeyDidMappingByVerificationMethod } from '../modules/dids/domain/key-type' +import { DidCommV1Service, IndyAgentService } from '../modules/dids/domain/service' +import { didKeyToInstanceOfKey, verkeyToInstanceOfKey } from '../modules/dids/helpers' +import { DidResolverService } from '../modules/dids/services/DidResolverService' import { MessageRepository } from '../storage/MessageRepository' import { MessageValidator } from '../utils/MessageValidator' +import { getProtocolScheme } from '../utils/uri' import { EnvelopeService } from './EnvelopeService' import { TransportService } from './TransportService' +export interface ResolvedDidCommService { + id: string + serviceEndpoint: string + recipientKeys: Key[] + routingKeys: Key[] +} + export interface TransportPriorityOptions { schemes: string[] restrictive?: boolean @@ -28,18 +44,21 @@ export class MessageSender { private transportService: TransportService private messageRepository: MessageRepository private logger: Logger + private didResolverService: DidResolverService public readonly outboundTransports: OutboundTransport[] = [] public constructor( envelopeService: EnvelopeService, transportService: TransportService, @inject(InjectionSymbols.MessageRepository) messageRepository: MessageRepository, - @inject(InjectionSymbols.Logger) logger: Logger + @inject(InjectionSymbols.Logger) logger: Logger, + didResolverService: DidResolverService ) { this.envelopeService = envelopeService this.transportService = transportService this.messageRepository = messageRepository this.logger = logger + this.didResolverService = didResolverService this.outboundTransports = [] } @@ -56,10 +75,10 @@ export class MessageSender { message: AgentMessage endpoint: string }): Promise { - const wireMessage = await this.envelopeService.packMessage(message, keys) + const encryptedMessage = await this.envelopeService.packMessage(message, keys) return { - payload: wireMessage, + payload: encryptedMessage, responseRequested: message.hasAnyReturnRoute(), endpoint, } @@ -72,18 +91,17 @@ export class MessageSender { if (!session.keys) { throw new AriesFrameworkError(`There are no keys for the given ${session.type} transport session.`) } - const wireMessage = await this.envelopeService.packMessage(message, session.keys) - - await session.send(wireMessage) + const encryptedMessage = await this.envelopeService.packMessage(message, session.keys) + await session.send(encryptedMessage) } public async sendPackage({ connection, - packedMessage, + encryptedMessage, options, }: { connection: ConnectionRecord - packedMessage: WireMessage + encryptedMessage: EncryptedMessage options?: { transportPriority?: TransportPriorityOptions } }) { const errors: Error[] = [] @@ -92,7 +110,7 @@ export class MessageSender { const session = this.transportService.findSessionByConnectionId(connection.id) if (session?.inboundMessage?.hasReturnRouting()) { try { - await session.send(packedMessage) + await session.send(encryptedMessage) return } catch (error) { errors.push(error) @@ -111,10 +129,11 @@ export class MessageSender { for await (const service of services) { this.logger.debug(`Sending outbound message to service:`, { service }) try { + const protocolScheme = getProtocolScheme(service.serviceEndpoint) for (const transport of this.outboundTransports) { - if (transport.supportedSchemes.includes(service.protocolScheme)) { + if (transport.supportedSchemes.includes(protocolScheme)) { await transport.sendMessage({ - payload: packedMessage, + payload: encryptedMessage, endpoint: service.serviceEndpoint, connectionId: connection.id, }) @@ -137,13 +156,13 @@ export class MessageSender { // If the other party shared a queue service endpoint in their did doc we queue the message if (queueService) { this.logger.debug(`Queue packed message for connection ${connection.id} (${connection.theirLabel})`) - this.messageRepository.add(connection.id, packedMessage) + this.messageRepository.add(connection.id, encryptedMessage) return } // Message is undeliverable this.logger.error(`Message is undeliverable to connection ${connection.id} (${connection.theirLabel})`, { - message: packedMessage, + message: encryptedMessage, errors, connection, }) @@ -156,7 +175,7 @@ export class MessageSender { transportPriority?: TransportPriorityOptions } ) { - const { connection, payload } = outboundMessage + const { connection, outOfBand, sessionId, payload } = outboundMessage const errors: Error[] = [] this.logger.debug('Send outbound message', { @@ -164,8 +183,18 @@ export class MessageSender { connectionId: connection.id, }) - // Try to send to already open session - const session = this.transportService.findSessionByConnectionId(connection.id) + let session: TransportSession | undefined + + if (sessionId) { + session = this.transportService.findSessionById(sessionId) + } + if (!session) { + // Try to send to already open session + session = + this.transportService.findSessionByConnectionId(connection.id) || + (outOfBand && this.transportService.findSessionByOutOfBandId(outOfBand.id)) + } + if (session?.inboundMessage?.hasReturnRouting(payload.threadId)) { this.logger.debug(`Found session with return routing for message '${payload.id}' (connection '${connection.id}'`) try { @@ -178,18 +207,35 @@ export class MessageSender { } // Retrieve DIDComm services - const { services, queueService } = await this.retrieveServicesByConnection(connection, options?.transportPriority) + const { services, queueService } = await this.retrieveServicesByConnection( + connection, + options?.transportPriority, + outOfBand + ) + + const ourDidDocument = await this.resolveDidDocument(connection.did) + const ourAuthenticationKeys = getAuthenticationKeys(ourDidDocument) + + // TODO We're selecting just the first authentication key. Is it ok? + // We can probably learn something from the didcomm-rust implementation, which looks at crypto compatibility to make sure the + // other party can decrypt the message. https://github.com/sicpa-dlab/didcomm-rust/blob/9a24b3b60f07a11822666dda46e5616a138af056/src/message/pack_encrypted/mod.rs#L33-L44 + // This will become more relevant when we support different encrypt envelopes. One thing to take into account though is that currently we only store the recipientKeys + // as defined in the didcomm services, while it could be for example that the first authentication key is not defined in the recipientKeys, in which case we wouldn't + // even be interoperable between two AFJ agents. So we should either pick the first key that is defined in the recipientKeys, or we should make sure to store all + // keys defined in the did document as tags so we can retrieve it, even if it's not defined in the recipientKeys. This, again, will become simpler once we use didcomm v2 + // as the `from` field in a received message will identity the did used so we don't have to store all keys in tags to be able to find the connections associated with + // an incoming message. + const [firstOurAuthenticationKey] = ourAuthenticationKeys + const shouldUseReturnRoute = !this.transportService.hasInboundEndpoint(ourDidDocument) // Loop trough all available services and try to send the message for await (const service of services) { try { - // Enable return routing if the - const shouldUseReturnRoute = !this.transportService.hasInboundEndpoint(connection.didDoc) - + // Enable return routing if the our did document does not have any inbound endpoint for given sender key await this.sendMessageToService({ message: payload, service, - senderKey: connection.verkey, + senderKey: firstOurAuthenticationKey, returnRoute: shouldUseReturnRoute, connectionId: connection.id, }) @@ -213,12 +259,12 @@ export class MessageSender { const keys = { recipientKeys: queueService.recipientKeys, - routingKeys: queueService.routingKeys || [], - senderKey: connection.verkey, + routingKeys: queueService.routingKeys, + senderKey: firstOurAuthenticationKey, } - const wireMessage = await this.envelopeService.packMessage(payload, keys) - this.messageRepository.add(connection.id, wireMessage) + const encryptedMessage = await this.envelopeService.packMessage(payload, keys) + this.messageRepository.add(connection.id, encryptedMessage) return } @@ -239,8 +285,8 @@ export class MessageSender { connectionId, }: { message: AgentMessage - service: DidCommService - senderKey: string + service: ResolvedDidCommService + senderKey: Key returnRoute?: boolean connectionId?: string }) { @@ -248,11 +294,14 @@ export class MessageSender { throw new AriesFrameworkError('Agent has no outbound transport!') } - this.logger.debug(`Sending outbound message to service:`, { messageId: message.id, service }) + this.logger.debug(`Sending outbound message to service:`, { + messageId: message.id, + service: { ...service, recipientKeys: 'omitted...', routingKeys: 'omitted...' }, + }) const keys = { recipientKeys: service.recipientKeys, - routingKeys: service.routingKeys || [], + routingKeys: service.routingKeys, senderKey, } @@ -279,51 +328,147 @@ export class MessageSender { outboundPackage.endpoint = service.serviceEndpoint outboundPackage.connectionId = connectionId for (const transport of this.outboundTransports) { - if (transport.supportedSchemes.includes(service.protocolScheme)) { + const protocolScheme = getProtocolScheme(service.serviceEndpoint) + if (!protocolScheme) { + this.logger.warn('Service does not have valid protocolScheme.') + } else if (transport.supportedSchemes.includes(protocolScheme)) { await transport.sendMessage(outboundPackage) break } } } + private async retrieveServicesFromDid(did: string) { + this.logger.debug(`Resolving services for did ${did}.`) + const didDocument = await this.resolveDidDocument(did) + + const didCommServices: ResolvedDidCommService[] = [] + + // FIXME: we currently retrieve did documents for all didcomm services in the did document, and we don't have caching + // yet so this will re-trigger ledger resolves for each one. Should we only resolve the first service, then the second service, etc...? + for (const didCommService of didDocument.didCommServices) { + if (didCommService instanceof IndyAgentService) { + // IndyAgentService (DidComm v0) has keys encoded as raw publicKeyBase58 (verkeys) + didCommServices.push({ + id: didCommService.id, + recipientKeys: didCommService.recipientKeys.map(verkeyToInstanceOfKey), + routingKeys: didCommService.routingKeys?.map(verkeyToInstanceOfKey) || [], + serviceEndpoint: didCommService.serviceEndpoint, + }) + } else if (didCommService instanceof DidCommV1Service) { + // Resolve dids to DIDDocs to retrieve routingKeys + const routingKeys = [] + for (const routingKey of didCommService.routingKeys ?? []) { + const routingDidDocument = await this.resolveDidDocument(routingKey) + routingKeys.push(keyReferenceToKey(routingDidDocument, routingKey)) + } + + // Dereference recipientKeys + const recipientKeys = didCommService.recipientKeys.map((recipientKey) => + keyReferenceToKey(didDocument, recipientKey) + ) + + // DidCommV1Service has keys encoded as key references + didCommServices.push({ + id: didCommService.id, + recipientKeys, + routingKeys, + serviceEndpoint: didCommService.serviceEndpoint, + }) + } + } + + return didCommServices + } + private async retrieveServicesByConnection( connection: ConnectionRecord, - transportPriority?: TransportPriorityOptions + transportPriority?: TransportPriorityOptions, + outOfBand?: OutOfBandRecord ) { this.logger.debug(`Retrieving services for connection '${connection.id}' (${connection.theirLabel})`, { transportPriority, + connection, }) - // Retrieve DIDComm services - const allServices = this.transportService.findDidCommServices(connection) - //Separate queue service out - let services = allServices.filter((s) => !isDidCommTransportQueue(s.serviceEndpoint)) - const queueService = allServices.find((s) => isDidCommTransportQueue(s.serviceEndpoint)) + let didCommServices: ResolvedDidCommService[] = [] + + if (connection.theirDid) { + this.logger.debug(`Resolving services for connection theirDid ${connection.theirDid}.`) + didCommServices = await this.retrieveServicesFromDid(connection.theirDid) + } else if (outOfBand) { + this.logger.debug(`Resolving services from out-of-band record ${outOfBand?.id}.`) + if (connection.isRequester) { + for (const service of outOfBand.outOfBandInvitation.services) { + // Resolve dids to DIDDocs to retrieve services + if (typeof service === 'string') { + didCommServices = await this.retrieveServicesFromDid(service) + } else { + // Out of band inline service contains keys encoded as did:key references + didCommServices.push({ + id: service.id, + recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey), + routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) || [], + serviceEndpoint: service.serviceEndpoint, + }) + } + } + } + } + + // Separate queue service out + let services = didCommServices.filter((s) => !isDidCommTransportQueue(s.serviceEndpoint)) + const queueService = didCommServices.find((s) => isDidCommTransportQueue(s.serviceEndpoint)) - //If restrictive will remove services not listed in schemes list + // If restrictive will remove services not listed in schemes list if (transportPriority?.restrictive) { services = services.filter((service) => { - const serviceSchema = service.protocolScheme + const serviceSchema = getProtocolScheme(service.serviceEndpoint) return transportPriority.schemes.includes(serviceSchema) }) } - //If transport priority is set we will sort services by our priority + // If transport priority is set we will sort services by our priority if (transportPriority?.schemes) { services = services.sort(function (a, b) { - const aScheme = a.protocolScheme - const bScheme = b.protocolScheme + const aScheme = getProtocolScheme(a.serviceEndpoint) + const bScheme = getProtocolScheme(b.serviceEndpoint) return transportPriority?.schemes.indexOf(aScheme) - transportPriority?.schemes.indexOf(bScheme) }) } this.logger.debug( - `Retrieved ${services.length} services for message to connection '${connection.id}'(${connection.theirLabel})'` + `Retrieved ${services.length} services for message to connection '${connection.id}'(${connection.theirLabel})'`, + { hasQueueService: queueService !== undefined } ) return { services, queueService } } + + private async resolveDidDocument(did: string) { + const { + didDocument, + didResolutionMetadata: { error, message }, + } = await this.didResolverService.resolve(did) + + if (!didDocument) { + throw new AriesFrameworkError(`Unable to resolve did document for did '${did}': ${error} ${message}`) + } + return didDocument + } } export function isDidCommTransportQueue(serviceEndpoint: string): serviceEndpoint is typeof DID_COMM_TRANSPORT_QUEUE { return serviceEndpoint === DID_COMM_TRANSPORT_QUEUE } + +function getAuthenticationKeys(didDocument: DidDocument) { + return ( + didDocument.authentication?.map((authentication) => { + const verificationMethod = + typeof authentication === 'string' ? didDocument.dereferenceVerificationMethod(authentication) : authentication + const { getKeyFromVerificationMethod } = getKeyDidMappingByVerificationMethod(verificationMethod) + const key = getKeyFromVerificationMethod(verificationMethod) + return key + }) ?? [] + ) +} diff --git a/packages/core/src/agent/TransportService.ts b/packages/core/src/agent/TransportService.ts index 01f784342a..12458f7413 100644 --- a/packages/core/src/agent/TransportService.ts +++ b/packages/core/src/agent/TransportService.ts @@ -1,28 +1,32 @@ -import type { DidDoc, IndyAgentService } from '../modules/connections/models' import type { ConnectionRecord } from '../modules/connections/repository' -import type { WireMessage } from '../types' +import type { DidDocument } from '../modules/dids' +import type { OutOfBandRecord } from '../modules/oob/repository' +import type { EncryptedMessage } from '../types' import type { AgentMessage } from './AgentMessage' import type { EnvelopeKeys } from './EnvelopeService' import { Lifecycle, scoped } from 'tsyringe' import { DID_COMM_TRANSPORT_QUEUE } from '../constants' -import { ConnectionRole, DidCommService } from '../modules/connections/models' @scoped(Lifecycle.ContainerScoped) export class TransportService { - private transportSessionTable: TransportSessionTable = {} + public transportSessionTable: TransportSessionTable = {} public saveSession(session: TransportSession) { this.transportSessionTable[session.id] = session } public findSessionByConnectionId(connectionId: string) { - return Object.values(this.transportSessionTable).find((session) => session.connection?.id === connectionId) + return Object.values(this.transportSessionTable).find((session) => session?.connection?.id === connectionId) } - public hasInboundEndpoint(didDoc: DidDoc): boolean { - return Boolean(didDoc.didCommServices.find((s) => s.serviceEndpoint !== DID_COMM_TRANSPORT_QUEUE)) + public findSessionByOutOfBandId(outOfBandId: string) { + return Object.values(this.transportSessionTable).find((session) => session?.outOfBand?.id === outOfBandId) + } + + public hasInboundEndpoint(didDocument: DidDocument): boolean { + return Boolean(didDocument.service?.find((s) => s.serviceEndpoint !== DID_COMM_TRANSPORT_QUEUE)) } public findSessionById(sessionId: string) { @@ -32,30 +36,10 @@ export class TransportService { public removeSession(session: TransportSession) { delete this.transportSessionTable[session.id] } - - public findDidCommServices(connection: ConnectionRecord): Array { - if (connection.theirDidDoc) { - return connection.theirDidDoc.didCommServices - } - - if (connection.role === ConnectionRole.Invitee && connection.invitation) { - const { invitation } = connection - if (invitation.serviceEndpoint) { - const service = new DidCommService({ - id: `${connection.id}-invitation`, - serviceEndpoint: invitation.serviceEndpoint, - recipientKeys: invitation.recipientKeys || [], - routingKeys: invitation.routingKeys || [], - }) - return [service] - } - } - return [] - } } interface TransportSessionTable { - [sessionId: string]: TransportSession + [sessionId: string]: TransportSession | undefined } export interface TransportSession { @@ -64,5 +48,7 @@ export interface TransportSession { keys?: EnvelopeKeys inboundMessage?: AgentMessage connection?: ConnectionRecord - send(wireMessage: WireMessage): Promise + outOfBand?: OutOfBandRecord + send(encryptedMessage: EncryptedMessage): Promise + close(): Promise } diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index ea909125d3..b56eb476fe 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -8,7 +8,7 @@ import { ConnectionsModule } from '../../modules/connections/ConnectionsModule' import { ConnectionRepository } from '../../modules/connections/repository/ConnectionRepository' import { ConnectionService } from '../../modules/connections/services/ConnectionService' import { TrustPingService } from '../../modules/connections/services/TrustPingService' -import { CredentialRepository, CredentialService } from '../../modules/credentials' +import { CredentialRepository } from '../../modules/credentials' import { CredentialsModule } from '../../modules/credentials/CredentialsModule' import { IndyLedgerService } from '../../modules/ledger' import { LedgerModule } from '../../modules/ledger/LedgerModule' @@ -74,22 +74,20 @@ describe('Agent', () => { const { walletConfig, ...withoutWalletConfig } = config agent = new Agent(withoutWalletConfig, dependencies) - const wallet = agent.injectionContainer.resolve(InjectionSymbols.Wallet) - expect(agent.isInitialized).toBe(false) - expect(wallet.isInitialized).toBe(false) + expect(agent.wallet.isInitialized).toBe(false) expect(agent.initialize()).rejects.toThrowError(WalletError) expect(agent.isInitialized).toBe(false) - expect(wallet.isInitialized).toBe(false) + expect(agent.wallet.isInitialized).toBe(false) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await wallet.initialize(walletConfig!) + await agent.wallet.initialize(walletConfig!) expect(agent.isInitialized).toBe(false) - expect(wallet.isInitialized).toBe(true) + expect(agent.wallet.isInitialized).toBe(true) await agent.initialize() - expect(wallet.isInitialized).toBe(true) + expect(agent.wallet.isInitialized).toBe(true) expect(agent.isInitialized).toBe(true) }) }) @@ -125,7 +123,6 @@ describe('Agent', () => { expect(container.resolve(ProofRepository)).toBeInstanceOf(ProofRepository) expect(container.resolve(CredentialsModule)).toBeInstanceOf(CredentialsModule) - expect(container.resolve(CredentialService)).toBeInstanceOf(CredentialService) expect(container.resolve(CredentialRepository)).toBeInstanceOf(CredentialRepository) expect(container.resolve(BasicMessagesModule)).toBeInstanceOf(BasicMessagesModule) @@ -169,7 +166,6 @@ describe('Agent', () => { expect(container.resolve(ProofRepository)).toBe(container.resolve(ProofRepository)) expect(container.resolve(CredentialsModule)).toBe(container.resolve(CredentialsModule)) - expect(container.resolve(CredentialService)).toBe(container.resolve(CredentialService)) expect(container.resolve(CredentialRepository)).toBe(container.resolve(CredentialRepository)) expect(container.resolve(BasicMessagesModule)).toBe(container.resolve(BasicMessagesModule)) diff --git a/packages/core/src/agent/__tests__/AgentMessage.test.ts b/packages/core/src/agent/__tests__/AgentMessage.test.ts index da8f7b11a6..66f4a1a8b9 100644 --- a/packages/core/src/agent/__tests__/AgentMessage.test.ts +++ b/packages/core/src/agent/__tests__/AgentMessage.test.ts @@ -1,4 +1,14 @@ import { TestMessage } from '../../../tests/TestMessage' +import { JsonTransformer } from '../../utils' +import { MessageValidator } from '../../utils/MessageValidator' +import { IsValidMessageType, parseMessageType } from '../../utils/messageType' +import { AgentMessage } from '../AgentMessage' + +class CustomProtocolMessage extends AgentMessage { + @IsValidMessageType(CustomProtocolMessage.type) + public readonly type = CustomProtocolMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/fake-protocol/1.5/message') +} describe('AgentMessage', () => { describe('toJSON', () => { @@ -12,4 +22,58 @@ describe('AgentMessage', () => { expect(jsonSov['@type']).toBe('did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation') }) }) + + describe('@IsValidMessageType', () => { + it('successfully validates if the message type is exactly the supported message type', async () => { + const json = { + '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + '@type': 'https://didcomm.org/fake-protocol/1.5/message', + } + + const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) + + await expect(MessageValidator.validate(message)).resolves.toBeUndefined() + }) + + it('successfully validates if the message type minor version is lower than the supported message type', async () => { + const json = { + '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + '@type': 'https://didcomm.org/fake-protocol/1.2/message', + } + + const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) + + await expect(MessageValidator.validate(message)).resolves.toBeUndefined() + }) + + it('successfully validates if the message type minor version is higher than the supported message type', async () => { + const json = { + '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + '@type': 'https://didcomm.org/fake-protocol/1.8/message', + } + + const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) + + await expect(MessageValidator.validate(message)).resolves.toBeUndefined() + }) + + it('throws a validation error if the message type major version differs from the supported message type', async () => { + expect.assertions(1) + + const json = { + '@id': 'd61c7e3d-d4af-469b-8d42-33fd14262e17', + '@type': 'https://didcomm.org/fake-protocol/2.0/message', + } + + const message = JsonTransformer.fromJSON(json, CustomProtocolMessage) + + await expect(MessageValidator.validate(message)).rejects.toMatchObject([ + { + constraints: { + isValidMessageType: 'type does not match the expected message type (only minor version may be lower)', + }, + }, + ]) + }) + }) }) diff --git a/packages/core/src/agent/__tests__/Dispatcher.test.ts b/packages/core/src/agent/__tests__/Dispatcher.test.ts index 3c94cb9dff..ec5f60160f 100644 --- a/packages/core/src/agent/__tests__/Dispatcher.test.ts +++ b/packages/core/src/agent/__tests__/Dispatcher.test.ts @@ -1,30 +1,40 @@ import type { Handler } from '../Handler' import { getAgentConfig } from '../../../tests/helpers' +import { parseMessageType } from '../../utils/messageType' import { AgentMessage } from '../AgentMessage' import { Dispatcher } from '../Dispatcher' import { EventEmitter } from '../EventEmitter' import { MessageSender } from '../MessageSender' +import { InboundMessageContext } from '../models/InboundMessageContext' class ConnectionInvitationTestMessage extends AgentMessage { - public static readonly type = 'https://didcomm.org/connections/1.0/invitation' + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/invitation') } class ConnectionRequestTestMessage extends AgentMessage { - public static readonly type = 'https://didcomm.org/connections/1.0/request' + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/request') } class ConnectionResponseTestMessage extends AgentMessage { - public static readonly type = 'https://didcomm.org/connections/1.0/response' + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/response') } class NotificationAckTestMessage extends AgentMessage { - public static readonly type = 'https://didcomm.org/notification/1.0/ack' + public static readonly type = parseMessageType('https://didcomm.org/notification/1.0/ack') } class CredentialProposalTestMessage extends AgentMessage { - public static readonly type = 'https://didcomm.org/issue-credential/1.0/credential-proposal' + public readonly type = CredentialProposalTestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/credential-proposal') +} + +class CustomProtocolMessage extends AgentMessage { + public readonly type = CustomProtocolMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/fake-protocol/1.5/message') } class TestHandler implements Handler { + // We want to pass various classes to test various behaviours so we dont need to strictly type it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any public constructor(classes: any[]) { this.supportedMessages = classes } @@ -40,25 +50,31 @@ describe('Dispatcher', () => { const agentConfig = getAgentConfig('DispatcherTest') const MessageSenderMock = MessageSender as jest.Mock const eventEmitter = new EventEmitter(agentConfig) + const fakeProtocolHandler = new TestHandler([CustomProtocolMessage]) + const connectionHandler = new TestHandler([ + ConnectionInvitationTestMessage, + ConnectionRequestTestMessage, + ConnectionResponseTestMessage, + ]) const dispatcher = new Dispatcher(new MessageSenderMock(), eventEmitter, agentConfig) - dispatcher.registerHandler( - new TestHandler([ConnectionInvitationTestMessage, ConnectionRequestTestMessage, ConnectionResponseTestMessage]) - ) + dispatcher.registerHandler(connectionHandler) dispatcher.registerHandler(new TestHandler([NotificationAckTestMessage])) dispatcher.registerHandler(new TestHandler([CredentialProposalTestMessage])) + dispatcher.registerHandler(fakeProtocolHandler) describe('supportedMessageTypes', () => { test('return all supported message types URIs', async () => { const messageTypes = dispatcher.supportedMessageTypes - expect(messageTypes).toEqual([ - 'https://didcomm.org/connections/1.0/invitation', - 'https://didcomm.org/connections/1.0/request', - 'https://didcomm.org/connections/1.0/response', - 'https://didcomm.org/notification/1.0/ack', - 'https://didcomm.org/issue-credential/1.0/credential-proposal', + expect(messageTypes).toMatchObject([ + { messageTypeUri: 'https://didcomm.org/connections/1.0/invitation' }, + { messageTypeUri: 'https://didcomm.org/connections/1.0/request' }, + { messageTypeUri: 'https://didcomm.org/connections/1.0/response' }, + { messageTypeUri: 'https://didcomm.org/notification/1.0/ack' }, + { messageTypeUri: 'https://didcomm.org/issue-credential/1.0/credential-proposal' }, + { messageTypeUri: 'https://didcomm.org/fake-protocol/1.5/message' }, ]) }) }) @@ -71,6 +87,7 @@ describe('Dispatcher', () => { 'https://didcomm.org/connections/1.0', 'https://didcomm.org/notification/1.0', 'https://didcomm.org/issue-credential/1.0', + 'https://didcomm.org/fake-protocol/1.5', ]) }) }) @@ -96,4 +113,54 @@ describe('Dispatcher', () => { expect(supportedProtocols).toEqual(['https://didcomm.org/connections/1.0']) }) }) + + describe('getMessageClassForType()', () => { + it('should return the correct message class for a registered message type', () => { + const messageClass = dispatcher.getMessageClassForType('https://didcomm.org/connections/1.0/invitation') + expect(messageClass).toBe(ConnectionInvitationTestMessage) + }) + + it('should return undefined if no message class is registered for the message type', () => { + const messageClass = dispatcher.getMessageClassForType('https://didcomm.org/non-existing/1.0/invitation') + expect(messageClass).toBeUndefined() + }) + + it('should return the message class with a higher minor version for the message type', () => { + const messageClass = dispatcher.getMessageClassForType('https://didcomm.org/fake-protocol/1.0/message') + expect(messageClass).toBe(CustomProtocolMessage) + }) + + it('should not return the message class with a different major version', () => { + const messageClass = dispatcher.getMessageClassForType('https://didcomm.org/fake-protocol/2.0/message') + expect(messageClass).toBeUndefined() + }) + }) + + describe('dispatch()', () => { + it('calls the handle method of the handler', async () => { + const dispatcher = new Dispatcher(new MessageSenderMock(), eventEmitter, agentConfig) + const customProtocolMessage = new CustomProtocolMessage() + const inboundMessageContext = new InboundMessageContext(customProtocolMessage) + + const mockHandle = jest.fn() + dispatcher.registerHandler({ supportedMessages: [CustomProtocolMessage], handle: mockHandle }) + + await dispatcher.dispatch(inboundMessageContext) + + expect(mockHandle).toHaveBeenNthCalledWith(1, inboundMessageContext) + }) + + it('throws an error if no handler for the message could be found', async () => { + const dispatcher = new Dispatcher(new MessageSenderMock(), eventEmitter, agentConfig) + const customProtocolMessage = new CustomProtocolMessage() + const inboundMessageContext = new InboundMessageContext(customProtocolMessage) + + const mockHandle = jest.fn() + dispatcher.registerHandler({ supportedMessages: [], handle: mockHandle }) + + await expect(dispatcher.dispatch(inboundMessageContext)).rejects.toThrow( + 'No handler for message type "https://didcomm.org/fake-protocol/1.5/message" found' + ) + }) + }) }) diff --git a/packages/core/src/agent/__tests__/MessageSender.test.ts b/packages/core/src/agent/__tests__/MessageSender.test.ts index 8f55f9bd5e..5c1ff20064 100644 --- a/packages/core/src/agent/__tests__/MessageSender.test.ts +++ b/packages/core/src/agent/__tests__/MessageSender.test.ts @@ -1,13 +1,18 @@ import type { ConnectionRecord } from '../../modules/connections' +import type { DidDocumentService } from '../../modules/dids' import type { MessageRepository } from '../../storage/MessageRepository' import type { OutboundTransport } from '../../transport' -import type { OutboundMessage, WireMessage } from '../../types' +import type { OutboundMessage, EncryptedMessage } from '../../types' +import type { ResolvedDidCommService } from '../MessageSender' import { TestMessage } from '../../../tests/TestMessage' import { getAgentConfig, getMockConnection, mockFunction } from '../../../tests/helpers' import testLogger from '../../../tests/logger' +import { Key, KeyType } from '../../crypto' import { ReturnRouteTypes } from '../../decorators/transport/TransportDecorator' -import { DidCommService } from '../../modules/connections' +import { DidDocument, VerificationMethod } from '../../modules/dids' +import { DidCommV1Service } from '../../modules/dids/domain/service/DidCommV1Service' +import { DidResolverService } from '../../modules/dids/services/DidResolverService' import { InMemoryMessageRepository } from '../../storage/InMemoryMessageRepository' import { EnvelopeService as EnvelopeServiceImpl } from '../EnvelopeService' import { MessageSender } from '../MessageSender' @@ -18,10 +23,13 @@ import { DummyTransportSession } from './stubs' jest.mock('../TransportService') jest.mock('../EnvelopeService') +jest.mock('../../modules/dids/services/DidResolverService') -const TransportServiceMock = TransportService as jest.MockedClass const logger = testLogger +const TransportServiceMock = TransportService as jest.MockedClass +const DidResolverServiceMock = DidResolverService as jest.Mock + class DummyOutboundTransport implements OutboundTransport { public start(): Promise { throw new Error('Method not implemented.') @@ -41,7 +49,7 @@ class DummyOutboundTransport implements OutboundTransport { describe('MessageSender', () => { const EnvelopeService = >(EnvelopeServiceImpl) - const wireMessage: WireMessage = { + const encryptedMessage: EncryptedMessage = { protected: 'base64url', iv: 'base64url', ciphertext: 'base64url', @@ -51,14 +59,19 @@ describe('MessageSender', () => { const enveloperService = new EnvelopeService() const envelopeServicePackMessageMock = mockFunction(enveloperService.packMessage) + const didResolverService = new DidResolverServiceMock() + const didResolverServiceResolveMock = mockFunction(didResolverService.resolve) + const inboundMessage = new TestMessage() inboundMessage.setReturnRouting(ReturnRouteTypes.all) + const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + const senderKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) const session = new DummyTransportSession('session-123') session.keys = { - recipientKeys: ['verkey'], + recipientKeys: [recipientKey], routingKeys: [], - senderKey: 'senderKey', + senderKey: senderKey, } session.inboundMessage = inboundMessage session.send = jest.fn() @@ -69,19 +82,19 @@ describe('MessageSender', () => { const transportService = new TransportService() const transportServiceFindSessionMock = mockFunction(transportService.findSessionByConnectionId) + const transportServiceFindSessionByIdMock = mockFunction(transportService.findSessionById) const transportServiceHasInboundEndpoint = mockFunction(transportService.hasInboundEndpoint) - const firstDidCommService = new DidCommService({ + const firstDidCommService = new DidCommV1Service({ id: `;indy`, serviceEndpoint: 'https://www.first-endpoint.com', - recipientKeys: ['verkey'], + recipientKeys: ['#authentication-1'], }) - const secondDidCommService = new DidCommService({ + const secondDidCommService = new DidCommV1Service({ id: `;indy`, serviceEndpoint: 'https://www.second-endpoint.com', - recipientKeys: ['verkey'], + recipientKeys: ['#authentication-1'], }) - const transportServiceFindServicesMock = mockFunction(transportService.findDidCommServices) let messageSender: MessageSender let outboundTransport: OutboundTransport @@ -92,16 +105,34 @@ describe('MessageSender', () => { describe('sendMessage', () => { beforeEach(() => { TransportServiceMock.mockClear() - transportServiceHasInboundEndpoint.mockReturnValue(true) + DidResolverServiceMock.mockClear() + outboundTransport = new DummyOutboundTransport() messageRepository = new InMemoryMessageRepository(getAgentConfig('MessageSender')) - messageSender = new MessageSender(enveloperService, transportService, messageRepository, logger) - connection = getMockConnection({ id: 'test-123', theirLabel: 'Test 123' }) - + messageSender = new MessageSender( + enveloperService, + transportService, + messageRepository, + logger, + didResolverService + ) + connection = getMockConnection({ + id: 'test-123', + did: 'did:peer:1mydid', + theirDid: 'did:peer:1theirdid', + theirLabel: 'Test 123', + }) outboundMessage = createOutboundMessage(connection, new TestMessage()) - envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(wireMessage)) - transportServiceFindServicesMock.mockReturnValue([firstDidCommService, secondDidCommService]) + envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage)) + transportServiceHasInboundEndpoint.mockReturnValue(true) + + const didDocumentInstance = getMockDidDocument({ service: [firstDidCommService, secondDidCommService] }) + didResolverServiceResolveMock.mockResolvedValue({ + didDocument: didDocumentInstance, + didResolutionMetadata: {}, + didDocumentMetadata: {}, + }) }) afterEach(() => { @@ -114,7 +145,12 @@ describe('MessageSender', () => { test('throw error when there is no service or queue', async () => { messageSender.registerOutboundTransport(outboundTransport) - transportServiceFindServicesMock.mockReturnValue([]) + + didResolverServiceResolveMock.mockResolvedValue({ + didDocument: getMockDidDocument({ service: [] }), + didResolutionMetadata: {}, + didDocumentMetadata: {}, + }) await expect(messageSender.sendMessage(outboundMessage)).rejects.toThrow( `Message is undeliverable to connection test-123 (Test 123)` @@ -133,13 +169,46 @@ describe('MessageSender', () => { expect(sendMessageSpy).toHaveBeenCalledWith({ connectionId: 'test-123', - payload: wireMessage, + payload: encryptedMessage, + endpoint: firstDidCommService.serviceEndpoint, + responseRequested: false, + }) + expect(sendMessageSpy).toHaveBeenCalledTimes(1) + }) + + test("resolves the did document using the did resolver if connection.theirDid starts with 'did:'", async () => { + messageSender.registerOutboundTransport(outboundTransport) + + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + + await messageSender.sendMessage(outboundMessage) + + expect(didResolverServiceResolveMock).toHaveBeenCalledWith(connection.theirDid) + expect(sendMessageSpy).toHaveBeenCalledWith({ + connectionId: 'test-123', + payload: encryptedMessage, endpoint: firstDidCommService.serviceEndpoint, responseRequested: false, }) expect(sendMessageSpy).toHaveBeenCalledTimes(1) }) + test("throws an error if connection.theirDid starts with 'did:' but the resolver can't resolve the did document", async () => { + messageSender.registerOutboundTransport(outboundTransport) + + didResolverServiceResolveMock.mockResolvedValue({ + didDocument: null, + didResolutionMetadata: { + error: 'notFound', + }, + didDocumentMetadata: {}, + }) + + await expect(messageSender.sendMessage(outboundMessage)).rejects.toThrowError( + `Unable to resolve did document for did '${connection.theirDid}': notFound` + ) + }) + test('call send message when session send method fails with missing keys', async () => { messageSender.registerOutboundTransport(outboundTransport) transportServiceFindSessionMock.mockReturnValue(sessionWithoutKeys) @@ -151,13 +220,28 @@ describe('MessageSender', () => { expect(sendMessageSpy).toHaveBeenCalledWith({ connectionId: 'test-123', - payload: wireMessage, + payload: encryptedMessage, endpoint: firstDidCommService.serviceEndpoint, responseRequested: false, }) expect(sendMessageSpy).toHaveBeenCalledTimes(1) }) + test('call send message on session when outbound message has sessionId attached', async () => { + transportServiceFindSessionByIdMock.mockReturnValue(session) + messageSender.registerOutboundTransport(outboundTransport) + const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') + const sendMessageToServiceSpy = jest.spyOn(messageSender, 'sendMessageToService') + + await messageSender.sendMessage({ ...outboundMessage, sessionId: 'session-123' }) + + expect(session.send).toHaveBeenCalledTimes(1) + expect(session.send).toHaveBeenNthCalledWith(1, encryptedMessage) + expect(sendMessageSpy).toHaveBeenCalledTimes(0) + expect(sendMessageToServiceSpy).toHaveBeenCalledTimes(0) + expect(transportServiceFindSessionByIdMock).toHaveBeenCalledWith('session-123') + }) + test('call send message on session when there is a session for a given connection', async () => { messageSender.registerOutboundTransport(outboundTransport) const sendMessageSpy = jest.spyOn(outboundTransport, 'sendMessage') @@ -165,13 +249,22 @@ describe('MessageSender', () => { await messageSender.sendMessage(outboundMessage) - expect(sendMessageToServiceSpy).toHaveBeenCalledWith({ + const [[sendMessage]] = sendMessageToServiceSpy.mock.calls + + expect(sendMessage).toMatchObject({ connectionId: 'test-123', message: outboundMessage.payload, - senderKey: connection.verkey, - service: firstDidCommService, returnRoute: false, + service: { + serviceEndpoint: firstDidCommService.serviceEndpoint, + }, }) + + expect(sendMessage.senderKey.publicKeyBase58).toEqual('EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d') + expect(sendMessage.service.recipientKeys.map((key) => key.publicKeyBase58)).toEqual([ + 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + ]) + expect(sendMessageToServiceSpy).toHaveBeenCalledTimes(1) expect(sendMessageSpy).toHaveBeenCalledTimes(1) }) @@ -186,25 +279,34 @@ describe('MessageSender', () => { await messageSender.sendMessage(outboundMessage) - expect(sendMessageToServiceSpy).toHaveBeenNthCalledWith(2, { + const [, [sendMessage]] = sendMessageToServiceSpy.mock.calls + expect(sendMessage).toMatchObject({ connectionId: 'test-123', message: outboundMessage.payload, - senderKey: connection.verkey, - service: secondDidCommService, returnRoute: false, + service: { + serviceEndpoint: secondDidCommService.serviceEndpoint, + }, }) + + expect(sendMessage.senderKey.publicKeyBase58).toEqual('EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d') + expect(sendMessage.service.recipientKeys.map((key) => key.publicKeyBase58)).toEqual([ + 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + ]) + expect(sendMessageToServiceSpy).toHaveBeenCalledTimes(2) expect(sendMessageSpy).toHaveBeenCalledTimes(2) }) }) describe('sendMessageToService', () => { - const service = new DidCommService({ + const service: ResolvedDidCommService = { id: 'out-of-band', - recipientKeys: ['someKey'], + recipientKeys: [Key.fromFingerprint('z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL')], + routingKeys: [], serviceEndpoint: 'https://example.com', - }) - const senderKey = 'someVerkey' + } + const senderKey = Key.fromFingerprint('z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') beforeEach(() => { outboundTransport = new DummyOutboundTransport() @@ -212,10 +314,11 @@ describe('MessageSender', () => { enveloperService, transportService, new InMemoryMessageRepository(getAgentConfig('MessageSenderTest')), - logger + logger, + didResolverService ) - envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(wireMessage)) + envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage)) }) afterEach(() => { @@ -243,7 +346,7 @@ describe('MessageSender', () => { }) expect(sendMessageSpy).toHaveBeenCalledWith({ - payload: wireMessage, + payload: encryptedMessage, endpoint: service.serviceEndpoint, responseRequested: false, }) @@ -264,7 +367,7 @@ describe('MessageSender', () => { }) expect(sendMessageSpy).toHaveBeenCalledWith({ - payload: wireMessage, + payload: encryptedMessage, endpoint: service.serviceEndpoint, responseRequested: true, }) @@ -276,10 +379,16 @@ describe('MessageSender', () => { beforeEach(() => { outboundTransport = new DummyOutboundTransport() messageRepository = new InMemoryMessageRepository(getAgentConfig('PackMessage')) - messageSender = new MessageSender(enveloperService, transportService, messageRepository, logger) - connection = getMockConnection({ id: 'test-123' }) + messageSender = new MessageSender( + enveloperService, + transportService, + messageRepository, + logger, + didResolverService + ) + connection = getMockConnection() - envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(wireMessage)) + envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(encryptedMessage)) }) afterEach(() => { @@ -291,17 +400,35 @@ describe('MessageSender', () => { const endpoint = 'https://example.com' const keys = { - recipientKeys: ['service.recipientKeys'], + recipientKeys: [recipientKey], routingKeys: [], - senderKey: connection.verkey, + senderKey: senderKey, } const result = await messageSender.packMessage({ message, keys, endpoint }) expect(result).toEqual({ - payload: wireMessage, + payload: encryptedMessage, responseRequested: message.hasAnyReturnRoute(), endpoint, }) }) }) }) + +function getMockDidDocument({ service }: { service: DidDocumentService[] }) { + return new DidDocument({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo', + alsoKnownAs: ['did:sov:SKJVx2kn373FNgvff1SbJo'], + controller: ['did:sov:SKJVx2kn373FNgvff1SbJo'], + verificationMethod: [], + service, + authentication: [ + new VerificationMethod({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo#authentication-1', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + }), + ], + }) +} diff --git a/packages/core/src/agent/__tests__/TransportService.test.ts b/packages/core/src/agent/__tests__/TransportService.test.ts index 5b396315a5..c16d00478b 100644 --- a/packages/core/src/agent/__tests__/TransportService.test.ts +++ b/packages/core/src/agent/__tests__/TransportService.test.ts @@ -1,67 +1,10 @@ import { getMockConnection } from '../../../tests/helpers' -import { ConnectionInvitationMessage, ConnectionRole, DidDoc, DidCommService } from '../../modules/connections' +import { DidExchangeRole } from '../../modules/connections' import { TransportService } from '../TransportService' import { DummyTransportSession } from './stubs' describe('TransportService', () => { - describe('findServices', () => { - let transportService: TransportService - let theirDidDoc: DidDoc - const testDidCommService = new DidCommService({ - id: `;indy`, - serviceEndpoint: 'https://example.com', - recipientKeys: ['verkey'], - }) - - beforeEach(() => { - theirDidDoc = new DidDoc({ - id: 'test-456', - publicKey: [], - authentication: [], - service: [testDidCommService], - }) - - transportService = new TransportService() - }) - - test(`returns empty array when there is no their DidDoc and role is ${ConnectionRole.Inviter}`, () => { - const connection = getMockConnection({ id: 'test-123', role: ConnectionRole.Inviter }) - connection.theirDidDoc = undefined - expect(transportService.findDidCommServices(connection)).toEqual([]) - }) - - test(`returns empty array when there is no their DidDoc, no invitation and role is ${ConnectionRole.Invitee}`, () => { - const connection = getMockConnection({ id: 'test-123', role: ConnectionRole.Invitee }) - connection.theirDidDoc = undefined - connection.invitation = undefined - expect(transportService.findDidCommServices(connection)).toEqual([]) - }) - - test(`returns service from their DidDoc`, () => { - const connection = getMockConnection({ id: 'test-123', theirDidDoc }) - expect(transportService.findDidCommServices(connection)).toEqual([testDidCommService]) - }) - - test(`returns service from invitation when there is no their DidDoc and role is ${ConnectionRole.Invitee}`, () => { - const invitation = new ConnectionInvitationMessage({ - label: 'test', - recipientKeys: ['verkey'], - serviceEndpoint: 'ws://invitationEndpoint.com', - }) - const connection = getMockConnection({ id: 'test-123', role: ConnectionRole.Invitee, invitation }) - connection.theirDidDoc = undefined - expect(transportService.findDidCommServices(connection)).toEqual([ - new DidCommService({ - id: 'test-123-invitation', - serviceEndpoint: 'ws://invitationEndpoint.com', - routingKeys: [], - recipientKeys: ['verkey'], - }), - ]) - }) - }) - describe('removeSession', () => { let transportService: TransportService @@ -70,7 +13,7 @@ describe('TransportService', () => { }) test(`remove session saved for a given connection`, () => { - const connection = getMockConnection({ id: 'test-123', role: ConnectionRole.Inviter }) + const connection = getMockConnection({ id: 'test-123', role: DidExchangeRole.Responder }) const session = new DummyTransportSession('dummy-session-123') session.connection = connection diff --git a/packages/core/src/agent/__tests__/stubs.ts b/packages/core/src/agent/__tests__/stubs.ts index 5bdb3b5bb6..49fcaae660 100644 --- a/packages/core/src/agent/__tests__/stubs.ts +++ b/packages/core/src/agent/__tests__/stubs.ts @@ -17,4 +17,8 @@ export class DummyTransportSession implements TransportSession { public send(): Promise { throw new Error('Method not implemented.') } + + public close(): Promise { + throw new Error('Method not implemented.') + } } diff --git a/packages/core/src/agent/helpers.ts b/packages/core/src/agent/helpers.ts index 314011abad..8bce437d96 100644 --- a/packages/core/src/agent/helpers.ts +++ b/packages/core/src/agent/helpers.ts @@ -1,23 +1,26 @@ +import type { Key } from '../crypto' import type { ConnectionRecord } from '../modules/connections' +import type { OutOfBandRecord } from '../modules/oob/repository' import type { OutboundMessage, OutboundServiceMessage } from '../types' import type { AgentMessage } from './AgentMessage' - -import { DidCommService } from '../modules/connections/models/did/service/DidCommService' +import type { ResolvedDidCommService } from './MessageSender' export function createOutboundMessage( connection: ConnectionRecord, - payload: T + payload: T, + outOfBand?: OutOfBandRecord ): OutboundMessage { return { connection, + outOfBand, payload, } } export function createOutboundServiceMessage(options: { payload: T - service: DidCommService - senderKey: string + service: ResolvedDidCommService + senderKey: Key }): OutboundServiceMessage { return options } @@ -25,5 +28,7 @@ export function createOutboundServiceMessage { public message: T public connection?: ConnectionRecord - public senderVerkey?: string - public recipientVerkey?: string + public sessionId?: string + public senderKey?: Key + public recipientKey?: Key public constructor(message: T, context: MessageContextParams = {}) { this.message = message - this.recipientVerkey = context.recipientVerkey - this.senderVerkey = context.senderVerkey + this.recipientKey = context.recipientKey + this.senderKey = context.senderKey this.connection = context.connection + this.sessionId = context.sessionId } /** diff --git a/packages/core/src/crypto/BbsService.ts b/packages/core/src/crypto/BbsService.ts new file mode 100644 index 0000000000..c00e814249 --- /dev/null +++ b/packages/core/src/crypto/BbsService.ts @@ -0,0 +1,151 @@ +import type { CreateKeyOptions } from '../wallet' +import type { BlsKeyPair as _BlsKeyPair } from '@mattrglobal/bbs-signatures' + +import { + bls12381toBbs, + generateBls12381G2KeyPair, + generateBls12381G1KeyPair, + sign, + verify, +} from '@mattrglobal/bbs-signatures' + +import { TypedArrayEncoder } from '../utils/TypedArrayEncoder' +import { Buffer } from '../utils/buffer' +import { WalletError } from '../wallet/error' + +import { KeyType } from './KeyType' + +export interface BlsKeyPair { + publicKeyBase58: string + privateKeyBase58: string + keyType: Extract +} + +interface BbsCreateKeyOptions extends CreateKeyOptions { + keyType: Extract +} + +interface BbsSignOptions { + messages: Buffer | Buffer[] + publicKey: Buffer + privateKey: Buffer +} + +interface BbsVerifyOptions { + publicKey: Buffer + signature: Buffer + messages: Buffer | Buffer[] +} + +export class BbsService { + /** + * Create an instance of a Key class for the following key types: + * - Bls12381g1 + * - Bls12381g2 + * + * @param keyType KeyType The type of key to be created (see above for the accepted types) + * + * @returns A Key class with the public key and key type + * + * @throws {WalletError} When a key could not be created + * @throws {WalletError} When the method is called with an invalid keytype + */ + public static async createKey({ keyType, seed }: BbsCreateKeyOptions): Promise { + // Generate bytes from the seed as required by the bbs-signatures libraries + const seedBytes = seed ? TypedArrayEncoder.fromString(seed) : undefined + + // Temporary keypair holder + let blsKeyPair: Required<_BlsKeyPair> + + switch (keyType) { + case KeyType.Bls12381g1: + // Generate a bls12-381G1 keypair + blsKeyPair = await generateBls12381G1KeyPair(seedBytes) + break + case KeyType.Bls12381g2: + // Generate a bls12-381G2 keypair + blsKeyPair = await generateBls12381G2KeyPair(seedBytes) + break + default: + // additional check. Should never be hit as this function will only be called from a place where + // a key type check already happened. + throw new WalletError(`Cannot create key with the BbsService for key type: ${keyType}`) + } + + return { + keyType, + publicKeyBase58: TypedArrayEncoder.toBase58(blsKeyPair.publicKey), + privateKeyBase58: TypedArrayEncoder.toBase58(blsKeyPair.secretKey), + } + } + + /** + * Sign an arbitrary amount of messages, in byte form, with a keypair + * + * @param messages Buffer[] List of messages in Buffer form + * @param publicKey Buffer Publickey required for the signing process + * @param privateKey Buffer PrivateKey required for the signing process + * + * @returns A Buffer containing the signature of the messages + * + * @throws {WalletError} When there are no supplied messages + */ + public static async sign({ messages, publicKey, privateKey }: BbsSignOptions): Promise { + if (messages.length === 0) throw new WalletError('Unable to create a signature without any messages') + // Check if it is a single message or list and if it is a single message convert it to a list + const normalizedMessages = (TypedArrayEncoder.isTypedArray(messages) ? [messages as Buffer] : messages) as Buffer[] + + // Get the Uint8Array variant of all the messages + const messageBuffers = normalizedMessages.map((m) => Uint8Array.from(m)) + + const bbsKeyPair = await bls12381toBbs({ + keyPair: { publicKey: Uint8Array.from(publicKey), secretKey: Uint8Array.from(privateKey) }, + messageCount: normalizedMessages.length, + }) + + // Sign the messages via the keyPair + const signature = await sign({ + keyPair: bbsKeyPair, + messages: messageBuffers, + }) + + // Convert the Uint8Array signature to a Buffer type + return Buffer.from(signature) + } + + /** + * Verify an arbitrary amount of messages with their signature created with their key pair + * + * @param publicKey Buffer The public key used to sign the messages + * @param messages Buffer[] The messages that have to be verified if they are signed + * @param signature Buffer The signature that has to be verified if it was created with the messages and public key + * + * @returns A boolean whether the signature is create with the public key over the messages + * + * @throws {WalletError} When the message list is empty + * @throws {WalletError} When the verification process failed + */ + public static async verify({ signature, messages, publicKey }: BbsVerifyOptions): Promise { + if (messages.length === 0) throw new WalletError('Unable to create a signature without any messages') + // Check if it is a single message or list and if it is a single message convert it to a list + const normalizedMessages = (TypedArrayEncoder.isTypedArray(messages) ? [messages as Buffer] : messages) as Buffer[] + + // Get the Uint8Array variant of all the messages + const messageBuffers = normalizedMessages.map((m) => Uint8Array.from(m)) + + const bbsKeyPair = await bls12381toBbs({ + keyPair: { publicKey: Uint8Array.from(publicKey) }, + messageCount: normalizedMessages.length, + }) + + // Verify the signature against the messages with their public key + const { verified, error } = await verify({ signature, messages: messageBuffers, publicKey: bbsKeyPair.publicKey }) + + // If the messages could not be verified and an error occured + if (!verified && error) { + throw new WalletError(`Could not verify the signature against the messages: ${error}`) + } + + return verified + } +} diff --git a/packages/core/src/crypto/JwsService.ts b/packages/core/src/crypto/JwsService.ts new file mode 100644 index 0000000000..09caa2cc67 --- /dev/null +++ b/packages/core/src/crypto/JwsService.ts @@ -0,0 +1,130 @@ +import type { Buffer } from '../utils' +import type { Jws, JwsGeneralFormat } from './JwsTypes' + +import { inject, Lifecycle, scoped } from 'tsyringe' + +import { InjectionSymbols } from '../constants' +import { AriesFrameworkError } from '../error' +import { JsonEncoder, TypedArrayEncoder } from '../utils' +import { Wallet } from '../wallet' +import { WalletError } from '../wallet/error' + +import { Key } from './Key' +import { KeyType } from './KeyType' + +// TODO: support more key types, more generic jws format +const JWS_KEY_TYPE = 'OKP' +const JWS_CURVE = 'Ed25519' +const JWS_ALG = 'EdDSA' + +@scoped(Lifecycle.ContainerScoped) +export class JwsService { + private wallet: Wallet + + public constructor(@inject(InjectionSymbols.Wallet) wallet: Wallet) { + this.wallet = wallet + } + + public async createJws({ payload, verkey, header }: CreateJwsOptions): Promise { + const base64Payload = TypedArrayEncoder.toBase64URL(payload) + const base64Protected = JsonEncoder.toBase64URL(this.buildProtected(verkey)) + const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + + const signature = TypedArrayEncoder.toBase64URL( + await this.wallet.sign({ data: TypedArrayEncoder.fromString(`${base64Protected}.${base64Payload}`), key }) + ) + + return { + protected: base64Protected, + signature, + header, + } + } + + /** + * Verify a JWS + */ + public async verifyJws({ jws, payload }: VerifyJwsOptions): Promise { + const base64Payload = TypedArrayEncoder.toBase64URL(payload) + const signatures = 'signatures' in jws ? jws.signatures : [jws] + + if (signatures.length === 0) { + throw new AriesFrameworkError('Unable to verify JWS: No entries in JWS signatures array.') + } + + const signerVerkeys = [] + for (const jws of signatures) { + const protectedJson = JsonEncoder.fromBase64(jws.protected) + + const isValidKeyType = protectedJson?.jwk?.kty === JWS_KEY_TYPE + const isValidCurve = protectedJson?.jwk?.crv === JWS_CURVE + const isValidAlg = protectedJson?.alg === JWS_ALG + + if (!isValidKeyType || !isValidCurve || !isValidAlg) { + throw new AriesFrameworkError('Invalid protected header') + } + + const data = TypedArrayEncoder.fromString(`${jws.protected}.${base64Payload}`) + const signature = TypedArrayEncoder.fromBase64(jws.signature) + + const verkey = TypedArrayEncoder.toBase58(TypedArrayEncoder.fromBase64(protectedJson?.jwk?.x)) + const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + signerVerkeys.push(verkey) + + try { + const isValid = await this.wallet.verify({ key, data, signature }) + + if (!isValid) { + return { + isValid: false, + signerVerkeys: [], + } + } + } catch (error) { + // WalletError probably means signature verification failed. Would be useful to add + // more specific error type in wallet.verify method + if (error instanceof WalletError) { + return { + isValid: false, + signerVerkeys: [], + } + } + + throw error + } + } + + return { isValid: true, signerVerkeys } + } + + /** + * @todo This currently only work with a single alg, key type and curve + * This needs to be extended with other formats in the future + */ + private buildProtected(verkey: string) { + return { + alg: 'EdDSA', + jwk: { + kty: 'OKP', + crv: 'Ed25519', + x: TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromBase58(verkey)), + }, + } + } +} + +export interface CreateJwsOptions { + verkey: string + payload: Buffer + header: Record +} + +export interface VerifyJwsOptions { + jws: Jws + payload: Buffer +} + +export interface VerifyJwsResult { + isValid: boolean + signerVerkeys: string[] +} diff --git a/packages/core/src/crypto/JwsTypes.ts b/packages/core/src/crypto/JwsTypes.ts new file mode 100644 index 0000000000..6f3b65d9eb --- /dev/null +++ b/packages/core/src/crypto/JwsTypes.ts @@ -0,0 +1,11 @@ +export interface JwsGeneralFormat { + header: Record + signature: string + protected: string +} + +export interface JwsFlattenedFormat { + signatures: JwsGeneralFormat[] +} + +export type Jws = JwsGeneralFormat | JwsFlattenedFormat diff --git a/packages/core/src/crypto/Key.ts b/packages/core/src/crypto/Key.ts new file mode 100644 index 0000000000..c5d03507f0 --- /dev/null +++ b/packages/core/src/crypto/Key.ts @@ -0,0 +1,53 @@ +import type { KeyType } from './KeyType' + +import { Buffer, MultiBaseEncoder, TypedArrayEncoder, VarintEncoder } from '../utils' + +import { getKeyTypeByMultiCodecPrefix, getMultiCodecPrefixByKeytype } from './multiCodecKey' + +export class Key { + public readonly publicKey: Buffer + public readonly keyType: KeyType + + public constructor(publicKey: Uint8Array, keyType: KeyType) { + this.publicKey = Buffer.from(publicKey) + this.keyType = keyType + } + + public static fromPublicKey(publicKey: Uint8Array, keyType: KeyType) { + return new Key(Buffer.from(publicKey), keyType) + } + + public static fromPublicKeyBase58(publicKey: string, keyType: KeyType) { + const publicKeyBytes = TypedArrayEncoder.fromBase58(publicKey) + + return Key.fromPublicKey(publicKeyBytes, keyType) + } + + public static fromFingerprint(fingerprint: string) { + const { data } = MultiBaseEncoder.decode(fingerprint) + const [code, byteLength] = VarintEncoder.decode(data) + + const publicKey = Buffer.from(data.slice(byteLength)) + const keyType = getKeyTypeByMultiCodecPrefix(code) + + return new Key(publicKey, keyType) + } + + public get prefixedPublicKey() { + const multiCodecPrefix = getMultiCodecPrefixByKeytype(this.keyType) + + // Create Buffer with length of the prefix bytes, then use varint to fill the prefix bytes + const prefixBytes = VarintEncoder.encode(multiCodecPrefix) + + // Combine prefix with public key + return Buffer.concat([prefixBytes, this.publicKey]) + } + + public get fingerprint() { + return `z${TypedArrayEncoder.toBase58(this.prefixedPublicKey)}` + } + + public get publicKeyBase58() { + return TypedArrayEncoder.toBase58(this.publicKey) + } +} diff --git a/packages/core/src/crypto/KeyType.ts b/packages/core/src/crypto/KeyType.ts new file mode 100644 index 0000000000..858762f670 --- /dev/null +++ b/packages/core/src/crypto/KeyType.ts @@ -0,0 +1,7 @@ +export enum KeyType { + Ed25519 = 'ed25519', + Bls12381g1g2 = 'bls12381g1g2', + Bls12381g1 = 'bls12381g1', + Bls12381g2 = 'bls12381g2', + X25519 = 'x25519', +} diff --git a/packages/core/src/crypto/LdKeyPair.ts b/packages/core/src/crypto/LdKeyPair.ts new file mode 100644 index 0000000000..3a46c47c1a --- /dev/null +++ b/packages/core/src/crypto/LdKeyPair.ts @@ -0,0 +1,51 @@ +import type { VerificationMethod } from '../modules/dids' + +export interface LdKeyPairOptions { + id: string + controller: string +} + +export abstract class LdKeyPair { + public readonly id: string + public readonly controller: string + public abstract type: string + + public constructor(options: LdKeyPairOptions) { + this.id = options.id + this.controller = options.controller + } + + public static async generate(): Promise { + throw new Error('Not implemented') + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public static async from(verificationMethod: VerificationMethod): Promise { + throw new Error('Abstract method from() must be implemented in subclass.') + } + + public export(publicKey = false, privateKey = false) { + if (!publicKey && !privateKey) { + throw new Error('Export requires specifying either "publicKey" or "privateKey".') + } + const key = { + id: this.id, + type: this.type, + controller: this.controller, + } + + return key + } + + public abstract fingerprint(): string + + public abstract verifyFingerprint(fingerprint: string): boolean + + public abstract signer(): { + sign: (data: { data: Uint8Array | Uint8Array[] }) => Promise> + } + + public abstract verifier(): { + verify: (data: { data: Uint8Array | Uint8Array[]; signature: Uint8Array }) => Promise + } +} diff --git a/packages/core/src/crypto/WalletKeyPair.ts b/packages/core/src/crypto/WalletKeyPair.ts new file mode 100644 index 0000000000..bf7873b99c --- /dev/null +++ b/packages/core/src/crypto/WalletKeyPair.ts @@ -0,0 +1,123 @@ +import type { Wallet } from '..' +import type { Key } from './Key' +import type { LdKeyPairOptions } from './LdKeyPair' + +import { VerificationMethod } from '../modules/dids' +import { getKeyDidMappingByVerificationMethod } from '../modules/dids/domain/key-type/keyDidMapping' +import { JsonTransformer } from '../utils' +import { MessageValidator } from '../utils/MessageValidator' +import { Buffer } from '../utils/buffer' + +import { LdKeyPair } from './LdKeyPair' + +interface WalletKeyPairOptions extends LdKeyPairOptions { + wallet: Wallet + key: Key +} + +export function createWalletKeyPairClass(wallet: Wallet) { + return class WalletKeyPair extends LdKeyPair { + public wallet: Wallet + public key: Key + public type: string + + public constructor(options: WalletKeyPairOptions) { + super(options) + this.wallet = options.wallet + this.key = options.key + this.type = options.key.keyType + } + + public static async generate(): Promise { + throw new Error('Not implemented') + } + + public fingerprint(): string { + throw new Error('Method not implemented.') + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public verifyFingerprint(fingerprint: string): boolean { + throw new Error('Method not implemented.') + } + + public static async from(verificationMethod: VerificationMethod): Promise { + const vMethod = JsonTransformer.fromJSON(verificationMethod, VerificationMethod) + await MessageValidator.validate(vMethod) + const { getKeyFromVerificationMethod } = getKeyDidMappingByVerificationMethod(vMethod) + const key = getKeyFromVerificationMethod(vMethod) + + return new WalletKeyPair({ + id: vMethod.id, + controller: vMethod.controller, + wallet: wallet, + key: key, + }) + } + + /** + * This method returns a wrapped wallet.sign method. The method is being wrapped so we can covert between Uint8Array and Buffer. This is to make it compatible with the external signature libraries. + */ + public signer(): { sign: (data: { data: Uint8Array | Uint8Array[] }) => Promise } { + // wrap function for conversion + const wrappedSign = async (data: { data: Uint8Array | Uint8Array[] }): Promise => { + let converted: Buffer | Buffer[] = [] + + // convert uint8array to buffer + if (Array.isArray(data.data)) { + converted = data.data.map((d) => Buffer.from(d)) + } else { + converted = Buffer.from(data.data) + } + + // sign + const result = await wallet.sign({ + data: converted, + key: this.key, + }) + + // convert result buffer to uint8array + return Uint8Array.from(result) + } + + return { + sign: wrappedSign.bind(this), + } + } + + /** + * This method returns a wrapped wallet.verify method. The method is being wrapped so we can covert between Uint8Array and Buffer. This is to make it compatible with the external signature libraries. + */ + public verifier(): { + verify: (data: { data: Uint8Array | Uint8Array[]; signature: Uint8Array }) => Promise + } { + const wrappedVerify = async (data: { + data: Uint8Array | Uint8Array[] + signature: Uint8Array + }): Promise => { + let converted: Buffer | Buffer[] = [] + + // convert uint8array to buffer + if (Array.isArray(data.data)) { + converted = data.data.map((d) => Buffer.from(d)) + } else { + converted = Buffer.from(data.data) + } + + // verify + return wallet.verify({ + data: converted, + signature: Buffer.from(data.signature), + key: this.key, + }) + } + return { + verify: wrappedVerify.bind(this), + } + } + + public get publicKeyBuffer(): Uint8Array { + return new Uint8Array(this.key.publicKey) + } + } +} diff --git a/packages/core/src/crypto/__tests__/JwsService.test.ts b/packages/core/src/crypto/__tests__/JwsService.test.ts new file mode 100644 index 0000000000..87ced7bd95 --- /dev/null +++ b/packages/core/src/crypto/__tests__/JwsService.test.ts @@ -0,0 +1,95 @@ +import type { Wallet } from '@aries-framework/core' + +import { getAgentConfig } from '../../../tests/helpers' +import { DidKey } from '../../modules/dids' +import { Buffer, JsonEncoder } from '../../utils' +import { IndyWallet } from '../../wallet/IndyWallet' +import { JwsService } from '../JwsService' +import { Key } from '../Key' +import { KeyType } from '../KeyType' + +import * as didJwsz6Mkf from './__fixtures__/didJwsz6Mkf' +import * as didJwsz6Mkv from './__fixtures__/didJwsz6Mkv' + +describe('JwsService', () => { + let wallet: Wallet + let jwsService: JwsService + + beforeAll(async () => { + const config = getAgentConfig('JwsService') + wallet = new IndyWallet(config) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(config.walletConfig!) + + jwsService = new JwsService(wallet) + }) + + afterAll(async () => { + await wallet.delete() + }) + + describe('createJws', () => { + it('creates a jws for the payload with the key associated with the verkey', async () => { + const { verkey } = await wallet.createDid({ seed: didJwsz6Mkf.SEED }) + + const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON) + const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + const kid = new DidKey(key).did + + const jws = await jwsService.createJws({ + payload, + verkey, + header: { kid }, + }) + + expect(jws).toEqual(didJwsz6Mkf.JWS_JSON) + }) + }) + + describe('verifyJws', () => { + it('returns true if the jws signature matches the payload', async () => { + const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON) + + const { isValid, signerVerkeys } = await jwsService.verifyJws({ + payload, + jws: didJwsz6Mkf.JWS_JSON, + }) + + expect(isValid).toBe(true) + expect(signerVerkeys).toEqual([didJwsz6Mkf.VERKEY]) + }) + + it('returns all verkeys that signed the jws', async () => { + const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON) + + const { isValid, signerVerkeys } = await jwsService.verifyJws({ + payload, + jws: { signatures: [didJwsz6Mkf.JWS_JSON, didJwsz6Mkv.JWS_JSON] }, + }) + + expect(isValid).toBe(true) + expect(signerVerkeys).toEqual([didJwsz6Mkf.VERKEY, didJwsz6Mkv.VERKEY]) + }) + + it('returns false if the jws signature does not match the payload', async () => { + const payload = JsonEncoder.toBuffer({ ...didJwsz6Mkf.DATA_JSON, did: 'another_did' }) + + const { isValid, signerVerkeys } = await jwsService.verifyJws({ + payload, + jws: didJwsz6Mkf.JWS_JSON, + }) + + expect(isValid).toBe(false) + expect(signerVerkeys).toMatchObject([]) + }) + + it('throws an error if the jws signatures array does not contain a JWS', async () => { + await expect( + jwsService.verifyJws({ + payload: new Buffer([]), + jws: { signatures: [] }, + }) + ).rejects.toThrowError('Unable to verify JWS: No entries in JWS signatures array.') + }) + }) +}) diff --git a/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkf.ts b/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkf.ts new file mode 100644 index 0000000000..8524f12301 --- /dev/null +++ b/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkf.ts @@ -0,0 +1,26 @@ +export const SEED = '00000000000000000000000000000My2' +export const VERKEY = 'kqa2HyagzfMAq42H5f9u3UMwnSBPQx2QfrSyXbUPxMn' + +export const DATA_JSON = { + did: 'did', + did_doc: { + '@context': 'https://w3id.org/did/v1', + service: [ + { + id: 'did:example:123456789abcdefghi#did-communication', + type: 'did-communication', + priority: 0, + recipientKeys: ['someVerkey'], + routingKeys: [], + serviceEndpoint: 'https://agent.example.com/', + }, + ], + }, +} + +export const JWS_JSON = { + header: { kid: 'did:key:z6MkfD6ccYE22Y9pHKtixeczk92MmMi2oJCP6gmNooZVKB9A' }, + protected: + 'eyJhbGciOiJFZERTQSIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IkN6cmtiNjQ1MzdrVUVGRkN5SXI4STgxUWJJRGk2MnNrbU41Rm41LU1zVkUifX0', + signature: 'OsDP4FM8792J9JlessA9IXv4YUYjIGcIAnPPrEJmgxYomMwDoH-h2DMAF5YF2VtsHHyhGN_0HryDjWSEAZdYBQ', +} diff --git a/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkv.ts b/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkv.ts new file mode 100644 index 0000000000..fe31ea8808 --- /dev/null +++ b/packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkv.ts @@ -0,0 +1,28 @@ +export const SEED = '00000000000000000000000000000My1' +export const VERKEY = 'GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa' + +export const DATA_JSON = { + did: 'did', + did_doc: { + '@context': 'https://w3id.org/did/v1', + service: [ + { + id: 'did:example:123456789abcdefghi#did-communication', + type: 'did-communication', + priority: 0, + recipientKeys: ['someVerkey'], + routingKeys: [], + serviceEndpoint: 'https://agent.example.com/', + }, + ], + }, +} + +export const JWS_JSON = { + header: { + kid: 'did:key:z6MkvBpZTRb7tjuUF5AkmhG1JDV928hZbg5KAQJcogvhz9ax', + }, + protected: + 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3ZCcFpUUmI3dGp1VUY1QWttaEcxSkRWOTI4aFpiZzVLQVFKY29ndmh6OWF4IiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiNmNaMmJaS21LaVVpRjlNTEtDVjhJSVlJRXNPTEhzSkc1cUJKOVNyUVlCayIsImtpZCI6ImRpZDprZXk6ejZNa3ZCcFpUUmI3dGp1VUY1QWttaEcxSkRWOTI4aFpiZzVLQVFKY29ndmh6OWF4In19', + signature: 'eA3MPRpSTt5NR8EZkDNb849E9qfrlUm8-StWPA4kMp-qcH7oEc2-1En4fgpz_IWinEbVxCLbmKhWNyaTAuHNAg', +} diff --git a/packages/core/src/crypto/index.ts b/packages/core/src/crypto/index.ts new file mode 100644 index 0000000000..4c598a5a2a --- /dev/null +++ b/packages/core/src/crypto/index.ts @@ -0,0 +1,2 @@ +export { KeyType } from './KeyType' +export { Key } from './Key' diff --git a/packages/core/src/crypto/multiCodecKey.ts b/packages/core/src/crypto/multiCodecKey.ts new file mode 100644 index 0000000000..20d3f4b070 --- /dev/null +++ b/packages/core/src/crypto/multiCodecKey.ts @@ -0,0 +1,31 @@ +import { KeyType } from './KeyType' + +// based on https://github.com/multiformats/multicodec/blob/master/table.csv +const multiCodecPrefixMap: Record = { + 234: KeyType.Bls12381g1, + 235: KeyType.Bls12381g2, + 236: KeyType.X25519, + 237: KeyType.Ed25519, + 238: KeyType.Bls12381g1g2, +} + +export function getKeyTypeByMultiCodecPrefix(multiCodecPrefix: number): KeyType { + const keyType = multiCodecPrefixMap[multiCodecPrefix] + + if (!keyType) { + throw new Error(`Unsupported key type from multicodec code '${multiCodecPrefix}'`) + } + + return keyType +} + +export function getMultiCodecPrefixByKeytype(keyType: KeyType): number { + const codes = Object.keys(multiCodecPrefixMap) + const code = codes.find((key) => multiCodecPrefixMap[key] === keyType) + + if (!code) { + throw new Error(`Could not find multicodec prefix for key type '${keyType}'`) + } + + return Number(code) +} diff --git a/packages/core/src/crypto/signature-suites/JwsLinkedDataSignature.ts b/packages/core/src/crypto/signature-suites/JwsLinkedDataSignature.ts new file mode 100644 index 0000000000..e062d503da --- /dev/null +++ b/packages/core/src/crypto/signature-suites/JwsLinkedDataSignature.ts @@ -0,0 +1,270 @@ +/*! + * Copyright (c) 2020-2021 Digital Bazaar, Inc. All rights reserved. + */ +import type { DocumentLoader, Proof, VerificationMethod } from '../../utils' +import type { LdKeyPair } from '../LdKeyPair' + +import { suites } from '../../../types/jsonld-signatures' +import { AriesFrameworkError } from '../../error' +import { TypedArrayEncoder, JsonEncoder } from '../../utils' + +const LinkedDataSignature = suites.LinkedDataSignature +export interface JwsLinkedDataSignatureOptions { + type: string + algorithm: string + LDKeyClass: typeof LdKeyPair + key?: LdKeyPair + proof: Proof + date: string + contextUrl: string + useNativeCanonize: boolean +} + +export class JwsLinkedDataSignature extends LinkedDataSignature { + /** + * @param options - Options hashmap. + * @param options.type - Provided by subclass. + * @param options.alg - JWS alg provided by subclass. + * @param [options.LDKeyClass] - Provided by subclass or subclass + * overrides `getVerificationMethod`. + * + * Either a `key` OR at least one of `signer`/`verifier` is required. + * + * @param [options.key] - An optional key object (containing an + * `id` property, and either `signer` or `verifier`, depending on the + * intended operation. Useful for when the application is managing keys + * itself (when using a KMS, you never have access to the private key, + * and so should use the `signer` param instead). + * + * Advanced optional parameters and overrides. + * + * @param [options.proof] - A JSON-LD document with options to use + * for the `proof` node. Any other custom fields can be provided here + * using a context different from `security-v2`. + * @param [options.date] - Signing date to use if not passed. + * @param options.contextUrl - JSON-LD context url that corresponds + * to this signature suite. Used for enforcing suite context during the + * `sign()` operation. + * @param [options.useNativeCanonize] - Whether to use a native + * canonize algorithm. + */ + public constructor(options: JwsLinkedDataSignatureOptions) { + super({ + type: options.type, + LDKeyClass: options.LDKeyClass, + contextUrl: options.contextUrl, + key: options.key, + signer: undefined, + verifier: undefined, + proof: options.proof, + date: options.date, + useNativeCanonize: options.useNativeCanonize, + }) + this.alg = options.algorithm + } + + /** + * @param options - Options hashmap. + * @param options.verifyData - The data to sign. + * @param options.proof - A JSON-LD document with options to use + * for the `proof` node. Any other custom fields can be provided here + * using a context different from `security-v2`. + * + * @returns The proof containing the signature value. + */ + public async sign(options: { verifyData: Uint8Array; proof: Proof }) { + if (!(this.signer && typeof this.signer.sign === 'function')) { + throw new Error('A signer API has not been specified.') + } + // JWS header + const header = { + alg: this.alg, + b64: false, + crit: ['b64'], + } + + /* + +-------+-----------------------------------------------------------+ + | "b64" | JWS Signing Input Formula | + +-------+-----------------------------------------------------------+ + | true | ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || | + | | BASE64URL(JWS Payload)) | + | | | + | false | ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.') || | + | | JWS Payload | + +-------+-----------------------------------------------------------+ + */ + + // create JWS data and sign + const encodedHeader = JsonEncoder.toBase64URL(header) + + const data = _createJws({ encodedHeader, verifyData: options.verifyData }) + + const signature = await this.signer.sign({ data }) + + // create detached content signature + const encodedSignature = TypedArrayEncoder.toBase64URL(signature) + options.proof.jws = encodedHeader + '..' + encodedSignature + return options.proof + } + + /** + * @param options - Options hashmap. + * @param options.verifyData - The data to verify. + * @param options.verificationMethod - A verification method. + * @param options.proof - The proof to be verified. + * + * @returns Resolves with the verification result. + */ + public async verifySignature(options: { + verifyData: Uint8Array + verificationMethod: VerificationMethod + proof: Proof + }) { + if (!(options.proof.jws && typeof options.proof.jws === 'string' && options.proof.jws.includes('.'))) { + throw new TypeError('The proof does not include a valid "jws" property.') + } + // add payload into detached content signature + const [encodedHeader /*payload*/, , encodedSignature] = options.proof.jws.split('.') + + let header + try { + header = JsonEncoder.fromBase64(encodedHeader) + } catch (e) { + throw new Error('Could not parse JWS header; ' + e) + } + if (!(header && typeof header === 'object')) { + throw new Error('Invalid JWS header.') + } + + // confirm header matches all expectations + if ( + !( + header.alg === this.alg && + header.b64 === false && + Array.isArray(header.crit) && + header.crit.length === 1 && + header.crit[0] === 'b64' + ) && + Object.keys(header).length === 3 + ) { + throw new Error(`Invalid JWS header parameters for ${this.type}.`) + } + + // do signature verification + const signature = TypedArrayEncoder.fromBase64(encodedSignature) + + const data = _createJws({ encodedHeader, verifyData: options.verifyData }) + + let { verifier } = this + if (!verifier) { + const key = await this.LDKeyClass.from(options.verificationMethod) + verifier = key.verifier() + } + return verifier.verify({ data, signature }) + } + + public async getVerificationMethod(options: { proof: Proof; documentLoader?: DocumentLoader }) { + if (this.key) { + // This happens most often during sign() operations. For verify(), + // the expectation is that the verification method will be fetched + // by the documentLoader (below), not provided as a `key` parameter. + return this.key.export({ publicKey: true }) + } + + let { verificationMethod } = options.proof + + if (typeof verificationMethod === 'object' && verificationMethod !== null) { + verificationMethod = verificationMethod.id + } + + if (!verificationMethod) { + throw new Error('No "verificationMethod" found in proof.') + } + + if (!options.documentLoader) { + throw new AriesFrameworkError( + 'Missing custom document loader. This is required for resolving verification methods.' + ) + } + + const { document } = await options.documentLoader(verificationMethod) + + verificationMethod = typeof document === 'string' ? JSON.parse(document) : document + + await this.assertVerificationMethod(verificationMethod) + return verificationMethod + } + + /** + * Checks whether a given proof exists in the document. + * + * @param options - Options hashmap. + * @param options.proof - A proof. + * @param options.document - A JSON-LD document. + * @param options.purpose - A jsonld-signatures ProofPurpose + * instance (e.g. AssertionProofPurpose, AuthenticationProofPurpose, etc). + * @param options.documentLoader - A secure document loader (it is + * recommended to use one that provides static known documents, instead of + * fetching from the web) for returning contexts, controller documents, + * keys, and other relevant URLs needed for the proof. + * @param [options.expansionMap] - A custom expansion map that is + * passed to the JSON-LD processor; by default a function that will throw + * an error when unmapped properties are detected in the input, use `false` + * to turn this off and allow unmapped properties to be dropped or use a + * custom function. + * + * @returns Whether a match for the proof was found. + */ + public async matchProof(options: { + proof: Proof + document: VerificationMethod + // eslint-disable-next-line @typescript-eslint/no-explicit-any + purpose: any + documentLoader?: DocumentLoader + expansionMap?: () => void + }) { + const proofMatches = await super.matchProof({ + proof: options.proof, + document: options.document, + purpose: options.purpose, + documentLoader: options.documentLoader, + expansionMap: options.expansionMap, + }) + if (!proofMatches) { + return false + } + // NOTE: When subclassing this suite: Extending suites will need to check + + if (!this.key) { + // no key specified, so assume this suite matches and it can be retrieved + return true + } + + const { verificationMethod } = options.proof + + // only match if the key specified matches the one in the proof + if (typeof verificationMethod === 'object') { + return verificationMethod.id === this.key.id + } + return verificationMethod === this.key.id + } +} + +/** + * Creates the bytes ready for signing. + * + * @param {object} options - Options hashmap. + * @param {string} options.encodedHeader - A base64url encoded JWT header. + * @param {Uint8Array} options.verifyData - Payload to sign/verify. + * @returns {Uint8Array} A combined byte array for signing. + */ +function _createJws(options: { encodedHeader: string; verifyData: Uint8Array }): Uint8Array { + const encodedHeaderBytes = TypedArrayEncoder.fromString(options.encodedHeader + '.') + + // concatenate the two uint8arrays + const data = new Uint8Array(encodedHeaderBytes.length + options.verifyData.length) + data.set(encodedHeaderBytes, 0) + data.set(options.verifyData, encodedHeaderBytes.length) + return data +} diff --git a/packages/core/src/crypto/signature-suites/bbs/BbsBlsSignature2020.ts b/packages/core/src/crypto/signature-suites/bbs/BbsBlsSignature2020.ts new file mode 100644 index 0000000000..094d697e57 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/BbsBlsSignature2020.ts @@ -0,0 +1,412 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonObject } from '../../../types' +import type { DocumentLoader, Proof, VerificationMethod } from '../../../utils' +import type { + SignatureSuiteOptions, + CreateProofOptions, + CanonizeOptions, + CreateVerifyDataOptions, + VerifyProofOptions, + VerifySignatureOptions, + SuiteSignOptions, +} from './types' + +import jsonld from '../../../../types/jsonld' +import { suites } from '../../../../types/jsonld-signatures' +import { AriesFrameworkError } from '../../../error' +import { SECURITY_CONTEXT_BBS_URL, SECURITY_CONTEXT_URL } from '../../../modules/vc/constants' +import { w3cDate, TypedArrayEncoder } from '../../../utils' + +/** + * A BBS+ signature suite for use with BLS12-381 key pairs + */ +export class BbsBlsSignature2020 extends suites.LinkedDataProof { + private proof: Record + /** + * Default constructor + * @param options {SignatureSuiteOptions} options for constructing the signature suite + */ + public constructor(options: SignatureSuiteOptions = {}) { + const { verificationMethod, signer, key, date, useNativeCanonize, LDKeyClass } = options + // validate common options + if (verificationMethod !== undefined && typeof verificationMethod !== 'string') { + throw new TypeError('"verificationMethod" must be a URL string.') + } + super({ + type: 'BbsBlsSignature2020', + }) + + this.proof = { + '@context': [ + { + sec: 'https://w3id.org/security#', + proof: { + '@id': 'sec:proof', + '@type': '@id', + '@container': '@graph', + }, + }, + SECURITY_CONTEXT_BBS_URL, + ], + type: 'BbsBlsSignature2020', + } + + this.LDKeyClass = LDKeyClass + this.signer = signer + this.verificationMethod = verificationMethod + this.proofSignatureKey = 'proofValue' + if (key) { + if (verificationMethod === undefined) { + this.verificationMethod = key.id + } + this.key = key + if (typeof key.signer === 'function') { + this.signer = key.signer() + } + if (typeof key.verifier === 'function') { + this.verifier = key.verifier() + } + } + if (date) { + this.date = new Date(date) + + if (isNaN(this.date)) { + throw TypeError(`"date" "${date}" is not a valid date.`) + } + } + this.useNativeCanonize = useNativeCanonize + } + + public ensureSuiteContext({ document }: { document: Record }) { + if ( + document['@context'] === SECURITY_CONTEXT_BBS_URL || + (Array.isArray(document['@context']) && document['@context'].includes(SECURITY_CONTEXT_BBS_URL)) + ) { + // document already includes the required context + return + } + throw new TypeError( + `The document to be signed must contain this suite's @context, ` + `"${SECURITY_CONTEXT_BBS_URL}".` + ) + } + + /** + * @param options {CreateProofOptions} options for creating the proof + * + * @returns {Promise} Resolves with the created proof object. + */ + public async createProof(options: CreateProofOptions): Promise> { + const { document, purpose, documentLoader, expansionMap, compactProof } = options + + let proof: JsonObject + + // use proof JSON-LD document passed to API + if (this.proof) { + proof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { + documentLoader, + expansionMap, + compactToRelative: true, + }) + } else { + // create proof JSON-LD document + proof = { '@context': SECURITY_CONTEXT_URL } + } + + // ensure proof type is set + proof.type = this.type + + // set default `now` date if not given in `proof` or `options` + let date = this.date + if (proof.created === undefined && date === undefined) { + date = new Date() + } + + // ensure date is in string format + if (date !== undefined && typeof date !== 'string') { + date = w3cDate(date) + } + + // add API overrides + if (date !== undefined) { + proof.created = date + } + + if (this.verificationMethod !== undefined) { + proof.verificationMethod = this.verificationMethod + } + + // allow purpose to update the proof; the `proof` is in the + // SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must + // ensure any added fields are also represented in that same `@context` + proof = await purpose.update(proof, { + document, + suite: this, + documentLoader, + expansionMap, + }) + + // create data to sign + const verifyData = ( + await this.createVerifyData({ + document, + proof, + documentLoader, + expansionMap, + compactProof, + }) + ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) + + // sign data + proof = await this.sign({ + verifyData, + document, + proof, + documentLoader, + expansionMap, + }) + delete proof['@context'] + + return proof + } + + /** + * @param options {object} options for verifying the proof. + * + * @returns {Promise<{object}>} Resolves with the verification result. + */ + public async verifyProof(options: VerifyProofOptions): Promise> { + const { proof, document, documentLoader, expansionMap, purpose } = options + + try { + // create data to verify + const verifyData = ( + await this.createVerifyData({ + document, + proof, + documentLoader, + expansionMap, + compactProof: false, + }) + ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) + + // fetch verification method + const verificationMethod = await this.getVerificationMethod({ + proof, + documentLoader, + }) + + // verify signature on data + const verified = await this.verifySignature({ + verifyData, + verificationMethod, + document, + proof, + documentLoader, + expansionMap, + }) + if (!verified) { + throw new Error('Invalid signature.') + } + + // ensure proof was performed for a valid purpose + const { valid, error } = await purpose.validate(proof, { + document, + suite: this, + verificationMethod, + documentLoader, + expansionMap, + }) + if (!valid) { + throw error + } + + return { verified: true } + } catch (error) { + return { verified: false, error } + } + } + + public async canonize(input: Record, options: CanonizeOptions): Promise { + const { documentLoader, expansionMap, skipExpansion } = options + return jsonld.canonize(input, { + algorithm: 'URDNA2015', + format: 'application/n-quads', + documentLoader, + expansionMap, + skipExpansion, + useNative: this.useNativeCanonize, + }) + } + + public async canonizeProof(proof: Record, options: CanonizeOptions): Promise { + const { documentLoader, expansionMap } = options + proof = { ...proof } + delete proof[this.proofSignatureKey] + return this.canonize(proof, { + documentLoader, + expansionMap, + skipExpansion: false, + }) + } + + /** + * @param document {CreateVerifyDataOptions} options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyData(options: CreateVerifyDataOptions): Promise { + const { proof, document, documentLoader, expansionMap } = options + + const proof2 = { ...proof, '@context': document['@context'] } + + const proofStatements = await this.createVerifyProofData(proof2, { + documentLoader, + expansionMap, + }) + const documentStatements = await this.createVerifyDocumentData(document, { + documentLoader, + expansionMap, + }) + + // concatenate c14n proof options and c14n document + return proofStatements.concat(documentStatements) + } + + /** + * @param proof to canonicalize + * @param options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyProofData( + proof: Record, + { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + ): Promise { + const c14nProofOptions = await this.canonizeProof(proof, { + documentLoader, + expansionMap, + }) + + return c14nProofOptions.split('\n').filter((_) => _.length > 0) + } + + /** + * @param document to canonicalize + * @param options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyDocumentData( + document: Record, + { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + ): Promise { + const c14nDocument = await this.canonize(document, { + documentLoader, + expansionMap, + }) + + return c14nDocument.split('\n').filter((_) => _.length > 0) + } + + /** + * @param document {object} to be signed. + * @param proof {object} + * @param documentLoader {function} + * @param expansionMap {function} + */ + public async getVerificationMethod({ + proof, + documentLoader, + }: { + proof: Proof + documentLoader?: DocumentLoader + }): Promise { + let { verificationMethod } = proof + + if (typeof verificationMethod === 'object' && verificationMethod !== null) { + verificationMethod = verificationMethod.id + } + + if (!verificationMethod) { + throw new Error('No "verificationMethod" found in proof.') + } + + if (!documentLoader) { + throw new AriesFrameworkError( + 'Missing custom document loader. This is required for resolving verification methods.' + ) + } + + const { document } = await documentLoader(verificationMethod) + + if (!document) { + throw new Error(`Verification method ${verificationMethod} not found.`) + } + + // ensure verification method has not been revoked + if (document.revoked !== undefined) { + throw new Error('The verification method has been revoked.') + } + + return document as VerificationMethod + } + + /** + * @param options {SuiteSignOptions} Options for signing. + * + * @returns {Promise<{object}>} the proof containing the signature value. + */ + public async sign(options: SuiteSignOptions): Promise { + const { verifyData, proof } = options + + if (!(this.signer && typeof this.signer.sign === 'function')) { + throw new Error('A signer API with sign function has not been specified.') + } + + const proofValue: Uint8Array = await this.signer.sign({ + data: verifyData, + }) + + proof[this.proofSignatureKey] = TypedArrayEncoder.toBase64(proofValue) + + return proof as Proof + } + + /** + * @param verifyData {VerifySignatureOptions} Options to verify the signature. + * + * @returns {Promise} + */ + public async verifySignature(options: VerifySignatureOptions): Promise { + const { verificationMethod, verifyData, proof } = options + let { verifier } = this + + if (!verifier) { + const key = await this.LDKeyClass.from(verificationMethod) + verifier = key.verifier(key, this.alg, this.type) + } + + return await verifier.verify({ + data: verifyData, + signature: new Uint8Array(TypedArrayEncoder.fromBase64(proof[this.proofSignatureKey] as string)), + }) + } + + public static proofType = [ + 'BbsBlsSignature2020', + 'sec:BbsBlsSignature2020', + 'https://w3id.org/security#BbsBlsSignature2020', + ] +} diff --git a/packages/core/src/crypto/signature-suites/bbs/BbsBlsSignatureProof2020.ts b/packages/core/src/crypto/signature-suites/bbs/BbsBlsSignatureProof2020.ts new file mode 100644 index 0000000000..c785986435 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/BbsBlsSignatureProof2020.ts @@ -0,0 +1,422 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonObject } from '../../../types' +import type { DocumentLoader, Proof } from '../../../utils' +import type { DeriveProofOptions, VerifyProofOptions, CreateVerifyDataOptions, CanonizeOptions } from './types' +import type { VerifyProofResult } from './types/VerifyProofResult' + +import { blsCreateProof, blsVerifyProof } from '@mattrglobal/bbs-signatures' +import { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair' +import { randomBytes } from '@stablelib/random' + +import jsonld from '../../../../types/jsonld' +import { suites } from '../../../../types/jsonld-signatures' +import { AriesFrameworkError } from '../../../error' +import { SECURITY_CONTEXT_URL } from '../../../modules/vc/constants' +import { TypedArrayEncoder } from '../../../utils' + +import { BbsBlsSignature2020 } from './BbsBlsSignature2020' + +export class BbsBlsSignatureProof2020 extends suites.LinkedDataProof { + public constructor({ useNativeCanonize, key, LDKeyClass }: Record = {}) { + super({ + type: 'BbsBlsSignatureProof2020', + }) + + this.proof = { + '@context': [ + { + sec: 'https://w3id.org/security#', + proof: { + '@id': 'sec:proof', + '@type': '@id', + '@container': '@graph', + }, + }, + 'https://w3id.org/security/bbs/v1', + ], + type: 'BbsBlsSignatureProof2020', + } + this.mappedDerivedProofType = 'BbsBlsSignature2020' + this.supportedDeriveProofType = BbsBlsSignatureProof2020.supportedDerivedProofType + + this.LDKeyClass = LDKeyClass ?? Bls12381G2KeyPair + this.proofSignatureKey = 'proofValue' + this.key = key + this.useNativeCanonize = useNativeCanonize + } + + /** + * Derive a proof from a proof and reveal document + * + * @param options {object} options for deriving a proof. + * + * @returns {Promise} Resolves with the derived proof object. + */ + public async deriveProof(options: DeriveProofOptions): Promise> { + const { document, proof, revealDocument, documentLoader, expansionMap } = options + let { nonce } = options + + const proofType = proof.type + + if (typeof proofType !== 'string') { + throw new TypeError(`Expected proof.type to be of type 'string', got ${typeof proofType} instead.`) + } + + // Validate that the input proof document has a proof compatible with this suite + if (!BbsBlsSignatureProof2020.supportedDerivedProofType.includes(proofType)) { + throw new TypeError( + `proof document proof incompatible, expected proof types of ${JSON.stringify( + BbsBlsSignatureProof2020.supportedDerivedProofType + )} received ${proof.type}` + ) + } + + const signatureBase58 = proof[this.proofSignatureKey] + + if (typeof signatureBase58 !== 'string') { + throw new TypeError(`Expected signature to be a base58 encoded string, got ${typeof signatureBase58} instead.`) + } + + //Extract the BBS signature from the input proof + const signature = TypedArrayEncoder.fromBase64(signatureBase58) + + //Initialize the BBS signature suite + const suite = new BbsBlsSignature2020() + + //Initialize the derived proof + let derivedProof + if (this.proof) { + // use proof JSON-LD document passed to API + derivedProof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { + documentLoader, + expansionMap, + compactToRelative: false, + }) + } else { + // create proof JSON-LD document + derivedProof = { '@context': SECURITY_CONTEXT_URL } + } + + // ensure proof type is set + derivedProof.type = this.type + + // Get the input document statements + const documentStatements = await suite.createVerifyDocumentData(document, { + documentLoader, + expansionMap, + }) + + // Get the proof statements + const proofStatements = await suite.createVerifyProofData(proof, { + documentLoader, + expansionMap, + }) + + // Transform any blank node identifiers for the input + // document statements into actual node identifiers + // e.g _:c14n0 => urn:bnid:_:c14n0 + const transformedInputDocumentStatements = documentStatements.map((element) => + element.replace(/(_:c14n[0-9]+)/g, '') + ) + + //Transform the resulting RDF statements back into JSON-LD + const compactInputProofDocument = await jsonld.fromRDF(transformedInputDocumentStatements.join('\n')) + + // Frame the result to create the reveal document result + const revealDocumentResult = await jsonld.frame(compactInputProofDocument, revealDocument, { documentLoader }) + + // Canonicalize the resulting reveal document + const revealDocumentStatements = await suite.createVerifyDocumentData(revealDocumentResult, { + documentLoader, + expansionMap, + }) + + //Get the indicies of the revealed statements from the transformed input document offset + //by the number of proof statements + const numberOfProofStatements = proofStatements.length + + //Always reveal all the statements associated to the original proof + //these are always the first statements in the normalized form + const proofRevealIndicies = Array.from(Array(numberOfProofStatements).keys()) + + //Reveal the statements indicated from the reveal document + const documentRevealIndicies = revealDocumentStatements.map( + (key) => transformedInputDocumentStatements.indexOf(key) + numberOfProofStatements + ) + + // Check there is not a mismatch + if (documentRevealIndicies.length !== revealDocumentStatements.length) { + throw new Error('Some statements in the reveal document not found in original proof') + } + + // Combine all indicies to get the resulting list of revealed indicies + const revealIndicies = proofRevealIndicies.concat(documentRevealIndicies) + + // Create a nonce if one is not supplied + if (!nonce) { + nonce = randomBytes(50) + } + + // Set the nonce on the derived proof + // derivedProof.nonce = Buffer.from(nonce).toString('base64') + derivedProof.nonce = TypedArrayEncoder.toBase64(nonce) + + //Combine all the input statements that + //were originally signed to generate the proof + const allInputStatements: Uint8Array[] = proofStatements + .concat(documentStatements) + .map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) + + // Fetch the verification method + const verificationMethod = await this.getVerificationMethod({ + proof, + documentLoader, + }) + + // Construct a key pair class from the returned verification method + const key = verificationMethod.publicKeyJwk + ? await this.LDKeyClass.fromJwk(verificationMethod) + : await this.LDKeyClass.from(verificationMethod) + + // Compute the proof + const outputProof = await blsCreateProof({ + signature, + publicKey: key.publicKeyBuffer, + messages: allInputStatements, + nonce, + revealed: revealIndicies, + }) + + // Set the proof value on the derived proof + derivedProof.proofValue = TypedArrayEncoder.toBase64(outputProof) + + // Set the relevant proof elements on the derived proof from the input proof + derivedProof.verificationMethod = proof.verificationMethod + derivedProof.proofPurpose = proof.proofPurpose + derivedProof.created = proof.created + + return { + document: { ...revealDocumentResult }, + proof: derivedProof, + } + } + + /** + * @param options {object} options for verifying the proof. + * + * @returns {Promise<{object}>} Resolves with the verification result. + */ + public async verifyProof(options: VerifyProofOptions): Promise { + const { document, documentLoader, expansionMap, purpose } = options + const { proof } = options + + try { + proof.type = this.mappedDerivedProofType + + const proofIncludingDocumentContext = { ...proof, '@context': document['@context'] } + + // Get the proof statements + const proofStatements = await this.createVerifyProofData(proofIncludingDocumentContext, { + documentLoader, + expansionMap, + }) + + // Get the document statements + const documentStatements = await this.createVerifyProofData(document, { + documentLoader, + expansionMap, + }) + + // Transform the blank node identifier placeholders for the document statements + // back into actual blank node identifiers + const transformedDocumentStatements = documentStatements.map((element) => + element.replace(//g, '$1') + ) + + // Combine all the statements to be verified + const statementsToVerify: Uint8Array[] = proofStatements + .concat(transformedDocumentStatements) + .map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) + + // Fetch the verification method + const verificationMethod = await this.getVerificationMethod({ + proof, + documentLoader, + }) + + // Construct a key pair class from the returned verification method + const key = verificationMethod.publicKeyJwk + ? await this.LDKeyClass.fromJwk(verificationMethod) + : await this.LDKeyClass.from(verificationMethod) + + const proofValue = proof.proofValue + + if (typeof proofValue !== 'string') { + throw new AriesFrameworkError(`Expected proof.proofValue to be of type 'string', got ${typeof proof}`) + } + + // Verify the proof + const verified = await blsVerifyProof({ + proof: TypedArrayEncoder.fromBase64(proofValue), + publicKey: key.publicKeyBuffer, + messages: statementsToVerify, + nonce: TypedArrayEncoder.fromBase64(proof.nonce as string), + }) + + // Ensure proof was performed for a valid purpose + const { valid, error } = await purpose.validate(proof, { + document, + suite: this, + verificationMethod, + documentLoader, + expansionMap, + }) + if (!valid) { + throw error + } + + return verified + } catch (error) { + return { verified: false, error } + } + } + + public async canonize(input: JsonObject, options: CanonizeOptions): Promise { + const { documentLoader, expansionMap, skipExpansion } = options + return jsonld.canonize(input, { + algorithm: 'URDNA2015', + format: 'application/n-quads', + documentLoader, + expansionMap, + skipExpansion, + useNative: this.useNativeCanonize, + }) + } + + public async canonizeProof(proof: JsonObject, options: CanonizeOptions): Promise { + const { documentLoader, expansionMap } = options + proof = { ...proof } + + delete proof.nonce + delete proof.proofValue + + return this.canonize(proof, { + documentLoader, + expansionMap, + skipExpansion: false, + }) + } + + /** + * @param document {CreateVerifyDataOptions} options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyData(options: CreateVerifyDataOptions): Promise { + const { proof, document, documentLoader, expansionMap } = options + + const proofStatements = await this.createVerifyProofData(proof, { + documentLoader, + expansionMap, + }) + const documentStatements = await this.createVerifyDocumentData(document, { + documentLoader, + expansionMap, + }) + + // concatenate c14n proof options and c14n document + return proofStatements.concat(documentStatements) + } + + /** + * @param proof to canonicalize + * @param options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyProofData( + proof: JsonObject, + { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + ): Promise { + const c14nProofOptions = await this.canonizeProof(proof, { + documentLoader, + expansionMap, + }) + + return c14nProofOptions.split('\n').filter((_) => _.length > 0) + } + + /** + * @param document to canonicalize + * @param options to create verify data + * + * @returns {Promise<{string[]>}. + */ + public async createVerifyDocumentData( + document: JsonObject, + { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + ): Promise { + const c14nDocument = await this.canonize(document, { + documentLoader, + expansionMap, + }) + + return c14nDocument.split('\n').filter((_) => _.length > 0) + } + + public async getVerificationMethod(options: { proof: Proof; documentLoader?: DocumentLoader }) { + if (this.key) { + // This happens most often during sign() operations. For verify(), + // the expectation is that the verification method will be fetched + // by the documentLoader (below), not provided as a `key` parameter. + return this.key.export({ publicKey: true }) + } + + let { verificationMethod } = options.proof + + if (typeof verificationMethod === 'object' && verificationMethod !== null) { + verificationMethod = verificationMethod.id + } + + if (!verificationMethod) { + throw new Error('No "verificationMethod" found in proof.') + } + + if (!options.documentLoader) { + throw new AriesFrameworkError( + 'Missing custom document loader. This is required for resolving verification methods.' + ) + } + + const { document } = await options.documentLoader(verificationMethod) + + verificationMethod = typeof document === 'string' ? JSON.parse(document) : document + + // await this.assertVerificationMethod(verificationMethod) + return verificationMethod + } + + public static proofType = [ + 'BbsBlsSignatureProof2020', + 'sec:BbsBlsSignatureProof2020', + 'https://w3id.org/security#BbsBlsSignatureProof2020', + ] + + public static supportedDerivedProofType = [ + 'BbsBlsSignature2020', + 'sec:BbsBlsSignature2020', + 'https://w3id.org/security#BbsBlsSignature2020', + ] +} diff --git a/packages/core/src/crypto/signature-suites/bbs/deriveProof.ts b/packages/core/src/crypto/signature-suites/bbs/deriveProof.ts new file mode 100644 index 0000000000..012b15d323 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/deriveProof.ts @@ -0,0 +1,129 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonObject } from '../../../types' + +import jsonld from '../../../../types/jsonld' +import { SECURITY_PROOF_URL } from '../../../modules/vc/constants' +import { W3cVerifiableCredential } from '../../../modules/vc/models' +import { JsonTransformer, getProofs, getTypeInfo } from '../../../utils' + +/** + * Derives a proof from a document featuring a supported linked data proof + * + * NOTE - This is a temporary API extending JSON-LD signatures + * + * @param proofDocument A document featuring a linked data proof capable of proof derivation + * @param revealDocument A document of the form of a JSON-LD frame describing the terms to selectively derive from the proof document + * @param options Options for proof derivation + */ +export const deriveProof = async ( + proofDocument: JsonObject, + revealDocument: JsonObject, + { suite, skipProofCompaction, documentLoader, expansionMap, nonce }: any +): Promise => { + if (!suite) { + throw new TypeError('"options.suite" is required.') + } + + if (Array.isArray(proofDocument)) { + throw new TypeError('proofDocument should be an object not an array.') + } + + const { proofs, document } = await getProofs({ + document: proofDocument, + proofType: suite.supportedDeriveProofType, + documentLoader, + expansionMap, + }) + + if (proofs.length === 0) { + throw new Error(`There were not any proofs provided that can be used to derive a proof with this suite.`) + } + let derivedProof + + derivedProof = await suite.deriveProof({ + document, + proof: proofs[0], + revealDocument, + documentLoader, + expansionMap, + nonce, + }) + + if (proofs.length > 1) { + // convert the proof property value from object ot array of objects + derivedProof = { ...derivedProof, proof: [derivedProof.proof] } + + // drop the first proof because it's already been processed + proofs.splice(0, 1) + + // add all the additional proofs to the derivedProof document + for (const proof of proofs) { + const additionalDerivedProofValue = await suite.deriveProof({ + document, + proof, + revealDocument, + documentLoader, + expansionMap, + }) + derivedProof.proof.push(additionalDerivedProofValue.proof) + } + } + + if (!skipProofCompaction) { + /* eslint-disable prefer-const */ + let expandedProof: Record = { + [SECURITY_PROOF_URL]: { + '@graph': derivedProof.proof, + }, + } + + // account for type-scoped `proof` definition by getting document types + const { types, alias } = await getTypeInfo(derivedProof.document, { + documentLoader, + expansionMap, + }) + + expandedProof['@type'] = types + + const ctx = jsonld.getValues(derivedProof.document, '@context') + + const compactProof = await jsonld.compact(expandedProof, ctx, { + documentLoader, + expansionMap, + compactToRelative: false, + }) + + delete compactProof[alias] + delete compactProof['@context'] + + /** + * removes the @included tag when multiple proofs exist because the + * @included tag messes up the canonicalized bytes leading to a bad + * signature that won't verify. + **/ + if (compactProof.proof?.['@included']) { + compactProof.proof = compactProof.proof['@included'] + } + + // add proof to document + const key = Object.keys(compactProof)[0] + jsonld.addValue(derivedProof.document, key, compactProof[key]) + } else { + delete derivedProof.proof['@context'] + jsonld.addValue(derivedProof.document, 'proof', derivedProof.proof) + } + + return JsonTransformer.fromJSON(derivedProof.document, W3cVerifiableCredential) +} diff --git a/packages/core/src/crypto/signature-suites/bbs/index.ts b/packages/core/src/crypto/signature-suites/bbs/index.ts new file mode 100644 index 0000000000..47275d0d9a --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair' +export { BbsBlsSignature2020 } from './BbsBlsSignature2020' +export { BbsBlsSignatureProof2020 } from './BbsBlsSignatureProof2020' +export * from './types' + +export { deriveProof } from './deriveProof' diff --git a/packages/core/src/crypto/signature-suites/bbs/types/CanonizeOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/CanonizeOptions.ts new file mode 100644 index 0000000000..856baecbde --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/CanonizeOptions.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DocumentLoader } from '../../../../utils' + +/** + * Options for canonizing a document + */ +export interface CanonizeOptions { + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader + /** + * Optional expansion map + */ + // eslint-disable-next-line + expansionMap?: () => void + /** + * Indicates whether to skip expansion during canonization + */ + readonly skipExpansion?: boolean +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/CreateProofOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/CreateProofOptions.ts new file mode 100644 index 0000000000..60e06c0185 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/CreateProofOptions.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ProofPurpose } from '../../../../modules/vc/proof-purposes/ProofPurpose' +import type { JsonObject } from '../../../../types' +import type { DocumentLoader } from '../../../../utils' + +/** + * Options for creating a proof + */ +export interface CreateProofOptions { + /** + * Document to create the proof for + */ + readonly document: JsonObject + /** + * The proof purpose to specify for the generated proof + */ + readonly purpose: ProofPurpose + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader + /** + * Optional expansion map + */ + expansionMap?: () => void + /** + * Indicates whether to compact the resulting proof + */ + readonly compactProof: boolean +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/CreateVerifyDataOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/CreateVerifyDataOptions.ts new file mode 100644 index 0000000000..ba493b44a2 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/CreateVerifyDataOptions.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonObject } from '../../../../types' +import type { DocumentLoader } from '../../../../utils' + +/** + * Options for creating a proof + */ +export interface CreateVerifyDataOptions { + /** + * Document to create the proof for + */ + readonly document: JsonObject + /** + * The proof + */ + readonly proof: JsonObject + /** + * Optional custom document loader + */ + + documentLoader?: DocumentLoader + /** + * Optional expansion map + */ + + expansionMap?: () => void + /** + * Indicates whether to compact the proof + */ + readonly compactProof: boolean +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/DeriveProofOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/DeriveProofOptions.ts new file mode 100644 index 0000000000..68a1322e5f --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/DeriveProofOptions.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonObject } from '../../../../types' +import type { DocumentLoader, Proof } from '../../../../utils' + +/** + * Options for creating a proof + */ +export interface DeriveProofOptions { + /** + * Document outlining what statements to reveal + */ + readonly revealDocument: JsonObject + /** + * The document featuring the proof to derive from + */ + readonly document: JsonObject + /** + * The proof for the document + */ + readonly proof: Proof + /** + * Optional custom document loader + */ + // eslint-disable-next-line + documentLoader?: DocumentLoader + /** + * Optional expansion map + */ + // eslint-disable-next-line + expansionMap?: () => void + /** + * Nonce to include in the derived proof + */ + readonly nonce?: Uint8Array + /** + * Indicates whether to compact the resulting proof + */ + readonly skipProofCompaction?: boolean +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/DidDocumentPublicKey.ts b/packages/core/src/crypto/signature-suites/bbs/types/DidDocumentPublicKey.ts new file mode 100644 index 0000000000..d8a7476e1f --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/DidDocumentPublicKey.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { PublicJsonWebKey } from './JsonWebKey' + +/** + * Interface for the public key definition entry in a DID Document. + * @see https://w3c-ccg.github.io/did-spec/#public-keys + */ +export interface DidDocumentPublicKey { + /** + * Fully qualified identifier of this public key, e.g. did:example:entity.id#keys-1 + */ + readonly id: string + + /** + * The type of this public key, as defined in: https://w3c-ccg.github.io/ld-cryptosuite-registry/ + */ + readonly type: string + + /** + * The DID of the controller of this key. + */ + readonly controller?: string + + /** + * The value of the public key in Base58 format. Only one value field will be present. + */ + readonly publicKeyBase58?: string + + /** + * Public key in JWK format. + * @see https://w3c-ccg.github.io/did-spec/#public-keys + */ + readonly publicKeyJwk?: PublicJsonWebKey + + /** + * Public key in HEX format. + * @see https://w3c-ccg.github.io/did-spec/#public-keys + */ + readonly publicKeyHex?: string +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/GetProofsOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/GetProofsOptions.ts new file mode 100644 index 0000000000..5dae685de7 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/GetProofsOptions.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonObject } from '../../../../types' +import type { DocumentLoader } from '../../../../utils' + +/** + * Options for getting a proof from a JSON-LD document + */ +export interface GetProofsOptions { + /** + * The JSON-LD document to extract the proofs from. + */ + readonly document: JsonObject + /** + * Optional the proof type(s) to filter the returned proofs by + */ + readonly proofType?: string | readonly string[] + /** + * Optional custom document loader + */ + documentLoader?(): DocumentLoader + /** + * Optional expansion map + */ + expansionMap?(): () => void + /** + * Optional property to indicate whether to skip compacting the resulting proof + */ + readonly skipProofCompaction?: boolean +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/GetProofsResult.ts b/packages/core/src/crypto/signature-suites/bbs/types/GetProofsResult.ts new file mode 100644 index 0000000000..d96eb8b814 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/GetProofsResult.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonArray, JsonObject } from '../../../../types' + +/** + * Result for getting proofs from a JSON-LD document + */ +export interface GetProofsResult { + /** + * The JSON-LD document with the linked data proofs removed. + */ + document: JsonObject + /** + * The list of proofs that matched the requested type. + */ + proofs: JsonArray +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/GetTypeOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/GetTypeOptions.ts new file mode 100644 index 0000000000..5dd396da4b --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/GetTypeOptions.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DocumentLoader } from '../../../../utils' + +/** + * Options for getting the type from a JSON-LD document + */ +export interface GetTypeOptions { + /** + * Optional custom document loader + */ + // eslint-disable-next-line + documentLoader?: DocumentLoader + /** + * Optional expansion map + */ + // eslint-disable-next-line + expansionMap?: () => void +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/JsonWebKey.ts b/packages/core/src/crypto/signature-suites/bbs/types/JsonWebKey.ts new file mode 100644 index 0000000000..a027778879 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/JsonWebKey.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum JwkKty { + OctetKeyPair = 'OKP', + EC = 'EC', + RSA = 'RSA', +} + +export interface JwkEc { + readonly kty: JwkKty.EC + readonly crv: string + readonly d?: string + readonly x?: string + readonly y?: string + readonly kid?: string +} + +export interface JwkOctetKeyPair { + readonly kty: JwkKty.OctetKeyPair + readonly crv: string + readonly d?: string + readonly x?: string + readonly y?: string + readonly kid?: string +} + +export interface JwkRsa { + readonly kty: JwkKty.RSA + readonly e: string + readonly n: string +} + +export interface JwkRsaPrivate extends JwkRsa { + readonly d: string + readonly p: string + readonly q: string + readonly dp: string + readonly dq: string + readonly qi: string +} +export type JsonWebKey = JwkOctetKeyPair | JwkEc | JwkRsa | JwkRsaPrivate +export type PublicJsonWebKey = JwkOctetKeyPair | JwkEc | JwkRsa diff --git a/packages/core/src/crypto/signature-suites/bbs/types/KeyPairOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/KeyPairOptions.ts new file mode 100644 index 0000000000..624029cd9c --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/KeyPairOptions.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Options for constructing a key pair + */ +export interface KeyPairOptions { + /** + * The key id + */ + readonly id?: string + /** + * The key controller + */ + readonly controller?: string + /** + * Base58 encoding of the private key + */ + readonly privateKeyBase58?: string + /** + * Base58 encoding of the public key + */ + readonly publicKeyBase58: string +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/KeyPairSigner.ts b/packages/core/src/crypto/signature-suites/bbs/types/KeyPairSigner.ts new file mode 100644 index 0000000000..2aaa37f7cf --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/KeyPairSigner.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Key pair signer + */ +export interface KeyPairSigner { + /** + * Signer function + */ + readonly sign: (options: KeyPairSignerOptions) => Promise +} + +/** + * Key pair signer options + */ +export interface KeyPairSignerOptions { + readonly data: Uint8Array | Uint8Array[] +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/KeyPairVerifier.ts b/packages/core/src/crypto/signature-suites/bbs/types/KeyPairVerifier.ts new file mode 100644 index 0000000000..ed89f3bffe --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/KeyPairVerifier.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Key pair verifier + */ +export interface KeyPairVerifier { + /** + * Key pair verify function + */ + readonly verify: (options: KeyPairVerifierOptions) => Promise +} + +/** + * Key pair verifier options + */ +export interface KeyPairVerifierOptions { + readonly data: Uint8Array | Uint8Array[] + readonly signature: Uint8Array +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/SignatureSuiteOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/SignatureSuiteOptions.ts new file mode 100644 index 0000000000..d3f2ba95ba --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/SignatureSuiteOptions.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonArray } from '../../../../types' +import type { LdKeyPair } from '../../../LdKeyPair' +import type { KeyPairSigner } from './KeyPairSigner' +import type { Bls12381G2KeyPair } from '@mattrglobal/bls12381-key-pair' + +/** + * Options for constructing a signature suite + */ +export interface SignatureSuiteOptions { + /** + * An optional signer interface for handling the sign operation + */ + readonly signer?: KeyPairSigner + /** + * The key pair used to generate the proof + */ + readonly key?: Bls12381G2KeyPair + /** + * A key id URL to the paired public key used for verifying the proof + */ + readonly verificationMethod?: string + /** + * The `created` date to report in generated proofs + */ + readonly date?: string | Date + /** + * Indicates whether to use the native implementation + * of RDF Dataset Normalization + */ + readonly useNativeCanonize?: boolean + /** + * Additional proof elements + */ + readonly proof?: JsonArray + /** + * Linked Data Key class implementation + */ + readonly LDKeyClass?: LdKeyPair +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/SuiteSignOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/SuiteSignOptions.ts new file mode 100644 index 0000000000..9bed5d5644 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/SuiteSignOptions.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonObject } from '../../../../types' +import type { DocumentLoader } from '../../../../utils' + +/** + * Options for signing using a signature suite + */ +export interface SuiteSignOptions { + /** + * Input document to sign + */ + readonly document: JsonObject + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader + /** + * Optional expansion map + */ + expansionMap?: () => void + /** + * The array of statements to sign + */ + readonly verifyData: readonly Uint8Array[] + /** + * The proof + */ + readonly proof: JsonObject +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/VerifyProofOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/VerifyProofOptions.ts new file mode 100644 index 0000000000..ba3538e7d4 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/VerifyProofOptions.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ProofPurpose } from '../../../../modules/vc/proof-purposes/ProofPurpose' +import type { JsonObject } from '../../../../types' +import type { DocumentLoader, Proof } from '../../../../utils' + +/** + * Options for verifying a proof + */ +export interface VerifyProofOptions { + /** + * The proof + */ + readonly proof: Proof + /** + * The document + */ + readonly document: JsonObject + /** + * The proof purpose to specify for the generated proof + */ + readonly purpose: ProofPurpose + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader + /** + * Optional expansion map + */ + expansionMap?: () => void +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/VerifyProofResult.ts b/packages/core/src/crypto/signature-suites/bbs/types/VerifyProofResult.ts new file mode 100644 index 0000000000..96996d006d --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/VerifyProofResult.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Result of calling verify proof + */ +export interface VerifyProofResult { + /** + * A boolean indicating if the verification was successful + */ + readonly verified: boolean + /** + * A string representing the error if the verification failed + */ + readonly error?: unknown +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/VerifySignatureOptions.ts b/packages/core/src/crypto/signature-suites/bbs/types/VerifySignatureOptions.ts new file mode 100644 index 0000000000..02eb2c54b1 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/VerifySignatureOptions.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JsonObject } from '../../../../types' +import type { DocumentLoader, Proof, VerificationMethod } from '../../../../utils' + +/** + * Options for verifying a signature + */ +export interface VerifySignatureOptions { + /** + * Document to verify + */ + readonly document: JsonObject + /** + * Array of statements to verify + */ + readonly verifyData: Uint8Array[] + /** + * Verification method to verify the signature against + */ + readonly verificationMethod: VerificationMethod + /** + * Proof to verify + */ + readonly proof: Proof + /** + * Optional custom document loader + */ + documentLoader?: DocumentLoader + /** + * Optional expansion map + */ + expansionMap?: () => void +} diff --git a/packages/core/src/crypto/signature-suites/bbs/types/index.ts b/packages/core/src/crypto/signature-suites/bbs/types/index.ts new file mode 100644 index 0000000000..f3436316be --- /dev/null +++ b/packages/core/src/crypto/signature-suites/bbs/types/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 - MATTR Limited + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { KeyPairOptions } from './KeyPairOptions' +export { KeyPairSigner } from './KeyPairSigner' +export { KeyPairVerifier } from './KeyPairVerifier' +export { SignatureSuiteOptions } from './SignatureSuiteOptions' +export { CreateProofOptions } from './CreateProofOptions' +export { VerifyProofOptions } from './VerifyProofOptions' +export { CanonizeOptions } from './CanonizeOptions' +export { CreateVerifyDataOptions } from './CreateVerifyDataOptions' +export { VerifySignatureOptions } from './VerifySignatureOptions' +export { SuiteSignOptions } from './SuiteSignOptions' +export { DeriveProofOptions } from './DeriveProofOptions' +export { DidDocumentPublicKey } from './DidDocumentPublicKey' +export { GetProofsOptions } from './GetProofsOptions' +export { GetProofsResult } from './GetProofsResult' +export { GetTypeOptions } from './GetTypeOptions' diff --git a/packages/core/src/crypto/signature-suites/ed25519/Ed25519Signature2018.ts b/packages/core/src/crypto/signature-suites/ed25519/Ed25519Signature2018.ts new file mode 100644 index 0000000000..d32f747056 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/ed25519/Ed25519Signature2018.ts @@ -0,0 +1,223 @@ +import type { DocumentLoader, JsonLdDoc, Proof, VerificationMethod } from '../../../utils' +import type { JwsLinkedDataSignatureOptions } from '../JwsLinkedDataSignature' + +import jsonld from '../../../../types/jsonld' +import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_URL } from '../../../modules/vc/constants' +import { TypedArrayEncoder, MultiBaseEncoder, _includesContext } from '../../../utils' +import { JwsLinkedDataSignature } from '../JwsLinkedDataSignature' + +import { ED25519_SUITE_CONTEXT_URL_2018, ED25519_SUITE_CONTEXT_URL_2020 } from './constants' +import { ed25519Signature2018Context } from './context' + +type Ed25519Signature2018Options = Pick< + JwsLinkedDataSignatureOptions, + 'key' | 'proof' | 'date' | 'useNativeCanonize' | 'LDKeyClass' +> + +export class Ed25519Signature2018 extends JwsLinkedDataSignature { + public static CONTEXT_URL = ED25519_SUITE_CONTEXT_URL_2018 + public static CONTEXT = ed25519Signature2018Context.get(ED25519_SUITE_CONTEXT_URL_2018) + + /** + * @param {object} options - Options hashmap. + * + * Either a `key` OR at least one of `signer`/`verifier` is required. + * + * @param {object} [options.key] - An optional key object (containing an + * `id` property, and either `signer` or `verifier`, depending on the + * intended operation. Useful for when the application is managing keys + * itself (when using a KMS, you never have access to the private key, + * and so should use the `signer` param instead). + * @param {Function} [options.signer] - Signer function that returns an + * object with an async sign() method. This is useful when interfacing + * with a KMS (since you don't get access to the private key and its + * `signer()`, the KMS client gives you only the signer function to use). + * @param {Function} [options.verifier] - Verifier function that returns + * an object with an async `verify()` method. Useful when working with a + * KMS-provided verifier function. + * + * Advanced optional parameters and overrides. + * + * @param {object} [options.proof] - A JSON-LD document with options to use + * for the `proof` node. Any other custom fields can be provided here + * using a context different from security-v2). + * @param {string|Date} [options.date] - Signing date to use if not passed. + * @param {boolean} [options.useNativeCanonize] - Whether to use a native + * canonize algorithm. + */ + public constructor(options: Ed25519Signature2018Options) { + super({ + type: 'Ed25519Signature2018', + algorithm: 'EdDSA', + LDKeyClass: options.LDKeyClass, + contextUrl: ED25519_SUITE_CONTEXT_URL_2018, + key: options.key, + proof: options.proof, + date: options.date, + useNativeCanonize: options.useNativeCanonize, + }) + this.requiredKeyType = 'Ed25519VerificationKey2018' + } + + public async assertVerificationMethod(document: JsonLdDoc) { + if (!_includesCompatibleContext({ document: document })) { + // For DID Documents, since keys do not have their own contexts, + // the suite context is usually provided by the documentLoader logic + throw new TypeError(`The verification method (key) must contain "${this.contextUrl}".`) + } + + if (!(_isEd2018Key(document) || _isEd2020Key(document))) { + throw new Error(`Invalid key type. Key type must be "${this.requiredKeyType}".`) + } + + // ensure verification method has not been revoked + if (document.revoked !== undefined) { + throw new Error('The verification method has been revoked.') + } + } + + public async getVerificationMethod(options: { proof: Proof; documentLoader?: DocumentLoader }) { + let verificationMethod = await super.getVerificationMethod({ + proof: options.proof, + documentLoader: options.documentLoader, + }) + + // convert Ed25519VerificationKey2020 to Ed25519VerificationKey2018 + if (_isEd2020Key(verificationMethod)) { + // -- convert multibase to base58 -- + const publicKeyBuffer = MultiBaseEncoder.decode(verificationMethod.publicKeyMultibase) + + // -- update context -- + // remove 2020 context + const context2020Index = verificationMethod['@context'].indexOf(ED25519_SUITE_CONTEXT_URL_2020) + verificationMethod['@context'].splice(context2020Index, 1) + + // add 2018 context + verificationMethod['@context'].push(ED25519_SUITE_CONTEXT_URL_2018) + + // -- update type + verificationMethod.type = 'Ed25519VerificationKey2018' + + verificationMethod = { + ...verificationMethod, + publicKeyMultibase: undefined, + publicKeyBase58: TypedArrayEncoder.toBase58(publicKeyBuffer.data), + } + } + + return verificationMethod + } + + /** + * Ensures the document to be signed contains the required signature suite + * specific `@context`, by either adding it (if `addSuiteContext` is true), + * or throwing an error if it's missing. + * + * @override + * + * @param {object} options - Options hashmap. + * @param {object} options.document - JSON-LD document to be signed. + * @param {boolean} options.addSuiteContext - Add suite context? + */ + public ensureSuiteContext(options: { document: JsonLdDoc; addSuiteContext: boolean }) { + if (_includesCompatibleContext({ document: options.document })) { + return + } + + super.ensureSuiteContext({ document: options.document, addSuiteContext: options.addSuiteContext }) + } + + /** + * Checks whether a given proof exists in the document. + * + * @override + * + * @param {object} options - Options hashmap. + * @param {object} options.proof - A proof. + * @param {object} options.document - A JSON-LD document. + * @param {object} options.purpose - A jsonld-signatures ProofPurpose + * instance (e.g. AssertionProofPurpose, AuthenticationProofPurpose, etc). + * @param {Function} options.documentLoader - A secure document loader (it is + * recommended to use one that provides static known documents, instead of + * fetching from the web) for returning contexts, controller documents, + * keys, and other relevant URLs needed for the proof. + * @param {Function} [options.expansionMap] - A custom expansion map that is + * passed to the JSON-LD processor; by default a function that will throw + * an error when unmapped properties are detected in the input, use `false` + * to turn this off and allow unmapped properties to be dropped or use a + * custom function. + * + * @returns {Promise} Whether a match for the proof was found. + */ + public async matchProof(options: { + proof: Proof + document: VerificationMethod + // eslint-disable-next-line @typescript-eslint/no-explicit-any + purpose: any + documentLoader?: DocumentLoader + expansionMap?: () => void + }) { + if (!_includesCompatibleContext({ document: options.document })) { + return false + } + return super.matchProof({ + proof: options.proof, + document: options.document, + purpose: options.purpose, + documentLoader: options.documentLoader, + expansionMap: options.expansionMap, + }) + } +} + +function _includesCompatibleContext(options: { document: JsonLdDoc }) { + // Handle the unfortunate Ed25519Signature2018 / credentials/v1 collision + const hasEd2018 = _includesContext({ + document: options.document, + contextUrl: ED25519_SUITE_CONTEXT_URL_2018, + }) + const hasEd2020 = _includesContext({ + document: options.document, + contextUrl: ED25519_SUITE_CONTEXT_URL_2020, + }) + const hasCred = _includesContext({ document: options.document, contextUrl: CREDENTIALS_CONTEXT_V1_URL }) + const hasSecV2 = _includesContext({ document: options.document, contextUrl: SECURITY_CONTEXT_URL }) + + // TODO: the console.warn statements below should probably be replaced with logging statements. However, this would currently require injection and I'm not sure we want to do that. + if (hasEd2018 && hasCred) { + // Warn if both are present + // console.warn('Warning: The ed25519-2018/v1 and credentials/v1 ' + 'contexts are incompatible.') + // console.warn('For VCs using Ed25519Signature2018 suite,' + ' using the credentials/v1 context is sufficient.') + return false + } + + if (hasEd2018 && hasSecV2) { + // Warn if both are present + // console.warn('Warning: The ed25519-2018/v1 and security/v2 ' + 'contexts are incompatible.') + // console.warn('For VCs using Ed25519Signature2018 suite,' + ' using the security/v2 context is sufficient.') + return false + } + + // Either one by itself is fine, for this suite + return hasEd2018 || hasEd2020 || hasCred || hasSecV2 +} + +function _isEd2018Key(verificationMethod: JsonLdDoc) { + const hasEd2018 = _includesContext({ + document: verificationMethod, + contextUrl: ED25519_SUITE_CONTEXT_URL_2018, + }) + + // @ts-ignore - .hasValue is not part of the public API + return hasEd2018 && jsonld.hasValue(verificationMethod, 'type', 'Ed25519VerificationKey2018') +} + +function _isEd2020Key(verificationMethod: JsonLdDoc) { + const hasEd2020 = _includesContext({ + document: verificationMethod, + contextUrl: ED25519_SUITE_CONTEXT_URL_2020, + }) + + // @ts-ignore - .hasValue is not part of the public API + return hasEd2020 && jsonld.hasValue(verificationMethod, 'type', 'Ed25519VerificationKey2020') +} diff --git a/packages/core/src/crypto/signature-suites/ed25519/constants.ts b/packages/core/src/crypto/signature-suites/ed25519/constants.ts new file mode 100644 index 0000000000..881a7ea3b1 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/ed25519/constants.ts @@ -0,0 +1,2 @@ +export const ED25519_SUITE_CONTEXT_URL_2018 = 'https://w3id.org/security/suites/ed25519-2018/v1' +export const ED25519_SUITE_CONTEXT_URL_2020 = 'https://w3id.org/security/suites/ed25519-2020/v1' diff --git a/packages/core/src/crypto/signature-suites/ed25519/context.ts b/packages/core/src/crypto/signature-suites/ed25519/context.ts new file mode 100644 index 0000000000..1f2c6af92c --- /dev/null +++ b/packages/core/src/crypto/signature-suites/ed25519/context.ts @@ -0,0 +1,99 @@ +import { ED25519_SUITE_CONTEXT_URL_2018 } from './constants' + +export const context = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + Ed25519VerificationKey2018: { + '@id': 'https://w3id.org/security#Ed25519VerificationKey2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + publicKeyBase58: { + '@id': 'https://w3id.org/security#publicKeyBase58', + }, + }, + }, + Ed25519Signature2018: { + '@id': 'https://w3id.org/security#Ed25519Signature2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + jws: { + '@id': 'https://w3id.org/security#jws', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + }, +} + +const ed25519Signature2018Context = new Map() +ed25519Signature2018Context.set(ED25519_SUITE_CONTEXT_URL_2018, context) + +export { ed25519Signature2018Context } diff --git a/packages/core/src/crypto/signature-suites/index.ts b/packages/core/src/crypto/signature-suites/index.ts new file mode 100644 index 0000000000..7eecb7ef25 --- /dev/null +++ b/packages/core/src/crypto/signature-suites/index.ts @@ -0,0 +1,2 @@ +export * from './ed25519/Ed25519Signature2018' +export * from './JwsLinkedDataSignature' diff --git a/packages/core/src/decorators/ack/AckDecorator.test.ts b/packages/core/src/decorators/ack/AckDecorator.test.ts index 152b014c3a..fe0ccba759 100644 --- a/packages/core/src/decorators/ack/AckDecorator.test.ts +++ b/packages/core/src/decorators/ack/AckDecorator.test.ts @@ -14,7 +14,13 @@ describe('Decorators | AckDecoratorExtension', () => { test('transforms AckDecorator class to JSON', () => { const message = new TestMessage() message.setPleaseAck() - expect(message.toJSON()).toEqual({ '~please_ack': {} }) + expect(message.toJSON()).toEqual({ + '@id': undefined, + '@type': undefined, + '~please_ack': { + on: ['RECEIPT'], + }, + }) }) test('transforms Json to AckDecorator class', () => { diff --git a/packages/core/src/decorators/ack/AckDecorator.ts b/packages/core/src/decorators/ack/AckDecorator.ts index cb04be571a..a647a1a49f 100644 --- a/packages/core/src/decorators/ack/AckDecorator.ts +++ b/packages/core/src/decorators/ack/AckDecorator.ts @@ -1,4 +1,21 @@ +import { IsArray, IsEnum } from 'class-validator' + +export enum AckValues { + Receipt = 'RECEIPT', + Outcome = 'OUTCOME', +} + /** * Represents `~please_ack` decorator */ -export class AckDecorator {} +export class AckDecorator { + public constructor(options: { on: [AckValues.Receipt] }) { + if (options) { + this.on = options.on + } + } + + @IsEnum(AckValues, { each: true }) + @IsArray() + public on!: AckValues[] +} diff --git a/packages/core/src/decorators/ack/AckDecoratorExtension.ts b/packages/core/src/decorators/ack/AckDecoratorExtension.ts index 8185c5ea38..059c734bcc 100644 --- a/packages/core/src/decorators/ack/AckDecoratorExtension.ts +++ b/packages/core/src/decorators/ack/AckDecoratorExtension.ts @@ -3,7 +3,7 @@ import type { BaseMessageConstructor } from '../../agent/BaseMessage' import { Expose, Type } from 'class-transformer' import { IsInstance, IsOptional, ValidateNested } from 'class-validator' -import { AckDecorator } from './AckDecorator' +import { AckDecorator, AckValues } from './AckDecorator' export function AckDecorated(Base: T) { class AckDecoratorExtension extends Base { @@ -14,8 +14,8 @@ export function AckDecorated(Base: T) { @IsOptional() public pleaseAck?: AckDecorator - public setPleaseAck() { - this.pleaseAck = new AckDecorator() + public setPleaseAck(on?: [AckValues.Receipt]) { + this.pleaseAck = new AckDecorator({ on: on ?? [AckValues.Receipt] }) } public getPleaseAck(): AckDecorator | undefined { diff --git a/packages/core/src/decorators/attachment/Attachment.test.ts b/packages/core/src/decorators/attachment/Attachment.test.ts deleted file mode 100644 index 8f21c58ccd..0000000000 --- a/packages/core/src/decorators/attachment/Attachment.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { JsonTransformer } from '../..' - -import { Attachment } from './Attachment' - -const mockJson = { - '@id': 'ceffce22-6471-43e4-8945-b604091981c9', - description: 'A small picture of a cat', - filename: 'cat.png', - 'mime-type': 'text/plain', - lastmod_time: new Date(), - byte_count: 9200, - data: { - json: { - hello: 'world!', - }, - sha256: '00d7b2068a0b237f14a7979bbfc01ad62f60792e459467bfc4a7d3b9a6dbbe3e', - }, -} - -const id = 'ceffce22-6471-43e4-8945-b604091981c9' -const description = 'A small picture of a cat' -const filename = 'cat.png' -const mimeType = 'text/plain' -const lastmodTime = new Date() -const byteCount = 9200 -const data = { - json: { - hello: 'world!', - }, - sha256: '00d7b2068a0b237f14a7979bbfc01ad62f60792e459467bfc4a7d3b9a6dbbe3e', -} - -describe('Decorators | Attachment', () => { - it('should correctly transform Json to Attachment class', () => { - const decorator = JsonTransformer.fromJSON(mockJson, Attachment) - - expect(decorator.id).toBe(mockJson['@id']) - expect(decorator.description).toBe(mockJson.description) - expect(decorator.filename).toBe(mockJson.filename) - expect(decorator.lastmodTime).toEqual(mockJson.lastmod_time) - expect(decorator.byteCount).toEqual(mockJson.byte_count) - expect(decorator.data).toEqual(mockJson.data) - }) - - it('should correctly transform Attachment class to Json', () => { - const decorator = new Attachment({ - id, - description, - filename, - mimeType, - lastmodTime, - byteCount, - data, - }) - - const json = JsonTransformer.toJSON(decorator) - const transformed = { - '@id': id, - description, - filename, - 'mime-type': mimeType, - lastmod_time: lastmodTime, - byte_count: byteCount, - data, - } - - expect(json).toEqual(transformed) - }) -}) diff --git a/packages/core/src/decorators/attachment/Attachment.ts b/packages/core/src/decorators/attachment/Attachment.ts index 5dea2fb778..b39fa52d8d 100644 --- a/packages/core/src/decorators/attachment/Attachment.ts +++ b/packages/core/src/decorators/attachment/Attachment.ts @@ -1,3 +1,5 @@ +import type { JwsGeneralFormat } from '../../crypto/JwsTypes' + import { Expose, Type } from 'class-transformer' import { IsBase64, @@ -11,6 +13,9 @@ import { ValidateNested, } from 'class-validator' +import { Jws } from '../../crypto/JwsTypes' +import { AriesFrameworkError } from '../../error' +import { JsonEncoder } from '../../utils/JsonEncoder' import { uuid } from '../../utils/uuid' export interface AttachmentOptions { @@ -27,7 +32,7 @@ export interface AttachmentDataOptions { base64?: string json?: Record links?: string[] - jws?: Record + jws?: Jws sha256?: string } @@ -35,16 +40,6 @@ export interface AttachmentDataOptions { * A JSON object that gives access to the actual content of the attachment */ export class AttachmentData { - public constructor(options: AttachmentDataOptions) { - if (options) { - this.base64 = options.base64 - this.json = options.json - this.links = options.links - this.jws = options.jws - this.sha256 = options.sha256 - } - } - /** * Base64-encoded data, when representing arbitrary content inline instead of via links. Optional. */ @@ -69,7 +64,7 @@ export class AttachmentData { * A JSON Web Signature over the content of the attachment. Optional. */ @IsOptional() - public jws?: Record + public jws?: Jws /** * The hash of the content. Optional. @@ -77,6 +72,16 @@ export class AttachmentData { @IsOptional() @IsHash('sha256') public sha256?: string + + public constructor(options: AttachmentDataOptions) { + if (options) { + this.base64 = options.base64 + this.json = options.json + this.links = options.links + this.jws = options.jws + this.sha256 = options.sha256 + } + } } /** @@ -142,4 +147,34 @@ export class Attachment { @ValidateNested() @IsInstance(AttachmentData) public data!: AttachmentData + + /* + * Helper function returning JSON representation of attachment data (if present). Tries to obtain the data from .base64 or .json, throws an error otherwise + */ + public getDataAsJson(): T { + if (typeof this.data.base64 === 'string') { + return JsonEncoder.fromBase64(this.data.base64) as T + } else if (this.data.json) { + return this.data.json as T + } else { + throw new AriesFrameworkError('No attachment data found in `json` or `base64` data fields.') + } + } + + public addJws(jws: JwsGeneralFormat) { + // If no JWS yet, assign to current JWS + if (!this.data.jws) { + this.data.jws = jws + } + // Is already jws array, add to it + else if ('signatures' in this.data.jws) { + this.data.jws.signatures.push(jws) + } + // If already single JWS, transform to general jws format + else { + this.data.jws = { + signatures: [this.data.jws, jws], + } + } + } } diff --git a/packages/core/src/decorators/attachment/AttachmentExtension.ts b/packages/core/src/decorators/attachment/AttachmentExtension.ts index 58cec91ee0..67ab28d578 100644 --- a/packages/core/src/decorators/attachment/AttachmentExtension.ts +++ b/packages/core/src/decorators/attachment/AttachmentExtension.ts @@ -15,17 +15,17 @@ export function AttachmentDecorated(Base: T) { @ValidateNested() @IsInstance(Attachment, { each: true }) @IsOptional() - public attachments?: Attachment[] + public appendedAttachments?: Attachment[] - public getAttachmentById(id: string): Attachment | undefined { - return this.attachments?.find((attachment) => attachment.id === id) + public getAppendedAttachmentById(id: string): Attachment | undefined { + return this.appendedAttachments?.find((attachment) => attachment.id === id) } - public addAttachment(attachment: Attachment): void { - if (this.attachments) { - this.attachments?.push(attachment) + public addAppendedAttachment(attachment: Attachment): void { + if (this.appendedAttachments) { + this.appendedAttachments.push(attachment) } else { - this.attachments = [attachment] + this.appendedAttachments = [attachment] } } } diff --git a/packages/core/src/decorators/attachment/__tests__/Attachment.test.ts b/packages/core/src/decorators/attachment/__tests__/Attachment.test.ts new file mode 100644 index 0000000000..487c846291 --- /dev/null +++ b/packages/core/src/decorators/attachment/__tests__/Attachment.test.ts @@ -0,0 +1,126 @@ +import * as didJwsz6Mkf from '../../../crypto/__tests__/__fixtures__/didJwsz6Mkf' +import * as didJwsz6Mkv from '../../../crypto/__tests__/__fixtures__/didJwsz6Mkv' +import { JsonEncoder } from '../../../utils/JsonEncoder' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { Attachment, AttachmentData } from '../Attachment' + +const mockJson = { + '@id': 'ceffce22-6471-43e4-8945-b604091981c9', + description: 'A small picture of a cat', + filename: 'cat.png', + 'mime-type': 'text/plain', + lastmod_time: new Date(), + byte_count: 9200, + data: { + json: { + hello: 'world!', + }, + sha256: '00d7b2068a0b237f14a7979bbfc01ad62f60792e459467bfc4a7d3b9a6dbbe3e', + }, +} + +const mockJsonBase64 = { + '@id': 'ceffce22-6471-43e4-8945-b604091981c9', + description: 'A small picture of a cat', + filename: 'cat.png', + 'mime-type': 'text/plain', + lastmod_time: new Date(), + byte_count: 9200, + data: { + base64: JsonEncoder.toBase64(mockJson.data.json), + }, +} + +const id = 'ceffce22-6471-43e4-8945-b604091981c9' +const description = 'A small picture of a cat' +const filename = 'cat.png' +const mimeType = 'text/plain' +const lastmodTime = new Date() +const byteCount = 9200 +const data = { + json: { + hello: 'world!', + }, + sha256: '00d7b2068a0b237f14a7979bbfc01ad62f60792e459467bfc4a7d3b9a6dbbe3e', +} +const dataInstance = new AttachmentData(data) + +describe('Decorators | Attachment', () => { + it('should correctly transform Json to Attachment class', () => { + const decorator = JsonTransformer.fromJSON(mockJson, Attachment) + + expect(decorator.id).toBe(mockJson['@id']) + expect(decorator.description).toBe(mockJson.description) + expect(decorator.filename).toBe(mockJson.filename) + expect(decorator.lastmodTime).toEqual(mockJson.lastmod_time) + expect(decorator.byteCount).toEqual(mockJson.byte_count) + expect(decorator.data).toMatchObject(mockJson.data) + }) + + it('should correctly transform Attachment class to Json', () => { + const decorator = new Attachment({ + id, + description, + filename, + mimeType, + lastmodTime, + byteCount, + data: dataInstance, + }) + + const json = JsonTransformer.toJSON(decorator) + const transformed = { + '@id': id, + description, + filename, + 'mime-type': mimeType, + lastmod_time: lastmodTime, + byte_count: byteCount, + data, + } + + expect(json).toMatchObject(transformed) + }) + + it('should return the data correctly if only JSON exists', () => { + const decorator = JsonTransformer.fromJSON(mockJson, Attachment) + + const gotData = decorator.getDataAsJson() + expect(decorator.data.json).toEqual(gotData) + }) + + it('should return the data correctly if only Base64 exists', () => { + const decorator = JsonTransformer.fromJSON(mockJsonBase64, Attachment) + + const gotData = decorator.getDataAsJson() + expect(mockJson.data.json).toEqual(gotData) + }) + + describe('addJws', () => { + it('correctly adds the jws to the data', async () => { + const base64 = JsonEncoder.toBase64(didJwsz6Mkf.DATA_JSON) + const attachment = new Attachment({ + id: 'some-uuid', + data: new AttachmentData({ + base64, + }), + }) + + expect(attachment.data.jws).toBeUndefined() + + attachment.addJws(didJwsz6Mkf.JWS_JSON) + expect(attachment.data.jws).toEqual(didJwsz6Mkf.JWS_JSON) + + attachment.addJws(didJwsz6Mkv.JWS_JSON) + expect(attachment.data.jws).toEqual({ signatures: [didJwsz6Mkf.JWS_JSON, didJwsz6Mkv.JWS_JSON] }) + + expect(JsonTransformer.toJSON(attachment)).toMatchObject({ + '@id': 'some-uuid', + data: { + base64: JsonEncoder.toBase64(didJwsz6Mkf.DATA_JSON), + jws: { signatures: [didJwsz6Mkf.JWS_JSON, didJwsz6Mkv.JWS_JSON] }, + }, + }) + }) + }) +}) diff --git a/packages/core/src/decorators/service/ServiceDecorator.ts b/packages/core/src/decorators/service/ServiceDecorator.ts index e00a8bcdf1..72ee1226fe 100644 --- a/packages/core/src/decorators/service/ServiceDecorator.ts +++ b/packages/core/src/decorators/service/ServiceDecorator.ts @@ -1,6 +1,8 @@ +import type { ResolvedDidCommService } from '../../agent/MessageSender' + import { IsArray, IsOptional, IsString } from 'class-validator' -import { DidCommService } from '../../modules/connections/models/did/service/DidCommService' +import { verkeyToInstanceOfKey } from '../../modules/dids/helpers' import { uuid } from '../../utils/uuid' export interface ServiceDecoratorOptions { @@ -36,12 +38,12 @@ export class ServiceDecorator { @IsString() public serviceEndpoint!: string - public toDidCommService(id?: string) { - return new DidCommService({ - id: id ?? uuid(), - recipientKeys: this.recipientKeys, - routingKeys: this.routingKeys, + public get resolvedDidCommService(): ResolvedDidCommService { + return { + id: uuid(), + recipientKeys: this.recipientKeys.map(verkeyToInstanceOfKey), + routingKeys: this.routingKeys?.map(verkeyToInstanceOfKey) ?? [], serviceEndpoint: this.serviceEndpoint, - }) + } } } diff --git a/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts b/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts index df9b57a013..749332603f 100644 --- a/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts +++ b/packages/core/src/decorators/signature/SignatureDecoratorUtils.test.ts @@ -43,7 +43,7 @@ describe('Decorators | Signature | SignatureDecoratorUtils', () => { const config = getAgentConfig('SignatureDecoratorUtilsTest') wallet = new IndyWallet(config) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await wallet.initialize(config.walletConfig!) + await wallet.createAndOpen(config.walletConfig!) }) afterAll(async () => { @@ -76,7 +76,7 @@ describe('Decorators | Signature | SignatureDecoratorUtils', () => { try { await unpackAndVerifySignatureDecorator(wronglySignedData, wallet) } catch (error) { - expect(error.message).toEqual('Signature is not valid!') + expect(error.message).toEqual('Signature is not valid') } }) }) diff --git a/packages/core/src/decorators/signature/SignatureDecoratorUtils.ts b/packages/core/src/decorators/signature/SignatureDecoratorUtils.ts index 536da0b71e..dedbde2610 100644 --- a/packages/core/src/decorators/signature/SignatureDecoratorUtils.ts +++ b/packages/core/src/decorators/signature/SignatureDecoratorUtils.ts @@ -1,8 +1,9 @@ import type { Wallet } from '../../wallet/Wallet' +import { Key, KeyType } from '../../crypto' import { AriesFrameworkError } from '../../error' -import { BufferEncoder } from '../../utils/BufferEncoder' import { JsonEncoder } from '../../utils/JsonEncoder' +import { TypedArrayEncoder } from '../../utils/TypedArrayEncoder' import { Buffer } from '../../utils/buffer' import timestamp from '../../utils/timestamp' @@ -21,15 +22,17 @@ export async function unpackAndVerifySignatureDecorator( wallet: Wallet ): Promise> { const signerVerkey = decorator.signer + const key = Key.fromPublicKeyBase58(signerVerkey, KeyType.Ed25519) // first 8 bytes are for 64 bit integer from unix epoch - const signedData = BufferEncoder.fromBase64(decorator.signatureData) - const signature = BufferEncoder.fromBase64(decorator.signature) + const signedData = TypedArrayEncoder.fromBase64(decorator.signatureData) + const signature = TypedArrayEncoder.fromBase64(decorator.signature) - const isValid = await wallet.verify(signerVerkey, signedData, signature) + // const isValid = await wallet.verify(signerVerkey, signedData, signature) + const isValid = await wallet.verify({ signature, data: signedData, key }) if (!isValid) { - throw new AriesFrameworkError('Signature is not valid!') + throw new AriesFrameworkError('Signature is not valid') } // TODO: return Connection instance instead of raw json @@ -47,13 +50,14 @@ export async function unpackAndVerifySignatureDecorator( */ export async function signData(data: unknown, wallet: Wallet, signerKey: string): Promise { const dataBuffer = Buffer.concat([timestamp(), JsonEncoder.toBuffer(data)]) + const key = Key.fromPublicKeyBase58(signerKey, KeyType.Ed25519) - const signatureBuffer = await wallet.sign(dataBuffer, signerKey) + const signatureBuffer = await wallet.sign({ key, data: dataBuffer }) const signatureDecorator = new SignatureDecorator({ signatureType: 'https://didcomm.org/signature/1.0/ed25519Sha512_single', - signature: BufferEncoder.toBase64URL(signatureBuffer), - signatureData: BufferEncoder.toBase64URL(dataBuffer), + signature: TypedArrayEncoder.toBase64URL(signatureBuffer), + signatureData: TypedArrayEncoder.toBase64URL(dataBuffer), signer: signerKey, }) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index adba1a0507..11f8a86502 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,15 +2,21 @@ import 'reflect-metadata' export { Agent } from './agent/Agent' +export { EventEmitter } from './agent/EventEmitter' +export { Handler, HandlerInboundMessage } from './agent/Handler' +export { InboundMessageContext } from './agent/models/InboundMessageContext' export { AgentConfig } from './agent/AgentConfig' export { AgentMessage } from './agent/AgentMessage' export { Dispatcher } from './agent/Dispatcher' export { MessageSender } from './agent/MessageSender' export type { AgentDependencies } from './agent/AgentDependencies' -export type { InitConfig, OutboundPackage, WireMessage } from './types' +export type { InitConfig, OutboundPackage, EncryptedMessage } from './types' export { DidCommMimeType } from './types' export type { FileSystem } from './storage/FileSystem' +export { BaseRecord } from './storage/BaseRecord' export { InMemoryMessageRepository } from './storage/InMemoryMessageRepository' +export { Repository } from './storage/Repository' +export { StorageService } from './storage/StorageService' export { getDirFromFilePath } from './utils/path' export { InjectionSymbols } from './constants' export type { Wallet } from './wallet/Wallet' @@ -30,6 +36,9 @@ export * from './utils/JsonTransformer' export * from './logger' export * from './error' export * from './wallet/error' +export { parseMessageType, IsValidMessageType } from './utils/messageType' + +export * from './agent/Events' const utils = { uuid, diff --git a/packages/core/src/logger/ConsoleLogger.ts b/packages/core/src/logger/ConsoleLogger.ts index 0575e4cfff..f5c92d9d80 100644 --- a/packages/core/src/logger/ConsoleLogger.ts +++ b/packages/core/src/logger/ConsoleLogger.ts @@ -2,24 +2,7 @@ import { BaseLogger } from './BaseLogger' import { LogLevel } from './Logger' - -/* - * The replacer parameter allows you to specify a function that replaces values with your own. We can use it to control what gets stringified. - */ -function replaceError(_: unknown, value: unknown) { - if (value instanceof Error) { - const newValue = Object.getOwnPropertyNames(value).reduce( - (obj, propName) => { - obj[propName] = (value as unknown as Record)[propName] - return obj - }, - { name: value.name } as Record - ) - return newValue - } - - return value -} +import { replaceError } from './replaceError' export class ConsoleLogger extends BaseLogger { // Map our log levels to console levels diff --git a/packages/core/src/logger/replaceError.ts b/packages/core/src/logger/replaceError.ts new file mode 100644 index 0000000000..023679e354 --- /dev/null +++ b/packages/core/src/logger/replaceError.ts @@ -0,0 +1,17 @@ +/* + * The replacer parameter allows you to specify a function that replaces values with your own. We can use it to control what gets stringified. + */ +export function replaceError(_: unknown, value: unknown) { + if (value instanceof Error) { + const newValue = Object.getOwnPropertyNames(value).reduce( + (obj, propName) => { + obj[propName] = (value as unknown as Record)[propName] + return obj + }, + { name: value.name } as Record + ) + return newValue + } + + return value +} diff --git a/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts b/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts index 79f5ccbd12..086ed35276 100644 --- a/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts +++ b/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts @@ -17,7 +17,6 @@ import { BasicMessageService } from '../services' describe('BasicMessageService', () => { const mockConnectionRecord = getMockConnection({ id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', - verkey: '71X9Y1aSPK11ariWUYQCYMjSewf2Kw2JFGeygEf9uZd9', did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', }) @@ -29,7 +28,7 @@ describe('BasicMessageService', () => { agentConfig = getAgentConfig('BasicMessageServiceTest') wallet = new IndyWallet(agentConfig) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await wallet.initialize(agentConfig.walletConfig!) + await wallet.createAndOpen(agentConfig.walletConfig!) storageService = new IndyStorageService(wallet, agentConfig) }) @@ -57,10 +56,7 @@ describe('BasicMessageService', () => { content: 'message', }) - const messageContext = new InboundMessageContext(basicMessage, { - senderVerkey: 'senderKey', - recipientVerkey: 'recipientKey', - }) + const messageContext = new InboundMessageContext(basicMessage) await basicMessageService.save(messageContext, mockConnectionRecord) @@ -69,7 +65,7 @@ describe('BasicMessageService', () => { payload: { basicMessageRecord: expect.objectContaining({ connectionId: mockConnectionRecord.id, - id: basicMessage.id, + id: expect.any(String), sentTime: basicMessage.sentTime.toISOString(), content: basicMessage.content, role: BasicMessageRole.Receiver, diff --git a/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts b/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts index ebee4ab1f7..3e10a3cb0c 100644 --- a/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts +++ b/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts @@ -1,7 +1,6 @@ import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' import type { BasicMessageService } from '../services/BasicMessageService' -import { AriesFrameworkError } from '../../../error' import { BasicMessage } from '../messages' export class BasicMessageHandler implements Handler { @@ -13,16 +12,7 @@ export class BasicMessageHandler implements Handler { } public async handle(messageContext: HandlerInboundMessage) { - const connection = messageContext.connection - - if (!connection) { - throw new AriesFrameworkError(`Connection for verkey ${messageContext.recipientVerkey} not found!`) - } - - if (!connection.theirKey) { - throw new AriesFrameworkError(`Connection with verkey ${connection.verkey} has no recipient keys.`) - } - + const connection = messageContext.assertReadyConnection() await this.basicMessageService.save(messageContext, connection) } } diff --git a/packages/core/src/modules/basic-messages/messages/BasicMessage.ts b/packages/core/src/modules/basic-messages/messages/BasicMessage.ts index a29d92fd22..cdae9d3fa6 100644 --- a/packages/core/src/modules/basic-messages/messages/BasicMessage.ts +++ b/packages/core/src/modules/basic-messages/messages/BasicMessage.ts @@ -1,7 +1,8 @@ import { Expose, Transform } from 'class-transformer' -import { Equals, IsDate, IsString } from 'class-validator' +import { IsDate, IsString } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' import { DateParser } from '../../../utils/transformers' export class BasicMessage extends AgentMessage { @@ -21,9 +22,9 @@ export class BasicMessage extends AgentMessage { } } - @Equals(BasicMessage.type) - public readonly type = BasicMessage.type - public static readonly type = 'https://didcomm.org/basicmessage/1.0/message' + @IsValidMessageType(BasicMessage.type) + public readonly type = BasicMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/basicmessage/1.0/message') @Expose({ name: 'sent_time' }) @Transform(({ value }) => DateParser(value)) diff --git a/packages/core/src/modules/basic-messages/services/BasicMessageService.ts b/packages/core/src/modules/basic-messages/services/BasicMessageService.ts index 9f08c0d471..b00f98a6ba 100644 --- a/packages/core/src/modules/basic-messages/services/BasicMessageService.ts +++ b/packages/core/src/modules/basic-messages/services/BasicMessageService.ts @@ -22,10 +22,10 @@ export class BasicMessageService { } public async createMessage(message: string, connectionRecord: ConnectionRecord) { + connectionRecord.assertReady() const basicMessage = new BasicMessage({ content: message }) const basicMessageRecord = new BasicMessageRecord({ - id: basicMessage.id, sentTime: basicMessage.sentTime.toISOString(), content: basicMessage.content, connectionId: connectionRecord.id, @@ -46,7 +46,6 @@ export class BasicMessageService { */ public async save({ message }: InboundMessageContext, connection: ConnectionRecord) { const basicMessageRecord = new BasicMessageRecord({ - id: message.id, sentTime: message.sentTime.toISOString(), content: message.content, connectionId: connection.id, diff --git a/packages/core/src/modules/common/messages/AckMessage.ts b/packages/core/src/modules/common/messages/AckMessage.ts index 0ec63e327d..7e833a9513 100644 --- a/packages/core/src/modules/common/messages/AckMessage.ts +++ b/packages/core/src/modules/common/messages/AckMessage.ts @@ -1,8 +1,7 @@ -import { Equals, IsEnum } from 'class-validator' +import { IsEnum } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' - -import { CommonMessageType } from './CommonMessageType' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' /** * Ack message status types @@ -40,9 +39,9 @@ export class AckMessage extends AgentMessage { } } - @Equals(AckMessage.type) - public readonly type: string = AckMessage.type - public static readonly type: string = CommonMessageType.Ack + @IsValidMessageType(AckMessage.type) + public readonly type: string = AckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/notification/1.0/ack') @IsEnum(AckStatus) public status!: AckStatus diff --git a/packages/core/src/modules/common/messages/CommonMessageType.ts b/packages/core/src/modules/common/messages/CommonMessageType.ts deleted file mode 100644 index 0aba02a8dc..0000000000 --- a/packages/core/src/modules/common/messages/CommonMessageType.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum CommonMessageType { - Ack = 'https://didcomm.org/notification/1.0/ack', -} diff --git a/packages/core/src/modules/connections/ConnectionEvents.ts b/packages/core/src/modules/connections/ConnectionEvents.ts index e9108ccae9..327a897dda 100644 --- a/packages/core/src/modules/connections/ConnectionEvents.ts +++ b/packages/core/src/modules/connections/ConnectionEvents.ts @@ -1,5 +1,5 @@ import type { BaseEvent } from '../../agent/Events' -import type { ConnectionState } from './models/ConnectionState' +import type { DidExchangeState } from './models' import type { ConnectionRecord } from './repository/ConnectionRecord' export enum ConnectionEventTypes { @@ -10,6 +10,6 @@ export interface ConnectionStateChangedEvent extends BaseEvent { type: typeof ConnectionEventTypes.ConnectionStateChanged payload: { connectionRecord: ConnectionRecord - previousState: ConnectionState | null + previousState: DidExchangeState | null } } diff --git a/packages/core/src/modules/connections/ConnectionsModule.ts b/packages/core/src/modules/connections/ConnectionsModule.ts index ad4588f283..a2ff085f81 100644 --- a/packages/core/src/modules/connections/ConnectionsModule.ts +++ b/packages/core/src/modules/connections/ConnectionsModule.ts @@ -1,4 +1,7 @@ +import type { Key } from '../../crypto' +import type { OutOfBandRecord } from '../oob/repository' import type { ConnectionRecord } from './repository/ConnectionRecord' +import type { Routing } from './services' import { Lifecycle, scoped } from 'tsyringe' @@ -6,135 +9,103 @@ import { AgentConfig } from '../../agent/AgentConfig' import { Dispatcher } from '../../agent/Dispatcher' import { MessageSender } from '../../agent/MessageSender' import { createOutboundMessage } from '../../agent/helpers' +import { AriesFrameworkError } from '../../error' +import { DidResolverService } from '../dids' +import { DidRepository } from '../dids/repository' +import { OutOfBandService } from '../oob/OutOfBandService' import { MediationRecipientService } from '../routing/services/MediationRecipientService' +import { DidExchangeProtocol } from './DidExchangeProtocol' import { ConnectionRequestHandler, ConnectionResponseHandler, AckMessageHandler, TrustPingMessageHandler, TrustPingResponseMessageHandler, + DidExchangeRequestHandler, + DidExchangeResponseHandler, + DidExchangeCompleteHandler, } from './handlers' -import { ConnectionInvitationMessage } from './messages' +import { HandshakeProtocol } from './models' import { ConnectionService } from './services/ConnectionService' import { TrustPingService } from './services/TrustPingService' @scoped(Lifecycle.ContainerScoped) export class ConnectionsModule { private agentConfig: AgentConfig + private didExchangeProtocol: DidExchangeProtocol private connectionService: ConnectionService + private outOfBandService: OutOfBandService private messageSender: MessageSender private trustPingService: TrustPingService private mediationRecipientService: MediationRecipientService + private didRepository: DidRepository + private didResolverService: DidResolverService public constructor( dispatcher: Dispatcher, agentConfig: AgentConfig, + didExchangeProtocol: DidExchangeProtocol, connectionService: ConnectionService, + outOfBandService: OutOfBandService, trustPingService: TrustPingService, mediationRecipientService: MediationRecipientService, + didRepository: DidRepository, + didResolverService: DidResolverService, messageSender: MessageSender ) { this.agentConfig = agentConfig + this.didExchangeProtocol = didExchangeProtocol this.connectionService = connectionService + this.outOfBandService = outOfBandService this.trustPingService = trustPingService this.mediationRecipientService = mediationRecipientService + this.didRepository = didRepository this.messageSender = messageSender + this.didResolverService = didResolverService this.registerHandlers(dispatcher) } - public async createConnection(config?: { - autoAcceptConnection?: boolean - alias?: string - mediatorId?: string - multiUseInvitation?: boolean - myLabel?: string - myImageUrl?: string - }): Promise<{ - invitation: ConnectionInvitationMessage - connectionRecord: ConnectionRecord - }> { - const mediationRecord = await this.mediationRecipientService.discoverMediation(config?.mediatorId) - const myRouting = await this.mediationRecipientService.getRouting(mediationRecord) - - const { connectionRecord: connectionRecord, message: invitation } = await this.connectionService.createInvitation({ - autoAcceptConnection: config?.autoAcceptConnection, - alias: config?.alias, - routing: myRouting, - multiUseInvitation: config?.multiUseInvitation, - myLabel: config?.myLabel, - myImageUrl: config?.myImageUrl, - }) - - return { connectionRecord, invitation } - } - - /** - * Receive connection invitation as invitee and create connection. If auto accepting is enabled - * via either the config passed in the function or the global agent config, a connection - * request message will be send. - * - * @param invitationJson json object containing the invitation to receive - * @param config config for handling of invitation - * @returns new connection record - */ - public async receiveInvitation( - invitation: ConnectionInvitationMessage, - config?: { + public async acceptOutOfBandInvitation( + outOfBandRecord: OutOfBandRecord, + config: { autoAcceptConnection?: boolean + label?: string alias?: string + imageUrl?: string mediatorId?: string + protocol: HandshakeProtocol + routing?: Routing } - ): Promise { - const mediationRecord = await this.mediationRecipientService.discoverMediation(config?.mediatorId) - const myRouting = await this.mediationRecipientService.getRouting(mediationRecord) + ) { + const { protocol, label, alias, imageUrl, autoAcceptConnection } = config - let connection = await this.connectionService.processInvitation(invitation, { - autoAcceptConnection: config?.autoAcceptConnection, - alias: config?.alias, - routing: myRouting, - }) - // if auto accept is enabled (either on the record or the global agent config) - // we directly send a connection request - if (connection.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) { - connection = await this.acceptInvitation(connection.id) - } - return connection - } + const routing = + config.routing || (await this.mediationRecipientService.getRouting({ mediatorId: config?.mediatorId })) - /** - * Receive connection invitation as invitee encoded as url and create connection. If auto accepting is enabled - * via either the config passed in the function or the global agent config, a connection - * request message will be send. - * - * @param invitationUrl url containing a base64 encoded invitation to receive - * @param config config for handling of invitation - * @returns new connection record - */ - public async receiveInvitationFromUrl( - invitationUrl: string, - config?: { - autoAcceptConnection?: boolean - alias?: string - mediatorId?: string + let result + if (protocol === HandshakeProtocol.DidExchange) { + result = await this.didExchangeProtocol.createRequest(outOfBandRecord, { + label, + alias, + routing, + autoAcceptConnection, + }) + } else if (protocol === HandshakeProtocol.Connections) { + result = await this.connectionService.createRequest(outOfBandRecord, { + label, + alias, + imageUrl, + routing, + autoAcceptConnection, + }) + } else { + throw new AriesFrameworkError(`Unsupported handshake protocol ${protocol}.`) } - ): Promise { - const invitation = await ConnectionInvitationMessage.fromUrl(invitationUrl) - return this.receiveInvitation(invitation, config) - } - - /** - * Accept a connection invitation as invitee (by sending a connection request message) for the connection with the specified connection id. - * This is not needed when auto accepting of connections is enabled. - * - * @param connectionId the id of the connection for which to accept the invitation - * @returns connection record - */ - public async acceptInvitation(connectionId: string): Promise { - const { message, connectionRecord: connectionRecord } = await this.connectionService.createRequest(connectionId) - const outbound = createOutboundMessage(connectionRecord, message) - await this.messageSender.sendMessage(outbound) + const { message, connectionRecord } = result + const outboundMessage = createOutboundMessage(connectionRecord, message, outOfBandRecord) + await this.messageSender.sendMessage(outboundMessage) return connectionRecord } @@ -146,11 +117,29 @@ export class ConnectionsModule { * @returns connection record */ public async acceptRequest(connectionId: string): Promise { - const { message, connectionRecord: connectionRecord } = await this.connectionService.createResponse(connectionId) + const connectionRecord = await this.connectionService.findById(connectionId) + if (!connectionRecord) { + throw new AriesFrameworkError(`Connection record ${connectionId} not found.`) + } + if (!connectionRecord.outOfBandId) { + throw new AriesFrameworkError(`Connection record ${connectionId} does not have out-of-band record.`) + } - const outbound = createOutboundMessage(connectionRecord, message) - await this.messageSender.sendMessage(outbound) + const outOfBandRecord = await this.outOfBandService.findById(connectionRecord.outOfBandId) + if (!outOfBandRecord) { + throw new AriesFrameworkError(`Out-of-band record ${connectionRecord.outOfBandId} not found.`) + } + + let outboundMessage + if (connectionRecord.protocol === HandshakeProtocol.DidExchange) { + const message = await this.didExchangeProtocol.createResponse(connectionRecord, outOfBandRecord) + outboundMessage = createOutboundMessage(connectionRecord, message) + } else { + const { message } = await this.connectionService.createResponse(connectionRecord, outOfBandRecord) + outboundMessage = createOutboundMessage(connectionRecord, message) + } + await this.messageSender.sendMessage(outboundMessage) return connectionRecord } @@ -162,18 +151,34 @@ export class ConnectionsModule { * @returns connection record */ public async acceptResponse(connectionId: string): Promise { - const { message, connectionRecord: connectionRecord } = await this.connectionService.createTrustPing(connectionId, { - responseRequested: false, - }) + const connectionRecord = await this.connectionService.getById(connectionId) - const outbound = createOutboundMessage(connectionRecord, message) - await this.messageSender.sendMessage(outbound) + let outboundMessage + if (connectionRecord.protocol === HandshakeProtocol.DidExchange) { + if (!connectionRecord.outOfBandId) { + throw new AriesFrameworkError(`Connection ${connectionRecord.id} does not have outOfBandId!`) + } + const outOfBandRecord = await this.outOfBandService.findById(connectionRecord.outOfBandId) + if (!outOfBandRecord) { + throw new AriesFrameworkError( + `OutOfBand record for connection ${connectionRecord.id} with outOfBandId ${connectionRecord.outOfBandId} not found!` + ) + } + const message = await this.didExchangeProtocol.createComplete(connectionRecord, outOfBandRecord) + outboundMessage = createOutboundMessage(connectionRecord, message) + } else { + const { message } = await this.connectionService.createTrustPing(connectionRecord, { + responseRequested: false, + }) + outboundMessage = createOutboundMessage(connectionRecord, message) + } + await this.messageSender.sendMessage(outboundMessage) return connectionRecord } - public async returnWhenIsConnected(connectionId: string): Promise { - return this.connectionService.returnWhenIsConnected(connectionId) + public async returnWhenIsConnected(connectionId: string, options?: { timeoutMs: number }): Promise { + return this.connectionService.returnWhenIsConnected(connectionId, options?.timeoutMs) } /** @@ -216,37 +221,28 @@ export class ConnectionsModule { return this.connectionService.deleteById(connectionId) } - /** - * Find connection by verkey. - * - * @param verkey the verkey to search for - * @returns the connection record, or null if not found - * @throws {RecordDuplicateError} if multiple connections are found for the given verkey - */ - public findByVerkey(verkey: string): Promise { - return this.connectionService.findByVerkey(verkey) - } + public async findByKeys({ senderKey, recipientKey }: { senderKey: Key; recipientKey: Key }) { + const theirDidRecord = await this.didRepository.findByRecipientKey(senderKey) + if (theirDidRecord) { + const ourDidRecord = await this.didRepository.findByRecipientKey(recipientKey) + if (ourDidRecord) { + const connectionRecord = await this.connectionService.findSingleByQuery({ + did: ourDidRecord.id, + theirDid: theirDidRecord.id, + }) + if (connectionRecord && connectionRecord.isReady) return connectionRecord + } + } - /** - * Find connection by their verkey. - * - * @param verkey the verkey to search for - * @returns the connection record, or null if not found - * @throws {RecordDuplicateError} if multiple connections are found for the given verkey - */ - public findByTheirKey(verkey: string): Promise { - return this.connectionService.findByTheirKey(verkey) + this.agentConfig.logger.debug( + `No connection record found for encrypted message with recipient key ${recipientKey.fingerprint} and sender key ${senderKey.fingerprint}` + ) + + return null } - /** - * Find connection by Invitation key. - * - * @param key the invitation key to search for - * @returns the connection record, or null if not found - * @throws {RecordDuplicateError} if multiple connections are found for the given verkey - */ - public findByInvitationKey(key: string): Promise { - return this.connectionService.findByInvitationKey(key) + public async findAllByOutOfBandId(outOfBandId: string) { + return this.connectionService.findAllByOutOfBandId(outOfBandId) } /** @@ -261,13 +257,55 @@ export class ConnectionsModule { return this.connectionService.getByThreadId(threadId) } + public async findByDid(did: string): Promise { + return this.connectionService.findByTheirDid(did) + } + + public async findByInvitationDid(invitationDid: string): Promise { + return this.connectionService.findByInvitationDid(invitationDid) + } + private registerHandlers(dispatcher: Dispatcher) { dispatcher.registerHandler( - new ConnectionRequestHandler(this.connectionService, this.agentConfig, this.mediationRecipientService) + new ConnectionRequestHandler( + this.agentConfig, + this.connectionService, + this.outOfBandService, + this.mediationRecipientService, + this.didRepository + ) + ) + dispatcher.registerHandler( + new ConnectionResponseHandler( + this.agentConfig, + this.connectionService, + this.outOfBandService, + this.didResolverService + ) ) - dispatcher.registerHandler(new ConnectionResponseHandler(this.connectionService, this.agentConfig)) dispatcher.registerHandler(new AckMessageHandler(this.connectionService)) dispatcher.registerHandler(new TrustPingMessageHandler(this.trustPingService, this.connectionService)) dispatcher.registerHandler(new TrustPingResponseMessageHandler(this.trustPingService)) + + dispatcher.registerHandler( + new DidExchangeRequestHandler( + this.agentConfig, + this.didExchangeProtocol, + this.outOfBandService, + this.mediationRecipientService, + this.didRepository + ) + ) + + dispatcher.registerHandler( + new DidExchangeResponseHandler( + this.agentConfig, + this.didExchangeProtocol, + this.outOfBandService, + this.connectionService, + this.didResolverService + ) + ) + dispatcher.registerHandler(new DidExchangeCompleteHandler(this.didExchangeProtocol, this.outOfBandService)) } } diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts new file mode 100644 index 0000000000..f36872b219 --- /dev/null +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -0,0 +1,532 @@ +import type { ResolvedDidCommService } from '../../agent/MessageSender' +import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' +import type { Logger } from '../../logger' +import type { ParsedMessageType } from '../../utils/messageType' +import type { OutOfBandDidCommService } from '../oob/domain/OutOfBandDidCommService' +import type { OutOfBandRecord } from '../oob/repository' +import type { ConnectionRecord } from './repository' +import type { Routing } from './services/ConnectionService' + +import { Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../agent/AgentConfig' +import { Key, KeyType } from '../../crypto' +import { JwsService } from '../../crypto/JwsService' +import { Attachment, AttachmentData } from '../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../error' +import { JsonEncoder } from '../../utils/JsonEncoder' +import { JsonTransformer } from '../../utils/JsonTransformer' +import { DidDocument } from '../dids' +import { DidDocumentRole } from '../dids/domain/DidDocumentRole' +import { createDidDocumentFromServices } from '../dids/domain/createPeerDidFromServices' +import { getKeyDidMappingByVerificationMethod } from '../dids/domain/key-type' +import { didKeyToInstanceOfKey, didKeyToVerkey } from '../dids/helpers' +import { DidKey } from '../dids/methods/key/DidKey' +import { getNumAlgoFromPeerDid, PeerDidNumAlgo } from '../dids/methods/peer/didPeer' +import { didDocumentJsonToNumAlgo1Did } from '../dids/methods/peer/peerDidNumAlgo1' +import { DidRecord, DidRepository } from '../dids/repository' + +import { DidExchangeStateMachine } from './DidExchangeStateMachine' +import { DidExchangeProblemReportError, DidExchangeProblemReportReason } from './errors' +import { DidExchangeCompleteMessage } from './messages/DidExchangeCompleteMessage' +import { DidExchangeRequestMessage } from './messages/DidExchangeRequestMessage' +import { DidExchangeResponseMessage } from './messages/DidExchangeResponseMessage' +import { HandshakeProtocol, DidExchangeRole, DidExchangeState } from './models' +import { ConnectionService } from './services' + +interface DidExchangeRequestParams { + label?: string + alias?: string + goal?: string + goalCode?: string + routing: Routing + autoAcceptConnection?: boolean +} + +@scoped(Lifecycle.ContainerScoped) +export class DidExchangeProtocol { + private config: AgentConfig + private connectionService: ConnectionService + private jwsService: JwsService + private didRepository: DidRepository + private logger: Logger + + public constructor( + config: AgentConfig, + connectionService: ConnectionService, + didRepository: DidRepository, + jwsService: JwsService + ) { + this.config = config + this.connectionService = connectionService + this.didRepository = didRepository + this.jwsService = jwsService + this.logger = config.logger + } + + public async createRequest( + outOfBandRecord: OutOfBandRecord, + params: DidExchangeRequestParams + ): Promise<{ message: DidExchangeRequestMessage; connectionRecord: ConnectionRecord }> { + this.logger.debug(`Create message ${DidExchangeRequestMessage.type} start`, { outOfBandRecord, params }) + + const { outOfBandInvitation } = outOfBandRecord + const { alias, goal, goalCode, routing, autoAcceptConnection } = params + + const { did, mediatorId } = routing + + // TODO: We should store only one did that we'll use to send the request message with success. + // We take just the first one for now. + const [invitationDid] = outOfBandInvitation.invitationDids + + const connectionRecord = await this.connectionService.createConnection({ + protocol: HandshakeProtocol.DidExchange, + role: DidExchangeRole.Requester, + alias, + state: DidExchangeState.InvitationReceived, + theirLabel: outOfBandInvitation.label, + multiUseInvitation: false, + did, + mediatorId, + autoAcceptConnection: outOfBandRecord.autoAcceptConnection, + outOfBandId: outOfBandRecord.id, + invitationDid, + }) + + DidExchangeStateMachine.assertCreateMessageState(DidExchangeRequestMessage.type, connectionRecord) + + // Create message + const label = params.label ?? this.config.label + const { verkey } = routing + const didDocument = await this.createPeerDidDoc(this.routingToServices(routing)) + const parentThreadId = outOfBandInvitation.id + + const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode }) + + // Create sign attachment containing didDoc + if (getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { + const didDocAttach = await this.createSignedAttachment(didDocument, [verkey].map(didKeyToVerkey)) + message.didDoc = didDocAttach + } + + connectionRecord.did = didDocument.id + connectionRecord.threadId = message.id + + if (autoAcceptConnection !== undefined || autoAcceptConnection !== null) { + connectionRecord.autoAcceptConnection = autoAcceptConnection + } + + await this.updateState(DidExchangeRequestMessage.type, connectionRecord) + this.logger.debug(`Create message ${DidExchangeRequestMessage.type} end`, { + connectionRecord, + message, + }) + return { message, connectionRecord } + } + + public async processRequest( + messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord, + routing?: Routing + ): Promise { + this.logger.debug(`Process message ${DidExchangeRequestMessage.type} start`, messageContext) + + // TODO check oob role is sender + // TODO check oob state is await-response + // TODO check there is no connection record for particular oob record + + const { did, mediatorId } = routing ? routing : outOfBandRecord + if (!did) { + throw new AriesFrameworkError('Out-of-band record does not have did attribute.') + } + + const { message } = messageContext + + // Check corresponding invitation ID is the request's ~thread.pthid + // TODO Maybe we can do it in handler, but that actually does not make sense because we try to find oob by parent thread ID there. + if (!message.thread?.parentThreadId || message.thread?.parentThreadId !== outOfBandRecord.getTags().invitationId) { + throw new DidExchangeProblemReportError('Missing reference to invitation.', { + problemCode: DidExchangeProblemReportReason.RequestNotAccepted, + }) + } + + // If the responder wishes to continue the exchange, they will persist the received information in their wallet. + + if (!message.did.startsWith('did:peer:')) { + throw new DidExchangeProblemReportError( + `Message contains unsupported did ${message.did}. Supported dids are [did:peer]`, + { + problemCode: DidExchangeProblemReportReason.RequestNotAccepted, + } + ) + } + const numAlgo = getNumAlgoFromPeerDid(message.did) + if (numAlgo !== PeerDidNumAlgo.GenesisDoc) { + throw new DidExchangeProblemReportError( + `Unsupported numalgo ${numAlgo}. Supported numalgos are [${PeerDidNumAlgo.GenesisDoc}]`, + { + problemCode: DidExchangeProblemReportReason.RequestNotAccepted, + } + ) + } + + const didDocument = await this.extractDidDocument(message) + const didRecord = new DidRecord({ + id: message.did, + role: DidDocumentRole.Received, + // It is important to take the did document from the PeerDid class + // as it will have the id property + didDocument, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + + this.logger.debug('Saving DID record', { + id: didRecord.id, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + + await this.didRepository.save(didRecord) + + const connectionRecord = await this.connectionService.createConnection({ + protocol: HandshakeProtocol.DidExchange, + role: DidExchangeRole.Responder, + state: DidExchangeState.RequestReceived, + multiUseInvitation: false, + did, + mediatorId, + autoAcceptConnection: outOfBandRecord.autoAcceptConnection, + outOfBandId: outOfBandRecord.id, + }) + connectionRecord.theirDid = message.did + connectionRecord.theirLabel = message.label + connectionRecord.threadId = message.threadId + + await this.updateState(DidExchangeRequestMessage.type, connectionRecord) + this.logger.debug(`Process message ${DidExchangeRequestMessage.type} end`, connectionRecord) + return connectionRecord + } + + public async createResponse( + connectionRecord: ConnectionRecord, + outOfBandRecord: OutOfBandRecord, + routing?: Routing + ): Promise { + this.logger.debug(`Create message ${DidExchangeResponseMessage.type} start`, connectionRecord) + DidExchangeStateMachine.assertCreateMessageState(DidExchangeResponseMessage.type, connectionRecord) + + const { did } = routing ? routing : outOfBandRecord + if (!did) { + throw new AriesFrameworkError('Out-of-band record does not have did attribute.') + } + + const { threadId } = connectionRecord + + if (!threadId) { + throw new AriesFrameworkError('Missing threadId on connection record.') + } + + let services: ResolvedDidCommService[] = [] + if (routing) { + services = this.routingToServices(routing) + } else if (outOfBandRecord) { + const inlineServices = outOfBandRecord.outOfBandInvitation.services.filter( + (service) => typeof service !== 'string' + ) as OutOfBandDidCommService[] + + services = inlineServices.map((service) => ({ + id: service.id, + serviceEndpoint: service.serviceEndpoint, + recipientKeys: service.recipientKeys.map(didKeyToInstanceOfKey), + routingKeys: service.routingKeys?.map(didKeyToInstanceOfKey) ?? [], + })) + } + + const didDocument = await this.createPeerDidDoc(services) + const message = new DidExchangeResponseMessage({ did: didDocument.id, threadId }) + + if (getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { + const didDocAttach = await this.createSignedAttachment( + didDocument, + Array.from( + new Set( + services + .map((s) => s.recipientKeys) + .reduce((acc, curr) => acc.concat(curr), []) + .map((key) => key.publicKeyBase58) + ) + ) + ) + message.didDoc = didDocAttach + } + + connectionRecord.did = didDocument.id + + await this.updateState(DidExchangeResponseMessage.type, connectionRecord) + this.logger.debug(`Create message ${DidExchangeResponseMessage.type} end`, { connectionRecord, message }) + return message + } + + public async processResponse( + messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord + ): Promise { + this.logger.debug(`Process message ${DidExchangeResponseMessage.type} start`, messageContext) + const { connection: connectionRecord, message } = messageContext + + if (!connectionRecord) { + throw new AriesFrameworkError('No connection record in message context.') + } + + DidExchangeStateMachine.assertProcessMessageState(DidExchangeResponseMessage.type, connectionRecord) + + if (!message.thread?.threadId || message.thread?.threadId !== connectionRecord.threadId) { + throw new DidExchangeProblemReportError('Invalid or missing thread ID.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + + if (!message.did.startsWith('did:peer:')) { + throw new DidExchangeProblemReportError( + `Message contains unsupported did ${message.did}. Supported dids are [did:peer]`, + { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + } + ) + } + const numAlgo = getNumAlgoFromPeerDid(message.did) + if (numAlgo !== PeerDidNumAlgo.GenesisDoc) { + throw new DidExchangeProblemReportError( + `Unsupported numalgo ${numAlgo}. Supported numalgos are [${PeerDidNumAlgo.GenesisDoc}]`, + { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + } + ) + } + + const didDocument = await this.extractDidDocument( + message, + outOfBandRecord.getRecipientKeys().map((key) => key.publicKeyBase58) + ) + const didRecord = new DidRecord({ + id: message.did, + role: DidDocumentRole.Received, + didDocument, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + + this.logger.debug('Saving DID record', { + id: didRecord.id, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + + await this.didRepository.save(didRecord) + + connectionRecord.theirDid = message.did + + await this.updateState(DidExchangeResponseMessage.type, connectionRecord) + this.logger.debug(`Process message ${DidExchangeResponseMessage.type} end`, connectionRecord) + return connectionRecord + } + + public async createComplete( + connectionRecord: ConnectionRecord, + outOfBandRecord: OutOfBandRecord + ): Promise { + this.logger.debug(`Create message ${DidExchangeCompleteMessage.type} start`, connectionRecord) + DidExchangeStateMachine.assertCreateMessageState(DidExchangeCompleteMessage.type, connectionRecord) + + const threadId = connectionRecord.threadId + const parentThreadId = outOfBandRecord.outOfBandInvitation.id + + if (!threadId) { + throw new AriesFrameworkError(`Connection record ${connectionRecord.id} does not have 'threadId' attribute.`) + } + + if (!parentThreadId) { + throw new AriesFrameworkError( + `Connection record ${connectionRecord.id} does not have 'parentThreadId' attribute.` + ) + } + + const message = new DidExchangeCompleteMessage({ threadId, parentThreadId }) + + await this.updateState(DidExchangeCompleteMessage.type, connectionRecord) + this.logger.debug(`Create message ${DidExchangeCompleteMessage.type} end`, { connectionRecord, message }) + return message + } + + public async processComplete( + messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord + ): Promise { + this.logger.debug(`Process message ${DidExchangeCompleteMessage.type} start`, messageContext) + const { connection: connectionRecord, message } = messageContext + + if (!connectionRecord) { + throw new AriesFrameworkError('No connection record in message context.') + } + + DidExchangeStateMachine.assertProcessMessageState(DidExchangeCompleteMessage.type, connectionRecord) + + if (message.threadId !== connectionRecord.threadId) { + throw new DidExchangeProblemReportError('Invalid or missing thread ID.', { + problemCode: DidExchangeProblemReportReason.CompleteRejected, + }) + } + + if (!message.thread?.parentThreadId || message.thread?.parentThreadId !== outOfBandRecord.getTags().invitationId) { + throw new DidExchangeProblemReportError('Invalid or missing parent thread ID referencing to the invitation.', { + problemCode: DidExchangeProblemReportReason.CompleteRejected, + }) + } + + await this.updateState(DidExchangeCompleteMessage.type, connectionRecord) + this.logger.debug(`Process message ${DidExchangeCompleteMessage.type} end`, { connectionRecord }) + return connectionRecord + } + + private async updateState(messageType: ParsedMessageType, connectionRecord: ConnectionRecord) { + this.logger.debug(`Updating state`, { connectionRecord }) + const nextState = DidExchangeStateMachine.nextState(messageType, connectionRecord) + return this.connectionService.updateState(connectionRecord, nextState) + } + + private async createPeerDidDoc(services: ResolvedDidCommService[]) { + const didDocument = createDidDocumentFromServices(services) + + const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON()) + didDocument.id = peerDid + + const didRecord = new DidRecord({ + id: peerDid, + role: DidDocumentRole.Created, + didDocument, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + + this.logger.debug('Saving DID record', { + id: didRecord.id, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + + await this.didRepository.save(didRecord) + this.logger.debug('Did record created.', didRecord) + return didDocument + } + + private async createSignedAttachment(didDoc: DidDocument, verkeys: string[]) { + const didDocAttach = new Attachment({ + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(didDoc), + }), + }) + + await Promise.all( + verkeys.map(async (verkey) => { + const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + const kid = new DidKey(key).did + const payload = JsonEncoder.toBuffer(didDoc) + + const jws = await this.jwsService.createJws({ + payload, + verkey, + header: { + kid, + }, + }) + didDocAttach.addJws(jws) + }) + ) + + return didDocAttach + } + + /** + * Extracts DID document as is from request or response message attachment and verifies its signature. + * + * @param message DID request or DID response message + * @param invitationKeys array containing keys from connection invitation that could be used for signing of DID document + * @returns verified DID document content from message attachment + */ + private async extractDidDocument( + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58: string[] = [] + ): Promise { + if (!message.didDoc) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + throw new DidExchangeProblemReportError('DID Document attachment is missing.', { problemCode }) + } + const didDocumentAttachment = message.didDoc + const jws = didDocumentAttachment.data.jws + + if (!jws) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + throw new DidExchangeProblemReportError('DID Document signature is missing.', { problemCode }) + } + + const json = didDocumentAttachment.getDataAsJson() as Record + this.logger.trace('DidDocument JSON', json) + + const payload = JsonEncoder.toBuffer(json) + const { isValid, signerVerkeys } = await this.jwsService.verifyJws({ jws, payload }) + + const didDocument = JsonTransformer.fromJSON(json, DidDocument) + const didDocumentKeysBase58 = didDocument.authentication + ?.map((authentication) => { + const verificationMethod = + typeof authentication === 'string' + ? didDocument.dereferenceVerificationMethod(authentication) + : authentication + const { getKeyFromVerificationMethod } = getKeyDidMappingByVerificationMethod(verificationMethod) + const key = getKeyFromVerificationMethod(verificationMethod) + return key.publicKeyBase58 + }) + .concat(invitationKeysBase58) + + this.logger.trace('JWS verification result', { isValid, signerVerkeys, didDocumentKeysBase58 }) + + if (!isValid || !signerVerkeys.every((verkey) => didDocumentKeysBase58?.includes(verkey))) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + throw new DidExchangeProblemReportError('DID Document signature is invalid.', { problemCode }) + } + + return didDocument + } + + private routingToServices(routing: Routing): ResolvedDidCommService[] { + return routing.endpoints.map((endpoint, index) => ({ + id: `#inline-${index}`, + serviceEndpoint: endpoint, + recipientKeys: [Key.fromPublicKeyBase58(routing.verkey, KeyType.Ed25519)], + routingKeys: routing.routingKeys.map((routingKey) => Key.fromPublicKeyBase58(routingKey, KeyType.Ed25519)) || [], + })) + } +} diff --git a/packages/core/src/modules/connections/DidExchangeStateMachine.ts b/packages/core/src/modules/connections/DidExchangeStateMachine.ts new file mode 100644 index 0000000000..3f32f4e48d --- /dev/null +++ b/packages/core/src/modules/connections/DidExchangeStateMachine.ts @@ -0,0 +1,88 @@ +import type { ParsedMessageType } from '../../utils/messageType' +import type { ConnectionRecord } from './repository' + +import { AriesFrameworkError } from '../../error' +import { canHandleMessageType } from '../../utils/messageType' + +import { DidExchangeRequestMessage, DidExchangeResponseMessage, DidExchangeCompleteMessage } from './messages' +import { DidExchangeState, DidExchangeRole } from './models' + +export class DidExchangeStateMachine { + private static createMessageStateRules = [ + { + message: DidExchangeRequestMessage, + state: DidExchangeState.InvitationReceived, + role: DidExchangeRole.Requester, + nextState: DidExchangeState.RequestSent, + }, + { + message: DidExchangeResponseMessage, + state: DidExchangeState.RequestReceived, + role: DidExchangeRole.Responder, + nextState: DidExchangeState.ResponseSent, + }, + { + message: DidExchangeCompleteMessage, + state: DidExchangeState.ResponseReceived, + role: DidExchangeRole.Requester, + nextState: DidExchangeState.Completed, + }, + ] + + private static processMessageStateRules = [ + { + message: DidExchangeRequestMessage, + state: DidExchangeState.InvitationSent, + role: DidExchangeRole.Responder, + nextState: DidExchangeState.RequestReceived, + }, + { + message: DidExchangeResponseMessage, + state: DidExchangeState.RequestSent, + role: DidExchangeRole.Requester, + nextState: DidExchangeState.ResponseReceived, + }, + { + message: DidExchangeCompleteMessage, + state: DidExchangeState.ResponseSent, + role: DidExchangeRole.Responder, + nextState: DidExchangeState.Completed, + }, + ] + + public static assertCreateMessageState(messageType: ParsedMessageType, record: ConnectionRecord) { + const rule = this.createMessageStateRules.find((r) => canHandleMessageType(r.message, messageType)) + if (!rule) { + throw new AriesFrameworkError(`Could not find create message rule for ${messageType}`) + } + if (rule.state !== record.state || rule.role !== record.role) { + throw new AriesFrameworkError( + `Record with role ${record.role} is in invalid state ${record.state} to create ${messageType}. Expected state for role ${rule.role} is ${rule.state}.` + ) + } + } + + public static assertProcessMessageState(messageType: ParsedMessageType, record: ConnectionRecord) { + const rule = this.processMessageStateRules.find((r) => canHandleMessageType(r.message, messageType)) + if (!rule) { + throw new AriesFrameworkError(`Could not find create message rule for ${messageType}`) + } + if (rule.state !== record.state || rule.role !== record.role) { + throw new AriesFrameworkError( + `Record with role ${record.role} is in invalid state ${record.state} to process ${messageType}. Expected state for role ${rule.role} is ${rule.state}.` + ) + } + } + + public static nextState(messageType: ParsedMessageType, record: ConnectionRecord) { + const rule = this.createMessageStateRules + .concat(this.processMessageStateRules) + .find((r) => canHandleMessageType(r.message, messageType) && r.role === record.role) + if (!rule) { + throw new AriesFrameworkError( + `Could not find create message rule for messageType ${messageType}, state ${record.state} and role ${record.role}` + ) + } + return rule.nextState + } +} diff --git a/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts index 9defd20d6a..d386a967c8 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionInvitationMessage.test.ts @@ -8,7 +8,7 @@ import { ConnectionInvitationMessage } from '../messages/ConnectionInvitationMes describe('ConnectionInvitationMessage', () => { it('should allow routingKeys to be left out of inline invitation', async () => { const json = { - '@type': ConnectionInvitationMessage.type, + '@type': ConnectionInvitationMessage.type.messageTypeUri, '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], serviceEndpoint: 'https://example.com', @@ -20,7 +20,7 @@ describe('ConnectionInvitationMessage', () => { it('should throw error if both did and inline keys / endpoint are missing', async () => { const json = { - '@type': ConnectionInvitationMessage.type, + '@type': ConnectionInvitationMessage.type.messageTypeUri, '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', label: 'test', } diff --git a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts index 2d85581269..7a6f52c8e4 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts @@ -1,48 +1,59 @@ import type { Wallet } from '../../../wallet/Wallet' import type { Routing } from '../services/ConnectionService' -import { getAgentConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' +import { getAgentConfig, getMockConnection, getMockOutOfBand, mockFunction } from '../../../../tests/helpers' import { AgentMessage } from '../../../agent/AgentMessage' import { EventEmitter } from '../../../agent/EventEmitter' import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import { SignatureDecorator } from '../../../decorators/signature/SignatureDecorator' +import { Key, KeyType } from '../../../crypto' import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils' import { JsonTransformer } from '../../../utils/JsonTransformer' import { uuid } from '../../../utils/uuid' import { IndyWallet } from '../../../wallet/IndyWallet' import { AckMessage, AckStatus } from '../../common' +import { DidKey, IndyAgentService } from '../../dids' +import { DidCommV1Service } from '../../dids/domain/service/DidCommV1Service' +import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' +import { DidRepository } from '../../dids/repository' +import { OutOfBandRole } from '../../oob/domain/OutOfBandRole' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { ConnectionRequestMessage, ConnectionResponseMessage, TrustPingMessage } from '../messages' import { - ConnectionInvitationMessage, - ConnectionRequestMessage, - ConnectionResponseMessage, - TrustPingMessage, -} from '../messages' -import { Connection, ConnectionState, ConnectionRole, DidDoc, DidCommService } from '../models' -import { ConnectionRecord } from '../repository/ConnectionRecord' + Connection, + DidDoc, + EmbeddedAuthentication, + Ed25119Sig2018, + DidExchangeRole, + DidExchangeState, +} from '../models' import { ConnectionRepository } from '../repository/ConnectionRepository' import { ConnectionService } from '../services/ConnectionService' +import { convertToNewDidDocument } from '../services/helpers' jest.mock('../repository/ConnectionRepository') +jest.mock('../../dids/repository/DidRepository') const ConnectionRepositoryMock = ConnectionRepository as jest.Mock +const DidRepositoryMock = DidRepository as jest.Mock const connectionImageUrl = 'https://example.com/image.png' describe('ConnectionService', () => { - const config = getAgentConfig('ConnectionServiceTest', { + const agentConfig = getAgentConfig('ConnectionServiceTest', { endpoints: ['http://agent.com:8080'], connectionImageUrl, }) let wallet: Wallet let connectionRepository: ConnectionRepository + let didRepository: DidRepository let connectionService: ConnectionService let eventEmitter: EventEmitter let myRouting: Routing beforeAll(async () => { - wallet = new IndyWallet(config) + wallet = new IndyWallet(agentConfig) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await wallet.initialize(config.walletConfig!) + await wallet.createAndOpen(agentConfig.walletConfig!) }) afterAll(async () => { @@ -50,283 +61,107 @@ describe('ConnectionService', () => { }) beforeEach(async () => { - eventEmitter = new EventEmitter(config) + eventEmitter = new EventEmitter(agentConfig) connectionRepository = new ConnectionRepositoryMock() - connectionService = new ConnectionService(wallet, config, connectionRepository, eventEmitter) + didRepository = new DidRepositoryMock() + connectionService = new ConnectionService(wallet, agentConfig, connectionRepository, didRepository, eventEmitter) myRouting = { did: 'fakeDid', verkey: 'fakeVerkey', - endpoints: config.endpoints ?? [], + endpoints: agentConfig.endpoints ?? [], routingKeys: [], mediatorId: 'fakeMediatorId', } }) - describe('createInvitation', () => { - it('returns a connection record with values set', async () => { - expect.assertions(9) - const { connectionRecord, message } = await connectionService.createInvitation({ routing: myRouting }) - - expect(connectionRecord.type).toBe('ConnectionRecord') - expect(connectionRecord.role).toBe(ConnectionRole.Inviter) - expect(connectionRecord.state).toBe(ConnectionState.Invited) - expect(connectionRecord.autoAcceptConnection).toBeUndefined() - expect(connectionRecord.id).toEqual(expect.any(String)) - expect(connectionRecord.verkey).toEqual(expect.any(String)) - expect(connectionRecord.mediatorId).toEqual('fakeMediatorId') - expect(message.imageUrl).toBe(connectionImageUrl) - expect(connectionRecord.getTags()).toEqual( - expect.objectContaining({ - verkey: connectionRecord.verkey, - }) - ) - }) - - it('returns a connection record with invitation', async () => { - expect.assertions(1) - - const { message: invitation } = await connectionService.createInvitation({ routing: myRouting }) - - expect(invitation).toEqual( - expect.objectContaining({ - label: config.label, - recipientKeys: [expect.any(String)], - routingKeys: [], - serviceEndpoint: config.endpoints[0], - }) - ) - }) - - it('saves the connection record in the connection repository', async () => { - expect.assertions(1) - - const saveSpy = jest.spyOn(connectionRepository, 'save') - - await connectionService.createInvitation({ routing: myRouting }) - - expect(saveSpy).toHaveBeenCalledWith(expect.any(ConnectionRecord)) - }) - - it('returns a connection record with the autoAcceptConnection parameter from the config', async () => { - expect.assertions(3) - - const { connectionRecord: connectionTrue } = await connectionService.createInvitation({ - autoAcceptConnection: true, - routing: myRouting, - }) - const { connectionRecord: connectionFalse } = await connectionService.createInvitation({ - autoAcceptConnection: false, - routing: myRouting, - }) - const { connectionRecord: connectionUndefined } = await connectionService.createInvitation({ routing: myRouting }) - - expect(connectionTrue.autoAcceptConnection).toBe(true) - expect(connectionFalse.autoAcceptConnection).toBe(false) - expect(connectionUndefined.autoAcceptConnection).toBeUndefined() - }) - - it('returns a connection record with the alias parameter from the config', async () => { - expect.assertions(2) - - const { connectionRecord: aliasDefined } = await connectionService.createInvitation({ - alias: 'test-alias', - routing: myRouting, - }) - const { connectionRecord: aliasUndefined } = await connectionService.createInvitation({ routing: myRouting }) - - expect(aliasDefined.alias).toBe('test-alias') - expect(aliasUndefined.alias).toBeUndefined() - }) - - it('returns a connection record with the multiUseInvitation parameter from the config', async () => { - expect.assertions(2) - - const { connectionRecord: multiUseDefined } = await connectionService.createInvitation({ - multiUseInvitation: true, - routing: myRouting, - }) - const { connectionRecord: multiUseUndefined } = await connectionService.createInvitation({ routing: myRouting }) - - expect(multiUseDefined.multiUseInvitation).toBe(true) - // Defaults to false - expect(multiUseUndefined.multiUseInvitation).toBe(false) - }) - - it('returns a connection record with the custom label from the config', async () => { - expect.assertions(1) - - const { message: invitation } = await connectionService.createInvitation({ - routing: myRouting, - myLabel: 'custom-label', - }) - - expect(invitation).toEqual( - expect.objectContaining({ - label: 'custom-label', - recipientKeys: [expect.any(String)], - routingKeys: [], - serviceEndpoint: config.endpoints[0], - }) - ) - }) - - it('returns a connection record with the custom image url from the config', async () => { - expect.assertions(1) - - const { message: invitation } = await connectionService.createInvitation({ - routing: myRouting, - myImageUrl: 'custom-image-url', - }) - - expect(invitation).toEqual( - expect.objectContaining({ - label: config.label, - imageUrl: 'custom-image-url', - recipientKeys: [expect.any(String)], - routingKeys: [], - serviceEndpoint: config.endpoints[0], - }) - ) - }) - }) - - describe('processInvitation', () => { - it('returns a connection record containing the information from the connection invitation', async () => { - expect.assertions(12) - - const recipientKey = 'key-1' - const invitation = new ConnectionInvitationMessage({ - label: 'test label', - recipientKeys: [recipientKey], - serviceEndpoint: 'https://test.com/msg', - imageUrl: connectionImageUrl, - }) - - const connection = await connectionService.processInvitation(invitation, { routing: myRouting }) - const connectionAlias = await connectionService.processInvitation(invitation, { - alias: 'test-alias', - routing: myRouting, - }) - - expect(connection.role).toBe(ConnectionRole.Invitee) - expect(connection.state).toBe(ConnectionState.Invited) - expect(connection.autoAcceptConnection).toBeUndefined() - expect(connection.id).toEqual(expect.any(String)) - expect(connection.verkey).toEqual(expect.any(String)) - expect(connection.mediatorId).toEqual('fakeMediatorId') - expect(connection.getTags()).toEqual( - expect.objectContaining({ - verkey: connection.verkey, - invitationKey: recipientKey, - }) - ) - expect(connection.invitation).toMatchObject(invitation) - expect(connection.alias).toBeUndefined() - expect(connectionAlias.alias).toBe('test-alias') - expect(connection.theirLabel).toBe('test label') - expect(connection.imageUrl).toBe(connectionImageUrl) - }) - - it('returns a connection record with the autoAcceptConnection parameter from the config', async () => { - expect.assertions(3) - - const invitation = new ConnectionInvitationMessage({ - did: 'did:sov:test', - label: 'test label', - }) - - const connectionTrue = await connectionService.processInvitation(invitation, { - autoAcceptConnection: true, - routing: myRouting, - }) - const connectionFalse = await connectionService.processInvitation(invitation, { - autoAcceptConnection: false, - routing: myRouting, - }) - const connectionUndefined = await connectionService.processInvitation(invitation, { routing: myRouting }) - - expect(connectionTrue.autoAcceptConnection).toBe(true) - expect(connectionFalse.autoAcceptConnection).toBe(false) - expect(connectionUndefined.autoAcceptConnection).toBeUndefined() - }) - - it('returns a connection record with the alias parameter from the config', async () => { - expect.assertions(2) - - const invitation = new ConnectionInvitationMessage({ - did: 'did:sov:test', - label: 'test label', - }) - - const aliasDefined = await connectionService.processInvitation(invitation, { - alias: 'test-alias', - routing: myRouting, - }) - const aliasUndefined = await connectionService.processInvitation(invitation, { routing: myRouting }) - - expect(aliasDefined.alias).toBe('test-alias') - expect(aliasUndefined.alias).toBeUndefined() - }) - }) - describe('createRequest', () => { it('returns a connection request message containing the information from the connection record', async () => { expect.assertions(5) - const connection = getMockConnection() - mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(connection)) - - const { connectionRecord: connectionRecord, message } = await connectionService.createRequest('test') - - expect(connectionRecord.state).toBe(ConnectionState.Requested) - expect(message.label).toBe(config.label) - expect(message.connection.did).toBe('test-did') - expect(message.connection.didDoc).toEqual(connection.didDoc) + const outOfBand = getMockOutOfBand({ state: OutOfBandState.PrepareResponse }) + const config = { routing: myRouting } + + const { connectionRecord, message } = await connectionService.createRequest(outOfBand, config) + + expect(connectionRecord.state).toBe(DidExchangeState.RequestSent) + expect(message.label).toBe(agentConfig.label) + expect(message.connection.did).toBe('fakeDid') + expect(message.connection.didDoc).toEqual( + new DidDoc({ + id: 'fakeDid', + publicKey: [ + new Ed25119Sig2018({ + id: `fakeDid#1`, + controller: 'fakeDid', + publicKeyBase58: 'fakeVerkey', + }), + ], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `fakeDid#1`, + controller: 'fakeDid', + publicKeyBase58: 'fakeVerkey', + }) + ), + ], + service: [ + new IndyAgentService({ + id: `fakeDid#IndyAgentService`, + serviceEndpoint: agentConfig.endpoints[0], + recipientKeys: ['fakeVerkey'], + routingKeys: [], + }), + ], + }) + ) expect(message.imageUrl).toBe(connectionImageUrl) }) it('returns a connection request message containing a custom label', async () => { expect.assertions(1) - const connection = getMockConnection() - mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(connection)) + const outOfBand = getMockOutOfBand({ state: OutOfBandState.PrepareResponse }) + const config = { label: 'Custom label', routing: myRouting } - const { message } = await connectionService.createRequest('test', { myLabel: 'custom-label' }) + const { message } = await connectionService.createRequest(outOfBand, config) - expect(message.label).toBe('custom-label') + expect(message.label).toBe('Custom label') }) it('returns a connection request message containing a custom image url', async () => { expect.assertions(1) - const connection = getMockConnection() - mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(connection)) + const outOfBand = getMockOutOfBand({ state: OutOfBandState.PrepareResponse }) + const config = { imageUrl: 'custom-image-url', routing: myRouting } - const { message } = await connectionService.createRequest('test', { myImageUrl: 'custom-image-url' }) + const { message } = await connectionService.createRequest(outOfBand, config) expect(message.imageUrl).toBe('custom-image-url') }) - it(`throws an error when connection role is ${ConnectionRole.Inviter} and not ${ConnectionRole.Invitee}`, async () => { + it(`throws an error when out-of-band role is not ${OutOfBandRole.Receiver}`, async () => { expect.assertions(1) - mockFunction(connectionRepository.getById).mockReturnValue( - Promise.resolve(getMockConnection({ role: ConnectionRole.Inviter })) - ) - return expect(connectionService.createRequest('test')).rejects.toThrowError( - `Connection record has invalid role ${ConnectionRole.Inviter}. Expected role ${ConnectionRole.Invitee}.` + const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Sender, state: OutOfBandState.PrepareResponse }) + const config = { routing: myRouting } + + return expect(connectionService.createRequest(outOfBand, config)).rejects.toThrowError( + `Invalid out-of-band record role ${OutOfBandRole.Sender}, expected is ${OutOfBandRole.Receiver}.` ) }) - const invalidConnectionStates = [ConnectionState.Requested, ConnectionState.Responded, ConnectionState.Complete] + const invalidConnectionStates = [OutOfBandState.Initial, OutOfBandState.AwaitResponse, OutOfBandState.Done] test.each(invalidConnectionStates)( - `throws an error when connection state is %s and not ${ConnectionState.Invited}`, + `throws an error when out-of-band state is %s and not ${OutOfBandState.PrepareResponse}`, (state) => { expect.assertions(1) - mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(getMockConnection({ state }))) - return expect(connectionService.createRequest('test')).rejects.toThrowError( - `Connection record is in invalid state ${state}. Valid states are: ${ConnectionState.Invited}.` + const outOfBand = getMockOutOfBand({ state }) + const config = { routing: myRouting } + + return expect(connectionService.createRequest(outOfBand, config)).rejects.toThrowError( + `Invalid out-of-band record state ${state}, valid states are: ${OutOfBandState.PrepareResponse}.` ) } ) @@ -334,26 +169,27 @@ describe('ConnectionService', () => { describe('processRequest', () => { it('returns a connection record containing the information from the connection request', async () => { - expect.assertions(7) - - const connectionRecord = getMockConnection({ - state: ConnectionState.Invited, - verkey: 'my-key', - role: ConnectionRole.Inviter, - }) - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(connectionRecord)) + expect.assertions(5) const theirDid = 'their-did' - const theirVerkey = 'their-verkey' + const theirKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) const theirDidDoc = new DidDoc({ id: theirDid, publicKey: [], - authentication: [], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `${theirDid}#key-id`, + controller: theirDid, + publicKeyBase58: theirKey.publicKeyBase58, + }) + ), + ], service: [ - new DidCommService({ + new DidCommV1Service({ id: `${theirDid};indy`, serviceEndpoint: 'https://endpoint.com', - recipientKeys: [theirVerkey], + recipientKeys: [`${theirDid}#key-id`], }), ], }) @@ -366,63 +202,54 @@ describe('ConnectionService', () => { }) const messageContext = new InboundMessageContext(connectionRequest, { - senderVerkey: theirVerkey, - recipientVerkey: 'my-key', + senderKey: theirKey, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), }) - const processedConnection = await connectionService.processRequest(messageContext) + const outOfBand = getMockOutOfBand({ + did: 'fakeDid', + mediatorId: 'fakeMediatorId', + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + }) + const processedConnection = await connectionService.processRequest(messageContext, outOfBand) - expect(processedConnection.state).toBe(ConnectionState.Requested) - expect(processedConnection.theirDid).toBe(theirDid) - expect(processedConnection.theirDidDoc).toEqual(theirDidDoc) - expect(processedConnection.theirKey).toBe(theirVerkey) + expect(processedConnection.state).toBe(DidExchangeState.RequestReceived) + expect(processedConnection.theirDid).toBe('did:peer:1zQmfPPbuG8vajHvYjGUW8CN5k9rLuuMmYSGBYwJqJDDUS72') expect(processedConnection.theirLabel).toBe('test-label') expect(processedConnection.threadId).toBe(connectionRequest.id) expect(processedConnection.imageUrl).toBe(connectionImageUrl) }) - it('throws an error when the connection cannot be found by verkey', async () => { - expect.assertions(1) - - const connectionRequest = new ConnectionRequestMessage({ - did: 'did', - label: 'test-label', - }) - - const messageContext = new InboundMessageContext(connectionRequest, { - recipientVerkey: 'test-verkey', - senderVerkey: 'sender-verkey', - }) - - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(null)) - return expect(connectionService.processRequest(messageContext)).rejects.toThrowError( - 'Unable to process connection request: connection for verkey test-verkey not found' - ) - }) - it('returns a new connection record containing the information from the connection request when multiUseInvitation is enabled on the connection', async () => { - expect.assertions(10) + expect.assertions(8) const connectionRecord = getMockConnection({ id: 'test', - state: ConnectionState.Invited, - verkey: 'my-key', - role: ConnectionRole.Inviter, + state: DidExchangeState.InvitationSent, + role: DidExchangeRole.Responder, multiUseInvitation: true, }) - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(connectionRecord)) const theirDid = 'their-did' - const theirVerkey = 'their-verkey' + const theirKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) const theirDidDoc = new DidDoc({ id: theirDid, publicKey: [], - authentication: [], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `${theirDid}#key-id`, + controller: theirDid, + publicKeyBase58: theirKey.publicKeyBase58, + }) + ), + ], service: [ - new DidCommService({ + new DidCommV1Service({ id: `${theirDid};indy`, serviceEndpoint: 'https://endpoint.com', - recipientKeys: [theirVerkey], + recipientKeys: [`${theirDid}#key-id`], }), ], }) @@ -435,107 +262,78 @@ describe('ConnectionService', () => { const messageContext = new InboundMessageContext(connectionRequest, { connection: connectionRecord, - senderVerkey: theirVerkey, - recipientVerkey: 'my-key', + senderKey: theirKey, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), }) - const processedConnection = await connectionService.processRequest(messageContext, myRouting) + const outOfBand = getMockOutOfBand({ + did: 'fakeDid', + mediatorId: 'fakeMediatorId', + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + }) + const processedConnection = await connectionService.processRequest(messageContext, outOfBand) - expect(processedConnection.state).toBe(ConnectionState.Requested) - expect(processedConnection.theirDid).toBe(theirDid) - expect(processedConnection.theirDidDoc).toEqual(theirDidDoc) - expect(processedConnection.theirKey).toBe(theirVerkey) + expect(processedConnection.state).toBe(DidExchangeState.RequestReceived) + expect(processedConnection.theirDid).toBe('did:peer:1zQmfPPbuG8vajHvYjGUW8CN5k9rLuuMmYSGBYwJqJDDUS72') expect(processedConnection.theirLabel).toBe('test-label') expect(processedConnection.threadId).toBe(connectionRequest.id) expect(connectionRepository.save).toHaveBeenCalledTimes(1) expect(processedConnection.id).not.toBe(connectionRecord.id) expect(connectionRecord.id).toBe('test') - expect(connectionRecord.state).toBe(ConnectionState.Invited) - }) - - it(`throws an error when connection role is ${ConnectionRole.Invitee} and not ${ConnectionRole.Inviter}`, async () => { - expect.assertions(1) - - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue( - Promise.resolve(getMockConnection({ role: ConnectionRole.Invitee })) - ) - - const inboundMessage = new InboundMessageContext(jest.fn()(), { - senderVerkey: 'senderVerkey', - recipientVerkey: 'recipientVerkey', - }) - - return expect(connectionService.processRequest(inboundMessage)).rejects.toThrowError( - `Connection record has invalid role ${ConnectionRole.Invitee}. Expected role ${ConnectionRole.Inviter}.` - ) + expect(connectionRecord.state).toBe(DidExchangeState.InvitationSent) }) - it('throws an error when the message does not contain a did doc with any recipientKeys', async () => { + it('throws an error when the message does not contain a did doc', async () => { expect.assertions(1) - const recipientVerkey = 'test-verkey' - - const connection = getMockConnection({ - role: ConnectionRole.Inviter, - verkey: recipientVerkey, - }) - - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(connection)) - const connectionRequest = new ConnectionRequestMessage({ did: 'did', label: 'test-label', - didDoc: new DidDoc({ - id: 'did:test', - publicKey: [], - service: [], - authentication: [], - }), }) const messageContext = new InboundMessageContext(connectionRequest, { - recipientVerkey, - senderVerkey: 'sender-verkey', + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), }) - return expect(connectionService.processRequest(messageContext)).rejects.toThrowError( - `Connection with id ${connection.id} has no recipient keys.` + const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Sender, state: OutOfBandState.AwaitResponse }) + + return expect(connectionService.processRequest(messageContext, outOfBand)).rejects.toThrowError( + `Public DIDs are not supported yet` ) }) - it('throws an error when a request for a multi use invitation is processed without routing provided', async () => { - const connectionRecord = getMockConnection({ - state: ConnectionState.Invited, - verkey: 'my-key', - role: ConnectionRole.Inviter, - multiUseInvitation: true, - }) - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(connectionRecord)) + it(`throws an error when out-of-band role is not ${OutOfBandRole.Sender}`, async () => { + expect.assertions(1) - const theirDidDoc = new DidDoc({ - id: 'their-did', - publicKey: [], - authentication: [], - service: [], + const inboundMessage = new InboundMessageContext(jest.fn()(), { + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), }) - const connectionRequest = new ConnectionRequestMessage({ - did: 'their-did', - didDoc: theirDidDoc, - label: 'test-label', - }) + const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Receiver, state: OutOfBandState.AwaitResponse }) - const messageContext = new InboundMessageContext(connectionRequest, { - connection: connectionRecord, - senderVerkey: 'their-verkey', - recipientVerkey: 'my-key', - }) - - expect(connectionService.processRequest(messageContext)).rejects.toThrowError( - 'Cannot process request for multi-use invitation without routing object. Make sure to call processRequest with the routing parameter provided.' + return expect(connectionService.processRequest(inboundMessage, outOfBand)).rejects.toThrowError( + `Invalid out-of-band record role ${OutOfBandRole.Receiver}, expected is ${OutOfBandRole.Sender}.` ) }) + + const invalidOutOfBandStates = [OutOfBandState.Initial, OutOfBandState.PrepareResponse, OutOfBandState.Done] + test.each(invalidOutOfBandStates)( + `throws an error when out-of-band state is %s and not ${OutOfBandState.AwaitResponse}`, + (state) => { + expect.assertions(1) + + const inboundMessage = new InboundMessageContext(jest.fn()(), {}) + const outOfBand = getMockOutOfBand({ role: OutOfBandRole.Sender, state }) + + return expect(connectionService.processRequest(inboundMessage, outOfBand)).rejects.toThrowError( + `Invalid out-of-band record state ${state}, valid states are: ${OutOfBandState.AwaitResponse}.` + ) + } + ) }) describe('createResponse', () => { @@ -545,54 +343,90 @@ describe('ConnectionService', () => { // Needed for signing connection~sig const { did, verkey } = await wallet.createDid() const mockConnection = getMockConnection({ - did, - verkey, - state: ConnectionState.Requested, - role: ConnectionRole.Inviter, + state: DidExchangeState.RequestReceived, + role: DidExchangeRole.Responder, tags: { threadId: 'test', }, }) - mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(mockConnection)) - const { message, connectionRecord: connectionRecord } = await connectionService.createResponse('test') + const recipientKeys = [new DidKey(Key.fromPublicKeyBase58(verkey, KeyType.Ed25519))] + const outOfBand = getMockOutOfBand({ did, recipientKeys: recipientKeys.map((did) => did.did) }) + const mockDidDoc = new DidDoc({ + id: did, + publicKey: [ + new Ed25119Sig2018({ + id: `${did}#1`, + controller: did, + publicKeyBase58: verkey, + }), + ], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `${did}#1`, + controller: did, + publicKeyBase58: verkey, + }) + ), + ], + service: [ + new IndyAgentService({ + id: `${did}#IndyAgentService`, + serviceEndpoint: 'http://example.com', + recipientKeys: recipientKeys.map((did) => did.key.publicKeyBase58), + routingKeys: [], + }), + ], + }) + + const { message, connectionRecord: connectionRecord } = await connectionService.createResponse( + mockConnection, + outOfBand + ) const connection = new Connection({ - did: mockConnection.did, - didDoc: mockConnection.didDoc, + did, + didDoc: mockDidDoc, }) const plainConnection = JsonTransformer.toJSON(connection) - expect(connectionRecord.state).toBe(ConnectionState.Responded) + expect(connectionRecord.state).toBe(DidExchangeState.ResponseSent) expect(await unpackAndVerifySignatureDecorator(message.connectionSig, wallet)).toEqual(plainConnection) }) - it(`throws an error when connection role is ${ConnectionRole.Invitee} and not ${ConnectionRole.Inviter}`, async () => { + it(`throws an error when connection role is ${DidExchangeRole.Requester} and not ${DidExchangeRole.Responder}`, async () => { expect.assertions(1) - mockFunction(connectionRepository.getById).mockReturnValue( - Promise.resolve( - getMockConnection({ - role: ConnectionRole.Invitee, - state: ConnectionState.Requested, - }) - ) - ) - return expect(connectionService.createResponse('test')).rejects.toThrowError( - `Connection record has invalid role ${ConnectionRole.Invitee}. Expected role ${ConnectionRole.Inviter}.` + const connection = getMockConnection({ + role: DidExchangeRole.Requester, + state: DidExchangeState.RequestReceived, + }) + const outOfBand = getMockOutOfBand() + return expect(connectionService.createResponse(connection, outOfBand)).rejects.toThrowError( + `Connection record has invalid role ${DidExchangeRole.Requester}. Expected role ${DidExchangeRole.Responder}.` ) }) - const invalidConnectionStates = [ConnectionState.Invited, ConnectionState.Responded, ConnectionState.Complete] - test.each(invalidConnectionStates)( - `throws an error when connection state is %s and not ${ConnectionState.Requested}`, + const invalidOutOfBandStates = [ + DidExchangeState.InvitationSent, + DidExchangeState.InvitationReceived, + DidExchangeState.RequestSent, + DidExchangeState.ResponseSent, + DidExchangeState.ResponseReceived, + DidExchangeState.Completed, + DidExchangeState.Abandoned, + DidExchangeState.Start, + ] + test.each(invalidOutOfBandStates)( + `throws an error when connection state is %s and not ${DidExchangeState.RequestReceived}`, async (state) => { expect.assertions(1) - mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(getMockConnection({ state }))) - - return expect(connectionService.createResponse('test')).rejects.toThrowError( - `Connection record is in invalid state ${state}. Valid states are: ${ConnectionState.Requested}.` + const connection = getMockConnection({ state }) + const outOfBand = getMockOutOfBand() + return expect(connectionService.createResponse(connection, outOfBand)).rejects.toThrowError( + `Connection record is in invalid state ${state}. Valid states are: ${DidExchangeState.RequestReceived}.` ) } ) @@ -600,36 +434,38 @@ describe('ConnectionService', () => { describe('processResponse', () => { it('returns a connection record containing the information from the connection response', async () => { - expect.assertions(3) + expect.assertions(2) const { did, verkey } = await wallet.createDid() const { did: theirDid, verkey: theirVerkey } = await wallet.createDid() const connectionRecord = getMockConnection({ did, - verkey, - state: ConnectionState.Requested, - role: ConnectionRole.Invitee, - invitation: new ConnectionInvitationMessage({ - label: 'test', - // processResponse checks wether invitation key is same as signing key for connetion~sig - recipientKeys: [theirVerkey], - serviceEndpoint: 'test', - }), + state: DidExchangeState.RequestSent, + role: DidExchangeRole.Requester, }) - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(connectionRecord)) + + const theirKey = Key.fromPublicKeyBase58(theirVerkey, KeyType.Ed25519) const otherPartyConnection = new Connection({ did: theirDid, didDoc: new DidDoc({ id: theirDid, publicKey: [], - authentication: [], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `${theirDid}#key-id`, + controller: theirDid, + publicKeyBase58: theirKey.publicKeyBase58, + }) + ), + ], service: [ - new DidCommService({ + new DidCommV1Service({ id: `${did};indy`, serviceEndpoint: 'https://endpoint.com', - recipientKeys: [theirVerkey], + recipientKeys: [`${theirDid}#key-id`], }), ], }), @@ -643,40 +479,40 @@ describe('ConnectionService', () => { connectionSig, }) + const outOfBandRecord = getMockOutOfBand({ + recipientKeys: [new DidKey(theirKey).did], + }) const messageContext = new InboundMessageContext(connectionResponse, { connection: connectionRecord, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - senderVerkey: connectionRecord.theirKey!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - recipientVerkey: connectionRecord.myKey!, + senderKey: theirKey, + recipientKey: Key.fromPublicKeyBase58(verkey, KeyType.Ed25519), }) - const processedConnection = await connectionService.processResponse(messageContext) + const processedConnection = await connectionService.processResponse(messageContext, outOfBandRecord) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const peerDid = didDocumentJsonToNumAlgo1Did(convertToNewDidDocument(otherPartyConnection.didDoc!).toJSON()) - expect(processedConnection.state).toBe(ConnectionState.Responded) - expect(processedConnection.theirDid).toBe(theirDid) - expect(processedConnection.theirDidDoc).toEqual(otherPartyConnection.didDoc) + expect(processedConnection.state).toBe(DidExchangeState.ResponseReceived) + expect(processedConnection.theirDid).toBe(peerDid) }) - it(`throws an error when connection role is ${ConnectionRole.Inviter} and not ${ConnectionRole.Invitee}`, async () => { + it(`throws an error when connection role is ${DidExchangeRole.Responder} and not ${DidExchangeRole.Requester}`, async () => { expect.assertions(1) - const inboundMessage = new InboundMessageContext(jest.fn()(), { - senderVerkey: 'senderVerkey', - recipientVerkey: 'recipientVerkey', + const outOfBandRecord = getMockOutOfBand() + const connectionRecord = getMockConnection({ + role: DidExchangeRole.Responder, + state: DidExchangeState.RequestSent, + }) + const messageContext = new InboundMessageContext(jest.fn()(), { + connection: connectionRecord, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), }) - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue( - Promise.resolve( - getMockConnection({ - role: ConnectionRole.Inviter, - state: ConnectionState.Requested, - }) - ) - ) - - return expect(connectionService.processResponse(inboundMessage)).rejects.toThrowError( - `Connection record has invalid role ${ConnectionRole.Inviter}. Expected role ${ConnectionRole.Invitee}.` + return expect(connectionService.processResponse(messageContext, outOfBandRecord)).rejects.toThrowError( + `Connection record has invalid role ${DidExchangeRole.Responder}. Expected role ${DidExchangeRole.Requester}.` ) }) @@ -687,23 +523,31 @@ describe('ConnectionService', () => { const { did: theirDid, verkey: theirVerkey } = await wallet.createDid() const connectionRecord = getMockConnection({ did, - verkey, - role: ConnectionRole.Invitee, - state: ConnectionState.Requested, + role: DidExchangeRole.Requester, + state: DidExchangeState.RequestSent, }) - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(connectionRecord)) + + const theirKey = Key.fromPublicKeyBase58(theirVerkey, KeyType.Ed25519) const otherPartyConnection = new Connection({ did: theirDid, didDoc: new DidDoc({ id: theirDid, publicKey: [], - authentication: [], + authentication: [ + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: `${theirDid}#key-id`, + controller: theirDid, + publicKeyBase58: theirKey.publicKeyBase58, + }) + ), + ], service: [ - new DidCommService({ + new DidCommV1Service({ id: `${did};indy`, serviceEndpoint: 'https://endpoint.com', - recipientKeys: [theirVerkey], + recipientKeys: [`${theirDid}#key-id`], }), ], }), @@ -716,83 +560,52 @@ describe('ConnectionService', () => { connectionSig, }) + // Recipient key `verkey` is not the same as theirVerkey which was used to sign message, + // therefore it should cause a failure. + const outOfBandRecord = getMockOutOfBand({ + recipientKeys: [new DidKey(Key.fromPublicKeyBase58(verkey, KeyType.Ed25519)).did], + }) const messageContext = new InboundMessageContext(connectionResponse, { connection: connectionRecord, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - senderVerkey: connectionRecord.theirKey!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - recipientVerkey: connectionRecord.myKey!, + senderKey: theirKey, + recipientKey: Key.fromPublicKeyBase58(verkey, KeyType.Ed25519), }) - return expect(connectionService.processResponse(messageContext)).rejects.toThrowError( + return expect(connectionService.processResponse(messageContext, outOfBandRecord)).rejects.toThrowError( new RegExp( 'Connection object in connection response message is not signed with same key as recipient key in invitation' ) ) }) - it('throws an error when the connection cannot be found by verkey', async () => { + it('throws an error when the message does not contain a DID Document', async () => { expect.assertions(1) - const connectionResponse = new ConnectionResponseMessage({ - threadId: uuid(), - connectionSig: new SignatureDecorator({ - signature: '', - signatureData: '', - signatureType: '', - signer: '', - }), - }) - - const messageContext = new InboundMessageContext(connectionResponse, { - recipientVerkey: 'test-verkey', - senderVerkey: 'sender-verkey', - }) - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(null)) - - return expect(connectionService.processResponse(messageContext)).rejects.toThrowError( - 'Unable to process connection response: connection for verkey test-verkey not found' - ) - }) - - it('throws an error when the message does not contain a did doc with any recipientKeys', async () => { - expect.assertions(1) - - const { did, verkey } = await wallet.createDid() + const { did } = await wallet.createDid() const { did: theirDid, verkey: theirVerkey } = await wallet.createDid() const connectionRecord = getMockConnection({ did, - verkey, - state: ConnectionState.Requested, - invitation: new ConnectionInvitationMessage({ - label: 'test', - // processResponse checks wether invitation key is same as signing key for connetion~sig - recipientKeys: [theirVerkey], - serviceEndpoint: 'test', - }), + state: DidExchangeState.RequestSent, theirDid: undefined, - theirDidDoc: undefined, }) - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(connectionRecord)) - const otherPartyConnection = new Connection({ - did: theirDid, - }) + const theirKey = Key.fromPublicKeyBase58(theirVerkey, KeyType.Ed25519) + + const otherPartyConnection = new Connection({ did: theirDid }) const plainConnection = JsonTransformer.toJSON(otherPartyConnection) const connectionSig = await signData(plainConnection, wallet, theirVerkey) - const connectionResponse = new ConnectionResponseMessage({ - threadId: uuid(), - connectionSig, - }) + const connectionResponse = new ConnectionResponseMessage({ threadId: uuid(), connectionSig }) + const outOfBandRecord = getMockOutOfBand({ recipientKeys: [new DidKey(theirKey).did] }) const messageContext = new InboundMessageContext(connectionResponse, { - senderVerkey: 'senderVerkey', - recipientVerkey: 'recipientVerkey', + connection: connectionRecord, + recipientKey: Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519), }) - return expect(connectionService.processResponse(messageContext)).rejects.toThrowError( - `Connection with id ${connectionRecord.id} has no recipient keys.` + return expect(connectionService.processResponse(messageContext, outOfBandRecord)).rejects.toThrowError( + `DID Document is missing.` ) }) }) @@ -801,26 +614,31 @@ describe('ConnectionService', () => { it('returns a trust ping message', async () => { expect.assertions(2) - const mockConnection = getMockConnection({ - state: ConnectionState.Responded, - }) - mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(mockConnection)) + const mockConnection = getMockConnection({ state: DidExchangeState.ResponseReceived }) - const { message, connectionRecord: connectionRecord } = await connectionService.createTrustPing('test') + const { message, connectionRecord: connectionRecord } = await connectionService.createTrustPing(mockConnection) - expect(connectionRecord.state).toBe(ConnectionState.Complete) + expect(connectionRecord.state).toBe(DidExchangeState.Completed) expect(message).toEqual(expect.any(TrustPingMessage)) }) - const invalidConnectionStates = [ConnectionState.Invited, ConnectionState.Requested] + const invalidConnectionStates = [ + DidExchangeState.InvitationSent, + DidExchangeState.InvitationReceived, + DidExchangeState.RequestSent, + DidExchangeState.RequestReceived, + DidExchangeState.ResponseSent, + DidExchangeState.Abandoned, + DidExchangeState.Start, + ] test.each(invalidConnectionStates)( - `throws an error when connection state is %s and not ${ConnectionState.Responded} or ${ConnectionState.Complete}`, + `throws an error when connection state is %s and not ${DidExchangeState.ResponseReceived} or ${DidExchangeState.Completed}`, (state) => { expect.assertions(1) + const connection = getMockConnection({ state }) - mockFunction(connectionRepository.getById).mockReturnValue(Promise.resolve(getMockConnection({ state }))) - return expect(connectionService.createTrustPing('test')).rejects.toThrowError( - `Connection record is in invalid state ${state}. Valid states are: ${ConnectionState.Responded}, ${ConnectionState.Complete}.` + return expect(connectionService.createTrustPing(connection)).rejects.toThrowError( + `Connection record is in invalid state ${state}. Valid states are: ${DidExchangeState.ResponseReceived}, ${DidExchangeState.Completed}.` ) } ) @@ -835,21 +653,19 @@ describe('ConnectionService', () => { threadId: 'thread-id', }) - const messageContext = new InboundMessageContext(ack, { - recipientVerkey: 'test-verkey', - }) + const messageContext = new InboundMessageContext(ack, {}) return expect(connectionService.processAck(messageContext)).rejects.toThrowError( - 'Unable to process connection ack: connection for verkey test-verkey not found' + 'Unable to process connection ack: connection for recipient key undefined not found' ) }) - it('updates the state to Completed when the state is Responded and role is Inviter', async () => { + it('updates the state to Completed when the state is ResponseSent and role is Responder', async () => { expect.assertions(1) const connection = getMockConnection({ - state: ConnectionState.Responded, - role: ConnectionRole.Inviter, + state: DidExchangeState.ResponseSent, + role: DidExchangeRole.Responder, }) const ack = new AckMessage({ @@ -857,22 +673,19 @@ describe('ConnectionService', () => { threadId: 'thread-id', }) - const messageContext = new InboundMessageContext(ack, { - recipientVerkey: 'test-verkey', - connection, - }) + const messageContext = new InboundMessageContext(ack, { connection }) const updatedConnection = await connectionService.processAck(messageContext) - expect(updatedConnection.state).toBe(ConnectionState.Complete) + expect(updatedConnection.state).toBe(DidExchangeState.Completed) }) - it('does not update the state when the state is not Responded or the role is not Inviter', async () => { + it('does not update the state when the state is not ResponseSent or the role is not Responder', async () => { expect.assertions(1) const connection = getMockConnection({ - state: ConnectionState.Responded, - role: ConnectionRole.Invitee, + state: DidExchangeState.ResponseReceived, + role: DidExchangeRole.Requester, }) const ack = new AckMessage({ @@ -880,14 +693,11 @@ describe('ConnectionService', () => { threadId: 'thread-id', }) - const messageContext = new InboundMessageContext(ack, { - recipientVerkey: 'test-verkey', - connection, - }) + const messageContext = new InboundMessageContext(ack, { connection }) const updatedConnection = await connectionService.processAck(messageContext) - expect(updatedConnection.state).toBe(ConnectionState.Responded) + expect(updatedConnection.state).toBe(DidExchangeState.ResponseReceived) }) }) @@ -896,7 +706,7 @@ describe('ConnectionService', () => { expect.assertions(1) const messageContext = new InboundMessageContext(new AgentMessage(), { - connection: getMockConnection({ state: ConnectionState.Complete }), + connection: getMockConnection({ state: DidExchangeState.Completed }), }) expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext)).not.toThrow() @@ -906,7 +716,7 @@ describe('ConnectionService', () => { expect.assertions(1) const messageContext = new InboundMessageContext(new AgentMessage(), { - connection: getMockConnection({ state: ConnectionState.Invited }), + connection: getMockConnection({ state: DidExchangeState.InvitationReceived }), }) expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext)).toThrowError( @@ -931,19 +741,19 @@ describe('ConnectionService', () => { it('should not throw when a fully valid connection-less input is passed', () => { expect.assertions(1) - const senderKey = 'senderKey' - const recipientKey = 'recipientKey' + const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + const senderKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) const previousSentMessage = new AgentMessage() previousSentMessage.setService({ - recipientKeys: [recipientKey], + recipientKeys: [recipientKey.publicKeyBase58], serviceEndpoint: '', routingKeys: [], }) const previousReceivedMessage = new AgentMessage() previousReceivedMessage.setService({ - recipientKeys: [senderKey], + recipientKeys: [senderKey.publicKeyBase58], serviceEndpoint: '', routingKeys: [], }) @@ -954,10 +764,7 @@ describe('ConnectionService', () => { serviceEndpoint: '', routingKeys: [], }) - const messageContext = new InboundMessageContext(message, { - recipientVerkey: recipientKey, - senderVerkey: senderKey, - }) + const messageContext = new InboundMessageContext(message, { recipientKey, senderKey }) expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext, { @@ -990,7 +797,7 @@ describe('ConnectionService', () => { it('should throw an error when previousSentMessage and recipientKey are present, but recipient key is not present in recipientKeys of previously sent message ~service decorator', () => { expect.assertions(1) - const recipientKey = 'recipientKey' + const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) const previousSentMessage = new AgentMessage() previousSentMessage.setService({ @@ -1000,9 +807,7 @@ describe('ConnectionService', () => { }) const message = new AgentMessage() - const messageContext = new InboundMessageContext(message, { - recipientVerkey: recipientKey, - }) + const messageContext = new InboundMessageContext(message, { recipientKey }) expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext, { @@ -1047,7 +852,7 @@ describe('ConnectionService', () => { const message = new AgentMessage() const messageContext = new InboundMessageContext(message, { - senderVerkey: senderKey, + senderKey: Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519), }) expect(() => @@ -1070,11 +875,11 @@ describe('ConnectionService', () => { expect(result).toBe(expected) }) - it('getById should return value from connectionRepository.getSingleByQuery', async () => { + it('getByThreadId should return value from connectionRepository.getSingleByQuery', async () => { const expected = getMockConnection() - mockFunction(connectionRepository.getSingleByQuery).mockReturnValue(Promise.resolve(expected)) + mockFunction(connectionRepository.getByThreadId).mockReturnValue(Promise.resolve(expected)) const result = await connectionService.getByThreadId('threadId') - expect(connectionRepository.getSingleByQuery).toBeCalledWith({ threadId: 'threadId' }) + expect(connectionRepository.getByThreadId).toBeCalledWith('threadId') expect(result).toBe(expected) }) @@ -1088,24 +893,6 @@ describe('ConnectionService', () => { expect(result).toBe(expected) }) - it('findByVerkey should return value from connectionRepository.findSingleByQuery', async () => { - const expected = getMockConnection() - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(expected)) - const result = await connectionService.findByVerkey('verkey') - expect(connectionRepository.findSingleByQuery).toBeCalledWith({ verkey: 'verkey' }) - - expect(result).toBe(expected) - }) - - it('findByTheirKey should return value from connectionRepository.findSingleByQuery', async () => { - const expected = getMockConnection() - mockFunction(connectionRepository.findSingleByQuery).mockReturnValue(Promise.resolve(expected)) - const result = await connectionService.findByTheirKey('theirKey') - expect(connectionRepository.findSingleByQuery).toBeCalledWith({ theirKey: 'theirKey' }) - - expect(result).toBe(expected) - }) - it('getAll should return value from connectionRepository.getAll', async () => { const expected = [getMockConnection(), getMockConnection()] diff --git a/packages/core/src/modules/connections/__tests__/ConnectionState.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionState.test.ts deleted file mode 100644 index fba8caff43..0000000000 --- a/packages/core/src/modules/connections/__tests__/ConnectionState.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ConnectionState } from '../models/ConnectionState' - -describe('ConnectionState', () => { - test('state matches Connection 1.0 (RFC 0160) state value', () => { - expect(ConnectionState.Invited).toBe('invited') - expect(ConnectionState.Requested).toBe('requested') - expect(ConnectionState.Responded).toBe('responded') - expect(ConnectionState.Complete).toBe('complete') - }) -}) diff --git a/packages/core/src/modules/connections/__tests__/helpers.test.ts b/packages/core/src/modules/connections/__tests__/helpers.test.ts new file mode 100644 index 0000000000..3bdc2977fb --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/helpers.test.ts @@ -0,0 +1,103 @@ +import { DidCommV1Service, IndyAgentService, VerificationMethod } from '../../dids' +import { + DidDoc, + Ed25119Sig2018, + EddsaSaSigSecp256k1, + EmbeddedAuthentication, + ReferencedAuthentication, + RsaSig2018, +} from '../models' +import { convertToNewDidDocument } from '../services/helpers' + +const key = new Ed25119Sig2018({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo#4', + controller: 'did:sov:SKJVx2kn373FNgvff1SbJo', + publicKeyBase58: 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', +}) +const didDoc = new DidDoc({ + authentication: [ + new ReferencedAuthentication(key, 'Ed25519SignatureAuthentication2018'), + new EmbeddedAuthentication( + new Ed25119Sig2018({ + id: '#8', + controller: 'did:sov:SKJVx2kn373FNgvff1SbJo', + publicKeyBase58: '5UQ3drtEMMQXaLLmEywbciW92jZaQgRYgfuzXfonV8iz', + }) + ), + ], + id: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKey: [ + key, + new RsaSig2018({ + id: '#3', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC X...', + }), + new EddsaSaSigSecp256k1({ + id: '#6', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }), + ], + service: [ + new IndyAgentService({ + id: 'did:sov:SKJVx2kn373FNgvff1SbJo#service-1', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 5, + }), + new DidCommV1Service({ + id: '#service-2', + serviceEndpoint: 'https://agent.com', + recipientKeys: ['did:sov:SKJVx2kn373FNgvff1SbJo#4', '#8'], + routingKeys: [ + 'did:key:z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1#z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1', + ], + priority: 2, + }), + ], +}) + +describe('convertToNewDidDocument', () => { + test('create a new DidDocument and with authentication, publicKey and service from DidDoc', () => { + const oldDocument = didDoc + const newDocument = convertToNewDidDocument(oldDocument) + + expect(newDocument.authentication).toEqual(['#EoGusetS', '#5UQ3drtE']) + + expect(newDocument.verificationMethod).toEqual([ + new VerificationMethod({ + id: '#5UQ3drtE', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: '5UQ3drtEMMQXaLLmEywbciW92jZaQgRYgfuzXfonV8iz', + }), + new VerificationMethod({ + id: '#EoGusetS', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: 'EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d', + }), + ]) + + expect(newDocument.service).toEqual([ + new IndyAgentService({ + id: '#service-1', + serviceEndpoint: 'did:sov:SKJVx2kn373FNgvff1SbJo', + recipientKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + routingKeys: ['EoGusetSxDJktp493VCyh981nUnzMamTRjvBaHZAy68d'], + priority: 5, + }), + new DidCommV1Service({ + id: '#service-2', + serviceEndpoint: 'https://agent.com', + recipientKeys: ['#EoGusetS', '#5UQ3drtE'], + routingKeys: [ + 'did:key:z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1#z6MktFXxTu8tHkoE1Jtqj4ApYEg1c44qmU1p7kq7QZXBtJv1', + ], + priority: 2, + }), + ]) + }) +}) diff --git a/packages/core/src/modules/connections/errors/ConnectionProblemReportError.ts b/packages/core/src/modules/connections/errors/ConnectionProblemReportError.ts new file mode 100644 index 0000000000..764be043f9 --- /dev/null +++ b/packages/core/src/modules/connections/errors/ConnectionProblemReportError.ts @@ -0,0 +1,22 @@ +import type { ProblemReportErrorOptions } from '../../problem-reports' +import type { ConnectionProblemReportReason } from './ConnectionProblemReportReason' + +import { ProblemReportError } from '../../problem-reports' +import { ConnectionProblemReportMessage } from '../messages' + +interface ConnectionProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: ConnectionProblemReportReason +} +export class ConnectionProblemReportError extends ProblemReportError { + public problemReport: ConnectionProblemReportMessage + + public constructor(public message: string, { problemCode }: ConnectionProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new ConnectionProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/connections/errors/ConnectionProblemReportReason.ts b/packages/core/src/modules/connections/errors/ConnectionProblemReportReason.ts new file mode 100644 index 0000000000..06f81b83c3 --- /dev/null +++ b/packages/core/src/modules/connections/errors/ConnectionProblemReportReason.ts @@ -0,0 +1,11 @@ +/** + * Connection error code in RFC 160. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0160-connection-protocol/README.md#errors + */ +export enum ConnectionProblemReportReason { + RequestNotAccepted = 'request_not_accepted', + RequestProcessingError = 'request_processing_error', + ResponseNotAccepted = 'response_not_accepted', + ResponseProcessingError = 'response_processing_error', +} diff --git a/packages/core/src/modules/connections/errors/DidExchangeProblemReportError.ts b/packages/core/src/modules/connections/errors/DidExchangeProblemReportError.ts new file mode 100644 index 0000000000..17bf72ad9b --- /dev/null +++ b/packages/core/src/modules/connections/errors/DidExchangeProblemReportError.ts @@ -0,0 +1,22 @@ +import type { ProblemReportErrorOptions } from '../../problem-reports' +import type { DidExchangeProblemReportReason } from './DidExchangeProblemReportReason' + +import { ProblemReportError } from '../../problem-reports' +import { DidExchangeProblemReportMessage } from '../messages' + +interface DidExchangeProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: DidExchangeProblemReportReason +} +export class DidExchangeProblemReportError extends ProblemReportError { + public problemReport: DidExchangeProblemReportMessage + + public constructor(public message: string, { problemCode }: DidExchangeProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new DidExchangeProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/connections/errors/DidExchangeProblemReportReason.ts b/packages/core/src/modules/connections/errors/DidExchangeProblemReportReason.ts new file mode 100644 index 0000000000..28f31dc6d4 --- /dev/null +++ b/packages/core/src/modules/connections/errors/DidExchangeProblemReportReason.ts @@ -0,0 +1,12 @@ +/** + * Connection error code in RFC 0023. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#errors + */ +export const enum DidExchangeProblemReportReason { + RequestNotAccepted = 'request_not_accepted', + RequestProcessingError = 'request_processing_error', + ResponseNotAccepted = 'response_not_accepted', + ResponseProcessingError = 'response_processing_error', + CompleteRejected = 'complete_rejected', +} diff --git a/packages/core/src/modules/connections/errors/index.ts b/packages/core/src/modules/connections/errors/index.ts new file mode 100644 index 0000000000..c745a4cdde --- /dev/null +++ b/packages/core/src/modules/connections/errors/index.ts @@ -0,0 +1,4 @@ +export * from './ConnectionProblemReportError' +export * from './ConnectionProblemReportReason' +export * from './DidExchangeProblemReportError' +export * from './DidExchangeProblemReportReason' diff --git a/packages/core/src/modules/connections/handlers/ConnectionProblemReportHandler.ts b/packages/core/src/modules/connections/handlers/ConnectionProblemReportHandler.ts new file mode 100644 index 0000000000..b1b5896017 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/ConnectionProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { ConnectionService } from '../services' + +import { ConnectionProblemReportMessage } from '../messages' + +export class ConnectionProblemReportHandler implements Handler { + private connectionService: ConnectionService + public supportedMessages = [ConnectionProblemReportMessage] + + public constructor(connectionService: ConnectionService) { + this.connectionService = connectionService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.connectionService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/connections/handlers/ConnectionRequestHandler.ts b/packages/core/src/modules/connections/handlers/ConnectionRequestHandler.ts index 2f6051afd0..758ed3323f 100644 --- a/packages/core/src/modules/connections/handlers/ConnectionRequestHandler.ts +++ b/packages/core/src/modules/connections/handlers/ConnectionRequestHandler.ts @@ -1,52 +1,70 @@ import type { AgentConfig } from '../../../agent/AgentConfig' import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { DidRepository } from '../../dids/repository' +import type { OutOfBandService } from '../../oob/OutOfBandService' import type { MediationRecipientService } from '../../routing/services/MediationRecipientService' -import type { ConnectionService, Routing } from '../services/ConnectionService' +import type { ConnectionService } from '../services/ConnectionService' import { createOutboundMessage } from '../../../agent/helpers' import { AriesFrameworkError } from '../../../error/AriesFrameworkError' import { ConnectionRequestMessage } from '../messages' export class ConnectionRequestHandler implements Handler { - private connectionService: ConnectionService private agentConfig: AgentConfig + private connectionService: ConnectionService + private outOfBandService: OutOfBandService private mediationRecipientService: MediationRecipientService + private didRepository: DidRepository public supportedMessages = [ConnectionRequestMessage] public constructor( - connectionService: ConnectionService, agentConfig: AgentConfig, - mediationRecipientService: MediationRecipientService + connectionService: ConnectionService, + outOfBandService: OutOfBandService, + mediationRecipientService: MediationRecipientService, + didRepository: DidRepository ) { - this.connectionService = connectionService this.agentConfig = agentConfig + this.connectionService = connectionService + this.outOfBandService = outOfBandService this.mediationRecipientService = mediationRecipientService + this.didRepository = didRepository } public async handle(messageContext: HandlerInboundMessage) { - if (!messageContext.recipientVerkey || !messageContext.senderVerkey) { - throw new AriesFrameworkError('Unable to process connection request without senderVerkey or recipientVerkey') + const { connection, recipientKey, senderKey } = messageContext + + if (!recipientKey || !senderKey) { + throw new AriesFrameworkError('Unable to process connection request without senderVerkey or recipientKey') } - let connectionRecord = await this.connectionService.findByVerkey(messageContext.recipientVerkey) - if (!connectionRecord) { - throw new AriesFrameworkError(`Connection for verkey ${messageContext.recipientVerkey} not found!`) + const outOfBandRecord = await this.outOfBandService.findByRecipientKey(recipientKey) + + if (!outOfBandRecord) { + throw new AriesFrameworkError(`Out-of-band record for recipient key ${recipientKey.fingerprint} was not found.`) } - let routing: Routing | undefined + if (connection && !outOfBandRecord.reusable) { + throw new AriesFrameworkError( + `Connection record for non-reusable out-of-band ${outOfBandRecord.id} already exists.` + ) + } - // routing object is required for multi use invitation, because we're creating a - // new keypair that possibly needs to be registered at a mediator - if (connectionRecord.multiUseInvitation) { - const mediationRecord = await this.mediationRecipientService.discoverMediation() - routing = await this.mediationRecipientService.getRouting(mediationRecord) + const didRecord = await this.didRepository.findByRecipientKey(senderKey) + if (didRecord) { + throw new AriesFrameworkError(`Did record for sender key ${senderKey.fingerprint} already exists.`) } - connectionRecord = await this.connectionService.processRequest(messageContext, routing) + // TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable + let routing + if (outOfBandRecord.reusable) { + routing = await this.mediationRecipientService.getRouting() + } + const connectionRecord = await this.connectionService.processRequest(messageContext, outOfBandRecord, routing) if (connectionRecord?.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) { - const { message } = await this.connectionService.createResponse(connectionRecord.id) - return createOutboundMessage(connectionRecord, message) + const { message } = await this.connectionService.createResponse(connectionRecord, outOfBandRecord, routing) + return createOutboundMessage(connectionRecord, message, outOfBandRecord) } } } diff --git a/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts b/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts index c227ba4868..1cf86ae359 100644 --- a/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts +++ b/packages/core/src/modules/connections/handlers/ConnectionResponseHandler.ts @@ -1,28 +1,74 @@ import type { AgentConfig } from '../../../agent/AgentConfig' import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { DidResolverService } from '../../dids' +import type { OutOfBandService } from '../../oob/OutOfBandService' import type { ConnectionService } from '../services/ConnectionService' import { createOutboundMessage } from '../../../agent/helpers' +import { AriesFrameworkError } from '../../../error' import { ConnectionResponseMessage } from '../messages' export class ConnectionResponseHandler implements Handler { - private connectionService: ConnectionService private agentConfig: AgentConfig + private connectionService: ConnectionService + private outOfBandService: OutOfBandService + private didResolverService: DidResolverService + public supportedMessages = [ConnectionResponseMessage] - public constructor(connectionService: ConnectionService, agentConfig: AgentConfig) { - this.connectionService = connectionService + public constructor( + agentConfig: AgentConfig, + connectionService: ConnectionService, + outOfBandService: OutOfBandService, + didResolverService: DidResolverService + ) { this.agentConfig = agentConfig + this.connectionService = connectionService + this.outOfBandService = outOfBandService + this.didResolverService = didResolverService } public async handle(messageContext: HandlerInboundMessage) { - const connection = await this.connectionService.processResponse(messageContext) + const { recipientKey, senderKey, message } = messageContext + + if (!recipientKey || !senderKey) { + throw new AriesFrameworkError('Unable to process connection response without senderKey or recipientKey') + } + + const connectionRecord = await this.connectionService.getByThreadId(message.threadId) + if (!connectionRecord) { + throw new AriesFrameworkError(`Connection for thread ID ${message.threadId} not found!`) + } + + const ourDidDocument = await this.didResolverService.resolveDidDocument(connectionRecord.did) + if (!ourDidDocument) { + throw new AriesFrameworkError(`Did document for did ${connectionRecord.did} was not resolved!`) + } + + // Validate if recipient key is included in recipient keys of the did document resolved by + // connection record did + if (!ourDidDocument.recipientKeys.find((key) => key.fingerprint === recipientKey.fingerprint)) { + throw new AriesFrameworkError( + `Recipient key ${recipientKey.fingerprint} not found in did document recipient keys.` + ) + } + + const outOfBandRecord = + connectionRecord.outOfBandId && (await this.outOfBandService.findById(connectionRecord.outOfBandId)) + + if (!outOfBandRecord) { + throw new AriesFrameworkError(`Out-of-band record ${connectionRecord.outOfBandId} was not found.`) + } + + messageContext.connection = connectionRecord + // The presence of outOfBandRecord is not mandatory when the old connection invitation is used + const connection = await this.connectionService.processResponse(messageContext, outOfBandRecord) // TODO: should we only send ping message in case of autoAcceptConnection or always? // In AATH we have a separate step to send the ping. So for now we'll only do it // if auto accept is enable if (connection.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) { - const { message } = await this.connectionService.createTrustPing(connection.id, { responseRequested: false }) + const { message } = await this.connectionService.createTrustPing(connection, { responseRequested: false }) return createOutboundMessage(connection, message) } } diff --git a/packages/core/src/modules/connections/handlers/DidExchangeCompleteHandler.ts b/packages/core/src/modules/connections/handlers/DidExchangeCompleteHandler.ts new file mode 100644 index 0000000000..d3f4a6eae6 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidExchangeCompleteHandler.ts @@ -0,0 +1,49 @@ +import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { OutOfBandService } from '../../oob/OutOfBandService' +import type { DidExchangeProtocol } from '../DidExchangeProtocol' + +import { AriesFrameworkError } from '../../../error' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { DidExchangeCompleteMessage } from '../messages' +import { HandshakeProtocol } from '../models' + +export class DidExchangeCompleteHandler implements Handler { + private didExchangeProtocol: DidExchangeProtocol + private outOfBandService: OutOfBandService + public supportedMessages = [DidExchangeCompleteMessage] + + public constructor(didExchangeProtocol: DidExchangeProtocol, outOfBandService: OutOfBandService) { + this.didExchangeProtocol = didExchangeProtocol + this.outOfBandService = outOfBandService + } + + public async handle(messageContext: HandlerInboundMessage) { + const { connection: connectionRecord } = messageContext + + if (!connectionRecord) { + throw new AriesFrameworkError(`Connection is missing in message context`) + } + + const { protocol } = connectionRecord + if (protocol !== HandshakeProtocol.DidExchange) { + throw new AriesFrameworkError( + `Connection record protocol is ${protocol} but handler supports only ${HandshakeProtocol.DidExchange}.` + ) + } + + const { message } = messageContext + if (!message.thread?.parentThreadId) { + throw new AriesFrameworkError(`Message does not contain pthid attribute`) + } + const outOfBandRecord = await this.outOfBandService.findByInvitationId(message.thread?.parentThreadId) + + if (!outOfBandRecord) { + throw new AriesFrameworkError(`OutOfBand record for message ID ${message.thread?.parentThreadId} not found!`) + } + + if (!outOfBandRecord.reusable) { + await this.outOfBandService.updateState(outOfBandRecord, OutOfBandState.Done) + } + await this.didExchangeProtocol.processComplete(messageContext, outOfBandRecord) + } +} diff --git a/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts b/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts new file mode 100644 index 0000000000..9ffa837d70 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidExchangeRequestHandler.ts @@ -0,0 +1,84 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { DidRepository } from '../../dids/repository' +import type { OutOfBandService } from '../../oob/OutOfBandService' +import type { MediationRecipientService } from '../../routing/services/MediationRecipientService' +import type { DidExchangeProtocol } from '../DidExchangeProtocol' + +import { createOutboundMessage } from '../../../agent/helpers' +import { AriesFrameworkError } from '../../../error/AriesFrameworkError' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { DidExchangeRequestMessage } from '../messages' + +export class DidExchangeRequestHandler implements Handler { + private didExchangeProtocol: DidExchangeProtocol + private outOfBandService: OutOfBandService + private agentConfig: AgentConfig + private mediationRecipientService: MediationRecipientService + private didRepository: DidRepository + public supportedMessages = [DidExchangeRequestMessage] + + public constructor( + agentConfig: AgentConfig, + didExchangeProtocol: DidExchangeProtocol, + outOfBandService: OutOfBandService, + mediationRecipientService: MediationRecipientService, + didRepository: DidRepository + ) { + this.agentConfig = agentConfig + this.didExchangeProtocol = didExchangeProtocol + this.outOfBandService = outOfBandService + this.mediationRecipientService = mediationRecipientService + this.didRepository = didRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const { recipientKey, senderKey, message, connection } = messageContext + + if (!recipientKey || !senderKey) { + throw new AriesFrameworkError('Unable to process connection request without senderKey or recipientKey') + } + + if (!message.thread?.parentThreadId) { + throw new AriesFrameworkError(`Message does not contain 'pthid' attribute`) + } + const outOfBandRecord = await this.outOfBandService.findByInvitationId(message.thread.parentThreadId) + + if (!outOfBandRecord) { + throw new AriesFrameworkError(`OutOfBand record for message ID ${message.thread?.parentThreadId} not found!`) + } + + if (connection && !outOfBandRecord.reusable) { + throw new AriesFrameworkError( + `Connection record for non-reusable out-of-band ${outOfBandRecord.id} already exists.` + ) + } + + const didRecord = await this.didRepository.findByRecipientKey(senderKey) + if (didRecord) { + throw new AriesFrameworkError(`Did record for sender key ${senderKey.fingerprint} already exists.`) + } + + // TODO Shouldn't we check also if the keys match the keys from oob invitation services? + + if (outOfBandRecord.state === OutOfBandState.Done) { + throw new AriesFrameworkError( + 'Out-of-band record has been already processed and it does not accept any new requests' + ) + } + + // TODO: Allow rotation of keys used in the invitation for new ones not only when out-of-band is reusable + let routing + if (outOfBandRecord.reusable) { + routing = await this.mediationRecipientService.getRouting() + } + + const connectionRecord = await this.didExchangeProtocol.processRequest(messageContext, outOfBandRecord, routing) + + if (connectionRecord?.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) { + // TODO We should add an option to not pass routing and therefore do not rotate keys and use the keys from the invitation + const message = await this.didExchangeProtocol.createResponse(connectionRecord, outOfBandRecord, routing) + return createOutboundMessage(connectionRecord, message, outOfBandRecord) + } + } +} diff --git a/packages/core/src/modules/connections/handlers/DidExchangeResponseHandler.ts b/packages/core/src/modules/connections/handlers/DidExchangeResponseHandler.ts new file mode 100644 index 0000000000..ff66579e0a --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidExchangeResponseHandler.ts @@ -0,0 +1,114 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { DidResolverService } from '../../dids' +import type { OutOfBandService } from '../../oob/OutOfBandService' +import type { DidExchangeProtocol } from '../DidExchangeProtocol' +import type { ConnectionService } from '../services' + +import { createOutboundMessage } from '../../../agent/helpers' +import { AriesFrameworkError } from '../../../error' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { DidExchangeResponseMessage } from '../messages' +import { HandshakeProtocol } from '../models' + +export class DidExchangeResponseHandler implements Handler { + private agentConfig: AgentConfig + private didExchangeProtocol: DidExchangeProtocol + private outOfBandService: OutOfBandService + private connectionService: ConnectionService + private didResolverService: DidResolverService + public supportedMessages = [DidExchangeResponseMessage] + + public constructor( + agentConfig: AgentConfig, + didExchangeProtocol: DidExchangeProtocol, + outOfBandService: OutOfBandService, + connectionService: ConnectionService, + didResolverService: DidResolverService + ) { + this.agentConfig = agentConfig + this.didExchangeProtocol = didExchangeProtocol + this.outOfBandService = outOfBandService + this.connectionService = connectionService + this.didResolverService = didResolverService + } + + public async handle(messageContext: HandlerInboundMessage) { + const { recipientKey, senderKey, message } = messageContext + + if (!recipientKey || !senderKey) { + throw new AriesFrameworkError('Unable to process connection response without sender key or recipient key') + } + + const connectionRecord = await this.connectionService.getByThreadId(message.threadId) + if (!connectionRecord) { + throw new AriesFrameworkError(`Connection for thread ID ${message.threadId} not found!`) + } + + const ourDidDocument = await this.resolveDidDocument(connectionRecord.did) + if (!ourDidDocument) { + throw new AriesFrameworkError(`Did document for did ${connectionRecord.did} was not resolved!`) + } + + // Validate if recipient key is included in recipient keys of the did document resolved by + // connection record did + if (!ourDidDocument.recipientKeys.find((key) => key.fingerprint === recipientKey.fingerprint)) { + throw new AriesFrameworkError( + `Recipient key ${recipientKey.fingerprint} not found in did document recipient keys.` + ) + } + + const { protocol } = connectionRecord + if (protocol !== HandshakeProtocol.DidExchange) { + throw new AriesFrameworkError( + `Connection record protocol is ${protocol} but handler supports only ${HandshakeProtocol.DidExchange}.` + ) + } + + if (!connectionRecord.outOfBandId) { + throw new AriesFrameworkError(`Connection ${connectionRecord.id} does not have outOfBandId!`) + } + + const outOfBandRecord = await this.outOfBandService.findById(connectionRecord.outOfBandId) + + if (!outOfBandRecord) { + throw new AriesFrameworkError( + `OutOfBand record for connection ${connectionRecord.id} with outOfBandId ${connectionRecord.outOfBandId} not found!` + ) + } + + // TODO + // + // A connection request message is the only case when I can use the connection record found + // only based on recipient key without checking that `theirKey` is equal to sender key. + // + // The question is if we should do it here in this way or rather somewhere else to keep + // responsibility of all handlers aligned. + // + messageContext.connection = connectionRecord + const connection = await this.didExchangeProtocol.processResponse(messageContext, outOfBandRecord) + + // TODO: should we only send complete message in case of autoAcceptConnection or always? + // In AATH we have a separate step to send the complete. So for now we'll only do it + // if auto accept is enable + if (connection.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections) { + const message = await this.didExchangeProtocol.createComplete(connection, outOfBandRecord) + if (!outOfBandRecord.reusable) { + await this.outOfBandService.updateState(outOfBandRecord, OutOfBandState.Done) + } + return createOutboundMessage(connection, message) + } + } + + private async resolveDidDocument(did: string) { + const { + didDocument, + didResolutionMetadata: { error, message }, + } = await this.didResolverService.resolve(did) + + if (!didDocument) { + throw new AriesFrameworkError(`Unable to resolve did document for did '${did}': ${error} ${message}`) + } + return didDocument + } +} diff --git a/packages/core/src/modules/connections/handlers/TrustPingMessageHandler.ts b/packages/core/src/modules/connections/handlers/TrustPingMessageHandler.ts index 4b5c807016..6a37fee4b6 100644 --- a/packages/core/src/modules/connections/handlers/TrustPingMessageHandler.ts +++ b/packages/core/src/modules/connections/handlers/TrustPingMessageHandler.ts @@ -4,7 +4,7 @@ import type { TrustPingService } from '../services/TrustPingService' import { AriesFrameworkError } from '../../../error' import { TrustPingMessage } from '../messages' -import { ConnectionState } from '../models' +import { DidExchangeState } from '../models' export class TrustPingMessageHandler implements Handler { private trustPingService: TrustPingService @@ -17,15 +17,15 @@ export class TrustPingMessageHandler implements Handler { } public async handle(messageContext: HandlerInboundMessage) { - const { connection, recipientVerkey } = messageContext + const { connection, recipientKey } = messageContext if (!connection) { - throw new AriesFrameworkError(`Connection for verkey ${recipientVerkey} not found!`) + throw new AriesFrameworkError(`Connection for verkey ${recipientKey?.fingerprint} not found!`) } // TODO: This is better addressed in a middleware of some kind because // any message can transition the state to complete, not just an ack or trust ping - if (connection.state === ConnectionState.Responded) { - await this.connectionService.updateState(connection, ConnectionState.Complete) + if (connection.state === DidExchangeState.ResponseSent) { + await this.connectionService.updateState(connection, DidExchangeState.Completed) } return this.trustPingService.processPing(messageContext, connection) diff --git a/packages/core/src/modules/connections/handlers/index.ts b/packages/core/src/modules/connections/handlers/index.ts index 4fa2965953..09226eaf34 100644 --- a/packages/core/src/modules/connections/handlers/index.ts +++ b/packages/core/src/modules/connections/handlers/index.ts @@ -3,3 +3,6 @@ export * from './ConnectionRequestHandler' export * from './ConnectionResponseHandler' export * from './TrustPingMessageHandler' export * from './TrustPingResponseMessageHandler' +export * from './DidExchangeRequestHandler' +export * from './DidExchangeResponseHandler' +export * from './DidExchangeCompleteHandler' diff --git a/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts b/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts index a686d092c1..004429f115 100644 --- a/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts +++ b/packages/core/src/modules/connections/messages/ConnectionInvitationMessage.ts @@ -1,5 +1,5 @@ import { Transform } from 'class-transformer' -import { ArrayNotEmpty, Equals, IsArray, IsOptional, IsString, IsUrl, ValidateIf } from 'class-validator' +import { ArrayNotEmpty, IsArray, IsOptional, IsString, IsUrl, ValidateIf } from 'class-validator' import { parseUrl } from 'query-string' import { AgentMessage } from '../../../agent/AgentMessage' @@ -7,7 +7,7 @@ import { AriesFrameworkError } from '../../../error' import { JsonEncoder } from '../../../utils/JsonEncoder' import { JsonTransformer } from '../../../utils/JsonTransformer' import { MessageValidator } from '../../../utils/MessageValidator' -import { replaceLegacyDidSovPrefix } from '../../../utils/messageType' +import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../utils/messageType' export interface BaseInvitationOptions { id?: string @@ -61,12 +61,12 @@ export class ConnectionInvitationMessage extends AgentMessage { } } - @Equals(ConnectionInvitationMessage.type) + @IsValidMessageType(ConnectionInvitationMessage.type) @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { toClassOnly: true, }) - public readonly type = ConnectionInvitationMessage.type - public static readonly type = 'https://didcomm.org/connections/1.0/invitation' + public readonly type = ConnectionInvitationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/invitation') @IsString() public label!: string diff --git a/packages/core/src/modules/connections/messages/ConnectionProblemReportMessage.ts b/packages/core/src/modules/connections/messages/ConnectionProblemReportMessage.ts new file mode 100644 index 0000000000..3aaee94062 --- /dev/null +++ b/packages/core/src/modules/connections/messages/ConnectionProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage' + +export type ConnectionProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class ConnectionProblemReportMessage extends ProblemReportMessage { + /** + * Create new ConnectionProblemReportMessage instance. + * @param options + */ + public constructor(options: ConnectionProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(ConnectionProblemReportMessage.type) + public readonly type = ConnectionProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/connection/1.0/problem-report') +} diff --git a/packages/core/src/modules/connections/messages/ConnectionRequestMessage.ts b/packages/core/src/modules/connections/messages/ConnectionRequestMessage.ts index f1b0720847..e86912966d 100644 --- a/packages/core/src/modules/connections/messages/ConnectionRequestMessage.ts +++ b/packages/core/src/modules/connections/messages/ConnectionRequestMessage.ts @@ -1,9 +1,10 @@ import type { DidDoc } from '../models' import { Type } from 'class-transformer' -import { Equals, IsInstance, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator' +import { IsInstance, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' import { Connection } from '../models' export interface ConnectionRequestMessageOptions { @@ -39,9 +40,9 @@ export class ConnectionRequestMessage extends AgentMessage { } } - @Equals(ConnectionRequestMessage.type) - public readonly type = ConnectionRequestMessage.type - public static readonly type = 'https://didcomm.org/connections/1.0/request' + @IsValidMessageType(ConnectionRequestMessage.type) + public readonly type = ConnectionRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/request') @IsString() public label!: string diff --git a/packages/core/src/modules/connections/messages/ConnectionResponseMessage.ts b/packages/core/src/modules/connections/messages/ConnectionResponseMessage.ts index 6082bf1fc3..91a3a48498 100644 --- a/packages/core/src/modules/connections/messages/ConnectionResponseMessage.ts +++ b/packages/core/src/modules/connections/messages/ConnectionResponseMessage.ts @@ -1,8 +1,9 @@ import { Type, Expose } from 'class-transformer' -import { Equals, IsInstance, ValidateNested } from 'class-validator' +import { IsInstance, ValidateNested } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' import { SignatureDecorator } from '../../../decorators/signature/SignatureDecorator' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface ConnectionResponseMessageOptions { id?: string @@ -31,9 +32,9 @@ export class ConnectionResponseMessage extends AgentMessage { } } - @Equals(ConnectionResponseMessage.type) - public readonly type = ConnectionResponseMessage.type - public static readonly type = 'https://didcomm.org/connections/1.0/response' + @IsValidMessageType(ConnectionResponseMessage.type) + public readonly type = ConnectionResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/connections/1.0/response') @Type(() => SignatureDecorator) @ValidateNested() diff --git a/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts new file mode 100644 index 0000000000..3c142e76e8 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts @@ -0,0 +1,30 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DidExchangeCompleteMessageOptions { + id?: string + threadId: string + parentThreadId: string +} + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#3-exchange-complete + */ +export class DidExchangeCompleteMessage extends AgentMessage { + public constructor(options: DidExchangeCompleteMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + + this.setThread({ + threadId: options.threadId, + parentThreadId: options.parentThreadId, + }) + } + } + + @IsValidMessageType(DidExchangeCompleteMessage.type) + public readonly type = DidExchangeCompleteMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/complete') +} diff --git a/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts new file mode 100644 index 0000000000..ec8baf9880 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts @@ -0,0 +1,19 @@ +import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage' + +export type DidExchangeProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class DidExchangeProblemReportMessage extends ProblemReportMessage { + public constructor(options: DidExchangeProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(DidExchangeProblemReportMessage.type) + public readonly type = DidExchangeProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/problem-report') +} diff --git a/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts new file mode 100644 index 0000000000..4687bc0da4 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts @@ -0,0 +1,66 @@ +import { Expose, Type } from 'class-transformer' +import { IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { Attachment } from '../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DidExchangeRequestMessageOptions { + id?: string + parentThreadId: string + label: string + goalCode?: string + goal?: string + did: string +} + +/** + * Message to communicate the DID document to the other agent when creating a connection + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#1-exchange-request + */ +export class DidExchangeRequestMessage extends AgentMessage { + /** + * Create new DidExchangeRequestMessage instance. + * @param options + */ + public constructor(options: DidExchangeRequestMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.label = options.label + this.goalCode = options.goalCode + this.goal = options.goal + this.did = options.did + + this.setThread({ + threadId: this.id, + parentThreadId: options.parentThreadId, + }) + } + } + + @IsValidMessageType(DidExchangeRequestMessage.type) + public readonly type = DidExchangeRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/request') + + @IsString() + public readonly label?: string + + @Expose({ name: 'goal_code' }) + @IsOptional() + public readonly goalCode?: string + + @IsString() + @IsOptional() + public readonly goal?: string + + @IsString() + public readonly did!: string + + @Expose({ name: 'did_doc~attach' }) + @Type(() => Attachment) + @ValidateNested() + public didDoc?: Attachment +} diff --git a/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts new file mode 100644 index 0000000000..d0de9b6ec8 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts @@ -0,0 +1,48 @@ +import { Type, Expose } from 'class-transformer' +import { IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { Attachment } from '../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DidExchangeResponseMessageOptions { + id?: string + threadId: string + did: string +} + +/** + * Message part of connection protocol used to complete the connection + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#2-exchange-response + */ +export class DidExchangeResponseMessage extends AgentMessage { + /** + * Create new DidExchangeResponseMessage instance. + * @param options + */ + public constructor(options: DidExchangeResponseMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.did = options.did + + this.setThread({ + threadId: options.threadId, + }) + } + } + + @IsValidMessageType(DidExchangeResponseMessage.type) + public readonly type = DidExchangeResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/response') + + @IsString() + public readonly did!: string + + @Expose({ name: 'did_doc~attach' }) + @Type(() => Attachment) + @ValidateNested() + public didDoc?: Attachment +} diff --git a/packages/core/src/modules/connections/messages/TrustPingMessage.ts b/packages/core/src/modules/connections/messages/TrustPingMessage.ts index 4cc2b88318..036635eed6 100644 --- a/packages/core/src/modules/connections/messages/TrustPingMessage.ts +++ b/packages/core/src/modules/connections/messages/TrustPingMessage.ts @@ -1,9 +1,10 @@ import type { TimingDecorator } from '../../../decorators/timing/TimingDecorator' import { Expose } from 'class-transformer' -import { Equals, IsString, IsBoolean, IsOptional } from 'class-validator' +import { IsString, IsBoolean, IsOptional } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface TrustPingMessageOptions { comment?: string @@ -41,9 +42,9 @@ export class TrustPingMessage extends AgentMessage { } } - @Equals(TrustPingMessage.type) - public readonly type = TrustPingMessage.type - public static readonly type = 'https://didcomm.org/trust_ping/1.0/ping' + @IsValidMessageType(TrustPingMessage.type) + public readonly type = TrustPingMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/trust_ping/1.0/ping') @IsString() @IsOptional() diff --git a/packages/core/src/modules/connections/messages/TrustPingResponseMessage.ts b/packages/core/src/modules/connections/messages/TrustPingResponseMessage.ts index 7906e39844..23ace28316 100644 --- a/packages/core/src/modules/connections/messages/TrustPingResponseMessage.ts +++ b/packages/core/src/modules/connections/messages/TrustPingResponseMessage.ts @@ -1,8 +1,9 @@ import type { TimingDecorator } from '../../../decorators/timing/TimingDecorator' -import { Equals, IsOptional, IsString } from 'class-validator' +import { IsOptional, IsString } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface TrustPingResponseMessageOptions { comment?: string @@ -42,9 +43,9 @@ export class TrustPingResponseMessage extends AgentMessage { } } - @Equals(TrustPingResponseMessage.type) - public readonly type = TrustPingResponseMessage.type - public static readonly type = 'https://didcomm.org/trust_ping/1.0/ping_response' + @IsValidMessageType(TrustPingResponseMessage.type) + public readonly type = TrustPingResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/trust_ping/1.0/ping_response') @IsString() @IsOptional() diff --git a/packages/core/src/modules/connections/messages/index.ts b/packages/core/src/modules/connections/messages/index.ts index 6cb3241a61..7507e5ed56 100644 --- a/packages/core/src/modules/connections/messages/index.ts +++ b/packages/core/src/modules/connections/messages/index.ts @@ -3,3 +3,8 @@ export * from './ConnectionRequestMessage' export * from './ConnectionResponseMessage' export * from './TrustPingMessage' export * from './TrustPingResponseMessage' +export * from './ConnectionProblemReportMessage' +export * from './DidExchangeRequestMessage' +export * from './DidExchangeResponseMessage' +export * from './DidExchangeCompleteMessage' +export * from './DidExchangeProblemReportMessage' diff --git a/packages/core/src/modules/connections/models/ConnectionState.ts b/packages/core/src/modules/connections/models/ConnectionState.ts index 15071c2623..44025e3a89 100644 --- a/packages/core/src/modules/connections/models/ConnectionState.ts +++ b/packages/core/src/modules/connections/models/ConnectionState.ts @@ -1,13 +1,30 @@ +import { DidExchangeState } from './DidExchangeState' + /** * Connection states as defined in RFC 0160. * - * State 'null' from RFC is changed to 'init' - * * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0160-connection-protocol/README.md#states */ export enum ConnectionState { + Null = 'null', Invited = 'invited', Requested = 'requested', Responded = 'responded', Complete = 'complete', } + +export function rfc0160StateFromDidExchangeState(didExchangeState: DidExchangeState) { + const stateMapping = { + [DidExchangeState.Start]: ConnectionState.Null, + [DidExchangeState.Abandoned]: ConnectionState.Null, + [DidExchangeState.InvitationReceived]: ConnectionState.Invited, + [DidExchangeState.InvitationSent]: ConnectionState.Invited, + [DidExchangeState.RequestReceived]: ConnectionState.Requested, + [DidExchangeState.RequestSent]: ConnectionState.Requested, + [DidExchangeState.ResponseReceived]: ConnectionState.Responded, + [DidExchangeState.ResponseSent]: ConnectionState.Responded, + [DidExchangeState.Completed]: DidExchangeState.Completed, + } + + return stateMapping[didExchangeState] +} diff --git a/packages/core/src/modules/connections/models/DidExchangeRole.ts b/packages/core/src/modules/connections/models/DidExchangeRole.ts new file mode 100644 index 0000000000..9027757e96 --- /dev/null +++ b/packages/core/src/modules/connections/models/DidExchangeRole.ts @@ -0,0 +1,4 @@ +export const enum DidExchangeRole { + Requester = 'requester', + Responder = 'responder', +} diff --git a/packages/core/src/modules/connections/models/DidExchangeState.ts b/packages/core/src/modules/connections/models/DidExchangeState.ts new file mode 100644 index 0000000000..23decb1598 --- /dev/null +++ b/packages/core/src/modules/connections/models/DidExchangeState.ts @@ -0,0 +1,16 @@ +/** + * Connection states as defined in RFC 0023. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0023-did-exchange/README.md#state-machine-tables + */ +export const enum DidExchangeState { + Start = 'start', + InvitationSent = 'invitation-sent', + InvitationReceived = 'invitation-received', + RequestSent = 'request-sent', + RequestReceived = 'request-received', + ResponseSent = 'response-sent', + ResponseReceived = 'response-received', + Abandoned = 'abandoned', + Completed = 'completed', +} diff --git a/packages/core/src/modules/connections/models/HandshakeProtocol.ts b/packages/core/src/modules/connections/models/HandshakeProtocol.ts new file mode 100644 index 0000000000..bee2008144 --- /dev/null +++ b/packages/core/src/modules/connections/models/HandshakeProtocol.ts @@ -0,0 +1,4 @@ +export const enum HandshakeProtocol { + Connections = 'https://didcomm.org/connections/1.0', + DidExchange = 'https://didcomm.org/didexchange/1.0', +} diff --git a/packages/core/src/modules/connections/models/__tests__/ConnectionState.test.ts b/packages/core/src/modules/connections/models/__tests__/ConnectionState.test.ts new file mode 100644 index 0000000000..86860d8fff --- /dev/null +++ b/packages/core/src/modules/connections/models/__tests__/ConnectionState.test.ts @@ -0,0 +1,30 @@ +import { ConnectionState, rfc0160StateFromDidExchangeState } from '../ConnectionState' +import { DidExchangeState } from '../DidExchangeState' + +describe('ConnectionState', () => { + test('state matches Connection 1.0 (RFC 0160) state value', () => { + expect(ConnectionState.Null).toBe('null') + expect(ConnectionState.Invited).toBe('invited') + expect(ConnectionState.Requested).toBe('requested') + expect(ConnectionState.Responded).toBe('responded') + expect(ConnectionState.Complete).toBe('complete') + }) + + describe('rfc0160StateFromDidExchangeState', () => { + it('should return the connection state for all did exchanges states', () => { + expect(rfc0160StateFromDidExchangeState(DidExchangeState.Abandoned)).toEqual(ConnectionState.Null) + expect(rfc0160StateFromDidExchangeState(DidExchangeState.Start)).toEqual(ConnectionState.Null) + + expect(rfc0160StateFromDidExchangeState(DidExchangeState.InvitationReceived)).toEqual(ConnectionState.Invited) + expect(rfc0160StateFromDidExchangeState(DidExchangeState.InvitationSent)).toEqual(ConnectionState.Invited) + + expect(rfc0160StateFromDidExchangeState(DidExchangeState.RequestReceived)).toEqual(ConnectionState.Requested) + expect(rfc0160StateFromDidExchangeState(DidExchangeState.RequestSent)).toEqual(ConnectionState.Requested) + + expect(rfc0160StateFromDidExchangeState(DidExchangeState.ResponseReceived)).toEqual(ConnectionState.Responded) + expect(rfc0160StateFromDidExchangeState(DidExchangeState.ResponseReceived)).toEqual(ConnectionState.Responded) + + expect(rfc0160StateFromDidExchangeState(DidExchangeState.Completed)).toEqual(DidExchangeState.Completed) + }) + }) +}) diff --git a/packages/core/src/modules/connections/models/did/DidDoc.ts b/packages/core/src/modules/connections/models/did/DidDoc.ts index 0706e6d721..896d314221 100644 --- a/packages/core/src/modules/connections/models/did/DidDoc.ts +++ b/packages/core/src/modules/connections/models/did/DidDoc.ts @@ -1,13 +1,14 @@ +import type { DidDocumentService } from '../../../dids/domain/service' import type { Authentication } from './authentication' import type { PublicKey } from './publicKey' -import type { Service } from './service' import { Expose } from 'class-transformer' import { Equals, IsArray, IsString, ValidateNested } from 'class-validator' +import { ServiceTransformer, DidCommV1Service, IndyAgentService } from '../../../dids/domain/service' + import { AuthenticationTransformer } from './authentication' import { PublicKeyTransformer } from './publicKey' -import { DidCommService, IndyAgentService, ServiceTransformer } from './service' type DidDocOptions = Pick @@ -27,7 +28,7 @@ export class DidDoc { @IsArray() @ValidateNested() @ServiceTransformer() - public service: Service[] = [] + public service: DidDocumentService[] = [] @IsArray() @ValidateNested() @@ -57,7 +58,7 @@ export class DidDoc { * * @param type The type of service(s) to query. */ - public getServicesByType(type: string): S[] { + public getServicesByType(type: string): S[] { return this.service.filter((service) => service.type === type) as S[] } @@ -66,7 +67,9 @@ export class DidDoc { * * @param classType The class to query services. */ - public getServicesByClassType(classType: new (...args: never[]) => S): S[] { + public getServicesByClassType( + classType: new (...args: never[]) => S + ): S[] { return this.service.filter((service) => service instanceof classType) as S[] } @@ -74,10 +77,10 @@ export class DidDoc { * Get all DIDComm services ordered by priority descending. This means the highest * priority will be the first entry. */ - public get didCommServices(): Array { - const didCommServiceTypes = [IndyAgentService.type, DidCommService.type] + public get didCommServices(): Array { + const didCommServiceTypes = [IndyAgentService.type, DidCommV1Service.type] const services = this.service.filter((service) => didCommServiceTypes.includes(service.type)) as Array< - IndyAgentService | DidCommService + IndyAgentService | DidCommV1Service > // Sort services based on indicated priority diff --git a/packages/core/src/modules/connections/models/did/__tests__/DidDoc.test.ts b/packages/core/src/modules/connections/models/did/__tests__/DidDoc.test.ts index 3272624749..17023d6060 100644 --- a/packages/core/src/modules/connections/models/did/__tests__/DidDoc.test.ts +++ b/packages/core/src/modules/connections/models/did/__tests__/DidDoc.test.ts @@ -1,9 +1,9 @@ import { instanceToPlain, plainToInstance } from 'class-transformer' +import { DidCommV1Service, DidDocumentService, IndyAgentService } from '../../../../dids' import { DidDoc } from '../DidDoc' import { ReferencedAuthentication, EmbeddedAuthentication } from '../authentication' import { Ed25119Sig2018, EddsaSaSigSecp256k1, RsaSig2018 } from '../publicKey' -import { Service, IndyAgentService, DidCommService } from '../service' import diddoc from './diddoc.json' @@ -44,7 +44,7 @@ const didDoc = new DidDoc({ }), ], service: [ - new Service({ + new DidDocumentService({ id: '0', type: 'Mediator', serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', @@ -56,7 +56,7 @@ const didDoc = new DidDoc({ routingKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], priority: 5, }), - new DidCommService({ + new DidCommV1Service({ id: '7', serviceEndpoint: 'https://agent.com/did-comm', recipientKeys: ['DADEajsDSaksLng9h'], @@ -87,9 +87,9 @@ describe('Did | DidDoc', () => { expect(didDoc.publicKey[2]).toBeInstanceOf(EddsaSaSigSecp256k1) // Check Service - expect(didDoc.service[0]).toBeInstanceOf(Service) + expect(didDoc.service[0]).toBeInstanceOf(DidDocumentService) expect(didDoc.service[1]).toBeInstanceOf(IndyAgentService) - expect(didDoc.service[2]).toBeInstanceOf(DidCommService) + expect(didDoc.service[2]).toBeInstanceOf(DidCommV1Service) // Check Authentication expect(didDoc.authentication[0]).toBeInstanceOf(ReferencedAuthentication) diff --git a/packages/core/src/modules/connections/models/did/__tests__/Service.test.ts b/packages/core/src/modules/connections/models/did/__tests__/Service.test.ts deleted file mode 100644 index c0aeeb8919..0000000000 --- a/packages/core/src/modules/connections/models/did/__tests__/Service.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { instanceToPlain, plainToInstance } from 'class-transformer' - -import { Service, ServiceTransformer, serviceTypes, IndyAgentService, DidCommService } from '../service' - -describe('Did | Service', () => { - it('should correctly transform Json to Service class', async () => { - const json = { - id: 'test-id', - type: 'Mediator', - serviceEndpoint: 'https://example.com', - } - const service = plainToInstance(Service, json) - - expect(service.id).toBe(json.id) - expect(service.type).toBe(json.type) - expect(service.serviceEndpoint).toBe(json.serviceEndpoint) - }) - - it('should correctly transform Service class to Json', async () => { - const json = { - id: 'test-id', - type: 'Mediator', - serviceEndpoint: 'https://example.com', - } - - const service = new Service({ - ...json, - }) - - const transformed = instanceToPlain(service) - - expect(transformed).toEqual(json) - }) - - describe('IndyAgentService', () => { - it('should correctly transform Json to IndyAgentService class', async () => { - const json = { - id: 'test-id', - type: 'IndyAgent', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - priority: 10, - serviceEndpoint: 'https://example.com', - } - const service = plainToInstance(IndyAgentService, json) - - expect(service).toMatchObject(json) - }) - - it('should correctly transform IndyAgentService class to Json', async () => { - const json = { - id: 'test-id', - type: 'IndyAgent', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - priority: 10, - serviceEndpoint: 'https://example.com', - } - - const service = new IndyAgentService({ - ...json, - }) - - const transformed = instanceToPlain(service) - - expect(transformed).toEqual(json) - }) - - it("should set 'priority' to default (0) when not present in constructor or during transformation", async () => { - const json = { - id: 'test-id', - type: 'IndyAgent', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - serviceEndpoint: 'https://example.com', - } - - const transformService = plainToInstance(IndyAgentService, json) - const constructorService = new IndyAgentService({ ...json }) - - expect(transformService.priority).toBe(0) - expect(constructorService.priority).toBe(0) - - expect(instanceToPlain(transformService).priority).toBe(0) - expect(instanceToPlain(constructorService).priority).toBe(0) - }) - }) - - describe('DidCommService', () => { - it('should correctly transform Json to DidCommService class', async () => { - const json = { - id: 'test-id', - type: 'did-communication', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - accept: ['media-type'], - priority: 10, - serviceEndpoint: 'https://example.com', - } - const service = plainToInstance(DidCommService, json) - - expect(service).toMatchObject(json) - }) - - it('should correctly transform DidCommService class to Json', async () => { - const json = { - id: 'test-id', - type: 'did-communication', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - accept: ['media-type'], - priority: 10, - serviceEndpoint: 'https://example.com', - } - - const service = new DidCommService({ - ...json, - }) - - const transformed = instanceToPlain(service) - - expect(transformed).toEqual(json) - }) - - it("should set 'priority' to default (0) when not present in constructor or during transformation", async () => { - const json = { - id: 'test-id', - type: 'did-communication', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - accept: ['media-type'], - serviceEndpoint: 'https://example.com', - } - - const transformService = plainToInstance(DidCommService, json) - const constructorService = new DidCommService({ ...json }) - - expect(transformService.priority).toBe(0) - expect(constructorService.priority).toBe(0) - - expect(instanceToPlain(transformService).priority).toBe(0) - expect(instanceToPlain(constructorService).priority).toBe(0) - }) - }) - - describe('ServiceTransformer', () => { - class ServiceTransformerTest { - @ServiceTransformer() - public service: Service[] = [] - } - - it("should transform Json to default Service class when the 'type' key is not present in 'serviceTypes'", async () => { - const serviceJson = { - id: 'test-id', - type: 'Mediator', - serviceEndpoint: 'https://example.com', - } - - const serviceWrapperJson = { - service: [serviceJson], - } - const serviceWrapper = plainToInstance(ServiceTransformerTest, serviceWrapperJson) - - expect(serviceWrapper.service.length).toBe(1) - - const firstService = serviceWrapper.service[0] - expect(firstService).toBeInstanceOf(Service) - expect(firstService.id).toBe(serviceJson.id) - expect(firstService.type).toBe(serviceJson.type) - expect(firstService.serviceEndpoint).toBe(serviceJson.serviceEndpoint) - }) - - it("should transform Json to corresponding class when the 'type' key is present in 'serviceTypes'", async () => { - const serviceArray = [ - { - id: 'test-id', - type: 'IndyAgent', - recipientKeys: ['917a109d-eae3-42bc-9436-b02426d3ce2c', '348d5200-0f8f-42cc-aad9-61e0d082a674'], - routingKeys: ['0094df0b-7b6d-4ebb-82de-234a621fb615'], - priority: 10, - serviceEndpoint: 'https://example.com', - }, - ] - - const serviceWrapperJson = { - service: serviceArray, - } - const serviceWrapper = plainToInstance(ServiceTransformerTest, serviceWrapperJson) - - expect(serviceWrapper.service.length).toBe(serviceArray.length) - - serviceArray.forEach((serviceJson, i) => { - const service = serviceWrapper.service[i] - expect(service).toBeInstanceOf(serviceTypes[serviceJson.type]) - }) - }) - }) -}) diff --git a/packages/core/src/modules/connections/models/did/__tests__/diddoc.json b/packages/core/src/modules/connections/models/did/__tests__/diddoc.json index f0fd73f355..595fe73307 100644 --- a/packages/core/src/modules/connections/models/did/__tests__/diddoc.json +++ b/packages/core/src/modules/connections/models/did/__tests__/diddoc.json @@ -3,7 +3,7 @@ "id": "did:sov:LjgpST2rjsoxYegQDRm7EL", "publicKey": [ { - "id": "3", + "id": "#3", "type": "RsaVerificationKey2018", "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", "publicKeyPem": "-----BEGIN PUBLIC X..." @@ -15,7 +15,7 @@ "publicKeyBase58": "-----BEGIN PUBLIC 9..." }, { - "id": "6", + "id": "#6", "type": "Secp256k1VerificationKey2018", "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", "publicKeyHex": "-----BEGIN PUBLIC A..." @@ -28,7 +28,7 @@ "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" }, { - "id": "6", + "id": "#6", "type": "IndyAgent", "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], @@ -36,7 +36,7 @@ "priority": 5 }, { - "id": "7", + "id": "#7", "type": "did-communication", "serviceEndpoint": "https://agent.com/did-comm", "recipientKeys": ["DADEajsDSaksLng9h"], @@ -47,10 +47,10 @@ "authentication": [ { "type": "RsaSignatureAuthentication2018", - "publicKey": "3" + "publicKey": "#3" }, { - "id": "6", + "id": "#6", "type": "RsaVerificationKey2018", "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", "publicKeyPem": "-----BEGIN PUBLIC A..." diff --git a/packages/core/src/modules/connections/models/did/index.ts b/packages/core/src/modules/connections/models/did/index.ts index ed0f166127..f832050609 100644 --- a/packages/core/src/modules/connections/models/did/index.ts +++ b/packages/core/src/modules/connections/models/did/index.ts @@ -1,4 +1,3 @@ export * from './DidDoc' -export * from './service' export * from './publicKey' export * from './authentication' diff --git a/packages/core/src/modules/connections/models/did/publicKey/PublicKey.ts b/packages/core/src/modules/connections/models/did/publicKey/PublicKey.ts index 70e55441e7..43ecfcc05f 100644 --- a/packages/core/src/modules/connections/models/did/publicKey/PublicKey.ts +++ b/packages/core/src/modules/connections/models/did/publicKey/PublicKey.ts @@ -1,4 +1,4 @@ -import { IsString } from 'class-validator' +import { IsOptional, IsString } from 'class-validator' export class PublicKey { public constructor(options: { id: string; controller: string; type: string; value?: string }) { @@ -18,5 +18,8 @@ export class PublicKey { @IsString() public type!: string + + @IsString() + @IsOptional() public value?: string } diff --git a/packages/core/src/modules/connections/models/index.ts b/packages/core/src/modules/connections/models/index.ts index 22c3a78f74..0c8dd1b360 100644 --- a/packages/core/src/modules/connections/models/index.ts +++ b/packages/core/src/modules/connections/models/index.ts @@ -1,4 +1,7 @@ export * from './Connection' export * from './ConnectionRole' export * from './ConnectionState' +export * from './DidExchangeState' +export * from './DidExchangeRole' +export * from './HandshakeProtocol' export * from './did' diff --git a/packages/core/src/modules/connections/repository/ConnectionRecord.ts b/packages/core/src/modules/connections/repository/ConnectionRecord.ts index bb56d18ed8..7e9157b438 100644 --- a/packages/core/src/modules/connections/repository/ConnectionRecord.ts +++ b/packages/core/src/modules/connections/repository/ConnectionRecord.ts @@ -1,66 +1,55 @@ import type { TagsBase } from '../../../storage/BaseRecord' -import type { ConnectionRole } from '../models/ConnectionRole' - -import { Type } from 'class-transformer' +import type { HandshakeProtocol } from '../models' import { AriesFrameworkError } from '../../../error' import { BaseRecord } from '../../../storage/BaseRecord' import { uuid } from '../../../utils/uuid' -import { ConnectionInvitationMessage } from '../messages/ConnectionInvitationMessage' -import { ConnectionState } from '../models/ConnectionState' -import { DidDoc } from '../models/did/DidDoc' +import { rfc0160StateFromDidExchangeState, DidExchangeRole, DidExchangeState } from '../models' export interface ConnectionRecordProps { id?: string createdAt?: Date did: string - didDoc: DidDoc - verkey: string theirDid?: string - theirDidDoc?: DidDoc theirLabel?: string - invitation?: ConnectionInvitationMessage - state: ConnectionState - role: ConnectionRole + state: DidExchangeState + role: DidExchangeRole alias?: string autoAcceptConnection?: boolean threadId?: string tags?: CustomConnectionTags imageUrl?: string - multiUseInvitation: boolean + multiUseInvitation?: boolean mediatorId?: string + errorMessage?: string + protocol?: HandshakeProtocol + outOfBandId?: string + invitationDid?: string } export type CustomConnectionTags = TagsBase export type DefaultConnectionTags = { - state: ConnectionState - role: ConnectionRole - invitationKey?: string + state: DidExchangeState + role: DidExchangeRole threadId?: string - verkey?: string - theirKey?: string mediatorId?: string + did: string + theirDid?: string + outOfBandId?: string } export class ConnectionRecord extends BaseRecord implements ConnectionRecordProps { - public state!: ConnectionState - public role!: ConnectionRole + public state!: DidExchangeState + public role!: DidExchangeRole - @Type(() => DidDoc) - public didDoc!: DidDoc public did!: string - public verkey!: string - @Type(() => DidDoc) - public theirDidDoc?: DidDoc public theirDid?: string public theirLabel?: string - @Type(() => ConnectionInvitationMessage) - public invitation?: ConnectionInvitationMessage public alias?: string public autoAcceptConnection?: boolean public imageUrl?: string @@ -68,6 +57,10 @@ export class ConnectionRecord public threadId?: string public mediatorId?: string + public errorMessage?: string + public protocol?: HandshakeProtocol + public outOfBandId?: string + public invitationDid?: string public static readonly type = 'ConnectionRecord' public readonly type = ConnectionRecord.type @@ -79,72 +72,59 @@ export class ConnectionRecord this.id = props.id ?? uuid() this.createdAt = props.createdAt ?? new Date() this.did = props.did - this.didDoc = props.didDoc - this.verkey = props.verkey + this.invitationDid = props.invitationDid this.theirDid = props.theirDid - this.theirDidDoc = props.theirDidDoc this.theirLabel = props.theirLabel this.state = props.state this.role = props.role this.alias = props.alias this.autoAcceptConnection = props.autoAcceptConnection this._tags = props.tags ?? {} - this.invitation = props.invitation this.threadId = props.threadId this.imageUrl = props.imageUrl - this.multiUseInvitation = props.multiUseInvitation + this.multiUseInvitation = props.multiUseInvitation || false this.mediatorId = props.mediatorId + this.errorMessage = props.errorMessage + this.protocol = props.protocol + this.outOfBandId = props.outOfBandId } } public getTags() { - const invitationKey = (this.invitation?.recipientKeys && this.invitation.recipientKeys[0]) || undefined - return { ...this._tags, state: this.state, role: this.role, - invitationKey, threadId: this.threadId, - verkey: this.verkey, - theirKey: this.theirKey || undefined, mediatorId: this.mediatorId, + did: this.did, + theirDid: this.theirDid, + outOfBandId: this.outOfBandId, + invitationDid: this.invitationDid, } } - public get myKey() { - const [service] = this.didDoc?.didCommServices ?? [] - - if (!service) { - return null - } - - return service.recipientKeys[0] + public get isRequester() { + return this.role === DidExchangeRole.Requester } - public get theirKey() { - const [service] = this.theirDidDoc?.didCommServices ?? [] - - if (!service) { - return null - } - - return service.recipientKeys[0] + public get rfc0160State() { + return rfc0160StateFromDidExchangeState(this.state) } public get isReady() { - return [ConnectionState.Responded, ConnectionState.Complete].includes(this.state) + return this.state && [DidExchangeState.Completed, DidExchangeState.ResponseSent].includes(this.state) } public assertReady() { if (!this.isReady) { throw new AriesFrameworkError( - `Connection record is not ready to be used. Expected ${ConnectionState.Responded} or ${ConnectionState.Complete}, found invalid state ${this.state}` + `Connection record is not ready to be used. Expected ${DidExchangeState.ResponseSent}, ${DidExchangeState.ResponseReceived} or ${DidExchangeState.Completed}, found invalid state ${this.state}` ) } } - public assertState(expectedStates: ConnectionState | ConnectionState[]) { + public assertState(expectedStates: DidExchangeState | DidExchangeState[]) { if (!Array.isArray(expectedStates)) { expectedStates = [expectedStates] } @@ -156,7 +136,7 @@ export class ConnectionRecord } } - public assertRole(expectedRole: ConnectionRole) { + public assertRole(expectedRole: DidExchangeRole) { if (this.role !== expectedRole) { throw new AriesFrameworkError(`Connection record has invalid role ${this.role}. Expected role ${expectedRole}.`) } diff --git a/packages/core/src/modules/connections/repository/ConnectionRepository.ts b/packages/core/src/modules/connections/repository/ConnectionRepository.ts index 4a4c583584..6f0470d739 100644 --- a/packages/core/src/modules/connections/repository/ConnectionRepository.ts +++ b/packages/core/src/modules/connections/repository/ConnectionRepository.ts @@ -11,4 +11,15 @@ export class ConnectionRepository extends Repository { public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { super(ConnectionRecord, storageService) } + + public async findByDids({ ourDid, theirDid }: { ourDid: string; theirDid: string }) { + return this.findSingleByQuery({ + did: ourDid, + theirDid, + }) + } + + public getByThreadId(threadId: string): Promise { + return this.getSingleByQuery({ threadId }) + } } diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 333def2a95..7f6f1edeab 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -2,7 +2,11 @@ import type { AgentMessage } from '../../../agent/AgentMessage' import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' import type { Logger } from '../../../logger' import type { AckMessage } from '../../common' +import type { DidDocument } from '../../dids' +import type { OutOfBandDidCommService } from '../../oob/domain/OutOfBandDidCommService' +import type { OutOfBandRecord } from '../../oob/repository' import type { ConnectionStateChangedEvent } from '../ConnectionEvents' +import type { ConnectionProblemReportMessage } from '../messages' import type { CustomConnectionTags } from '../repository/ConnectionRecord' import { firstValueFrom, ReplaySubject } from 'rxjs' @@ -12,36 +16,50 @@ import { inject, scoped, Lifecycle } from 'tsyringe' import { AgentConfig } from '../../../agent/AgentConfig' import { EventEmitter } from '../../../agent/EventEmitter' import { InjectionSymbols } from '../../../constants' +import { Key } from '../../../crypto' import { signData, unpackAndVerifySignatureDecorator } from '../../../decorators/signature/SignatureDecoratorUtils' import { AriesFrameworkError } from '../../../error' import { JsonTransformer } from '../../../utils/JsonTransformer' import { MessageValidator } from '../../../utils/MessageValidator' import { Wallet } from '../../../wallet/Wallet' +import { IndyAgentService } from '../../dids' +import { DidDocumentRole } from '../../dids/domain/DidDocumentRole' +import { didKeyToVerkey } from '../../dids/helpers' +import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' +import { DidRepository, DidRecord } from '../../dids/repository' +import { OutOfBandRole } from '../../oob/domain/OutOfBandRole' +import { OutOfBandState } from '../../oob/domain/OutOfBandState' import { ConnectionEventTypes } from '../ConnectionEvents' +import { ConnectionProblemReportError, ConnectionProblemReportReason } from '../errors' +import { ConnectionRequestMessage, ConnectionResponseMessage, TrustPingMessage } from '../messages' import { - ConnectionInvitationMessage, - ConnectionRequestMessage, - ConnectionResponseMessage, - TrustPingMessage, -} from '../messages' -import { + DidExchangeRole, + DidExchangeState, Connection, - ConnectionState, - ConnectionRole, DidDoc, Ed25119Sig2018, - authenticationTypes, - ReferencedAuthentication, - IndyAgentService, + EmbeddedAuthentication, + HandshakeProtocol, } from '../models' import { ConnectionRecord } from '../repository/ConnectionRecord' import { ConnectionRepository } from '../repository/ConnectionRepository' +import { convertToNewDidDocument } from './helpers' + +export interface ConnectionRequestParams { + label?: string + imageUrl?: string + alias?: string + routing: Routing + autoAcceptConnection?: boolean +} + @scoped(Lifecycle.ContainerScoped) export class ConnectionService { private wallet: Wallet private config: AgentConfig private connectionRepository: ConnectionRepository + private didRepository: DidRepository private eventEmitter: EventEmitter private logger: Logger @@ -49,244 +67,204 @@ export class ConnectionService { @inject(InjectionSymbols.Wallet) wallet: Wallet, config: AgentConfig, connectionRepository: ConnectionRepository, + didRepository: DidRepository, eventEmitter: EventEmitter ) { this.wallet = wallet this.config = config this.connectionRepository = connectionRepository + this.didRepository = didRepository this.eventEmitter = eventEmitter this.logger = config.logger } /** - * Create a new connection record containing a connection invitation message + * Create a connection request message for a given out-of-band. * - * @param config config for creation of connection and invitation - * @returns new connection record + * @param outOfBandRecord out-of-band record for which to create a connection request + * @param config config for creation of connection request + * @returns outbound message containing connection request */ - public async createInvitation(config: { - routing: Routing - autoAcceptConnection?: boolean - alias?: string - multiUseInvitation?: boolean - myLabel?: string - myImageUrl?: string - }): Promise> { - // TODO: public did - const connectionRecord = await this.createConnection({ - role: ConnectionRole.Inviter, - state: ConnectionState.Invited, - alias: config?.alias, - routing: config.routing, - autoAcceptConnection: config?.autoAcceptConnection, - multiUseInvitation: config.multiUseInvitation ?? false, - }) - - const { didDoc } = connectionRecord - const [service] = didDoc.didCommServices - const invitation = new ConnectionInvitationMessage({ - label: config?.myLabel ?? this.config.label, - recipientKeys: service.recipientKeys, - serviceEndpoint: service.serviceEndpoint, - routingKeys: service.routingKeys, - imageUrl: config?.myImageUrl ?? this.config.connectionImageUrl, - }) + public async createRequest( + outOfBandRecord: OutOfBandRecord, + config: ConnectionRequestParams + ): Promise> { + this.logger.debug(`Create message ${ConnectionRequestMessage.type} start`, outOfBandRecord) + outOfBandRecord.assertRole(OutOfBandRole.Receiver) + outOfBandRecord.assertState(OutOfBandState.PrepareResponse) - connectionRecord.invitation = invitation + // TODO check there is no connection record for particular oob record - await this.connectionRepository.update(connectionRecord) + const { outOfBandInvitation } = outOfBandRecord - this.eventEmitter.emit({ - type: ConnectionEventTypes.ConnectionStateChanged, - payload: { - connectionRecord: connectionRecord, - previousState: null, - }, - }) + const { did, mediatorId } = config.routing + const didDoc = this.createDidDoc(config.routing) - return { connectionRecord: connectionRecord, message: invitation } - } + // TODO: We should store only one did that we'll use to send the request message with success. + // We take just the first one for now. + const [invitationDid] = outOfBandInvitation.invitationDids - /** - * Process a received invitation message. This will not accept the invitation - * or send an invitation request message. It will only create a connection record - * with all the information about the invitation stored. Use {@link ConnectionService.createRequest} - * after calling this function to create a connection request. - * - * @param invitation the invitation message to process - * @returns new connection record. - */ - public async processInvitation( - invitation: ConnectionInvitationMessage, - config: { - routing: Routing - autoAcceptConnection?: boolean - alias?: string - } - ): Promise { const connectionRecord = await this.createConnection({ - role: ConnectionRole.Invitee, - state: ConnectionState.Invited, + protocol: HandshakeProtocol.Connections, + role: DidExchangeRole.Requester, + state: DidExchangeState.InvitationReceived, + theirLabel: outOfBandInvitation.label, alias: config?.alias, - theirLabel: invitation.label, + did, + mediatorId, autoAcceptConnection: config?.autoAcceptConnection, - routing: config.routing, - invitation, - imageUrl: invitation.imageUrl, - tags: { - invitationKey: invitation.recipientKeys && invitation.recipientKeys[0], - }, multiUseInvitation: false, + outOfBandId: outOfBandRecord.id, + invitationDid, }) - await this.connectionRepository.update(connectionRecord) - this.eventEmitter.emit({ - type: ConnectionEventTypes.ConnectionStateChanged, - payload: { - connectionRecord: connectionRecord, - previousState: null, - }, - }) - - return connectionRecord - } - /** - * Create a connection request message for the connection with the specified connection id. - * - * @param connectionId the id of the connection for which to create a connection request - * @param config config for creation of connection request - * @returns outbound message containing connection request - */ - public async createRequest( - connectionId: string, - config?: { - myLabel?: string - myImageUrl?: string - } - ): Promise> { - const connectionRecord = await this.connectionRepository.getById(connectionId) + const { did: peerDid } = await this.createDid({ + role: DidDocumentRole.Created, + didDocument: convertToNewDidDocument(didDoc), + }) - connectionRecord.assertState(ConnectionState.Invited) - connectionRecord.assertRole(ConnectionRole.Invitee) + const { label, imageUrl, autoAcceptConnection } = config const connectionRequest = new ConnectionRequestMessage({ - label: config?.myLabel ?? this.config.label, + label: label ?? this.config.label, did: connectionRecord.did, - didDoc: connectionRecord.didDoc, - imageUrl: config?.myImageUrl ?? this.config.connectionImageUrl, + didDoc, + imageUrl: imageUrl ?? this.config.connectionImageUrl, }) - await this.updateState(connectionRecord, ConnectionState.Requested) + if (autoAcceptConnection !== undefined || autoAcceptConnection !== null) { + connectionRecord.autoAcceptConnection = config?.autoAcceptConnection + } + + connectionRecord.did = peerDid + connectionRecord.threadId = connectionRequest.id + await this.updateState(connectionRecord, DidExchangeState.RequestSent) return { - connectionRecord: connectionRecord, + connectionRecord, message: connectionRequest, } } - /** - * Process a received connection request message. This will not accept the connection request - * or send a connection response message. It will only update the existing connection record - * with all the new information from the connection request message. Use {@link ConnectionService.createResponse} - * after calling this function to create a connection response. - * - * @param messageContext the message context containing a connection request message - * @returns updated connection record - */ public async processRequest( messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord, routing?: Routing ): Promise { - const { message, recipientVerkey, senderVerkey } = messageContext + this.logger.debug(`Process message ${ConnectionRequestMessage.type} start`, messageContext) + outOfBandRecord.assertRole(OutOfBandRole.Sender) + outOfBandRecord.assertState(OutOfBandState.AwaitResponse) - if (!recipientVerkey || !senderVerkey) { - throw new AriesFrameworkError('Unable to process connection request without senderVerkey or recipientVerkey') - } + // TODO check there is no connection record for particular oob record - let connectionRecord = await this.findByVerkey(recipientVerkey) - if (!connectionRecord) { - throw new AriesFrameworkError( - `Unable to process connection request: connection for verkey ${recipientVerkey} not found` - ) + const { did, mediatorId } = routing ? routing : outOfBandRecord + if (!did) { + throw new AriesFrameworkError('Out-of-band record does not have did attribute.') } - connectionRecord.assertState(ConnectionState.Invited) - connectionRecord.assertRole(ConnectionRole.Inviter) + const { message } = messageContext if (!message.connection.didDoc) { - throw new AriesFrameworkError('Public DIDs are not supported yet') + throw new ConnectionProblemReportError('Public DIDs are not supported yet', { + problemCode: ConnectionProblemReportReason.RequestNotAccepted, + }) } - // Create new connection if using a multi use invitation - if (connectionRecord.multiUseInvitation) { - if (!routing) { - throw new AriesFrameworkError( - 'Cannot process request for multi-use invitation without routing object. Make sure to call processRequest with the routing parameter provided.' - ) - } + const connectionRecord = await this.createConnection({ + protocol: HandshakeProtocol.Connections, + role: DidExchangeRole.Responder, + state: DidExchangeState.RequestReceived, + multiUseInvitation: false, + did, + mediatorId, + autoAcceptConnection: outOfBandRecord.autoAcceptConnection, + }) - connectionRecord = await this.createConnection({ - role: connectionRecord.role, - state: connectionRecord.state, - multiUseInvitation: false, - routing, - autoAcceptConnection: connectionRecord.autoAcceptConnection, - invitation: connectionRecord.invitation, - tags: connectionRecord.getTags(), - }) - } + const { did: peerDid } = await this.createDid({ + role: DidDocumentRole.Received, + didDocument: convertToNewDidDocument(message.connection.didDoc), + }) - connectionRecord.theirDidDoc = message.connection.didDoc + connectionRecord.theirDid = peerDid connectionRecord.theirLabel = message.label connectionRecord.threadId = message.id - connectionRecord.theirDid = message.connection.did connectionRecord.imageUrl = message.imageUrl + connectionRecord.outOfBandId = outOfBandRecord.id - if (!connectionRecord.theirKey) { - throw new AriesFrameworkError(`Connection with id ${connectionRecord.id} has no recipient keys.`) - } - - await this.updateState(connectionRecord, ConnectionState.Requested) + await this.connectionRepository.update(connectionRecord) + this.eventEmitter.emit({ + type: ConnectionEventTypes.ConnectionStateChanged, + payload: { + connectionRecord, + previousState: null, + }, + }) + this.logger.debug(`Process message ${ConnectionRequestMessage.type} end`, connectionRecord) return connectionRecord } /** * Create a connection response message for the connection with the specified connection id. * - * @param connectionId the id of the connection for which to create a connection response + * @param connectionRecord the connection for which to create a connection response * @returns outbound message containing connection response */ public async createResponse( - connectionId: string + connectionRecord: ConnectionRecord, + outOfBandRecord: OutOfBandRecord, + routing?: Routing ): Promise> { - const connectionRecord = await this.connectionRepository.getById(connectionId) + this.logger.debug(`Create message ${ConnectionResponseMessage.type} start`, connectionRecord) + connectionRecord.assertState(DidExchangeState.RequestReceived) + connectionRecord.assertRole(DidExchangeRole.Responder) - connectionRecord.assertState(ConnectionState.Requested) - connectionRecord.assertRole(ConnectionRole.Inviter) + const { did } = routing ? routing : outOfBandRecord + if (!did) { + throw new AriesFrameworkError('Out-of-band record does not have did attribute.') + } + + const didDoc = routing + ? this.createDidDoc(routing) + : this.createDidDocFromServices( + did, + Key.fromFingerprint(outOfBandRecord.getTags().recipientKeyFingerprints[0]).publicKeyBase58, + outOfBandRecord.outOfBandInvitation.services.filter( + (s): s is OutOfBandDidCommService => typeof s !== 'string' + ) + ) + + const { did: peerDid } = await this.createDid({ + role: DidDocumentRole.Created, + didDocument: convertToNewDidDocument(didDoc), + }) const connection = new Connection({ - did: connectionRecord.did, - didDoc: connectionRecord.didDoc, + did, + didDoc, }) const connectionJson = JsonTransformer.toJSON(connection) if (!connectionRecord.threadId) { - throw new AriesFrameworkError(`Connection record with id ${connectionId} does not have a thread id`) + throw new AriesFrameworkError(`Connection record with id ${connectionRecord.id} does not have a thread id`) } - // Use invitationKey by default, fall back to verkey - const signingKey = (connectionRecord.getTag('invitationKey') as string) ?? connectionRecord.verkey + const signingKey = Key.fromFingerprint(outOfBandRecord.getTags().recipientKeyFingerprints[0]).publicKeyBase58 const connectionResponse = new ConnectionResponseMessage({ threadId: connectionRecord.threadId, connectionSig: await signData(connectionJson, this.wallet, signingKey), }) - await this.updateState(connectionRecord, ConnectionState.Responded) + connectionRecord.did = peerDid + await this.updateState(connectionRecord, DidExchangeState.ResponseSent) + this.logger.debug(`Create message ${ConnectionResponseMessage.type} end`, { + connectionRecord, + message: connectionResponse, + }) return { - connectionRecord: connectionRecord, + connectionRecord, message: connectionResponse, } } @@ -301,49 +279,68 @@ export class ConnectionService { * @returns updated connection record */ public async processResponse( - messageContext: InboundMessageContext + messageContext: InboundMessageContext, + outOfBandRecord: OutOfBandRecord ): Promise { - const { message, recipientVerkey, senderVerkey } = messageContext + this.logger.debug(`Process message ${ConnectionResponseMessage.type} start`, messageContext) + const { connection: connectionRecord, message, recipientKey, senderKey } = messageContext - if (!recipientVerkey || !senderVerkey) { - throw new AriesFrameworkError('Unable to process connection request without senderVerkey or recipientVerkey') + if (!recipientKey || !senderKey) { + throw new AriesFrameworkError('Unable to process connection request without senderKey or recipientKey') } - const connectionRecord = await this.findByVerkey(recipientVerkey) - if (!connectionRecord) { - throw new AriesFrameworkError( - `Unable to process connection response: connection for verkey ${recipientVerkey} not found` - ) + throw new AriesFrameworkError('No connection record in message context.') } - connectionRecord.assertState(ConnectionState.Requested) - connectionRecord.assertRole(ConnectionRole.Invitee) + connectionRecord.assertState(DidExchangeState.RequestSent) + connectionRecord.assertRole(DidExchangeRole.Requester) - const connectionJson = await unpackAndVerifySignatureDecorator(message.connectionSig, this.wallet) + let connectionJson = null + try { + connectionJson = await unpackAndVerifySignatureDecorator(message.connectionSig, this.wallet) + } catch (error) { + if (error instanceof AriesFrameworkError) { + throw new ConnectionProblemReportError(error.message, { + problemCode: ConnectionProblemReportReason.RequestProcessingError, + }) + } + throw error + } const connection = JsonTransformer.fromJSON(connectionJson, Connection) - await MessageValidator.validate(connection) + try { + await MessageValidator.validate(connection) + } catch (error) { + throw new Error(error) + } // Per the Connection RFC we must check if the key used to sign the connection~sig is the same key // as the recipient key(s) in the connection invitation message const signerVerkey = message.connectionSig.signer - const invitationKey = connectionRecord.getTags().invitationKey + + const invitationKey = Key.fromFingerprint(outOfBandRecord.getTags().recipientKeyFingerprints[0]).publicKeyBase58 + if (signerVerkey !== invitationKey) { - throw new AriesFrameworkError( - `Connection object in connection response message is not signed with same key as recipient key in invitation expected='${invitationKey}' received='${signerVerkey}'` + throw new ConnectionProblemReportError( + `Connection object in connection response message is not signed with same key as recipient key in invitation expected='${invitationKey}' received='${signerVerkey}'`, + { problemCode: ConnectionProblemReportReason.ResponseNotAccepted } ) } - connectionRecord.theirDid = connection.did - connectionRecord.theirDidDoc = connection.didDoc - connectionRecord.threadId = message.threadId - - if (!connectionRecord.theirKey) { - throw new AriesFrameworkError(`Connection with id ${connectionRecord.id} has no recipient keys.`) + if (!connection.didDoc) { + throw new AriesFrameworkError('DID Document is missing.') } - await this.updateState(connectionRecord, ConnectionState.Responded) + const { did: peerDid } = await this.createDid({ + role: DidDocumentRole.Received, + didDocument: convertToNewDidDocument(connection.didDoc), + }) + + connectionRecord.theirDid = peerDid + connectionRecord.threadId = message.threadId + + await this.updateState(connectionRecord, DidExchangeState.ResponseReceived) return connectionRecord } @@ -353,27 +350,28 @@ export class ConnectionService { * By default a trust ping message should elicit a response. If this is not desired the * `config.responseRequested` property can be set to `false`. * - * @param connectionId the id of the connection for which to create a trust ping message + * @param connectionRecord the connection for which to create a trust ping message * @param config the config for the trust ping message * @returns outbound message containing trust ping message */ public async createTrustPing( - connectionId: string, + connectionRecord: ConnectionRecord, config: { responseRequested?: boolean; comment?: string } = {} ): Promise> { - const connectionRecord = await this.connectionRepository.getById(connectionId) - - connectionRecord.assertState([ConnectionState.Responded, ConnectionState.Complete]) + connectionRecord.assertState([DidExchangeState.ResponseReceived, DidExchangeState.Completed]) // TODO: // - create ack message // - maybe this shouldn't be in the connection service? const trustPing = new TrustPingMessage(config) - await this.updateState(connectionRecord, ConnectionState.Complete) + // Only update connection record and emit an event if the state is not already 'Complete' + if (connectionRecord.state !== DidExchangeState.Completed) { + await this.updateState(connectionRecord, DidExchangeState.Completed) + } return { - connectionRecord: connectionRecord, + connectionRecord, message: trustPing, } } @@ -386,23 +384,69 @@ export class ConnectionService { * @returns updated connection record */ public async processAck(messageContext: InboundMessageContext): Promise { - const { connection, recipientVerkey } = messageContext + const { connection, recipientKey } = messageContext if (!connection) { throw new AriesFrameworkError( - `Unable to process connection ack: connection for verkey ${recipientVerkey} not found` + `Unable to process connection ack: connection for recipient key ${recipientKey?.fingerprint} not found` ) } // TODO: This is better addressed in a middleware of some kind because // any message can transition the state to complete, not just an ack or trust ping - if (connection.state === ConnectionState.Responded && connection.role === ConnectionRole.Inviter) { - await this.updateState(connection, ConnectionState.Complete) + if (connection.state === DidExchangeState.ResponseSent && connection.role === DidExchangeRole.Responder) { + await this.updateState(connection, DidExchangeState.Completed) } return connection } + /** + * Process a received {@link ProblemReportMessage}. + * + * @param messageContext The message context containing a connection problem report message + * @returns connection record associated with the connection problem report message + * + */ + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message: connectionProblemReportMessage, recipientKey, senderKey } = messageContext + + this.logger.debug(`Processing connection problem report for verkey ${recipientKey?.fingerprint}`) + + if (!recipientKey) { + throw new AriesFrameworkError('Unable to process connection problem report without recipientKey') + } + + let connectionRecord + const ourDidRecords = await this.didRepository.findAllByRecipientKey(recipientKey) + for (const ourDidRecord of ourDidRecords) { + connectionRecord = await this.findByOurDid(ourDidRecord.id) + } + + if (!connectionRecord) { + throw new AriesFrameworkError( + `Unable to process connection problem report: connection for recipient key ${recipientKey.fingerprint} not found` + ) + } + + const theirDidRecord = connectionRecord.theirDid && (await this.didRepository.findById(connectionRecord.theirDid)) + if (!theirDidRecord) { + throw new AriesFrameworkError(`Did record with id ${connectionRecord.theirDid} not found.`) + } + + if (senderKey) { + if (!theirDidRecord?.getTags().recipientKeyFingerprints?.includes(senderKey.fingerprint)) { + throw new AriesFrameworkError("Sender key doesn't match key of connection record") + } + } + + connectionRecord.errorMessage = `${connectionProblemReportMessage.description.code} : ${connectionProblemReportMessage.description.en}` + await this.update(connectionRecord) + return connectionRecord + } + /** * Assert that an inbound message either has a connection associated with it, * or has everything correctly set up for connection-less exchange. @@ -433,9 +477,12 @@ export class ConnectionService { type: message.type, }) + const recipientKey = messageContext.recipientKey && messageContext.recipientKey.publicKeyBase58 + const senderKey = messageContext.senderKey && messageContext.senderKey.publicKeyBase58 + if (previousSentMessage) { // If we have previously sent a message, it is not allowed to receive an OOB/unpacked message - if (!messageContext.recipientVerkey) { + if (!recipientKey) { throw new AriesFrameworkError( 'Cannot verify service without recipientKey on incoming message (received unpacked message)' ) @@ -443,10 +490,7 @@ export class ConnectionService { // Check if the inbound message recipient key is present // in the recipientKeys of previously sent message ~service decorator - if ( - !previousSentMessage?.service || - !previousSentMessage.service.recipientKeys.includes(messageContext.recipientVerkey) - ) { + if (!previousSentMessage?.service || !previousSentMessage.service.recipientKeys.includes(recipientKey)) { throw new AriesFrameworkError( 'Previously sent message ~service recipientKeys does not include current received message recipient key' ) @@ -455,7 +499,7 @@ export class ConnectionService { if (previousReceivedMessage) { // If we have previously received a message, it is not allowed to receive an OOB/unpacked/AnonCrypt message - if (!messageContext.senderVerkey) { + if (!senderKey) { throw new AriesFrameworkError( 'Cannot verify service without senderKey on incoming message (received AnonCrypt or unpacked message)' ) @@ -463,10 +507,7 @@ export class ConnectionService { // Check if the inbound message sender key is present // in the recipientKeys of previously received message ~service decorator - if ( - !previousReceivedMessage.service || - !previousReceivedMessage.service.recipientKeys.includes(messageContext.senderVerkey) - ) { + if (!previousReceivedMessage.service || !previousReceivedMessage.service.recipientKeys.includes(senderKey)) { throw new AriesFrameworkError( 'Previously received message ~service recipientKeys does not include current received message sender key' ) @@ -474,13 +515,13 @@ export class ConnectionService { } // If message is received unpacked/, we need to make sure it included a ~service decorator - if (!message.service && !messageContext.recipientVerkey) { + if (!message.service && !recipientKey) { throw new AriesFrameworkError('Message recipientKey must have ~service decorator') } } } - public async updateState(connectionRecord: ConnectionRecord, newState: ConnectionState) { + public async updateState(connectionRecord: ConnectionRecord, newState: DidExchangeState) { const previousState = connectionRecord.state connectionRecord.state = newState await this.connectionRepository.update(connectionRecord) @@ -488,12 +529,16 @@ export class ConnectionService { this.eventEmitter.emit({ type: ConnectionEventTypes.ConnectionStateChanged, payload: { - connectionRecord: connectionRecord, + connectionRecord, previousState, }, }) } + public update(connectionRecord: ConnectionRecord) { + return this.connectionRepository.update(connectionRecord) + } + /** * Retrieve all connections records * @@ -535,43 +580,8 @@ export class ConnectionService { return this.connectionRepository.delete(connectionRecord) } - /** - * Find connection by verkey. - * - * @param verkey the verkey to search for - * @returns the connection record, or null if not found - * @throws {RecordDuplicateError} if multiple connections are found for the given verkey - */ - public findByVerkey(verkey: string): Promise { - return this.connectionRepository.findSingleByQuery({ - verkey, - }) - } - - /** - * Find connection by their verkey. - * - * @param verkey the verkey to search for - * @returns the connection record, or null if not found - * @throws {RecordDuplicateError} if multiple connections are found for the given verkey - */ - public findByTheirKey(verkey: string): Promise { - return this.connectionRepository.findSingleByQuery({ - theirKey: verkey, - }) - } - - /** - * Find connection by invitation key. - * - * @param key the invitation key to search for - * @returns the connection record, or null if not found - * @throws {RecordDuplicateError} if multiple connections are found for the given verkey - */ - public findByInvitationKey(key: string): Promise { - return this.connectionRepository.findSingleByQuery({ - invitationKey: key, - }) + public async findSingleByQuery(query: { did: string; theirDid: string }) { + return this.connectionRepository.findSingleByQuery(query) } /** @@ -583,22 +593,87 @@ export class ConnectionService { * @returns The connection record */ public getByThreadId(threadId: string): Promise { - return this.connectionRepository.getSingleByQuery({ threadId }) + return this.connectionRepository.getByThreadId(threadId) + } + + public async findByTheirDid(did: string): Promise { + return this.connectionRepository.findSingleByQuery({ theirDid: did }) + } + + public async findByOurDid(did: string): Promise { + return this.connectionRepository.findSingleByQuery({ did }) + } + + public async findAllByOutOfBandId(outOfBandId: string) { + return this.connectionRepository.findByQuery({ outOfBandId }) + } + + public async findByInvitationDid(invitationDid: string) { + return this.connectionRepository.findByQuery({ invitationDid }) } - private async createConnection(options: { - role: ConnectionRole - state: ConnectionState - invitation?: ConnectionInvitationMessage + public async createConnection(options: { + role: DidExchangeRole + state: DidExchangeState alias?: string - routing: Routing + did: string + mediatorId?: string theirLabel?: string autoAcceptConnection?: boolean multiUseInvitation: boolean tags?: CustomConnectionTags imageUrl?: string + protocol?: HandshakeProtocol + outOfBandId?: string + invitationDid?: string }): Promise { - const { endpoints, did, verkey, routingKeys, mediatorId } = options.routing + const connectionRecord = new ConnectionRecord({ + did: options.did, + state: options.state, + role: options.role, + tags: options.tags, + alias: options.alias, + theirLabel: options.theirLabel, + autoAcceptConnection: options.autoAcceptConnection, + imageUrl: options.imageUrl, + multiUseInvitation: options.multiUseInvitation, + mediatorId: options.mediatorId, + protocol: options.protocol, + outOfBandId: options.outOfBandId, + invitationDid: options.invitationDid, + }) + await this.connectionRepository.save(connectionRecord) + return connectionRecord + } + + private async createDid({ role, didDocument }: { role: DidDocumentRole; didDocument: DidDocument }) { + const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON()) + didDocument.id = peerDid + const didRecord = new DidRecord({ + id: peerDid, + role, + didDocument, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + + this.logger.debug('Saving DID record', { + id: didRecord.id, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + + await this.didRepository.save(didRecord) + this.logger.debug('Did record created.', didRecord) + return { did: peerDid, didDocument } + } + + private createDidDoc(routing: Routing) { + const { endpoints, did, verkey, routingKeys } = routing const publicKey = new Ed25119Sig2018({ id: `${did}#1`, @@ -606,6 +681,10 @@ export class ConnectionService { publicKeyBase58: verkey, }) + // TODO: abstract the second parameter for ReferencedAuthentication away. This can be + // inferred from the publicKey class instance + const auth = new EmbeddedAuthentication(publicKey) + // IndyAgentService is old service type const services = endpoints.map( (endpoint, index) => @@ -619,40 +698,48 @@ export class ConnectionService { }) ) - // TODO: abstract the second parameter for ReferencedAuthentication away. This can be - // inferred from the publicKey class instance - const auth = new ReferencedAuthentication(publicKey, authenticationTypes[publicKey.type]) - - const didDoc = new DidDoc({ + return new DidDoc({ id: did, authentication: [auth], service: services, publicKey: [publicKey], }) + } - const connectionRecord = new ConnectionRecord({ - did, - didDoc, - verkey, - state: options.state, - role: options.role, - tags: options.tags, - invitation: options.invitation, - alias: options.alias, - theirLabel: options.theirLabel, - autoAcceptConnection: options.autoAcceptConnection, - imageUrl: options.imageUrl, - multiUseInvitation: options.multiUseInvitation, - mediatorId, + private createDidDocFromServices(did: string, recipientKey: string, services: OutOfBandDidCommService[]) { + const publicKey = new Ed25119Sig2018({ + id: `${did}#1`, + controller: did, + publicKeyBase58: recipientKey, }) - await this.connectionRepository.save(connectionRecord) - return connectionRecord + // TODO: abstract the second parameter for ReferencedAuthentication away. This can be + // inferred from the publicKey class instance + const auth = new EmbeddedAuthentication(publicKey) + + // IndyAgentService is old service type + const service = services.map( + (service, index) => + new IndyAgentService({ + id: `${did}#IndyAgentService`, + serviceEndpoint: service.serviceEndpoint, + recipientKeys: [recipientKey], + routingKeys: service.routingKeys?.map(didKeyToVerkey), + priority: index, + }) + ) + + return new DidDoc({ + id: did, + authentication: [auth], + service, + publicKey: [publicKey], + }) } public async returnWhenIsConnected(connectionId: string, timeoutMs = 20000): Promise { const isConnected = (connection: ConnectionRecord) => { - return connection.id === connectionId && connection.state === ConnectionState.Complete + return connection.id === connectionId && connection.state === DidExchangeState.Completed } const observable = this.eventEmitter.observable( diff --git a/packages/core/src/modules/connections/services/helpers.ts b/packages/core/src/modules/connections/services/helpers.ts new file mode 100644 index 0000000000..1b795d8f40 --- /dev/null +++ b/packages/core/src/modules/connections/services/helpers.ts @@ -0,0 +1,101 @@ +import type { DidDocument } from '../../dids' +import type { DidDoc, PublicKey } from '../models' + +import { Key, KeyType } from '../../../crypto' +import { AriesFrameworkError } from '../../../error' +import { IndyAgentService, DidCommV1Service, DidDocumentBuilder } from '../../dids' +import { getEd25519VerificationMethod } from '../../dids/domain/key-type/ed25519' +import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' +import { EmbeddedAuthentication } from '../models' + +export function convertToNewDidDocument(didDoc: DidDoc): DidDocument { + const didDocumentBuilder = new DidDocumentBuilder('') + + const oldIdNewIdMapping: { [key: string]: string } = {} + + didDoc.authentication.forEach((auth) => { + const { publicKey: pk } = auth + + // did:peer did documents can only use referenced keys. + if (pk.type === 'Ed25519VerificationKey2018' && pk.value) { + const ed25519VerificationMethod = convertPublicKeyToVerificationMethod(pk) + + const oldKeyId = normalizeId(pk.id) + oldIdNewIdMapping[oldKeyId] = ed25519VerificationMethod.id + didDocumentBuilder.addAuthentication(ed25519VerificationMethod.id) + + // Only the auth is embedded, we also need to add the key to the verificationMethod + // for referenced authentication this should already be the case + if (auth instanceof EmbeddedAuthentication) { + didDocumentBuilder.addVerificationMethod(ed25519VerificationMethod) + } + } + }) + + didDoc.publicKey.forEach((pk) => { + if (pk.type === 'Ed25519VerificationKey2018' && pk.value) { + const ed25519VerificationMethod = convertPublicKeyToVerificationMethod(pk) + + const oldKeyId = normalizeId(pk.id) + oldIdNewIdMapping[oldKeyId] = ed25519VerificationMethod.id + didDocumentBuilder.addVerificationMethod(ed25519VerificationMethod) + } + }) + + didDoc.didCommServices.forEach((service) => { + const serviceId = normalizeId(service.id) + + // For didcommv1, we need to replace the old id with the new ones + if (service instanceof DidCommV1Service) { + const recipientKeys = service.recipientKeys.map((keyId) => { + const oldKeyId = normalizeId(keyId) + return oldIdNewIdMapping[oldKeyId] + }) + + service = new DidCommV1Service({ + id: serviceId, + recipientKeys, + serviceEndpoint: service.serviceEndpoint, + routingKeys: service.routingKeys, + accept: service.accept, + priority: service.priority, + }) + } else if (service instanceof IndyAgentService) { + service = new IndyAgentService({ + id: serviceId, + recipientKeys: service.recipientKeys, + serviceEndpoint: service.serviceEndpoint, + routingKeys: service.routingKeys, + priority: service.priority, + }) + } + + didDocumentBuilder.addService(service) + }) + + const didDocument = didDocumentBuilder.build() + + const peerDid = didDocumentJsonToNumAlgo1Did(didDocument.toJSON()) + didDocument.id = peerDid + + return didDocument +} + +function normalizeId(fullId: string): `#${string}` { + const [, id] = fullId.split('#') + + return `#${id ?? fullId}` +} + +function convertPublicKeyToVerificationMethod(publicKey: PublicKey) { + if (!publicKey.value) { + throw new AriesFrameworkError(`Public key ${publicKey.id} does not have value property`) + } + const publicKeyBase58 = publicKey.value + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519) + return getEd25519VerificationMethod({ + id: `#${publicKeyBase58.slice(0, 8)}`, + key: ed25519Key, + controller: '#id', + }) +} diff --git a/packages/core/src/modules/credentials/CredentialEvents.ts b/packages/core/src/modules/credentials/CredentialEvents.ts index 1a43613f7e..f49dd964ac 100644 --- a/packages/core/src/modules/credentials/CredentialEvents.ts +++ b/packages/core/src/modules/credentials/CredentialEvents.ts @@ -1,14 +1,22 @@ import type { BaseEvent } from '../../agent/Events' import type { CredentialState } from './CredentialState' -import type { CredentialRecord } from './repository/CredentialRecord' +import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord' export enum CredentialEventTypes { CredentialStateChanged = 'CredentialStateChanged', + RevocationNotificationReceived = 'RevocationNotificationReceived', } export interface CredentialStateChangedEvent extends BaseEvent { type: typeof CredentialEventTypes.CredentialStateChanged payload: { - credentialRecord: CredentialRecord + credentialRecord: CredentialExchangeRecord previousState: CredentialState | null } } + +export interface RevocationNotificationReceivedEvent extends BaseEvent { + type: typeof CredentialEventTypes.RevocationNotificationReceived + payload: { + credentialRecord: CredentialExchangeRecord + } +} diff --git a/packages/core/src/modules/credentials/CredentialProtocolVersion.ts b/packages/core/src/modules/credentials/CredentialProtocolVersion.ts new file mode 100644 index 0000000000..5806577c30 --- /dev/null +++ b/packages/core/src/modules/credentials/CredentialProtocolVersion.ts @@ -0,0 +1,4 @@ +export enum CredentialProtocolVersion { + V1 = 'v1', + V2 = 'v2', +} diff --git a/packages/core/src/modules/credentials/CredentialResponseCoordinator.ts b/packages/core/src/modules/credentials/CredentialResponseCoordinator.ts index d076596061..4bb3ce4ebb 100644 --- a/packages/core/src/modules/credentials/CredentialResponseCoordinator.ts +++ b/packages/core/src/modules/credentials/CredentialResponseCoordinator.ts @@ -1,11 +1,9 @@ -import type { CredentialRecord } from './repository' - import { scoped, Lifecycle } from 'tsyringe' import { AgentConfig } from '../../agent/AgentConfig' +import { DidCommMessageRepository } from '../../storage' import { AutoAcceptCredential } from './CredentialAutoAcceptType' -import { CredentialUtils } from './CredentialUtils' /** * This class handles all the automation with all the messages in the issue credential protocol @@ -14,9 +12,11 @@ import { CredentialUtils } from './CredentialUtils' @scoped(Lifecycle.ContainerScoped) export class CredentialResponseCoordinator { private agentConfig: AgentConfig + private didCommMessageRepository: DidCommMessageRepository - public constructor(agentConfig: AgentConfig) { + public constructor(agentConfig: AgentConfig, didCommMessageRepository: DidCommMessageRepository) { this.agentConfig = agentConfig + this.didCommMessageRepository = didCommMessageRepository } /** @@ -25,144 +25,10 @@ export class CredentialResponseCoordinator { * - Otherwise the agent config * - Otherwise {@link AutoAcceptCredential.Never} is returned */ - private static composeAutoAccept( + public static composeAutoAccept( recordConfig: AutoAcceptCredential | undefined, agentConfig: AutoAcceptCredential | undefined ) { return recordConfig ?? agentConfig ?? AutoAcceptCredential.Never } - - /** - * Checks whether it should automatically respond to a proposal - */ - public shouldAutoRespondToProposal(credentialRecord: CredentialRecord) { - const autoAccept = CredentialResponseCoordinator.composeAutoAccept( - credentialRecord.autoAcceptCredential, - this.agentConfig.autoAcceptCredentials - ) - - if (autoAccept === AutoAcceptCredential.Always) { - return true - } else if (autoAccept === AutoAcceptCredential.ContentApproved) { - return ( - this.areProposalValuesValid(credentialRecord) && this.areProposalAndOfferDefinitionIdEqual(credentialRecord) - ) - } - return false - } - - /** - * Checks whether it should automatically respond to an offer - */ - public shouldAutoRespondToOffer(credentialRecord: CredentialRecord) { - const autoAccept = CredentialResponseCoordinator.composeAutoAccept( - credentialRecord.autoAcceptCredential, - this.agentConfig.autoAcceptCredentials - ) - - if (autoAccept === AutoAcceptCredential.Always) { - return true - } else if (autoAccept === AutoAcceptCredential.ContentApproved) { - return this.areOfferValuesValid(credentialRecord) && this.areProposalAndOfferDefinitionIdEqual(credentialRecord) - } - return false - } - - /** - * Checks whether it should automatically respond to a request - */ - public shouldAutoRespondToRequest(credentialRecord: CredentialRecord) { - const autoAccept = CredentialResponseCoordinator.composeAutoAccept( - credentialRecord.autoAcceptCredential, - this.agentConfig.autoAcceptCredentials - ) - - if (autoAccept === AutoAcceptCredential.Always) { - return true - } else if (autoAccept === AutoAcceptCredential.ContentApproved) { - return this.isRequestDefinitionIdValid(credentialRecord) - } - return false - } - - /** - * Checks whether it should automatically respond to the issuance of a credential - */ - public shouldAutoRespondToIssue(credentialRecord: CredentialRecord) { - const autoAccept = CredentialResponseCoordinator.composeAutoAccept( - credentialRecord.autoAcceptCredential, - this.agentConfig.autoAcceptCredentials - ) - - if (autoAccept === AutoAcceptCredential.Always) { - return true - } else if (autoAccept === AutoAcceptCredential.ContentApproved) { - return this.areCredentialValuesValid(credentialRecord) - } - return false - } - - private areProposalValuesValid(credentialRecord: CredentialRecord) { - const { proposalMessage, credentialAttributes } = credentialRecord - - if (proposalMessage && proposalMessage.credentialProposal && credentialAttributes) { - const proposalValues = CredentialUtils.convertAttributesToValues(proposalMessage.credentialProposal.attributes) - const defaultValues = CredentialUtils.convertAttributesToValues(credentialAttributes) - if (CredentialUtils.checkValuesMatch(proposalValues, defaultValues)) { - return true - } - } - return false - } - - private areOfferValuesValid(credentialRecord: CredentialRecord) { - const { offerMessage, credentialAttributes } = credentialRecord - - if (offerMessage && credentialAttributes) { - const offerValues = CredentialUtils.convertAttributesToValues(offerMessage.credentialPreview.attributes) - const defaultValues = CredentialUtils.convertAttributesToValues(credentialAttributes) - if (CredentialUtils.checkValuesMatch(offerValues, defaultValues)) { - return true - } - } - return false - } - - private areCredentialValuesValid(credentialRecord: CredentialRecord) { - if (credentialRecord.credentialAttributes && credentialRecord.credentialMessage) { - const indyCredential = credentialRecord.credentialMessage.indyCredential - - if (!indyCredential) { - this.agentConfig.logger.error(`Missing required base64 encoded attachment data for credential`) - return false - } - - const credentialMessageValues = indyCredential.values - const defaultValues = CredentialUtils.convertAttributesToValues(credentialRecord.credentialAttributes) - - if (CredentialUtils.checkValuesMatch(credentialMessageValues, defaultValues)) { - return true - } - } - return false - } - - private areProposalAndOfferDefinitionIdEqual(credentialRecord: CredentialRecord) { - const proposalCredentialDefinitionId = credentialRecord.proposalMessage?.credentialDefinitionId - const offerCredentialDefinitionId = credentialRecord.offerMessage?.indyCredentialOffer?.cred_def_id - return proposalCredentialDefinitionId === offerCredentialDefinitionId - } - - private isRequestDefinitionIdValid(credentialRecord: CredentialRecord) { - if (credentialRecord.proposalMessage || credentialRecord.offerMessage) { - const previousCredentialDefinitionId = - credentialRecord.offerMessage?.indyCredentialOffer?.cred_def_id ?? - credentialRecord.proposalMessage?.credentialDefinitionId - - if (previousCredentialDefinitionId === credentialRecord.requestMessage?.indyCredentialRequest?.cred_def_id) { - return true - } - } - return false - } } diff --git a/packages/core/src/modules/credentials/CredentialServiceOptions.ts b/packages/core/src/modules/credentials/CredentialServiceOptions.ts new file mode 100644 index 0000000000..e9af147adc --- /dev/null +++ b/packages/core/src/modules/credentials/CredentialServiceOptions.ts @@ -0,0 +1,87 @@ +import type { AgentMessage } from '../../agent/AgentMessage' +import type { Attachment } from '../../decorators/attachment/Attachment' +import type { LinkedAttachment } from '../../utils/LinkedAttachment' +import type { AutoAcceptCredential } from './CredentialAutoAcceptType' +import type { + AcceptOfferOptions, + AcceptProposalOptions, + AcceptRequestOptions, + NegotiateOfferOptions, + NegotiateProposalOptions, + OfferCredentialOptions, + RequestCredentialOptions, +} from './CredentialsModuleOptions' +import type { CredentialPreviewAttribute } from './models/CredentialPreviewAttributes' +import type { V1CredentialPreview } from './protocol/v1/V1CredentialPreview' +import type { ProposeCredentialMessageOptions } from './protocol/v1/messages' +import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord' + +export interface IndyCredentialPreview { + credentialDefinitionId?: string + attributes?: CredentialPreviewAttribute[] +} + +export interface CredentialProtocolMsgReturnType { + message: MessageType + credentialRecord: CredentialExchangeRecord +} + +export interface CredentialOfferTemplate { + credentialDefinitionId: string + comment?: string + preview: V1CredentialPreview + autoAcceptCredential?: AutoAcceptCredential + attachments?: Attachment[] + linkedAttachments?: LinkedAttachment[] +} + +export interface ServiceAcceptOfferOptions extends AcceptOfferOptions { + attachId?: string + credentialFormats: { + indy?: IndyCredentialPreview + jsonld?: { + // todo + } + } +} + +export interface ServiceOfferCredentialOptions extends OfferCredentialOptions { + connectionId?: string + attachId?: string + // offerAttachment?: Attachment +} + +export interface ServiceAcceptProposalOptions extends AcceptProposalOptions { + offerAttachment?: Attachment + proposalAttachment?: Attachment +} + +export interface ServiceAcceptRequestOptions extends AcceptRequestOptions { + attachId?: string +} +export interface ServiceNegotiateProposalOptions extends NegotiateProposalOptions { + offerAttachment?: Attachment +} + +export interface ServiceNegotiateOfferOptions extends NegotiateOfferOptions { + offerAttachment?: Attachment +} + +export interface ServiceRequestCredentialOptions extends RequestCredentialOptions { + attachId?: string + offerAttachment?: Attachment + requestAttachment?: Attachment +} + +export interface ServiceAcceptCredentialOptions { + credentialAttachment?: Attachment +} + +export type CredentialProposeOptions = Omit & { + linkedAttachments?: LinkedAttachment[] + autoAcceptCredential?: AutoAcceptCredential +} + +export interface DeleteCredentialOptions { + deleteAssociatedCredentials: boolean +} diff --git a/packages/core/src/modules/credentials/CredentialUtils.ts b/packages/core/src/modules/credentials/CredentialUtils.ts index 95d9b4c2be..791cd3bb5d 100644 --- a/packages/core/src/modules/credentials/CredentialUtils.ts +++ b/packages/core/src/modules/credentials/CredentialUtils.ts @@ -1,14 +1,17 @@ import type { LinkedAttachment } from '../../utils/LinkedAttachment' -import type { CredValues } from 'indy-sdk' +import type { V1CredentialPreview } from './protocol/v1/V1CredentialPreview' +import type { V2CredentialPreview } from './protocol/v2/V2CredentialPreview' +import type { CredValues, Schema } from 'indy-sdk' import BigNumber from 'bn.js' -import { sha256 } from 'js-sha256' import { AriesFrameworkError } from '../../error/AriesFrameworkError' +import { Hasher } from '../../utils' import { encodeAttachment } from '../../utils/attachment' +import { Buffer } from '../../utils/buffer' import { isBoolean, isNumber, isString } from '../../utils/type' -import { CredentialPreview, CredentialPreviewAttribute } from './messages/CredentialPreview' +import { CredentialPreviewAttribute } from './models/CredentialPreviewAttributes' export class CredentialUtils { /** @@ -19,26 +22,28 @@ export class CredentialUtils { * * @returns a modified version of the credential preview with the linked credentials * */ - public static createAndLinkAttachmentsToPreview(attachments: LinkedAttachment[], preview: CredentialPreview) { - const credentialPreview = new CredentialPreview({ attributes: [...preview.attributes] }) + public static createAndLinkAttachmentsToPreview( + attachments: LinkedAttachment[], + credentialPreview: V1CredentialPreview | V2CredentialPreview + ) { const credentialPreviewAttributeNames = credentialPreview.attributes.map((attribute) => attribute.name) attachments.forEach((linkedAttachment) => { if (credentialPreviewAttributeNames.includes(linkedAttachment.attributeName)) { throw new AriesFrameworkError( `linkedAttachment ${linkedAttachment.attributeName} already exists in the preview` ) + } else { + const credentialPreviewAttribute = new CredentialPreviewAttribute({ + name: linkedAttachment.attributeName, + mimeType: linkedAttachment.attachment.mimeType, + value: encodeAttachment(linkedAttachment.attachment), + }) + credentialPreview.attributes.push(credentialPreviewAttribute) } - const credentialPreviewAttribute = new CredentialPreviewAttribute({ - name: linkedAttachment.attributeName, - mimeType: linkedAttachment.attachment.mimeType, - value: encodeAttachment(linkedAttachment.attachment), - }) - credentialPreview.attributes.push(credentialPreviewAttribute) }) return credentialPreview } - /** * Converts int value to string * Converts string value: @@ -152,7 +157,7 @@ export class CredentialUtils { // If value is an int32 number string return as number string if (isString(value) && !isEmpty(value) && !isNaN(Number(value)) && this.isInt32(Number(value))) { - return value + return Number(value).toString() } if (isNumber(value)) { @@ -164,9 +169,23 @@ export class CredentialUtils { value = 'None' } - return new BigNumber(sha256.array(value as string)).toString() + return new BigNumber(Hasher.hash(Buffer.from(value as string), 'sha2-256')).toString() } + public static checkAttributesMatch(schema: Schema, credentialPreview: V1CredentialPreview | V2CredentialPreview) { + const schemaAttributes = schema.attrNames + const credAttributes = credentialPreview.attributes.map((a) => a.name) + + const difference = credAttributes + .filter((x) => !schemaAttributes.includes(x)) + .concat(schemaAttributes.filter((x) => !credAttributes.includes(x))) + + if (difference.length > 0) { + throw new AriesFrameworkError( + `The credential preview attributes do not match the schema attributes (difference is: ${difference}, needs: ${schemaAttributes})` + ) + } + } private static isInt32(number: number) { const minI32 = -2147483648 const maxI32 = 2147483647 diff --git a/packages/core/src/modules/credentials/CredentialsModule.ts b/packages/core/src/modules/credentials/CredentialsModule.ts index 5e1fab906e..2e4bbef562 100644 --- a/packages/core/src/modules/credentials/CredentialsModule.ts +++ b/packages/core/src/modules/credentials/CredentialsModule.ts @@ -1,297 +1,297 @@ -import type { AutoAcceptCredential } from './CredentialAutoAcceptType' -import type { OfferCredentialMessage, CredentialPreview } from './messages' -import type { CredentialRecord } from './repository/CredentialRecord' -import type { CredentialOfferTemplate, CredentialProposeOptions } from './services' +import type { AgentMessage } from '../../agent/AgentMessage' +import type { Logger } from '../../logger' +import type { DeleteCredentialOptions } from './CredentialServiceOptions' +import type { + AcceptOfferOptions, + AcceptProposalOptions, + AcceptRequestOptions, + NegotiateOfferOptions, + NegotiateProposalOptions, + OfferCredentialOptions, + ProposeCredentialOptions, + RequestCredentialOptions, +} from './CredentialsModuleOptions' +import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord' +import type { CredentialService } from './services/CredentialService' import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../agent/AgentConfig' -import { Dispatcher } from '../../agent/Dispatcher' import { MessageSender } from '../../agent/MessageSender' import { createOutboundMessage } from '../../agent/helpers' import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' import { AriesFrameworkError } from '../../error' -import { isLinkedAttachment } from '../../utils/attachment' -import { ConnectionService } from '../connections/services/ConnectionService' +import { DidCommMessageRole } from '../../storage' +import { DidCommMessageRepository } from '../../storage/didcomm/DidCommMessageRepository' +import { ConnectionService } from '../connections/services' import { MediationRecipientService } from '../routing' -import { CredentialResponseCoordinator } from './CredentialResponseCoordinator' -import { - CredentialAckHandler, - IssueCredentialHandler, - OfferCredentialHandler, - ProposeCredentialHandler, - RequestCredentialHandler, -} from './handlers' -import { CredentialService } from './services' +import { CredentialProtocolVersion } from './CredentialProtocolVersion' +import { CredentialState } from './CredentialState' +import { V1CredentialService } from './protocol/v1/V1CredentialService' +import { V2CredentialService } from './protocol/v2/V2CredentialService' +import { CredentialRepository } from './repository/CredentialRepository' + +export interface CredentialsModule { + // Proposal methods + proposeCredential(options: ProposeCredentialOptions): Promise + acceptProposal(options: AcceptProposalOptions): Promise + negotiateProposal(options: NegotiateProposalOptions): Promise + + // Offer methods + offerCredential(options: OfferCredentialOptions): Promise + acceptOffer(options: AcceptOfferOptions): Promise + declineOffer(credentialRecordId: string): Promise + negotiateOffer(options: NegotiateOfferOptions): Promise + // out of band + createOutOfBandOffer(options: OfferCredentialOptions): Promise<{ + message: AgentMessage + credentialRecord: CredentialExchangeRecord + }> + // Request + // This is for beginning the exchange with a request (no proposal or offer). Only possible + // (currently) with W3C. We will not implement this in phase I + // requestCredential(credentialOptions: RequestCredentialOptions): Promise + + // when the issuer accepts the request he issues the credential to the holder + acceptRequest(options: AcceptRequestOptions): Promise + + // Credential + acceptCredential(credentialRecordId: string): Promise + + // Record Methods + getAll(): Promise + getById(credentialRecordId: string): Promise + findById(credentialRecordId: string): Promise + deleteById(credentialRecordId: string, options?: DeleteCredentialOptions): Promise +} @scoped(Lifecycle.ContainerScoped) -export class CredentialsModule { +export class CredentialsModule implements CredentialsModule { private connectionService: ConnectionService - private credentialService: CredentialService private messageSender: MessageSender + private credentialRepository: CredentialRepository private agentConfig: AgentConfig - private credentialResponseCoordinator: CredentialResponseCoordinator - private mediationRecipientService: MediationRecipientService - + private didCommMessageRepo: DidCommMessageRepository + private v1Service: V1CredentialService + private v2Service: V2CredentialService + private mediatorRecipientService: MediationRecipientService + private logger: Logger + private serviceMap: { [key in CredentialProtocolVersion]: CredentialService } + + // note some of the parameters passed in here are temporary, as we intend + // to eventually remove CredentialsModule public constructor( - dispatcher: Dispatcher, - connectionService: ConnectionService, - credentialService: CredentialService, messageSender: MessageSender, + connectionService: ConnectionService, agentConfig: AgentConfig, - credentialResponseCoordinator: CredentialResponseCoordinator, - mediationRecipientService: MediationRecipientService + credentialRepository: CredentialRepository, + mediationRecipientService: MediationRecipientService, + didCommMessageRepository: DidCommMessageRepository, + v1Service: V1CredentialService, + v2Service: V2CredentialService ) { - this.connectionService = connectionService - this.credentialService = credentialService this.messageSender = messageSender + this.connectionService = connectionService + this.credentialRepository = credentialRepository this.agentConfig = agentConfig - this.credentialResponseCoordinator = credentialResponseCoordinator - this.mediationRecipientService = mediationRecipientService - this.registerHandlers(dispatcher) + this.mediatorRecipientService = mediationRecipientService + this.didCommMessageRepo = didCommMessageRepository + this.logger = agentConfig.logger + + this.v1Service = v1Service + this.v2Service = v2Service + + this.serviceMap = { + [CredentialProtocolVersion.V1]: this.v1Service, + [CredentialProtocolVersion.V2]: this.v2Service, + } + this.logger.debug(`Initializing Credentials Module for agent ${this.agentConfig.label}`) } - /** - * Initiate a new credential exchange as holder by sending a credential proposal message - * to the connection with the specified connection id. - * - * @param connectionId The connection to send the credential proposal to - * @param config Additional configuration to use for the proposal - * @returns Credential record associated with the sent proposal message - */ - public async proposeCredential(connectionId: string, config?: CredentialProposeOptions) { - const connection = await this.connectionService.getById(connectionId) + public getService(protocolVersion: CredentialProtocolVersion): CredentialService { + return this.serviceMap[protocolVersion] + } - const { message, credentialRecord } = await this.credentialService.createProposal(connection, config) + public async declineOffer(credentialRecordId: string): Promise { + const credentialRecord = await this.getById(credentialRecordId) + credentialRecord.assertState(CredentialState.OfferReceived) - const outbound = createOutboundMessage(connection, message) - await this.messageSender.sendMessage(outbound) + // with version we can get the Service + const service = this.getService(credentialRecord.protocolVersion) + await service.updateState(credentialRecord, CredentialState.Declined) return credentialRecord } - /** - * Accept a credential proposal as issuer (by sending a credential offer message) to the connection - * associated with the credential record. - * - * @param credentialRecordId The id of the credential record for which to accept the proposal - * @param config Additional configuration to use for the offer - * @returns Credential record associated with the credential offer - * - */ - public async acceptProposal( - credentialRecordId: string, - config?: { - comment?: string - credentialDefinitionId?: string - autoAcceptCredential?: AutoAcceptCredential - } - ) { - const credentialRecord = await this.credentialService.getById(credentialRecordId) - if (!credentialRecord.connectionId) { - throw new AriesFrameworkError( - `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support credential proposal or negotiation.` - ) - } - - const connection = await this.connectionService.getById(credentialRecord.connectionId) - - const credentialProposalMessage = credentialRecord.proposalMessage - if (!credentialProposalMessage?.credentialProposal) { - throw new AriesFrameworkError( - `Credential record with id ${credentialRecordId} is missing required credential proposal` - ) + public async negotiateOffer(options: NegotiateOfferOptions): Promise { + if (!options.credentialRecordId) { + throw new AriesFrameworkError(`No credential record id found in negotiateCredentialOffer`) } + const credentialRecord = await this.getById(options.credentialRecordId) + const version = credentialRecord.protocolVersion - const credentialDefinitionId = config?.credentialDefinitionId ?? credentialProposalMessage.credentialDefinitionId + const service = this.getService(version) + const { message } = await service.negotiateOffer(options, credentialRecord) - credentialRecord.linkedAttachments = credentialProposalMessage.attachments?.filter((attachment) => - isLinkedAttachment(attachment) - ) - - if (!credentialDefinitionId) { + if (!credentialRecord.connectionId) { throw new AriesFrameworkError( - 'Missing required credential definition id. If credential proposal message contains no credential definition id it must be passed to config.' + `No connection id for credential record ${credentialRecord.id} not found. Connection-less issuance does not support negotiation` ) } - - // TODO: check if it is possible to issue credential based on proposal filters - const { message } = await this.credentialService.createOfferAsResponse(credentialRecord, { - preview: credentialProposalMessage.credentialProposal, - credentialDefinitionId, - comment: config?.comment, - autoAcceptCredential: config?.autoAcceptCredential, - attachments: credentialRecord.linkedAttachments, - }) + const connection = await this.connectionService.getById(credentialRecord.connectionId) const outboundMessage = createOutboundMessage(connection, message) + await this.messageSender.sendMessage(outboundMessage) return credentialRecord } /** - * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection - * associated with the credential record. - * - * @param credentialRecordId The id of the credential record for which to accept the proposal - * @param preview The new preview for negotiation - * @param config Additional configuration to use for the offer - * @returns Credential record associated with the credential offer + * Initiate a new credential exchange as holder by sending a credential proposal message + * to the connection with the specified credential options * + * @param options configuration to use for the proposal + * @returns Credential exchange record associated with the sent proposal message */ - public async negotiateProposal( - credentialRecordId: string, - preview: CredentialPreview, - config?: { - comment?: string - credentialDefinitionId?: string - autoAcceptCredential?: AutoAcceptCredential - } - ) { - const credentialRecord = await this.credentialService.getById(credentialRecordId) - - if (!credentialRecord.connectionId) { - throw new AriesFrameworkError( - `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` - ) - } - const connection = await this.connectionService.getById(credentialRecord.connectionId) - const credentialProposalMessage = credentialRecord.proposalMessage + public async proposeCredential(options: ProposeCredentialOptions): Promise { + // get the version + const version = options.protocolVersion - if (!credentialProposalMessage?.credentialProposal) { - throw new AriesFrameworkError( - `Credential record with id ${credentialRecordId} is missing required credential proposal` - ) + // with version we can get the Service + if (!version) { + throw new AriesFrameworkError('Missing Protocol Version') } + const service = this.getService(version) - const credentialDefinitionId = config?.credentialDefinitionId ?? credentialProposalMessage.credentialDefinitionId + this.logger.debug(`Got a CredentialService object for version ${version}`) - if (!credentialDefinitionId) { - throw new AriesFrameworkError( - 'Missing required credential definition id. If credential proposal message contains no credential definition id it must be passed to config.' - ) - } + const connection = await this.connectionService.getById(options.connectionId) - const { message } = await this.credentialService.createOfferAsResponse(credentialRecord, { - preview, - credentialDefinitionId, - comment: config?.comment, - autoAcceptCredential: config?.autoAcceptCredential, - attachments: credentialRecord.linkedAttachments, - }) + // will get back a credential record -> map to Credential Exchange Record + const { credentialRecord, message } = await service.createProposal(options) - const outboundMessage = createOutboundMessage(connection, message) - await this.messageSender.sendMessage(outboundMessage) + this.logger.debug('We have a message (sending outbound): ', message) + + // send the message here + const outbound = createOutboundMessage(connection, message) + this.logger.debug('In proposeCredential: Send Proposal to Issuer') + await this.messageSender.sendMessage(outbound) return credentialRecord } /** - * Initiate a new credential exchange as issuer by sending a credential offer message - * to the connection with the specified connection id. + * Accept a credential proposal as issuer (by sending a credential offer message) to the connection + * associated with the credential record. + * + * @param options config object for the proposal (and subsequent offer) which replaces previous named parameters + * @returns Credential exchange record associated with the credential offer * - * @param connectionId The connection to send the credential offer to - * @param credentialTemplate The credential template to use for the offer - * @returns Credential record associated with the sent credential offer message */ - public async offerCredential( - connectionId: string, - credentialTemplate: CredentialOfferTemplate - ): Promise { - const connection = await this.connectionService.getById(connectionId) + public async acceptProposal(options: AcceptProposalOptions): Promise { + const credentialRecord = await this.getById(options.credentialRecordId) - const { message, credentialRecord } = await this.credentialService.createOffer(credentialTemplate, connection) + if (!credentialRecord.connectionId) { + throw new AriesFrameworkError('Missing connection id in v2 acceptCredentialProposal') + } + const version = credentialRecord.protocolVersion - const outboundMessage = createOutboundMessage(connection, message) - await this.messageSender.sendMessage(outboundMessage) + // with version we can get the Service + const service = this.getService(version) - return credentialRecord - } + // will get back a credential record -> map to Credential Exchange Record + const { message } = await service.acceptProposal(options, credentialRecord) - /** - * Initiate a new credential exchange as issuer by creating a credential offer - * not bound to any connection. The offer must be delivered out-of-band to the holder - * - * @param credentialTemplate The credential template to use for the offer - * @returns The credential record and credential offer message - */ - public async createOutOfBandOffer(credentialTemplate: CredentialOfferTemplate): Promise<{ - offerMessage: OfferCredentialMessage - credentialRecord: CredentialRecord - }> { - const { message, credentialRecord } = await this.credentialService.createOffer(credentialTemplate) - - // Create and set ~service decorator - const routing = await this.mediationRecipientService.getRouting() - message.service = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.verkey], - routingKeys: routing.routingKeys, - }) + const connection = await this.connectionService.getById(credentialRecord.connectionId) + + this.logger.debug('We have an offer message (sending outbound): ', message) + + // send the message here + const outbound = createOutboundMessage(connection, message) - // Save ~service decorator to record (to remember our verkey) - credentialRecord.offerMessage = message - await this.credentialService.update(credentialRecord) + this.logger.debug('In acceptCredentialProposal: Send Proposal to Issuer') + await this.messageSender.sendMessage(outbound) - return { credentialRecord, offerMessage: message } + return credentialRecord } /** * Accept a credential offer as holder (by sending a credential request message) to the connection * associated with the credential record. * - * @param credentialRecordId The id of the credential record for which to accept the offer - * @param config Additional configuration to use for the request - * @returns Credential record associated with the sent credential request message - * + * @param options The object containing config options of the offer to be accepted + * @returns Object containing offer associated credential record */ - public async acceptOffer( - credentialRecordId: string, - config?: { comment?: string; autoAcceptCredential?: AutoAcceptCredential } - ): Promise { - const record = await this.credentialService.getById(credentialRecordId) + public async acceptOffer(options: AcceptOfferOptions): Promise { + const record = await this.getById(options.credentialRecordId) + + const service = this.getService(record.protocolVersion) + + this.logger.debug(`Got a CredentialService object for this version; version = ${service.getVersion()}`) + + const offerMessage = await service.getOfferMessage(record.id) // Use connection if present if (record.connectionId) { const connection = await this.connectionService.getById(record.connectionId) - const { message, credentialRecord } = await this.credentialService.createRequest(record, { - ...config, - holderDid: connection.did, + const requestOptions: RequestCredentialOptions = { + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + } + const { message, credentialRecord } = await service.createRequest(record, requestOptions, connection.did) + + await this.didCommMessageRepo.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, }) + this.logger.debug('We have sent a credential request') const outboundMessage = createOutboundMessage(connection, message) + this.logger.debug('We have a proposal message (sending outbound): ', message) + await this.messageSender.sendMessage(outboundMessage) + await this.credentialRepository.update(credentialRecord) return credentialRecord } // Use ~service decorator otherwise - else if (record.offerMessage?.service) { + else if (offerMessage?.service) { // Create ~service decorator - const routing = await this.mediationRecipientService.getRouting() + const routing = await this.mediatorRecipientService.getRouting() const ourService = new ServiceDecorator({ serviceEndpoint: routing.endpoints[0], recipientKeys: [routing.verkey], routingKeys: routing.routingKeys, }) - const recipientService = record.offerMessage.service - - const { message, credentialRecord } = await this.credentialService.createRequest(record, { - ...config, - holderDid: ourService.recipientKeys[0], - }) + const recipientService = offerMessage.service + + const requestOptions: RequestCredentialOptions = { + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + } + const { message, credentialRecord } = await service.createRequest( + record, + requestOptions, + ourService.recipientKeys[0] + ) // Set and save ~service decorator to record (to remember our verkey) message.service = ourService - credentialRecord.requestMessage = message - await this.credentialService.update(credentialRecord) + await this.didCommMessageRepo.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + await this.credentialRepository.update(credentialRecord) await this.messageSender.sendMessageToService({ message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], returnRoute: true, }) @@ -306,88 +306,106 @@ export class CredentialsModule { } /** - * Declines an offer as holder - * @param credentialRecordId the id of the credential to be declined - * @returns credential record that was declined - */ - public async declineOffer(credentialRecordId: string) { - const credentialRecord = await this.credentialService.getById(credentialRecordId) - await this.credentialService.declineOffer(credentialRecord) - return credentialRecord - } - - /** - * Negotiate a credential offer as holder (by sending a credential proposal message) to the connection + * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection * associated with the credential record. * - * @param credentialRecordId The id of the credential record for which to accept the offer - * @param preview The new preview for negotiation - * @param config Additional configuration to use for the request - * @returns Credential record associated with the sent credential request message + * @param options configuration for the offer see {@link NegotiateProposalOptions} + * @returns Credential exchange record associated with the credential offer * */ - public async negotiateOffer( - credentialRecordId: string, - preview: CredentialPreview, - config?: { comment?: string; autoAcceptCredential?: AutoAcceptCredential } - ) { - const credentialRecord = await this.credentialService.getById(credentialRecordId) + public async negotiateProposal(options: NegotiateProposalOptions): Promise { + const credentialRecord = await this.getById(options.credentialRecordId) + + // get the version + const version = credentialRecord.protocolVersion + + // with version we can get the Service + const service = this.getService(version) + const { message } = await service.negotiateProposal(options, credentialRecord) if (!credentialRecord.connectionId) { throw new AriesFrameworkError( - `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` + `No connection id for credential record ${credentialRecord.id} not found. Connection-less issuance does not support negotiation` ) } const connection = await this.connectionService.getById(credentialRecord.connectionId) - - const { message } = await this.credentialService.createProposalAsResponse(credentialRecord, { - ...config, - credentialProposal: preview, - }) + // use record connection id to get the connection const outboundMessage = createOutboundMessage(connection, message) + await this.messageSender.sendMessage(outboundMessage) return credentialRecord } /** - * Accept a credential request as issuer (by sending a credential message) to the connection - * associated with the credential record. + * Initiate a new credential exchange as issuer by sending a credential offer message + * to the connection with the specified connection id. * - * @param credentialRecordId The id of the credential record for which to accept the request - * @param config Additional configuration to use for the credential - * @returns Credential record associated with the sent presentation message + * @param options config options for the credential offer + * @returns Credential exchange record associated with the sent credential offer message + */ + public async offerCredential(options: OfferCredentialOptions): Promise { + if (!options.connectionId) { + throw new AriesFrameworkError('Missing connectionId on offerCredential') + } + const connection = await this.connectionService.getById(options.connectionId) + + const service = this.getService(options.protocolVersion) + + this.logger.debug(`Got a CredentialService object for version ${options.protocolVersion}`) + const { message, credentialRecord } = await service.createOffer(options) + + this.logger.debug('Offer Message successfully created; message= ', message) + const outboundMessage = createOutboundMessage(connection, message) + await this.messageSender.sendMessage(outboundMessage) + return credentialRecord + } + + /** + * Accept a credential request as holder (by sending a credential request message) to the connection + * associated with the credential record. * + * @param options The object containing config options of the request + * @returns CredentialExchangeRecord updated with information pertaining to this request */ - public async acceptRequest( - credentialRecordId: string, - config?: { comment?: string; autoAcceptCredential?: AutoAcceptCredential } - ) { - const record = await this.credentialService.getById(credentialRecordId) - const { message, credentialRecord } = await this.credentialService.createCredential(record, config) + public async acceptRequest(options: AcceptRequestOptions): Promise { + if (!options.credentialRecordId) { + throw new AriesFrameworkError('Missing credential record id in acceptRequest') + } + const record = await this.getById(options.credentialRecordId) + + // with version we can get the Service + const service = this.getService(record.protocolVersion) + + this.logger.debug(`Got a CredentialService object for version ${record.protocolVersion}`) + + const { message, credentialRecord } = await service.createCredential(record, options) + this.logger.debug('We have a credential message (sending outbound): ', message) + + const requestMessage = await service.getRequestMessage(credentialRecord.id) + const offerMessage = await service.getOfferMessage(credentialRecord.id) // Use connection if present if (credentialRecord.connectionId) { const connection = await this.connectionService.getById(credentialRecord.connectionId) + const outboundMessage = createOutboundMessage(connection, message) await this.messageSender.sendMessage(outboundMessage) } // Use ~service decorator otherwise - else if (credentialRecord.requestMessage?.service && credentialRecord.offerMessage?.service) { - const recipientService = credentialRecord.requestMessage.service - const ourService = credentialRecord.offerMessage.service + else if (requestMessage?.service && offerMessage?.service) { + const recipientService = requestMessage.service + const ourService = offerMessage.service - // Set ~service, update message in record (for later use) - message.setService(ourService) - credentialRecord.credentialMessage = message - await this.credentialService.update(credentialRecord) + message.service = ourService + await this.credentialRepository.update(credentialRecord) await this.messageSender.sendMessageToService({ message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], returnRoute: true, }) } @@ -397,6 +415,11 @@ export class CredentialsModule { `Cannot accept request for credential record without connectionId or ~service decorator on credential offer / request.` ) } + await this.didCommMessageRepo.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) return credentialRecord } @@ -406,12 +429,21 @@ export class CredentialsModule { * associated with the credential record. * * @param credentialRecordId The id of the credential record for which to accept the credential - * @returns credential record associated with the sent credential acknowledgement message + * @returns credential exchange record associated with the sent credential acknowledgement message * */ - public async acceptCredential(credentialRecordId: string) { - const record = await this.credentialService.getById(credentialRecordId) - const { message, credentialRecord } = await this.credentialService.createAck(record) + public async acceptCredential(credentialRecordId: string): Promise { + const record = await this.getById(credentialRecordId) + + // with version we can get the Service + const service = this.getService(record.protocolVersion) + + this.logger.debug(`Got a CredentialService object for version ${record.protocolVersion}`) + + const { message, credentialRecord } = await service.createAck(record) + + const requestMessage = await service.getRequestMessage(credentialRecord.id) + const credentialMessage = await service.getCredentialMessage(credentialRecord.id) if (credentialRecord.connectionId) { const connection = await this.connectionService.getById(credentialRecord.connectionId) @@ -420,14 +452,14 @@ export class CredentialsModule { await this.messageSender.sendMessage(outboundMessage) } // Use ~service decorator otherwise - else if (credentialRecord.credentialMessage?.service && credentialRecord.requestMessage?.service) { - const recipientService = credentialRecord.credentialMessage.service - const ourService = credentialRecord.requestMessage.service + else if (credentialMessage?.service && requestMessage?.service) { + const recipientService = credentialMessage.service + const ourService = requestMessage.service await this.messageSender.sendMessageToService({ message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], returnRoute: true, }) } @@ -437,19 +469,9 @@ export class CredentialsModule { `Cannot accept credential without connectionId or ~service decorator on credential message.` ) } - return credentialRecord } - /** - * Retrieve all credential records - * - * @returns List containing all credential records - */ - public getAll(): Promise { - return this.credentialService.getAll() - } - /** * Retrieve a credential record by id * @@ -458,8 +480,17 @@ export class CredentialsModule { * @return The credential record * */ - public getById(credentialRecordId: string) { - return this.credentialService.getById(credentialRecordId) + public getById(credentialRecordId: string): Promise { + return this.credentialRepository.getById(credentialRecordId) + } + + /** + * Retrieve all credential records + * + * @returns List containing all credential records + */ + public getAll(): Promise { + return this.credentialRepository.getAll() } /** @@ -468,37 +499,43 @@ export class CredentialsModule { * @param credentialRecordId the credential record id * @returns The credential record or null if not found */ - public findById(connectionId: string): Promise { - return this.credentialService.findById(connectionId) + public findById(credentialRecordId: string): Promise { + return this.credentialRepository.findById(credentialRecordId) } /** - * Delete a credential record by id + * Delete a credential record by id, also calls service to delete from wallet * * @param credentialId the credential record id + * @param options the delete credential options for the delete operation */ - public async deleteById(credentialId: string) { - return this.credentialService.deleteById(credentialId) + public async deleteById(credentialId: string, options?: DeleteCredentialOptions) { + const credentialRecord = await this.getById(credentialId) + const service = this.getService(credentialRecord.protocolVersion) + return service.delete(credentialRecord, options) } - private registerHandlers(dispatcher: Dispatcher) { - dispatcher.registerHandler( - new ProposeCredentialHandler(this.credentialService, this.agentConfig, this.credentialResponseCoordinator) - ) - dispatcher.registerHandler( - new OfferCredentialHandler( - this.credentialService, - this.agentConfig, - this.credentialResponseCoordinator, - this.mediationRecipientService - ) - ) - dispatcher.registerHandler( - new RequestCredentialHandler(this.credentialService, this.agentConfig, this.credentialResponseCoordinator) - ) - dispatcher.registerHandler( - new IssueCredentialHandler(this.credentialService, this.agentConfig, this.credentialResponseCoordinator) - ) - dispatcher.registerHandler(new CredentialAckHandler(this.credentialService)) + /** + * Initiate a new credential exchange as issuer by creating a credential offer + * not bound to any connection. The offer must be delivered out-of-band to the holder + * @param options The credential options to use for the offer + * @returns The credential record and credential offer message + */ + public async createOutOfBandOffer(options: OfferCredentialOptions): Promise<{ + message: AgentMessage + credentialRecord: CredentialExchangeRecord + }> { + // with version we can get the Service + if (!options.protocolVersion) { + throw new AriesFrameworkError('Missing protocol version in createOutOfBandOffer') + } + const service = this.getService(options.protocolVersion) + + this.logger.debug(`Got a CredentialService object for version ${options.protocolVersion}`) + const { message, credentialRecord } = await service.createOutOfBandOffer(options) + + this.logger.debug('Offer Message successfully created; message= ', message) + + return { message, credentialRecord } } } diff --git a/packages/core/src/modules/credentials/CredentialsModuleOptions.ts b/packages/core/src/modules/credentials/CredentialsModuleOptions.ts new file mode 100644 index 0000000000..751bef079d --- /dev/null +++ b/packages/core/src/modules/credentials/CredentialsModuleOptions.ts @@ -0,0 +1,75 @@ +import type { AutoAcceptCredential } from './CredentialAutoAcceptType' +import type { CredentialProtocolVersion } from './CredentialProtocolVersion' +import type { + FormatServiceAcceptProposeCredentialFormats, + FormatServiceOfferCredentialFormats, + FormatServiceProposeCredentialFormats as FormatServiceProposeCredentialFormats, + FormatServiceRequestCredentialFormats, +} from './formats/models/CredentialFormatServiceOptions' + +// keys used to create a format service +export enum CredentialFormatType { + Indy = 'Indy', + // others to follow +} + +interface BaseOptions { + autoAcceptCredential?: AutoAcceptCredential + comment?: string +} + +// CREDENTIAL PROPOSAL +interface ProposeCredentialOptions extends BaseOptions { + connectionId: string + protocolVersion?: CredentialProtocolVersion + credentialFormats: FormatServiceProposeCredentialFormats +} + +interface AcceptProposalOptions extends BaseOptions { + connectionId?: string + protocolVersion: CredentialProtocolVersion + credentialRecordId: string + credentialFormats: FormatServiceAcceptProposeCredentialFormats +} + +interface NegotiateProposalOptions extends BaseOptions { + credentialRecordId: string + protocolVersion: CredentialProtocolVersion + credentialFormats: FormatServiceOfferCredentialFormats +} +// CREDENTIAL OFFER +interface OfferCredentialOptions extends BaseOptions { + credentialRecordId?: string + connectionId?: string + protocolVersion: CredentialProtocolVersion + credentialFormats: FormatServiceAcceptProposeCredentialFormats +} + +interface AcceptOfferOptions extends BaseOptions { + credentialRecordId: string +} + +interface NegotiateOfferOptions extends ProposeCredentialOptions { + credentialRecordId: string +} + +// CREDENTIAL REQUEST +interface RequestCredentialOptions extends BaseOptions { + connectionId?: string + credentialFormats?: FormatServiceRequestCredentialFormats +} + +interface AcceptRequestOptions extends BaseOptions { + credentialRecordId?: string +} + +export { + OfferCredentialOptions, + ProposeCredentialOptions, + AcceptProposalOptions, + NegotiateProposalOptions, + NegotiateOfferOptions, + AcceptOfferOptions, + RequestCredentialOptions, + AcceptRequestOptions, +} diff --git a/packages/core/src/modules/credentials/__tests__/CredentialInfo.test.ts b/packages/core/src/modules/credentials/__tests__/CredentialInfo.test.ts index b264950ed2..0a77e49958 100644 --- a/packages/core/src/modules/credentials/__tests__/CredentialInfo.test.ts +++ b/packages/core/src/modules/credentials/__tests__/CredentialInfo.test.ts @@ -1,4 +1,4 @@ -import { CredentialInfo } from '../models/CredentialInfo' +import { CredentialInfo } from '../protocol/v1/models/CredentialInfo' describe('CredentialInfo', () => { it('should return the correct property values', () => { diff --git a/packages/core/src/modules/credentials/__tests__/CredentialRecord.test.ts b/packages/core/src/modules/credentials/__tests__/CredentialRecord.test.ts index ede23faa95..ce74620d69 100644 --- a/packages/core/src/modules/credentials/__tests__/CredentialRecord.test.ts +++ b/packages/core/src/modules/credentials/__tests__/CredentialRecord.test.ts @@ -1,11 +1,13 @@ +import { CredentialProtocolVersion } from '../CredentialProtocolVersion' import { CredentialState } from '../CredentialState' -import { CredentialPreviewAttribute } from '../messages' -import { CredentialRecord } from '../repository/CredentialRecord' +import { CredentialPreviewAttribute } from '../models/CredentialPreviewAttributes' +import { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' +import { CredentialMetadataKeys } from '../repository/CredentialMetadataTypes' describe('CredentialRecord', () => { describe('getCredentialInfo()', () => { test('creates credential info object from credential record data', () => { - const credentialRecord = new CredentialRecord({ + const credentialRecord = new CredentialExchangeRecord({ connectionId: '28790bfe-1345-4c64-b21a-7d98982b3894', threadId: 'threadId', state: CredentialState.Done, @@ -15,9 +17,10 @@ describe('CredentialRecord', () => { value: '25', }), ], + protocolVersion: CredentialProtocolVersion.V1, }) - credentialRecord.metadata.set('_internal/indyCredential', { + credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, { credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', schemaId: 'TL1EaPFCZ8Si5aUrqScBDt:2:test-schema-1599055118161:1.0', }) diff --git a/packages/core/src/modules/credentials/__tests__/CredentialUtils.test.ts b/packages/core/src/modules/credentials/__tests__/CredentialUtils.test.ts index 9684e1832a..dc562db80e 100644 --- a/packages/core/src/modules/credentials/__tests__/CredentialUtils.test.ts +++ b/packages/core/src/modules/credentials/__tests__/CredentialUtils.test.ts @@ -1,5 +1,5 @@ import { CredentialUtils } from '../CredentialUtils' -import { CredentialPreviewAttribute } from '../messages/CredentialPreview' +import { CredentialPreviewAttribute } from '../models/CredentialPreviewAttributes' /** * Sample test cases for encoding/decoding of verifiable credential claims - Aries RFCs 0036 and 0037 @@ -74,6 +74,10 @@ const testEncodings: { [key: string]: { raw: string | number | boolean | null; e raw: '0.1', encoded: '9382477430624249591204401974786823110077201914483282671737639310288175260432', }, + 'leading zero number string': { + raw: '012345', + encoded: '12345', + }, 'chr 0': { raw: String.fromCharCode(0), encoded: '49846369543417741186729467304575255505141344055555831574636310663216789168157', diff --git a/packages/core/src/modules/credentials/__tests__/StubWallet.ts b/packages/core/src/modules/credentials/__tests__/StubWallet.ts deleted file mode 100644 index 02ed5860c3..0000000000 --- a/packages/core/src/modules/credentials/__tests__/StubWallet.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -import type { WireMessage, UnpackedMessageContext, WalletConfig } from '../../../types' -import type { Buffer } from '../../../utils/buffer' -import type { DidConfig, DidInfo, Wallet } from '../../../wallet/Wallet' - -export class StubWallet implements Wallet { - public get isInitialized() { - return true - } - - public get walletHandle() { - return 0 - } - public get publicDid() { - return undefined - } - public initialize(walletConfig: WalletConfig): Promise { - return Promise.resolve() - } - public close(): Promise { - throw new Error('Method not implemented.') - } - - public delete(): Promise { - throw new Error('Method not implemented.') - } - public initPublicDid(didConfig: DidConfig): Promise { - throw new Error('Method not implemented.') - } - public createDid(didConfig?: DidConfig | undefined): Promise { - throw new Error('Method not implemented.') - } - - public pack(payload: Record, recipientKeys: string[], senderVerkey?: string): Promise { - throw new Error('Method not implemented.') - } - public unpack(messagePackage: WireMessage): Promise { - throw new Error('Method not implemented.') - } - public sign(data: Buffer, verkey: string): Promise { - throw new Error('Method not implemented.') - } - public verify(signerVerkey: string, data: Buffer, signature: Buffer): Promise { - throw new Error('Method not implemented.') - } - - public async generateNonce(): Promise { - throw new Error('Method not implemented') - } -} diff --git a/packages/core/src/modules/credentials/__tests__/V1CredentialService.cred.test.ts b/packages/core/src/modules/credentials/__tests__/V1CredentialService.cred.test.ts new file mode 100644 index 0000000000..7a4b9cfccc --- /dev/null +++ b/packages/core/src/modules/credentials/__tests__/V1CredentialService.cred.test.ts @@ -0,0 +1,1187 @@ +import type { Logger } from '../../../../src/logger' +import type { AgentConfig } from '../../../agent/AgentConfig' +import type { ConnectionRecord } from '../../connections' +import type { ConnectionService } from '../../connections/services/ConnectionService' +import type { StoreCredentialOptions } from '../../indy/services/IndyHolderService' +import type { RevocationNotificationReceivedEvent, CredentialStateChangedEvent } from '../CredentialEvents' +import type { ServiceAcceptRequestOptions } from '../CredentialServiceOptions' +import type { RequestCredentialOptions } from '../CredentialsModuleOptions' +import type { CredentialPreviewAttribute } from '../models/CredentialPreviewAttributes' +import type { IndyCredentialMetadata } from '../protocol/v1/models/CredentialInfo' +import type { CustomCredentialTags } from '../repository/CredentialExchangeRecord' + +import { getAgentConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' +import { Dispatcher } from '../../../agent/Dispatcher' +import { EventEmitter } from '../../../agent/EventEmitter' +import { MessageSender } from '../../../agent/MessageSender' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' +import { AriesFrameworkError, RecordNotFoundError } from '../../../error' +import { DidCommMessageRepository } from '../../../storage' +import { JsonEncoder } from '../../../utils/JsonEncoder' +import { AckStatus } from '../../common' +import { DidExchangeState } from '../../connections' +import { IndyHolderService } from '../../indy/services/IndyHolderService' +import { IndyIssuerService } from '../../indy/services/IndyIssuerService' +import { IndyLedgerService } from '../../ledger/services' +import { MediationRecipientService } from '../../routing/services/MediationRecipientService' +import { CredentialEventTypes } from '../CredentialEvents' +import { CredentialProtocolVersion } from '../CredentialProtocolVersion' +import { CredentialState } from '../CredentialState' +import { CredentialUtils } from '../CredentialUtils' +import { CredentialFormatType } from '../CredentialsModuleOptions' +import { CredentialProblemReportReason } from '../errors/CredentialProblemReportReason' +import { IndyCredentialFormatService } from '../formats/indy/IndyCredentialFormatService' +import { V1CredentialPreview } from '../protocol/v1/V1CredentialPreview' +import { V1CredentialService } from '../protocol/v1/V1CredentialService' +import { + V1RequestCredentialMessage, + V1CredentialAckMessage, + INDY_CREDENTIAL_ATTACHMENT_ID, + INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + V1OfferCredentialMessage, + V1IssueCredentialMessage, + V1CredentialProblemReportMessage, +} from '../protocol/v1/messages' +import { V1RevocationNotificationMessage } from '../protocol/v1/messages/V1RevocationNotificationMessage' +import { V2RevocationNotificationMessage } from '../protocol/v2/messages/V2RevocationNotificationMessage' +import { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' +import { CredentialMetadataKeys } from '../repository/CredentialMetadataTypes' +import { CredentialRepository } from '../repository/CredentialRepository' +import { RevocationService } from '../services' + +import { credDef, credReq, credOffer, schema } from './fixtures' + +// Mock classes +jest.mock('../repository/CredentialRepository') +jest.mock('../../../modules/ledger/services/IndyLedgerService') +jest.mock('../../indy/services/IndyHolderService') +jest.mock('../../indy/services/IndyIssuerService') +jest.mock('../../../../src/storage/didcomm/DidCommMessageRepository') +jest.mock('../../routing/services/MediationRecipientService') + +// Mock typed object +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const IndyLedgerServiceMock = IndyLedgerService as jest.Mock +const IndyHolderServiceMock = IndyHolderService as jest.Mock +const IndyIssuerServiceMock = IndyIssuerService as jest.Mock +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const MessageSenderMock = MessageSender as jest.Mock +const MediationRecipientServiceMock = MediationRecipientService as jest.Mock + +const connection = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', +}) + +const offerAttachment = new Attachment({ + id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0', + }), +}) + +const requestAttachment = new Attachment({ + id: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(credReq), + }), +}) + +const credentialAttachment = new Attachment({ + id: INDY_CREDENTIAL_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64({ + values: CredentialUtils.convertAttributesToValues(credentialPreview.attributes), + }), + }), +}) + +const acceptRequestOptions: ServiceAcceptRequestOptions = { + attachId: INDY_CREDENTIAL_ATTACHMENT_ID, + comment: 'credential response comment', + credentialRecordId: undefined, +} + +// A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` +// object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. +const mockCredentialRecord = ({ + state, + metadata, + threadId, + connectionId, + tags, + id, + credentialAttributes, + indyRevocationRegistryId, + indyCredentialRevocationId, +}: { + state?: CredentialState + requestMessage?: V1RequestCredentialMessage + metadata?: IndyCredentialMetadata & { indyRequest: Record } + tags?: CustomCredentialTags + threadId?: string + connectionId?: string + credentialId?: string + id?: string + credentialAttributes?: CredentialPreviewAttribute[] + indyRevocationRegistryId?: string + indyCredentialRevocationId?: string +} = {}) => { + const offerMessage = new V1OfferCredentialMessage({ + comment: 'some comment', + credentialPreview: credentialPreview, + offerAttachments: [offerAttachment], + }) + + const credentialRecord = new CredentialExchangeRecord({ + id, + credentialAttributes: credentialAttributes || credentialPreview.attributes, + state: state || CredentialState.OfferSent, + threadId: threadId ?? offerMessage.id, + connectionId: connectionId ?? '123', + credentials: [ + { + credentialRecordType: CredentialFormatType.Indy, + credentialRecordId: '123456', + }, + ], + tags, + protocolVersion: CredentialProtocolVersion.V1, + }) + + if (metadata?.indyRequest) { + credentialRecord.metadata.set(CredentialMetadataKeys.IndyRequest, { ...metadata.indyRequest }) + } + + if (metadata?.schemaId) { + credentialRecord.metadata.add(CredentialMetadataKeys.IndyCredential, { + schemaId: metadata.schemaId, + }) + } + + if (metadata?.credentialDefinitionId) { + credentialRecord.metadata.add(CredentialMetadataKeys.IndyCredential, { + credentialDefinitionId: metadata.credentialDefinitionId, + }) + } + + credentialRecord.metadata.add(CredentialMetadataKeys.IndyCredential, { + indyCredentialRevocationId, + indyRevocationRegistryId, + }) + + return credentialRecord +} + +let credentialRequestMessage: V1RequestCredentialMessage +let credentialOfferMessage: V1OfferCredentialMessage +let credentialIssueMessage: V1IssueCredentialMessage +let revocationService: RevocationService +let logger: Logger + +describe('CredentialService', () => { + let credentialRepository: CredentialRepository + let indyLedgerService: IndyLedgerService + let indyIssuerService: IndyIssuerService + let indyHolderService: IndyHolderService + let eventEmitter: EventEmitter + let didCommMessageRepository: DidCommMessageRepository + let mediationRecipientService: MediationRecipientService + let messageSender: MessageSender + let agentConfig: AgentConfig + + let dispatcher: Dispatcher + let credentialService: V1CredentialService + + const initMessages = () => { + credentialRequestMessage = new V1RequestCredentialMessage({ + comment: 'abcd', + requestAttachments: [requestAttachment], + }) + credentialOfferMessage = new V1OfferCredentialMessage({ + comment: 'some comment', + credentialPreview: credentialPreview, + offerAttachments: [offerAttachment], + }) + credentialIssueMessage = new V1IssueCredentialMessage({ + comment: 'some comment', + credentialAttachments: [offerAttachment], + }) + + mockFunction(didCommMessageRepository.findAgentMessage).mockImplementation(async (options) => { + if (options.messageClass === V1OfferCredentialMessage) { + return credentialOfferMessage + } + if (options.messageClass === V1RequestCredentialMessage) { + return credentialRequestMessage + } + if (options.messageClass === V1IssueCredentialMessage) { + return credentialIssueMessage + } + return null + }) + } + + beforeEach(async () => { + credentialRepository = new CredentialRepositoryMock() + indyIssuerService = new IndyIssuerServiceMock() + didCommMessageRepository = new DidCommMessageRepositoryMock() + messageSender = new MessageSenderMock() + agentConfig = getAgentConfig('CredentialServiceTest') + mediationRecipientService = new MediationRecipientServiceMock() + indyHolderService = new IndyHolderServiceMock() + indyLedgerService = new IndyLedgerServiceMock() + mockFunction(indyLedgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) + + eventEmitter = new EventEmitter(agentConfig) + + dispatcher = new Dispatcher(messageSender, eventEmitter, agentConfig) + revocationService = new RevocationService(credentialRepository, eventEmitter, agentConfig) + logger = agentConfig.logger + + credentialService = new V1CredentialService( + { + getById: () => Promise.resolve(connection), + assertConnectionOrServiceDecorator: () => true, + } as unknown as ConnectionService, + didCommMessageRepository, + agentConfig, + mediationRecipientService, + dispatcher, + eventEmitter, + credentialRepository, + new IndyCredentialFormatService( + credentialRepository, + eventEmitter, + indyIssuerService, + indyLedgerService, + indyHolderService, + agentConfig + ), + revocationService + ) + mockFunction(indyLedgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) + mockFunction(indyLedgerService.getSchema).mockReturnValue(Promise.resolve(schema)) + }) + + describe('createCredentialRequest', () => { + let credentialRecord: CredentialExchangeRecord + beforeEach(() => { + credentialRecord = mockCredentialRecord({ + state: CredentialState.OfferReceived, + threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + initMessages() + }) + + test(`updates state to ${CredentialState.RequestSent}, set request metadata`, async () => { + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // mock offer so that the request works + + // when + const options: RequestCredentialOptions = {} + await credentialService.createRequest(credentialRecord, options, 'holderDid') + + // then + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord.toJSON()).toMatchObject({ + metadata: { '_internal/indyRequest': { cred_req: 'meta-data' } }, + state: CredentialState.RequestSent, + }) + }) + + test('returns credential request message base on existing credential offer message', async () => { + // given + const comment = 'credential request comment' + const options: RequestCredentialOptions = { + connectionId: credentialRecord.connectionId, + comment: 'credential request comment', + } + + // when + const { message: credentialRequest } = await credentialService.createRequest( + credentialRecord, + options, + 'holderDid' + ) + + // then + expect(credentialRequest.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/request-credential', + '~thread': { + thid: credentialRecord.threadId, + }, + comment, + 'requests~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + }) + }) + + const validState = CredentialState.OfferReceived + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + await Promise.all( + invalidCredentialStates.map(async (state) => { + await expect( + credentialService.createRequest(mockCredentialRecord({ state }), {}, 'holderDid') + ).rejects.toThrowError(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) + }) + ) + }) + }) + + describe('processCredentialRequest', () => { + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext + beforeEach(() => { + credential = mockCredentialRecord({ state: CredentialState.OfferSent }) + + const credentialRequest = new V1RequestCredentialMessage({ + comment: 'abcd', + requestAttachments: [requestAttachment], + }) + credentialRequest.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(credentialRequest, { + connection, + }) + initMessages() + }) + + test(`updates state to ${CredentialState.RequestReceived}, set request and returns credential record`, async () => { + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // given + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // when + const returnedCredentialRecord = await credentialService.processRequest(messageContext) + + // then + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { + threadId: 'somethreadid', + connectionId: connection.id, + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + expect(returnedCredentialRecord.state).toEqual(CredentialState.RequestReceived) + }) + + test(`emits stateChange event from ${CredentialState.OfferSent} to ${CredentialState.RequestReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // mock offer so that the request works + const returnedCredentialRecord = await credentialService.processRequest(messageContext) + + // then + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { + threadId: 'somethreadid', + connectionId: connection.id, + }) + expect(returnedCredentialRecord.state).toEqual(CredentialState.RequestReceived) + }) + + const validState = CredentialState.OfferSent + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + await Promise.all( + invalidCredentialStates.map(async (state) => { + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue( + Promise.resolve(mockCredentialRecord({ state })) + ) + await expect(credentialService.processRequest(messageContext)).rejects.toThrowError( + `Credential record is in invalid state ${state}. Valid states are: ${validState}.` + ) + }) + ) + }) + }) + + describe('createCredential', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let credential: CredentialExchangeRecord + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.RequestReceived, + requestMessage: new V1RequestCredentialMessage({ + comment: 'abcd', + requestAttachments: [requestAttachment], + }), + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + initMessages() + }) + test(`updates state to ${CredentialState.CredentialIssued}`, async () => { + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // when + await credentialService.createCredential(credential, acceptRequestOptions) + + // then + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject({ + state: CredentialState.CredentialIssued, + }) + }) + + test(`emits stateChange event from ${CredentialState.RequestReceived} to ${CredentialState.CredentialIssued}`, async () => { + const eventListenerMock = jest.fn() + + // given + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credential)) + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialService.createCredential(credential, acceptRequestOptions) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + payload: { + previousState: CredentialState.RequestReceived, + credentialRecord: expect.objectContaining({ + state: CredentialState.CredentialIssued, + }), + }, + }) + }) + + test('returns credential response message base on credential request message', async () => { + // given + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credential)) + const comment = 'credential response comment' + + // when + + const { message: credentialResponse } = await credentialService.createCredential(credential, acceptRequestOptions) + // then + expect(credentialResponse.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/issue-credential', + '~thread': { + thid: credential.threadId, + }, + comment, + 'credentials~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + '~please_ack': expect.any(Object), + }) + + // Value of `cred` should be as same as in the credential response message. + const [cred] = await indyIssuerService.createCredential({ + credentialOffer: credOffer, + credentialRequest: credReq, + credentialValues: {}, + }) + const [responseAttachment] = credentialResponse.credentialAttachments + expect(responseAttachment.getDataAsJson()).toEqual(cred) + }) + }) + + describe('processCredential', () => { + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.RequestSent, + requestMessage: new V1RequestCredentialMessage({ + requestAttachments: [requestAttachment], + }), + metadata: { indyRequest: { cred_req: 'meta-data' } }, + }) + + const credentialResponse = new V1IssueCredentialMessage({ + comment: 'abcd', + credentialAttachments: [credentialAttachment], + }) + credentialResponse.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(credentialResponse, { + connection, + }) + initMessages() + }) + + test('finds credential record by thread ID and saves credential attachment into the wallet', async () => { + const storeCredentialMock = indyHolderService.storeCredential as jest.Mock< + Promise, + [StoreCredentialOptions] + > + // given + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + // when + await credentialService.processCredential(messageContext) + + // then + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { + threadId: 'somethreadid', + connectionId: connection.id, + }) + + expect(storeCredentialMock).toHaveBeenNthCalledWith(1, { + credentialId: expect.any(String), + credentialRequestMetadata: { cred_req: 'meta-data' }, + credential: messageContext.message.indyCredential, + credentialDefinition: credDef, + }) + }) + }) + + describe('createAck', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let credential: CredentialExchangeRecord + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.CredentialReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + }) + + test(`updates state to ${CredentialState.Done}`, async () => { + // given + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // when + await credentialService.createAck(credential) + + // then + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject({ + state: CredentialState.Done, + }) + }) + + test(`emits stateChange event from ${CredentialState.CredentialReceived} to ${CredentialState.Done}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialService.createAck(credential) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + payload: { + previousState: CredentialState.CredentialReceived, + credentialRecord: expect.objectContaining({ + state: CredentialState.Done, + }), + }, + }) + }) + + test('returns credential response message base on credential request message', async () => { + // given + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credential)) + + // when + const { message: ackMessage } = await credentialService.createAck(credential) + + // then + expect(ackMessage.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/ack', + '~thread': { + thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + }, + }) + }) + + const validState = CredentialState.CredentialReceived + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + await Promise.all( + invalidCredentialStates.map(async (state) => { + await expect( + credentialService.createAck( + mockCredentialRecord({ state, threadId, connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190' }) + ) + ).rejects.toThrowError(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) + }) + ) + }) + }) + + describe('processAck', () => { + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.CredentialIssued, + }) + + const credentialRequest = new V1CredentialAckMessage({ + status: AckStatus.OK, + threadId: 'somethreadid', + }) + messageContext = new InboundMessageContext(credentialRequest, { + connection, + }) + initMessages() + }) + + test(`updates state to ${CredentialState.Done} and returns credential record`, async () => { + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // given + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // when + const returnedCredentialRecord = await credentialService.processAck(messageContext) + + // then + const expectedCredentialRecord = { + state: CredentialState.Done, + } + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { + threadId: 'somethreadid', + connectionId: connection.id, + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + }) + + describe('createProblemReport', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let credential: CredentialExchangeRecord + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.OfferReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + }) + + test('returns problem report message base once get error', async () => { + // given + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credential)) + + // when + const credentialProblemReportMessage = new V1CredentialProblemReportMessage({ + description: { + en: 'Indy error', + code: CredentialProblemReportReason.IssuanceAbandoned, + }, + }) + + credentialProblemReportMessage.setThread({ threadId }) + // then + expect(credentialProblemReportMessage.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/problem-report', + '~thread': { + thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + }, + }) + }) + }) + + describe('processProblemReport', () => { + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.OfferReceived, + }) + + const credentialProblemReportMessage = new V1CredentialProblemReportMessage({ + description: { + en: 'Indy error', + code: CredentialProblemReportReason.IssuanceAbandoned, + }, + }) + credentialProblemReportMessage.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(credentialProblemReportMessage, { + connection, + }) + }) + + test(`updates problem report error message and returns credential record`, async () => { + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // given + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // when + const returnedCredentialRecord = await credentialService.processProblemReport(messageContext) + + // then + const expectedCredentialRecord = { + errorMessage: 'issuance-abandoned: Indy error', + } + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { + threadId: 'somethreadid', + connectionId: connection.id, + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + }) + + describe('repository methods', () => { + it('getById should return value from credentialRepository.getById', async () => { + const expected = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(expected)) + const result = await credentialService.getById(expected.id) + expect(credentialRepository.getById).toBeCalledWith(expected.id) + + expect(result).toBe(expected) + }) + + it('getById should return value from credentialRepository.getSingleByQuery', async () => { + const expected = mockCredentialRecord() + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(expected)) + const result = await credentialService.getByThreadAndConnectionId('threadId', 'connectionId') + expect(credentialRepository.getSingleByQuery).toBeCalledWith({ + threadId: 'threadId', + connectionId: 'connectionId', + }) + + expect(result).toBe(expected) + }) + + it('findById should return value from credentialRepository.findById', async () => { + const expected = mockCredentialRecord() + mockFunction(credentialRepository.findById).mockReturnValue(Promise.resolve(expected)) + const result = await credentialService.findById(expected.id) + expect(credentialRepository.findById).toBeCalledWith(expected.id) + + expect(result).toBe(expected) + }) + + it('getAll should return value from credentialRepository.getAll', async () => { + const expected = [mockCredentialRecord(), mockCredentialRecord()] + + mockFunction(credentialRepository.getAll).mockReturnValue(Promise.resolve(expected)) + const result = await credentialService.getAll() + expect(credentialRepository.getAll).toBeCalledWith() + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + }) + + describe('deleteCredential', () => { + it('should call delete from repository', async () => { + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credentialRecord)) + + const repositoryDeleteSpy = jest.spyOn(credentialRepository, 'delete') + await credentialService.delete(credentialRecord) + expect(repositoryDeleteSpy).toHaveBeenNthCalledWith(1, credentialRecord) + }) + + it('deleteAssociatedCredential parameter should call deleteCredential in indyHolderService with credentialId', async () => { + const deleteCredentialMock = indyHolderService.deleteCredential as jest.Mock, [string]> + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credentialRecord)) + + await credentialService.delete(credentialRecord, { + deleteAssociatedCredentials: true, + }) + expect(deleteCredentialMock).toHaveBeenNthCalledWith(1, credentialRecord.credentials[0].credentialRecordId) + }) + }) + + describe('declineOffer', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249754' + let credential: CredentialExchangeRecord + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.OfferReceived, + tags: { threadId }, + }) + }) + + test(`updates state to ${CredentialState.Declined}`, async () => { + // given + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // when + await credentialService.declineOffer(credential) + + // then + const expectedCredentialState = { + state: CredentialState.Declined, + } + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + expect(repositoryUpdateSpy).toHaveBeenNthCalledWith(1, expect.objectContaining(expectedCredentialState)) + }) + + test(`emits stateChange event from ${CredentialState.OfferReceived} to ${CredentialState.Declined}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // given + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // when + await credentialService.declineOffer(credential) + + // then + expect(eventListenerMock).toHaveBeenCalledTimes(1) + const [[event]] = eventListenerMock.mock.calls + expect(event).toMatchObject({ + type: 'CredentialStateChanged', + payload: { + previousState: CredentialState.OfferReceived, + credentialRecord: expect.objectContaining({ + state: CredentialState.Declined, + }), + }, + }) + }) + + const validState = CredentialState.OfferReceived + const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) + test(`throws an error when state transition is invalid`, async () => { + await Promise.all( + invalidCredentialStates.map(async (state) => { + await expect( + credentialService.declineOffer(mockCredentialRecord({ state, tags: { threadId } })) + ).rejects.toThrowError(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) + }) + ) + }) + }) + + describe('revocationNotification', () => { + let credential: CredentialExchangeRecord + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.Done, + indyRevocationRegistryId: + 'AsB27X6KRrJFsqZ3unNAH6:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:default:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9', + indyCredentialRevocationId: '1', + connectionId: connection.id, + }) + logger = agentConfig.logger + }) + + test('Test revocation notification event being emitted for V1', async () => { + const eventListenerMock = jest.fn() + eventEmitter.on( + CredentialEventTypes.RevocationNotificationReceived, + eventListenerMock + ) + const date = new Date(2022) + + mockFunction(credentialRepository.getSingleByQuery).mockReturnValueOnce(Promise.resolve(credential)) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const spy = jest.spyOn(global, 'Date').mockImplementation(() => date) + + const { indyRevocationRegistryId, indyCredentialRevocationId } = credential.getTags() + const revocationNotificationThreadId = `indy::${indyRevocationRegistryId}::${indyCredentialRevocationId}` + + const revocationNotificationMessage = new V1RevocationNotificationMessage({ + issueThread: revocationNotificationThreadId, + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { + connection, + }) + + await revocationService.v1ProcessRevocationNotification(messageContext) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'RevocationNotificationReceived', + payload: { + credentialRecord: { + ...credential, + revocationNotification: { + revocationDate: date, + comment: 'Credential has been revoked', + }, + }, + }, + }) + + spy.mockRestore() + }) + + test('Error is logged when no matching credential found for revocation notification V1', async () => { + const loggerSpy = jest.spyOn(logger, 'warn') + + const revocationRegistryId = + 'ABC12D3EFgHIjKL4mnOPQ5:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:default:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9' + const credentialRevocationId = '2' + const revocationNotificationThreadId = `indy::${revocationRegistryId}::${credentialRevocationId}` + const recordNotFoundError = new RecordNotFoundError( + `No record found for given query '${JSON.stringify({ revocationRegistryId, credentialRevocationId })}'`, + { + recordType: CredentialExchangeRecord.type, + } + ) + + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.reject(recordNotFoundError)) + + const revocationNotificationMessage = new V1RevocationNotificationMessage({ + issueThread: revocationNotificationThreadId, + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { connection }) + + await revocationService.v1ProcessRevocationNotification(messageContext) + + expect(loggerSpy).toBeCalledWith('Failed to process revocation notification message', { + error: recordNotFoundError, + threadId: revocationNotificationThreadId, + }) + }) + + test('Error is logged when invalid threadId is passed for revocation notification V1', async () => { + const loggerSpy = jest.spyOn(logger, 'warn') + + const revocationNotificationThreadId = 'notIndy::invalidRevRegId::invalidCredRevId' + const invalidThreadFormatError = new AriesFrameworkError( + `Incorrect revocation notification threadId format: \n${revocationNotificationThreadId}\ndoes not match\n"indy::::"` + ) + + const revocationNotificationMessage = new V1RevocationNotificationMessage({ + issueThread: revocationNotificationThreadId, + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage) + + await revocationService.v1ProcessRevocationNotification(messageContext) + + expect(loggerSpy).toBeCalledWith('Failed to process revocation notification message', { + error: invalidThreadFormatError, + threadId: revocationNotificationThreadId, + }) + }) + + test('Test revocation notification event being emitted for V2', async () => { + const eventListenerMock = jest.fn() + eventEmitter.on( + CredentialEventTypes.RevocationNotificationReceived, + eventListenerMock + ) + const date = new Date(2022) + + mockFunction(credentialRepository.getSingleByQuery).mockReturnValueOnce(Promise.resolve(credential)) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const spy = jest.spyOn(global, 'Date').mockImplementation(() => date) + + const { indyRevocationRegistryId, indyCredentialRevocationId } = credential.getTags() + const revocationNotificationCredentialId = `${indyRevocationRegistryId}::${indyCredentialRevocationId}` + + const revocationNotificationMessage = new V2RevocationNotificationMessage({ + credentialId: revocationNotificationCredentialId, + revocationFormat: 'indy', + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { + connection, + }) + + await revocationService.v2ProcessRevocationNotification(messageContext) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'RevocationNotificationReceived', + payload: { + credentialRecord: { + ...credential, + revocationNotification: { + revocationDate: date, + comment: 'Credential has been revoked', + }, + }, + }, + }) + + spy.mockRestore() + }) + + test('Error is logged when no matching credential found for revocation notification V2', async () => { + const loggerSpy = jest.spyOn(logger, 'warn') + + const revocationRegistryId = + 'ABC12D3EFgHIjKL4mnOPQ5:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:default:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9' + const credentialRevocationId = '2' + const credentialId = `${revocationRegistryId}::${credentialRevocationId}` + + const recordNotFoundError = new RecordNotFoundError( + `No record found for given query '${JSON.stringify({ revocationRegistryId, credentialRevocationId })}'`, + { + recordType: CredentialExchangeRecord.type, + } + ) + + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.reject(recordNotFoundError)) + + const revocationNotificationMessage = new V2RevocationNotificationMessage({ + credentialId, + revocationFormat: 'indy', + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { connection }) + + await revocationService.v2ProcessRevocationNotification(messageContext) + + expect(loggerSpy).toBeCalledWith('Failed to process revocation notification message', { + error: recordNotFoundError, + credentialId, + }) + }) + + test('Error is logged when invalid credentialId is passed for revocation notification V2', async () => { + const loggerSpy = jest.spyOn(logger, 'warn') + + const invalidCredentialId = 'notIndy::invalidRevRegId::invalidCredRevId' + const invalidFormatError = new AriesFrameworkError( + `Incorrect revocation notification credentialId format: \n${invalidCredentialId}\ndoes not match\n"::"` + ) + + const revocationNotificationMessage = new V2RevocationNotificationMessage({ + credentialId: invalidCredentialId, + revocationFormat: 'indy', + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage) + + await revocationService.v2ProcessRevocationNotification(messageContext) + + expect(loggerSpy).toBeCalledWith('Failed to process revocation notification message', { + error: invalidFormatError, + credentialId: invalidCredentialId, + }) + }) + + test('Test error being thrown when connection does not match issuer', async () => { + const loggerSpy = jest.spyOn(logger, 'warn') + const date = new Date(2022) + + const error = new AriesFrameworkError( + "Credential record is associated with connection '123'. Current connection is 'fd9c5ddb-ec11-4acd-bc32-540736249746'" + ) + + mockFunction(credentialRepository.getSingleByQuery).mockReturnValueOnce(Promise.resolve(credential)) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const spy = jest.spyOn(global, 'Date').mockImplementation(() => date) + + const { indyRevocationRegistryId, indyCredentialRevocationId } = credential.getTags() + const revocationNotificationThreadId = `indy::${indyRevocationRegistryId}::${indyCredentialRevocationId}` + + const revocationNotificationMessage = new V1RevocationNotificationMessage({ + issueThread: revocationNotificationThreadId, + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage, { + connection: { + id: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + // eslint-disable-next-line @typescript-eslint/no-empty-function + assertReady: () => {}, + } as ConnectionRecord, + }) + + await revocationService.v1ProcessRevocationNotification(messageContext) + + expect(loggerSpy).toBeCalledWith('Failed to process revocation notification message', { + error, + threadId: revocationNotificationThreadId, + }) + + spy.mockRestore() + }) + + describe('revocation registry id validation', () => { + const revocationRegistryId = + 'ABC12D3EFgHIjKL4mnOPQ5:4:AsB27X6KRrJFsqZ3unNAH6:3:cl:48187:N4s7y-5hema_tag ;:CL_ACCUM:3b24a9b0-a979-41e0-9964-2292f2b1b7e9' + test('V1 allows any character in tag part of RevRegId', async () => { + const loggerSpy = jest.spyOn(logger, 'warn') + mockFunction(credentialRepository.getSingleByQuery).mockReturnValueOnce(Promise.resolve(credential)) + + const revocationNotificationThreadId = `indy::${revocationRegistryId}::2` + + const invalidThreadFormatError = new AriesFrameworkError( + `Incorrect revocation notification threadId format: \n${revocationNotificationThreadId}\ndoes not match\n"indy::::"` + ) + + const revocationNotificationMessage = new V1RevocationNotificationMessage({ + issueThread: revocationNotificationThreadId, + comment: 'Credential has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage) + + await revocationService.v1ProcessRevocationNotification(messageContext) + + expect(loggerSpy).not.toBeCalledWith('Failed to process revocation notification message', { + error: invalidThreadFormatError, + threadId: revocationNotificationThreadId, + }) + }) + + test('V2 allows any character in tag part of credential id', async () => { + const loggerSpy = jest.spyOn(logger, 'warn') + mockFunction(credentialRepository.getSingleByQuery).mockReturnValueOnce(Promise.resolve(credential)) + + const credentialId = `${revocationRegistryId}::2` + const invalidFormatError = new AriesFrameworkError( + `Incorrect revocation notification credentialId format: \n${credentialId}\ndoes not match\n"::"` + ) + + const revocationNotificationMessage = new V2RevocationNotificationMessage({ + credentialId: credentialId, + revocationFormat: 'indy', + comment: 'Credenti1al has been revoked', + }) + const messageContext = new InboundMessageContext(revocationNotificationMessage) + + await revocationService.v2ProcessRevocationNotification(messageContext) + + expect(loggerSpy).not.toBeCalledWith('Failed to process revocation notification message', { + error: invalidFormatError, + credentialId: credentialId, + }) + }) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/__tests__/V1CredentialService.offer.test.ts b/packages/core/src/modules/credentials/__tests__/V1CredentialService.offer.test.ts new file mode 100644 index 0000000000..7cd426ffbf --- /dev/null +++ b/packages/core/src/modules/credentials/__tests__/V1CredentialService.offer.test.ts @@ -0,0 +1,335 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' +import type { ConnectionService } from '../../connections/services/ConnectionService' +import type { CredentialStateChangedEvent } from '../CredentialEvents' +import type { OfferCredentialOptions } from '../CredentialsModuleOptions' + +import { Agent } from '../../../../src/agent/Agent' +import { Dispatcher } from '../../../../src/agent/Dispatcher' +import { DidCommMessageRepository } from '../../../../src/storage' +import { getAgentConfig, getBaseConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' +import { EventEmitter } from '../../../agent/EventEmitter' +import { MessageSender } from '../../../agent/MessageSender' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' +import { DidExchangeState } from '../../connections' +import { IndyHolderService } from '../../indy/services/IndyHolderService' +import { IndyIssuerService } from '../../indy/services/IndyIssuerService' +import { IndyLedgerService } from '../../ledger/services' +import { MediationRecipientService } from '../../routing/services/MediationRecipientService' +import { CredentialEventTypes } from '../CredentialEvents' +import { CredentialProtocolVersion } from '../CredentialProtocolVersion' +import { CredentialState } from '../CredentialState' +import { IndyCredentialFormatService } from '../formats' +import { V1CredentialPreview } from '../protocol/v1/V1CredentialPreview' +import { V1CredentialService } from '../protocol/v1/V1CredentialService' +import { INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, V1OfferCredentialMessage } from '../protocol/v1/messages' +import { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' +import { CredentialRepository } from '../repository/CredentialRepository' +import { RevocationService } from '../services' + +import { schema, credDef } from './fixtures' + +// Mock classes +jest.mock('../repository/CredentialRepository') +jest.mock('../../../../src/storage/didcomm/DidCommMessageRepository') +jest.mock('../../../modules/ledger/services/IndyLedgerService') +jest.mock('../../indy/services/IndyHolderService') +jest.mock('../../indy/services/IndyIssuerService') +jest.mock('../../routing/services/MediationRecipientService') + +// Mock typed object +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const IndyLedgerServiceMock = IndyLedgerService as jest.Mock +const IndyHolderServiceMock = IndyHolderService as jest.Mock +const IndyIssuerServiceMock = IndyIssuerService as jest.Mock +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const MessageSenderMock = MessageSender as jest.Mock +const MediationRecipientServiceMock = MediationRecipientService as jest.Mock + +const connection = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', +}) + +const badCredentialPreview = V1CredentialPreview.fromRecord({ + test: 'credential', + error: 'yes', +}) +const offerAttachment = new Attachment({ + id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0', + }), +}) + +const { config, agentDependencies: dependencies } = getBaseConfig('Agent Class Test V1 Cred') + +describe('CredentialService', () => { + let agent: Agent + let credentialRepository: CredentialRepository + let indyLedgerService: IndyLedgerService + let indyIssuerService: IndyIssuerService + let indyHolderService: IndyHolderService + let eventEmitter: EventEmitter + let didCommMessageRepository: DidCommMessageRepository + let mediationRecipientService: MediationRecipientService + let messageSender: MessageSender + let agentConfig: AgentConfig + + let dispatcher: Dispatcher + let credentialService: V1CredentialService + let revocationService: RevocationService + + beforeEach(async () => { + credentialRepository = new CredentialRepositoryMock() + indyIssuerService = new IndyIssuerServiceMock() + didCommMessageRepository = new DidCommMessageRepositoryMock() + messageSender = new MessageSenderMock() + mediationRecipientService = new MediationRecipientServiceMock() + indyHolderService = new IndyHolderServiceMock() + indyLedgerService = new IndyLedgerServiceMock() + mockFunction(indyLedgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) + agentConfig = getAgentConfig('CredentialServiceTest') + eventEmitter = new EventEmitter(agentConfig) + + dispatcher = new Dispatcher(messageSender, eventEmitter, agentConfig) + revocationService = new RevocationService(credentialRepository, eventEmitter, agentConfig) + + credentialService = new V1CredentialService( + { + getById: () => Promise.resolve(connection), + assertConnectionOrServiceDecorator: () => true, + } as unknown as ConnectionService, + didCommMessageRepository, + agentConfig, + mediationRecipientService, + dispatcher, + eventEmitter, + credentialRepository, + new IndyCredentialFormatService( + credentialRepository, + eventEmitter, + indyIssuerService, + indyLedgerService, + indyHolderService, + agentConfig + ), + revocationService + ) + mockFunction(indyLedgerService.getSchema).mockReturnValue(Promise.resolve(schema)) + }) + + describe('createCredentialOffer', () => { + let offerOptions: OfferCredentialOptions + + beforeEach(async () => { + offerOptions = { + comment: 'some comment', + connectionId: connection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + } + }) + + test(`creates credential record in ${CredentialState.OfferSent} state with offer, thread ID`, async () => { + const repositorySaveSpy = jest.spyOn(credentialRepository, 'save') + + await credentialService.createOffer(offerOptions) + + // then + expect(repositorySaveSpy).toHaveBeenCalledTimes(1) + + const [[createdCredentialRecord]] = repositorySaveSpy.mock.calls + expect(createdCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: createdCredentialRecord.threadId, + connectionId: connection.id, + state: CredentialState.OfferSent, + }) + }) + + test(`emits stateChange event with a new credential in ${CredentialState.OfferSent} state`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + await credentialService.createOffer(offerOptions) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + payload: { + previousState: null, + credentialRecord: expect.objectContaining({ + state: CredentialState.OfferSent, + }), + }, + }) + }) + + test('returns credential offer message', async () => { + const { message: credentialOffer } = await credentialService.createOffer(offerOptions) + expect(credentialOffer.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', + comment: 'some comment', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + ], + }, + 'offers~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + }) + }) + + test('throw error if credential preview attributes do not match with schema attributes', async () => { + offerOptions = { + ...offerOptions, + credentialFormats: { + indy: { + attributes: badCredentialPreview.attributes, + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + }, + }, + } + expect(credentialService.createOffer(offerOptions)).rejects.toThrowError( + `The credential preview attributes do not match the schema attributes (difference is: test,error,name,age, needs: name,age)` + ) + const credentialPreviewWithExtra = V1CredentialPreview.fromRecord({ + test: 'credential', + error: 'yes', + name: 'John', + age: '99', + }) + + offerOptions = { + ...offerOptions, + credentialFormats: { + indy: { + attributes: credentialPreviewWithExtra.attributes, + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + }, + }, + } + expect(credentialService.createOffer(offerOptions)).rejects.toThrowError( + `The credential preview attributes do not match the schema attributes (difference is: test,error, needs: name,age)` + ) + }) + }) + + describe('processCredentialOffer', () => { + let messageContext: InboundMessageContext + let credentialOfferMessage: V1OfferCredentialMessage + + beforeEach(async () => { + credentialOfferMessage = new V1OfferCredentialMessage({ + comment: 'some comment', + credentialPreview: credentialPreview, + offerAttachments: [offerAttachment], + }) + messageContext = new InboundMessageContext(credentialOfferMessage, { + connection, + }) + messageContext.connection = connection + }) + + test(`creates and return credential record in ${CredentialState.OfferReceived} state with offer, thread ID`, async () => { + const repositorySaveSpy = jest.spyOn(credentialRepository, 'save') + agent = new Agent(config, dependencies) + await agent.initialize() + expect(agent.isInitialized).toBe(true) + const agentConfig = getAgentConfig('CredentialServiceTest') + eventEmitter = new EventEmitter(agentConfig) + + const dispatcher = agent.injectionContainer.resolve(Dispatcher) + const mediationRecipientService = agent.injectionContainer.resolve(MediationRecipientService) + + credentialService = new V1CredentialService( + { + getById: () => Promise.resolve(connection), + assertConnectionOrServiceDecorator: () => true, + } as unknown as ConnectionService, + didCommMessageRepository, + agentConfig, + mediationRecipientService, + dispatcher, + eventEmitter, + credentialRepository, + new IndyCredentialFormatService( + credentialRepository, + eventEmitter, + indyIssuerService, + indyLedgerService, + indyHolderService, + agentConfig + ), + revocationService + ) + // when + const returnedCredentialRecord = await credentialService.processOffer(messageContext) + + // then + const expectedCredentialRecord = { + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: credentialOfferMessage.id, + connectionId: connection.id, + state: CredentialState.OfferReceived, + } + expect(repositorySaveSpy).toHaveBeenCalledTimes(1) + const [[createdCredentialRecord]] = repositorySaveSpy.mock.calls + expect(createdCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + + test(`emits stateChange event with ${CredentialState.OfferReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialService.processOffer(messageContext) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + payload: { + previousState: null, + credentialRecord: expect.objectContaining({ + state: CredentialState.OfferReceived, + }), + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/__tests__/CredentialService.test.ts b/packages/core/src/modules/credentials/__tests__/V2CredentialService.cred.test.ts similarity index 56% rename from packages/core/src/modules/credentials/__tests__/CredentialService.test.ts rename to packages/core/src/modules/credentials/__tests__/V2CredentialService.cred.test.ts index 1feb97de82..1a2bbb56e9 100644 --- a/packages/core/src/modules/credentials/__tests__/CredentialService.test.ts +++ b/packages/core/src/modules/credentials/__tests__/V2CredentialService.cred.test.ts @@ -1,59 +1,81 @@ +import type { AgentConfig } from '../../../../src/agent/AgentConfig' import type { ConnectionService } from '../../connections/services/ConnectionService' -import type { StoreCredentialOptions } from '../../indy/services/IndyHolderService' import type { CredentialStateChangedEvent } from '../CredentialEvents' -import type { CredentialPreviewAttribute } from '../messages' -import type { IndyCredentialMetadata } from '../models/CredentialInfo' -import type { CustomCredentialTags } from '../repository/CredentialRecord' -import type { CredentialOfferTemplate } from '../services' - -import { getAgentConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' +import type { AcceptRequestOptions, RequestCredentialOptions } from '../CredentialsModuleOptions' +import type { + CredentialFormatSpec, + FormatServiceRequestCredentialFormats, +} from '../formats/models/CredentialFormatServiceOptions' +import type { CredentialPreviewAttribute } from '../models/CredentialPreviewAttributes' +import type { IndyCredentialMetadata } from '../protocol/v1/models/CredentialInfo' +import type { V2IssueCredentialMessageProps } from '../protocol/v2/messages/V2IssueCredentialMessage' +import type { V2OfferCredentialMessageOptions } from '../protocol/v2/messages/V2OfferCredentialMessage' +import type { V2RequestCredentialMessageOptions } from '../protocol/v2/messages/V2RequestCredentialMessage' +import type { CustomCredentialTags } from '../repository/CredentialExchangeRecord' + +import { getAgentConfig, getBaseConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { Dispatcher } from '../../../agent/Dispatcher' import { EventEmitter } from '../../../agent/EventEmitter' import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' -import { RecordNotFoundError } from '../../../error' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../storage' import { JsonEncoder } from '../../../utils/JsonEncoder' -import { AckStatus } from '../../common' -import { ConnectionState } from '../../connections' +import { AckStatus } from '../../common/messages/AckMessage' +import { DidExchangeState } from '../../connections' import { IndyHolderService } from '../../indy/services/IndyHolderService' import { IndyIssuerService } from '../../indy/services/IndyIssuerService' import { IndyLedgerService } from '../../ledger/services' +import { MediationRecipientService } from '../../routing/services/MediationRecipientService' import { CredentialEventTypes } from '../CredentialEvents' +import { CredentialProtocolVersion } from '../CredentialProtocolVersion' import { CredentialState } from '../CredentialState' import { CredentialUtils } from '../CredentialUtils' +import { CredentialFormatType } from '../CredentialsModuleOptions' +import { CredentialProblemReportReason } from '../errors/CredentialProblemReportReason' +import { IndyCredentialFormatService } from '../formats' +import { V1CredentialPreview } from '../protocol/v1/V1CredentialPreview' import { - CredentialAckMessage, - CredentialPreview, INDY_CREDENTIAL_ATTACHMENT_ID, INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, - IssueCredentialMessage, - OfferCredentialMessage, - RequestCredentialMessage, -} from '../messages' -import { CredentialRecord } from '../repository/CredentialRecord' + V1OfferCredentialMessage, +} from '../protocol/v1/messages' +import { V2CredentialService } from '../protocol/v2/V2CredentialService' +import { V2CredentialAckMessage } from '../protocol/v2/messages/V2CredentialAckMessage' +import { V2CredentialProblemReportMessage } from '../protocol/v2/messages/V2CredentialProblemReportMessage' +import { V2IssueCredentialMessage } from '../protocol/v2/messages/V2IssueCredentialMessage' +import { V2OfferCredentialMessage } from '../protocol/v2/messages/V2OfferCredentialMessage' +import { V2RequestCredentialMessage } from '../protocol/v2/messages/V2RequestCredentialMessage' +import { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' +import { CredentialMetadataKeys } from '../repository/CredentialMetadataTypes' import { CredentialRepository } from '../repository/CredentialRepository' -import { CredentialService } from '../services' +import { RevocationService } from '../services' -import { credDef, credOffer, credReq } from './fixtures' +import { credDef, credReq, credOffer } from './fixtures' // Mock classes jest.mock('../repository/CredentialRepository') jest.mock('../../../modules/ledger/services/IndyLedgerService') jest.mock('../../indy/services/IndyHolderService') jest.mock('../../indy/services/IndyIssuerService') +jest.mock('../../../../src/storage/didcomm/DidCommMessageRepository') +jest.mock('../../routing/services/MediationRecipientService') // Mock typed object const CredentialRepositoryMock = CredentialRepository as jest.Mock const IndyLedgerServiceMock = IndyLedgerService as jest.Mock const IndyHolderServiceMock = IndyHolderService as jest.Mock const IndyIssuerServiceMock = IndyIssuerService as jest.Mock +const MediationRecipientServiceMock = MediationRecipientService as jest.Mock +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock const connection = getMockConnection({ id: '123', - state: ConnectionState.Complete, + state: DidExchangeState.Completed, }) -const credentialPreview = CredentialPreview.fromRecord({ +const credentialPreview = V1CredentialPreview.fromRecord({ name: 'John', age: '99', }) @@ -85,11 +107,41 @@ const credentialAttachment = new Attachment({ }), }) +const v2CredentialRequest: FormatServiceRequestCredentialFormats = { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + }, +} + +const offerOptions: V2OfferCredentialMessageOptions = { + id: '', + formats: [ + { + attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + format: 'hlindy/cred-abstract@v2.0', + }, + ], + comment: 'some comment', + credentialPreview: credentialPreview, + offerAttachments: [offerAttachment], + replacementId: undefined, +} +const requestFormat: CredentialFormatSpec = { + attachId: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + format: 'hlindy/cred-req@v2.0', +} + +const requestOptions: V2RequestCredentialMessageOptions = { + id: '', + formats: [requestFormat], + requestsAttach: [requestAttachment], +} + // A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` // object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. const mockCredentialRecord = ({ state, - requestMessage, metadata, threadId, connectionId, @@ -98,7 +150,6 @@ const mockCredentialRecord = ({ credentialAttributes, }: { state?: CredentialState - requestMessage?: RequestCredentialMessage metadata?: IndyCredentialMetadata & { indyRequest: Record } tags?: CustomCredentialTags threadId?: string @@ -106,35 +157,40 @@ const mockCredentialRecord = ({ id?: string credentialAttributes?: CredentialPreviewAttribute[] } = {}) => { - const offerMessage = new OfferCredentialMessage({ + const offerMessage = new V1OfferCredentialMessage({ comment: 'some comment', credentialPreview: credentialPreview, offerAttachments: [offerAttachment], }) - const credentialRecord = new CredentialRecord({ - offerMessage, + const credentialRecord = new CredentialExchangeRecord({ id, credentialAttributes: credentialAttributes || credentialPreview.attributes, - requestMessage, state: state || CredentialState.OfferSent, threadId: threadId ?? offerMessage.id, connectionId: connectionId ?? '123', + credentials: [ + { + credentialRecordType: CredentialFormatType.Indy, + credentialRecordId: '123456', + }, + ], tags, + protocolVersion: CredentialProtocolVersion.V2, }) if (metadata?.indyRequest) { - credentialRecord.metadata.set('_internal/indyRequest', { ...metadata.indyRequest }) + credentialRecord.metadata.set(CredentialMetadataKeys.IndyRequest, { ...metadata.indyRequest }) } if (metadata?.schemaId) { - credentialRecord.metadata.add('_internal/indyCredential', { + credentialRecord.metadata.add(CredentialMetadataKeys.IndyCredential, { schemaId: metadata.schemaId, }) } if (metadata?.credentialDefinitionId) { - credentialRecord.metadata.add('_internal/indyCredential', { + credentialRecord.metadata.add(CredentialMetadataKeys.IndyCredential, { credentialDefinitionId: metadata.credentialDefinitionId, }) } @@ -142,199 +198,107 @@ const mockCredentialRecord = ({ return credentialRecord } +const { config, agentDependencies: dependencies } = getBaseConfig('Agent Class Test V2 Cred') + +let credentialRequestMessage: V2RequestCredentialMessage +let credentialOfferMessage: V2OfferCredentialMessage describe('CredentialService', () => { + let agent: Agent let credentialRepository: CredentialRepository - let credentialService: CredentialService - let ledgerService: IndyLedgerService + let indyLedgerService: IndyLedgerService let indyIssuerService: IndyIssuerService let indyHolderService: IndyHolderService let eventEmitter: EventEmitter - - beforeEach(() => { - const agentConfig = getAgentConfig('CredentialServiceTest') + let didCommMessageRepository: DidCommMessageRepository + let mediationRecipientService: MediationRecipientService + let agentConfig: AgentConfig + + let dispatcher: Dispatcher + let credentialService: V2CredentialService + let revocationService: RevocationService + + const initMessages = () => { + credentialRequestMessage = new V2RequestCredentialMessage(requestOptions) + credentialOfferMessage = new V2OfferCredentialMessage(offerOptions) + mockFunction(didCommMessageRepository.findAgentMessage).mockImplementation(async (options) => { + if (options.messageClass === V2OfferCredentialMessage) { + return credentialOfferMessage + } + if (options.messageClass === V2RequestCredentialMessage) { + return credentialRequestMessage + } + return null + }) + } + beforeEach(async () => { credentialRepository = new CredentialRepositoryMock() indyIssuerService = new IndyIssuerServiceMock() + mediationRecipientService = new MediationRecipientServiceMock() indyHolderService = new IndyHolderServiceMock() - ledgerService = new IndyLedgerServiceMock() + indyLedgerService = new IndyLedgerServiceMock() + mockFunction(indyLedgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) + agent = new Agent(config, dependencies) + agentConfig = getAgentConfig('CredentialServiceTest') eventEmitter = new EventEmitter(agentConfig) + dispatcher = agent.injectionContainer.resolve(Dispatcher) + didCommMessageRepository = new DidCommMessageRepositoryMock() + revocationService = new RevocationService(credentialRepository, eventEmitter, agentConfig) - credentialService = new CredentialService( - credentialRepository, + credentialService = new V2CredentialService( { getById: () => Promise.resolve(connection), assertConnectionOrServiceDecorator: () => true, } as unknown as ConnectionService, - ledgerService, + credentialRepository, + eventEmitter, + dispatcher, agentConfig, - indyIssuerService, - indyHolderService, - eventEmitter + mediationRecipientService, + didCommMessageRepository, + new IndyCredentialFormatService( + credentialRepository, + eventEmitter, + indyIssuerService, + indyLedgerService, + indyHolderService, + agentConfig + ), + revocationService ) - - mockFunction(ledgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) - }) - - describe('createCredentialOffer', () => { - let credentialTemplate: CredentialOfferTemplate - - beforeEach(() => { - credentialTemplate = { - credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', - comment: 'some comment', - preview: credentialPreview, - } - }) - - test(`creates credential record in ${CredentialState.OfferSent} state with offer, thread ID`, async () => { - // given - const repositorySaveSpy = jest.spyOn(credentialRepository, 'save') - - // when - const { message: credentialOffer } = await credentialService.createOffer(credentialTemplate, connection) - - // then - expect(repositorySaveSpy).toHaveBeenCalledTimes(1) - const [[createdCredentialRecord]] = repositorySaveSpy.mock.calls - expect(createdCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: credentialOffer, - threadId: createdCredentialRecord.offerMessage?.id, - connectionId: connection.id, - state: CredentialState.OfferSent, - }) - }) - - test(`emits stateChange event with a new credential in ${CredentialState.OfferSent} state`, async () => { - const eventListenerMock = jest.fn() - eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) - - await credentialService.createOffer(credentialTemplate, connection) - - expect(eventListenerMock).toHaveBeenCalledWith({ - type: 'CredentialStateChanged', - payload: { - previousState: null, - credentialRecord: expect.objectContaining({ - state: CredentialState.OfferSent, - }), - }, - }) - }) - - test('returns credential offer message', async () => { - const { message: credentialOffer } = await credentialService.createOffer(credentialTemplate, connection) - - expect(credentialOffer.toJSON()).toMatchObject({ - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', - comment: 'some comment', - credential_preview: { - '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - 'mime-type': 'text/plain', - value: 'John', - }, - { - name: 'age', - 'mime-type': 'text/plain', - value: '99', - }, - ], - }, - 'offers~attach': [ - { - '@id': expect.any(String), - 'mime-type': 'application/json', - data: { - base64: expect.any(String), - }, - }, - ], - }) - }) - }) - - describe('processCredentialOffer', () => { - let messageContext: InboundMessageContext - let credentialOfferMessage: OfferCredentialMessage - - beforeEach(() => { - credentialOfferMessage = new OfferCredentialMessage({ - comment: 'some comment', - credentialPreview: credentialPreview, - offerAttachments: [offerAttachment], - }) - messageContext = new InboundMessageContext(credentialOfferMessage, { - connection, - }) - messageContext.connection = connection - }) - - test(`creates and return credential record in ${CredentialState.OfferReceived} state with offer, thread ID`, async () => { - const repositorySaveSpy = jest.spyOn(credentialRepository, 'save') - - // when - const returnedCredentialRecord = await credentialService.processOffer(messageContext) - - // then - const expectedCredentialRecord = { - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: credentialOfferMessage, - threadId: credentialOfferMessage.id, - connectionId: connection.id, - state: CredentialState.OfferReceived, - } - expect(repositorySaveSpy).toHaveBeenCalledTimes(1) - const [[createdCredentialRecord]] = repositorySaveSpy.mock.calls - expect(createdCredentialRecord).toMatchObject(expectedCredentialRecord) - expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) - }) - - test(`emits stateChange event with ${CredentialState.OfferReceived}`, async () => { - const eventListenerMock = jest.fn() - eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) - - // when - await credentialService.processOffer(messageContext) - - // then - expect(eventListenerMock).toHaveBeenCalledWith({ - type: 'CredentialStateChanged', - payload: { - previousState: null, - credentialRecord: expect.objectContaining({ - state: CredentialState.OfferReceived, - }), - }, - }) - }) }) describe('createCredentialRequest', () => { - let credentialRecord: CredentialRecord - + let credentialRecord: CredentialExchangeRecord + let credentialOfferMessage: V2OfferCredentialMessage beforeEach(() => { credentialRecord = mockCredentialRecord({ state: CredentialState.OfferReceived, threadId: 'fd9c5ddb-ec11-4acd-bc32-540736249746', connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', }) + initMessages() }) test(`updates state to ${CredentialState.RequestSent}, set request metadata`, async () => { + mediationRecipientService = agent.injectionContainer.resolve(MediationRecipientService) const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') - // when - await credentialService.createRequest(credentialRecord, { - holderDid: connection.did, + // mock offer so that the request works + + await didCommMessageRepository.saveAgentMessage({ + agentMessage: credentialOfferMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, }) + const requestOptions: RequestCredentialOptions = { + credentialFormats: v2CredentialRequest, + } + + // when + + await credentialService.createRequest(credentialRecord, requestOptions, 'holderDid') + // then expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls @@ -344,41 +308,24 @@ describe('CredentialService', () => { }) }) - test(`emits stateChange event with ${CredentialState.RequestSent}`, async () => { - const eventListenerMock = jest.fn() - eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) - - // when - await credentialService.createRequest(credentialRecord, { - holderDid: connection.did, - }) - - // then - expect(eventListenerMock).toHaveBeenCalledWith({ - type: 'CredentialStateChanged', - payload: { - previousState: CredentialState.OfferReceived, - credentialRecord: expect.objectContaining({ - state: CredentialState.RequestSent, - }), - }, - }) - }) - test('returns credential request message base on existing credential offer message', async () => { // given const comment = 'credential request comment' - + const options: RequestCredentialOptions = { + connectionId: credentialRecord.connectionId, + comment: 'credential request comment', + } // when - const { message: credentialRequest } = await credentialService.createRequest(credentialRecord, { - comment, - holderDid: connection.did, - }) + const { message: credentialRequest } = await credentialService.createRequest( + credentialRecord, + options, + 'holderDid' + ) // then expect(credentialRequest.toJSON()).toMatchObject({ '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/request-credential', + '@type': 'https://didcomm.org/issue-credential/2.0/request-credential', '~thread': { thid: credentialRecord.threadId, }, @@ -401,7 +348,7 @@ describe('CredentialService', () => { await Promise.all( invalidCredentialStates.map(async (state) => { await expect( - credentialService.createRequest(mockCredentialRecord({ state }), { holderDid: connection.id }) + credentialService.createRequest(mockCredentialRecord({ state }), {}, 'mockDid') ).rejects.toThrowError(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) }) ) @@ -409,25 +356,19 @@ describe('CredentialService', () => { }) describe('processCredentialRequest', () => { - let credential: CredentialRecord - let messageContext: InboundMessageContext - + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext beforeEach(() => { credential = mockCredentialRecord({ state: CredentialState.OfferSent }) - - const credentialRequest = new RequestCredentialMessage({ - comment: 'abcd', - requestAttachments: [requestAttachment], - }) - credentialRequest.setThread({ threadId: 'somethreadid' }) - messageContext = new InboundMessageContext(credentialRequest, { + initMessages() + credentialRequestMessage.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(credentialRequestMessage, { connection, }) }) test(`updates state to ${CredentialState.RequestReceived}, set request and returns credential record`, async () => { const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') - // given mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) @@ -439,32 +380,22 @@ describe('CredentialService', () => { threadId: 'somethreadid', connectionId: connection.id, }) - - const expectedCredentialRecord = { - state: CredentialState.RequestReceived, - requestMessage: messageContext.message, - } expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) - expect(repositoryUpdateSpy).toHaveBeenNthCalledWith(1, expect.objectContaining(expectedCredentialRecord)) - expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord.state).toEqual(CredentialState.RequestReceived) }) test(`emits stateChange event from ${CredentialState.OfferSent} to ${CredentialState.RequestReceived}`, async () => { const eventListenerMock = jest.fn() eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + const returnedCredentialRecord = await credentialService.processRequest(messageContext) - await credentialService.processRequest(messageContext) - - expect(eventListenerMock).toHaveBeenCalledWith({ - type: 'CredentialStateChanged', - payload: { - previousState: CredentialState.OfferSent, - credentialRecord: expect.objectContaining({ - state: CredentialState.RequestReceived, - }), - }, + // then + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { + threadId: 'somethreadid', + connectionId: connection.id, }) + expect(returnedCredentialRecord.state).toEqual(CredentialState.RequestReceived) }) const validState = CredentialState.OfferSent @@ -485,15 +416,12 @@ describe('CredentialService', () => { describe('createCredential', () => { const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' - let credential: CredentialRecord + let credential: CredentialExchangeRecord beforeEach(() => { + initMessages() credential = mockCredentialRecord({ state: CredentialState.RequestReceived, - requestMessage: new RequestCredentialMessage({ - comment: 'abcd', - requestAttachments: [requestAttachment], - }), threadId, connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', }) @@ -501,9 +429,13 @@ describe('CredentialService', () => { test(`updates state to ${CredentialState.CredentialIssued}`, async () => { const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') - // when - await credentialService.createCredential(credential) + + const acceptRequestOptions: AcceptRequestOptions = { + credentialRecordId: credential.id, + comment: 'credential response comment', + } + await credentialService.createCredential(credential, acceptRequestOptions) // then expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) @@ -515,13 +447,17 @@ describe('CredentialService', () => { test(`emits stateChange event from ${CredentialState.RequestReceived} to ${CredentialState.CredentialIssued}`, async () => { const eventListenerMock = jest.fn() - eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) // given mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credential)) + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) // when - await credentialService.createCredential(credential) + const acceptRequestOptions: AcceptRequestOptions = { + credentialRecordId: credential.id, + comment: 'credential response comment', + } + await credentialService.createCredential(credential, acceptRequestOptions) // then expect(eventListenerMock).toHaveBeenCalledWith({ @@ -541,12 +477,17 @@ describe('CredentialService', () => { const comment = 'credential response comment' // when - const { message: credentialResponse } = await credentialService.createCredential(credential, { comment }) + const options: AcceptRequestOptions = { + comment: 'credential response comment', + credentialRecordId: credential.id, + } + const { message: credentialResponse } = await credentialService.createCredential(credential, options) + const v2CredentialResponse = credentialResponse as V2IssueCredentialMessage // then expect(credentialResponse.toJSON()).toMatchObject({ '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/issue-credential', + '@type': 'https://didcomm.org/issue-credential/2.0/issue-credential', '~thread': { thid: credential.threadId, }, @@ -563,201 +504,53 @@ describe('CredentialService', () => { '~please_ack': expect.any(Object), }) - // We're using instance of `StubWallet`. Value of `cred` should be as same as in the credential response message. + // Value of `cred` should be as same as in the credential response message. const [cred] = await indyIssuerService.createCredential({ credentialOffer: credOffer, credentialRequest: credReq, credentialValues: {}, }) - const [responseAttachment] = credentialResponse.credentialAttachments - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(JsonEncoder.fromBase64(responseAttachment.data.base64!)).toEqual(cred) - }) - - test('throws error when credential record has no request', async () => { - // when, then - await expect( - credentialService.createCredential( - mockCredentialRecord({ - state: CredentialState.RequestReceived, - threadId, - connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', - }) - ) - ).rejects.toThrowError( - `Missing required base64 encoded attachment data for credential request with thread id ${threadId}` - ) - }) - - const validState = CredentialState.RequestReceived - const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async (state) => { - await expect( - credentialService.createCredential( - mockCredentialRecord({ - state, - threadId, - connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', - requestMessage: new RequestCredentialMessage({ - requestAttachments: [requestAttachment], - }), - }) - ) - ).rejects.toThrowError(`Credential record is in invalid state ${state}. Valid states are: ${validState}.`) - }) - ) + const [responseAttachment] = v2CredentialResponse.messageAttachment + expect(responseAttachment.getDataAsJson()).toEqual(cred) }) }) describe('processCredential', () => { - let credential: CredentialRecord - let messageContext: InboundMessageContext - + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext beforeEach(() => { credential = mockCredentialRecord({ state: CredentialState.RequestSent, - requestMessage: new RequestCredentialMessage({ - requestAttachments: [requestAttachment], - }), metadata: { indyRequest: { cred_req: 'meta-data' } }, }) - const credentialResponse = new IssueCredentialMessage({ + const props: V2IssueCredentialMessageProps = { comment: 'abcd', - credentialAttachments: [credentialAttachment], - }) + credentialsAttach: [credentialAttachment], + formats: [], + } + + const credentialResponse = new V2IssueCredentialMessage(props) credentialResponse.setThread({ threadId: 'somethreadid' }) messageContext = new InboundMessageContext(credentialResponse, { connection, }) + initMessages() }) test('finds credential record by thread ID and saves credential attachment into the wallet', async () => { - const storeCredentialMock = indyHolderService.storeCredential as jest.Mock< - Promise, - [StoreCredentialOptions] - > - - // given - mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) - - // when - await credentialService.processCredential(messageContext) - - // then - expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { - threadId: 'somethreadid', - connectionId: connection.id, - }) - - expect(storeCredentialMock).toHaveBeenNthCalledWith(1, { - credentialId: expect.any(String), - credentialRequestMetadata: { cred_req: 'meta-data' }, - credential: messageContext.message.indyCredential, - credentialDefinition: credDef, - }) - }) - - test(`updates state to ${CredentialState.CredentialReceived}, set credentialId and returns credential record`, async () => { - const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') - // given mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) - // when - const updatedCredential = await credentialService.processCredential(messageContext) + const record = await credentialService.processCredential(messageContext) - // then - const expectedCredentialRecord = { - credentialId: expect.any(String), - state: CredentialState.CredentialReceived, - } - expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) - const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls - expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) - expect(updatedCredential).toMatchObject(expectedCredentialRecord) - }) - - test(`emits stateChange event from ${CredentialState.RequestSent} to ${CredentialState.CredentialReceived}`, async () => { - const eventListenerMock = jest.fn() - eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) - - // given - mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) - - // when - await credentialService.processCredential(messageContext) - - // then - expect(eventListenerMock).toHaveBeenCalledWith({ - type: 'CredentialStateChanged', - payload: { - previousState: CredentialState.RequestSent, - credentialRecord: expect.objectContaining({ - state: CredentialState.CredentialReceived, - }), - }, - }) - }) - - test('throws error when credential record has no request metadata', async () => { - // given - mockFunction(credentialRepository.getSingleByQuery).mockReturnValue( - Promise.resolve( - mockCredentialRecord({ - state: CredentialState.RequestSent, - id: 'id', - }) - ) - ) - - // when, then - await expect(credentialService.processCredential(messageContext)).rejects.toThrowError( - `Missing required request metadata for credential with id id` - ) - }) - - test('throws error when credential attribute values does not match received credential values', async () => { - mockFunction(credentialRepository.getSingleByQuery).mockReturnValue( - Promise.resolve( - mockCredentialRecord({ - state: CredentialState.RequestSent, - id: 'id', - // Take only first value from credential - credentialAttributes: [credentialPreview.attributes[0]], - }) - ) - ) - - await expect(credentialService.processCredential(messageContext)).rejects.toThrowError() - }) - - const validState = CredentialState.RequestSent - const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async (state) => { - mockFunction(credentialRepository.getSingleByQuery).mockReturnValue( - Promise.resolve( - mockCredentialRecord({ - state, - metadata: { indyRequest: { cred_req: 'meta-data' } }, - }) - ) - ) - await expect(credentialService.processCredential(messageContext)).rejects.toThrowError( - `Credential record is in invalid state ${state}. Valid states are: ${validState}.` - ) - }) - ) + expect(record.credentialAttributes?.length).toBe(2) }) }) describe('createAck', () => { const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' - let credential: CredentialRecord + let credential: CredentialExchangeRecord beforeEach(() => { credential = mockCredentialRecord({ @@ -811,7 +604,7 @@ describe('CredentialService', () => { // then expect(ackMessage.toJSON()).toMatchObject({ '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/ack', + '@type': 'https://didcomm.org/issue-credential/2.0/ack', '~thread': { thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', }, @@ -834,15 +627,14 @@ describe('CredentialService', () => { }) describe('processAck', () => { - let credential: CredentialRecord - let messageContext: InboundMessageContext - + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext beforeEach(() => { credential = mockCredentialRecord({ state: CredentialState.CredentialIssued, }) - const credentialRequest = new CredentialAckMessage({ + const credentialRequest = new V2CredentialAckMessage({ status: AckStatus.OK, threadId: 'somethreadid', }) @@ -854,6 +646,7 @@ describe('CredentialService', () => { test(`updates state to ${CredentialState.Done} and returns credential record`, async () => { const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + initMessages() // given mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) @@ -873,52 +666,86 @@ describe('CredentialService', () => { expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) }) + }) - test(`emits stateChange event from ${CredentialState.CredentialIssued} to ${CredentialState.Done}`, async () => { - const eventListenerMock = jest.fn() - eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + describe('createProblemReport', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let credential: CredentialExchangeRecord + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.OfferReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + }) + test('returns problem report message base once get error', async () => { // given - mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credential)) // when - await credentialService.processAck(messageContext) + const credentialProblemReportMessage = new V2CredentialProblemReportMessage({ + description: { + en: 'Indy error', + code: CredentialProblemReportReason.IssuanceAbandoned, + }, + }) + credentialProblemReportMessage.setThread({ threadId }) // then - expect(eventListenerMock).toHaveBeenCalledWith({ - type: 'CredentialStateChanged', - payload: { - previousState: CredentialState.CredentialIssued, - credentialRecord: expect.objectContaining({ - state: CredentialState.Done, - }), + expect(credentialProblemReportMessage.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/problem-report', + '~thread': { + thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', }, }) }) + }) - test('throws error when there is no credential found by thread ID', async () => { - // given - mockFunction(credentialRepository.getSingleByQuery).mockReturnValue( - Promise.reject(new RecordNotFoundError('not found', { recordType: CredentialRecord.type })) - ) + describe('processProblemReport', () => { + let credential: CredentialExchangeRecord + let messageContext: InboundMessageContext + + beforeEach(() => { + credential = mockCredentialRecord({ + state: CredentialState.OfferReceived, + }) - // when, then - await expect(credentialService.processAck(messageContext)).rejects.toThrowError(RecordNotFoundError) + const credentialProblemReportMessage = new V2CredentialProblemReportMessage({ + description: { + en: 'Indy error', + code: CredentialProblemReportReason.IssuanceAbandoned, + }, + }) + credentialProblemReportMessage.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(credentialProblemReportMessage, { + connection, + }) }) - const validState = CredentialState.CredentialIssued - const invalidCredentialStates = Object.values(CredentialState).filter((state) => state !== validState) - test(`throws an error when state transition is invalid`, async () => { - await Promise.all( - invalidCredentialStates.map(async (state) => { - mockFunction(credentialRepository.getSingleByQuery).mockReturnValue( - Promise.resolve(mockCredentialRecord({ state })) - ) - await expect(credentialService.processAck(messageContext)).rejects.toThrowError( - `Credential record is in invalid state ${state}. Valid states are: ${validState}.` - ) - }) - ) + test(`updates problem report error message and returns credential record`, async () => { + const repositoryUpdateSpy = jest.spyOn(credentialRepository, 'update') + + // given + mockFunction(credentialRepository.getSingleByQuery).mockReturnValue(Promise.resolve(credential)) + + // when + const returnedCredentialRecord = await credentialService.processProblemReport(messageContext) + + // then + const expectedCredentialRecord = { + errorMessage: 'issuance-abandoned: Indy error', + } + expect(credentialRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { + threadId: 'somethreadid', + connectionId: connection.id, + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) }) }) @@ -964,9 +791,32 @@ describe('CredentialService', () => { }) }) + describe('deleteCredential', () => { + it('should call delete from repository', async () => { + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credentialRecord)) + + const repositoryDeleteSpy = jest.spyOn(credentialRepository, 'delete') + await credentialService.delete(credentialRecord) + expect(repositoryDeleteSpy).toHaveBeenNthCalledWith(1, credentialRecord) + }) + + it('deleteAssociatedCredential parameter should call deleteCredential in indyHolderService with credentialId', async () => { + const storeCredentialMock = indyHolderService.deleteCredential as jest.Mock, [string]> + + const credentialRecord = mockCredentialRecord() + mockFunction(credentialRepository.getById).mockReturnValue(Promise.resolve(credentialRecord)) + + await credentialService.delete(credentialRecord, { + deleteAssociatedCredentials: true, + }) + expect(storeCredentialMock).toHaveBeenNthCalledWith(1, credentialRecord.credentials[0].credentialRecordId) + }) + }) + describe('declineOffer', () => { const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249754' - let credential: CredentialRecord + let credential: CredentialExchangeRecord beforeEach(() => { credential = mockCredentialRecord({ diff --git a/packages/core/src/modules/credentials/__tests__/V2CredentialService.offer.test.ts b/packages/core/src/modules/credentials/__tests__/V2CredentialService.offer.test.ts new file mode 100644 index 0000000000..f28d9ccb8a --- /dev/null +++ b/packages/core/src/modules/credentials/__tests__/V2CredentialService.offer.test.ts @@ -0,0 +1,353 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' +import type { ConnectionService } from '../../connections/services/ConnectionService' +import type { CredentialStateChangedEvent } from '../CredentialEvents' +import type { OfferCredentialOptions } from '../CredentialsModuleOptions' +import type { V2OfferCredentialMessageOptions } from '../protocol/v2/messages/V2OfferCredentialMessage' + +import { getAgentConfig, getBaseConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { Dispatcher } from '../../../agent/Dispatcher' +import { EventEmitter } from '../../../agent/EventEmitter' +import { MessageSender } from '../../../agent/MessageSender' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' +import { DidCommMessageRepository } from '../../../storage' +import { DidExchangeState } from '../../connections' +import { IndyHolderService } from '../../indy/services/IndyHolderService' +import { IndyIssuerService } from '../../indy/services/IndyIssuerService' +import { IndyLedgerService } from '../../ledger/services' +import { MediationRecipientService } from '../../routing/services/MediationRecipientService' +import { CredentialEventTypes } from '../CredentialEvents' +import { CredentialProtocolVersion } from '../CredentialProtocolVersion' +import { CredentialState } from '../CredentialState' +import { IndyCredentialFormatService } from '../formats/indy/IndyCredentialFormatService' +import { V1CredentialPreview } from '../protocol/v1/V1CredentialPreview' +import { INDY_CREDENTIAL_OFFER_ATTACHMENT_ID } from '../protocol/v1/messages' +import { V2CredentialPreview } from '../protocol/v2/V2CredentialPreview' +import { V2CredentialService } from '../protocol/v2/V2CredentialService' +import { V2OfferCredentialMessage } from '../protocol/v2/messages/V2OfferCredentialMessage' +import { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' +import { CredentialRepository } from '../repository/CredentialRepository' +import { RevocationService } from '../services' + +import { credDef, schema } from './fixtures' + +// Mock classes +jest.mock('../repository/CredentialRepository') +jest.mock('../../../../src/storage/didcomm/DidCommMessageRepository') +jest.mock('../../../modules/ledger/services/IndyLedgerService') +jest.mock('../../indy/services/IndyHolderService') +jest.mock('../../indy/services/IndyIssuerService') +jest.mock('../../routing/services/MediationRecipientService') + +// Mock typed object +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const IndyLedgerServiceMock = IndyLedgerService as jest.Mock +const IndyHolderServiceMock = IndyHolderService as jest.Mock +const IndyIssuerServiceMock = IndyIssuerService as jest.Mock +const DidCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const MessageSenderMock = MessageSender as jest.Mock +const MediationRecipientServiceMock = MediationRecipientService as jest.Mock + +const connection = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', +}) + +const offerAttachment = new Attachment({ + id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJzY2hlbWFfaWQiOiJhYWEiLCJjcmVkX2RlZl9pZCI6IlRoN01wVGFSWlZSWW5QaWFiZHM4MVk6MzpDTDoxNzpUQUciLCJub25jZSI6Im5vbmNlIiwia2V5X2NvcnJlY3RuZXNzX3Byb29mIjp7fX0', + }), +}) + +const { config, agentDependencies: dependencies } = getBaseConfig('Agent Class Test V2 Offer') + +describe('CredentialService', () => { + let agent: Agent + let credentialRepository: CredentialRepository + let indyLedgerService: IndyLedgerService + let indyIssuerService: IndyIssuerService + let indyHolderService: IndyHolderService + let eventEmitter: EventEmitter + let didCommMessageRepository: DidCommMessageRepository + let mediationRecipientService: MediationRecipientService + let messageSender: MessageSender + let agentConfig: AgentConfig + + let dispatcher: Dispatcher + let credentialService: V2CredentialService + let revocationService: RevocationService + + beforeEach(async () => { + credentialRepository = new CredentialRepositoryMock() + indyIssuerService = new IndyIssuerServiceMock() + didCommMessageRepository = new DidCommMessageRepositoryMock() + messageSender = new MessageSenderMock() + mediationRecipientService = new MediationRecipientServiceMock() + indyHolderService = new IndyHolderServiceMock() + indyLedgerService = new IndyLedgerServiceMock() + mockFunction(indyLedgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) + agentConfig = getAgentConfig('CredentialServiceTest') + eventEmitter = new EventEmitter(agentConfig) + + dispatcher = new Dispatcher(messageSender, eventEmitter, agentConfig) + revocationService = new RevocationService(credentialRepository, eventEmitter, agentConfig) + + credentialService = new V2CredentialService( + { + getById: () => Promise.resolve(connection), + assertConnectionOrServiceDecorator: () => true, + } as unknown as ConnectionService, + credentialRepository, + eventEmitter, + dispatcher, + agentConfig, + mediationRecipientService, + didCommMessageRepository, + new IndyCredentialFormatService( + credentialRepository, + eventEmitter, + indyIssuerService, + indyLedgerService, + indyHolderService, + agentConfig + ), + revocationService + ) + mockFunction(indyLedgerService.getSchema).mockReturnValue(Promise.resolve(schema)) + }) + + describe('createCredentialOffer', () => { + let offerOptions: OfferCredentialOptions + + beforeEach(async () => { + offerOptions = { + comment: 'some comment', + connectionId: connection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + } + }) + + test(`creates credential record in ${CredentialState.OfferSent} state with offer, thread ID`, async () => { + // given + // agent = new Agent(config, dependencies) + // await agent.initialize() + // expect(agent.isInitialized).toBe(true) + const repositorySaveSpy = jest.spyOn(credentialRepository, 'save') + + await credentialService.createOffer(offerOptions) + + // then + expect(repositorySaveSpy).toHaveBeenCalledTimes(1) + + const [[createdCredentialRecord]] = repositorySaveSpy.mock.calls + expect(createdCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: createdCredentialRecord.threadId, + connectionId: connection.id, + state: CredentialState.OfferSent, + }) + }) + + test(`emits stateChange event with a new credential in ${CredentialState.OfferSent} state`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + await credentialService.createOffer(offerOptions) + + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + payload: { + previousState: null, + credentialRecord: expect.objectContaining({ + state: CredentialState.OfferSent, + }), + }, + }) + }) + + test('returns credential offer message', async () => { + const { message: credentialOffer } = await credentialService.createOffer(offerOptions) + + expect(credentialOffer.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'some comment', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + ], + }, + 'offers~attach': [ + { + '@id': expect.any(String), + 'mime-type': 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + }) + }) + + test('throw error if credential preview attributes do not match with schema attributes', async () => { + const badCredentialPreview = V2CredentialPreview.fromRecord({ + test: 'credential', + error: 'yes', + }) + + offerOptions = { + ...offerOptions, + credentialFormats: { + indy: { + attributes: badCredentialPreview.attributes, + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + }, + }, + } + expect(credentialService.createOffer(offerOptions)).rejects.toThrowError( + `The credential preview attributes do not match the schema attributes (difference is: test,error,name,age, needs: name,age)` + ) + const credentialPreviewWithExtra = V2CredentialPreview.fromRecord({ + test: 'credential', + error: 'yes', + name: 'John', + age: '99', + }) + + offerOptions = { + ...offerOptions, + credentialFormats: { + indy: { + attributes: credentialPreviewWithExtra.attributes, + credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TAG', + }, + }, + } + expect(credentialService.createOffer(offerOptions)).rejects.toThrowError( + `The credential preview attributes do not match the schema attributes (difference is: test,error, needs: name,age)` + ) + }) + }) + describe('processCredentialOffer', () => { + let messageContext: InboundMessageContext + let credentialOfferMessage: V2OfferCredentialMessage + + beforeEach(async () => { + const offerOptions: V2OfferCredentialMessageOptions = { + id: '', + formats: [ + { + attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + format: 'hlindy/cred-abstract@v2.0', + }, + ], + comment: 'some comment', + credentialPreview: credentialPreview, + offerAttachments: [offerAttachment], + replacementId: undefined, + } + credentialOfferMessage = new V2OfferCredentialMessage(offerOptions) + messageContext = new InboundMessageContext(credentialOfferMessage, { + connection, + }) + messageContext.connection = connection + }) + + test(`creates and return credential record in ${CredentialState.OfferReceived} state with offer, thread ID`, async () => { + const repositorySaveSpy = jest.spyOn(credentialRepository, 'save') + agent = new Agent(config, dependencies) + await agent.initialize() + expect(agent.isInitialized).toBe(true) + const agentConfig = getAgentConfig('CredentialServiceTest') + eventEmitter = new EventEmitter(agentConfig) + + const dispatcher = agent.injectionContainer.resolve(Dispatcher) + const mediationRecipientService = agent.injectionContainer.resolve(MediationRecipientService) + revocationService = new RevocationService(credentialRepository, eventEmitter, agentConfig) + + credentialService = new V2CredentialService( + { + getById: () => Promise.resolve(connection), + assertConnectionOrServiceDecorator: () => true, + } as unknown as ConnectionService, + credentialRepository, + eventEmitter, + dispatcher, + agentConfig, + mediationRecipientService, + didCommMessageRepository, + new IndyCredentialFormatService( + credentialRepository, + eventEmitter, + indyIssuerService, + indyLedgerService, + indyHolderService, + agentConfig + ), + revocationService + ) + // when + const returnedCredentialRecord = await credentialService.processOffer(messageContext) + + // then + const expectedCredentialRecord = { + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: credentialOfferMessage.id, + connectionId: connection.id, + state: CredentialState.OfferReceived, + } + expect(repositorySaveSpy).toHaveBeenCalledTimes(1) + const [[createdCredentialRecord]] = repositorySaveSpy.mock.calls + expect(createdCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + + test(`emits stateChange event with ${CredentialState.OfferReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(CredentialEventTypes.CredentialStateChanged, eventListenerMock) + + // when + await credentialService.processOffer(messageContext) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'CredentialStateChanged', + payload: { + previousState: null, + credentialRecord: expect.objectContaining({ + state: CredentialState.OfferReceived, + }), + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/__tests__/fixtures.ts b/packages/core/src/modules/credentials/__tests__/fixtures.ts index 87757bc90b..b8e5e7451c 100644 --- a/packages/core/src/modules/credentials/__tests__/fixtures.ts +++ b/packages/core/src/modules/credentials/__tests__/fixtures.ts @@ -1,3 +1,5 @@ +import type { Schema } from 'indy-sdk' + export const credDef = { ver: '1.0', id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:16:TAG', @@ -49,3 +51,12 @@ export const credReq = { }, nonce: '784158051402761459123237', } + +export const schema: Schema = { + name: 'schema', + attrNames: ['name', 'age'], + id: 'TL1EaPFCZ8Si5aUrqScBDt:2:test-schema-1599055118161:1.0', + seqNo: 989798923653, + ver: '1.0', + version: '1.0', +} diff --git a/packages/core/src/modules/credentials/errors/CredentialProblemReportError.ts b/packages/core/src/modules/credentials/errors/CredentialProblemReportError.ts new file mode 100644 index 0000000000..ffbe633004 --- /dev/null +++ b/packages/core/src/modules/credentials/errors/CredentialProblemReportError.ts @@ -0,0 +1,23 @@ +import type { ProblemReportErrorOptions } from '../../problem-reports' +import type { CredentialProblemReportReason } from './CredentialProblemReportReason' + +import { V1CredentialProblemReportMessage } from '../protocol/v1/messages' + +import { ProblemReportError } from './../../problem-reports/errors/ProblemReportError' + +interface CredentialProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: CredentialProblemReportReason +} +export class CredentialProblemReportError extends ProblemReportError { + public problemReport: V1CredentialProblemReportMessage + + public constructor(message: string, { problemCode }: CredentialProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new V1CredentialProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/credentials/errors/CredentialProblemReportReason.ts b/packages/core/src/modules/credentials/errors/CredentialProblemReportReason.ts new file mode 100644 index 0000000000..cf8bdb95bf --- /dev/null +++ b/packages/core/src/modules/credentials/errors/CredentialProblemReportReason.ts @@ -0,0 +1,8 @@ +/** + * Credential error code in RFC 0036. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0036-issue-credential/README.md + */ +export enum CredentialProblemReportReason { + IssuanceAbandoned = 'issuance-abandoned', +} diff --git a/packages/core/src/modules/credentials/errors/index.ts b/packages/core/src/modules/credentials/errors/index.ts new file mode 100644 index 0000000000..3d5c266524 --- /dev/null +++ b/packages/core/src/modules/credentials/errors/index.ts @@ -0,0 +1,2 @@ +export * from './CredentialProblemReportError' +export * from './CredentialProblemReportReason' diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatService.ts b/packages/core/src/modules/credentials/formats/CredentialFormatService.ts new file mode 100644 index 0000000000..840a664014 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/CredentialFormatService.ts @@ -0,0 +1,98 @@ +import type { EventEmitter } from '../../../agent/EventEmitter' +import type { + ServiceAcceptCredentialOptions, + ServiceAcceptProposalOptions, + ServiceOfferCredentialOptions, +} from '../CredentialServiceOptions' +import type { + AcceptRequestOptions, + ProposeCredentialOptions, + RequestCredentialOptions, +} from '../CredentialsModuleOptions' +import type { CredentialExchangeRecord, CredentialRepository } from '../repository' +import type { + FormatServiceCredentialAttachmentFormats, + CredentialFormatSpec, + HandlerAutoAcceptOptions, + FormatServiceOfferAttachmentFormats, + FormatServiceProposeAttachmentFormats, +} from './models/CredentialFormatServiceOptions' + +import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' +import { JsonEncoder } from '../../../utils/JsonEncoder' + +export abstract class CredentialFormatService { + protected credentialRepository: CredentialRepository + protected eventEmitter: EventEmitter + + public constructor(credentialRepository: CredentialRepository, eventEmitter: EventEmitter) { + this.credentialRepository = credentialRepository + this.eventEmitter = eventEmitter + } + + abstract createProposal(options: ProposeCredentialOptions): Promise + + abstract processProposal( + options: ServiceAcceptProposalOptions, + credentialRecord: CredentialExchangeRecord + ): Promise + + abstract createOffer(options: ServiceOfferCredentialOptions): Promise + + abstract processOffer(attachment: Attachment, credentialRecord: CredentialExchangeRecord): Promise + + abstract createRequest( + options: RequestCredentialOptions, + credentialRecord: CredentialExchangeRecord, + holderDid?: string + ): Promise + + abstract processRequest(options: RequestCredentialOptions, credentialRecord: CredentialExchangeRecord): void + + abstract createCredential( + options: AcceptRequestOptions, + credentialRecord: CredentialExchangeRecord, + requestAttachment: Attachment, + offerAttachment?: Attachment + ): Promise + + abstract processCredential( + options: ServiceAcceptCredentialOptions, + credentialRecord: CredentialExchangeRecord + ): Promise + + abstract shouldAutoRespondToProposal(options: HandlerAutoAcceptOptions): boolean + abstract shouldAutoRespondToRequest(options: HandlerAutoAcceptOptions): boolean + abstract shouldAutoRespondToCredential(options: HandlerAutoAcceptOptions): boolean + + abstract deleteCredentialById(credentialRecordId: string): Promise + + /** + * + * Returns an object of type {@link Attachment} for use in credential exchange messages. + * It looks up the correct format identifier and encodes the data as a base64 attachment. + * + * @param data The data to include in the attach object + * @param id the attach id from the formats component of the message + * @returns attachment to the credential proposal + */ + public getFormatData(data: unknown, id: string): Attachment { + const attachment: Attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(data), + }), + }) + return attachment + } + + /** + * Gets the attachment object for a given attachId. We need to get out the correct attachId for + * indy and then find the corresponding attachment (if there is one) + * @param formats the formats object containing the attachid + * @param messageAttachment the attachment containing the payload + * @returns The Attachment if found or undefined + */ + abstract getAttachment(formats: CredentialFormatSpec[], messageAttachment: Attachment[]): Attachment | undefined +} diff --git a/packages/core/src/modules/credentials/formats/index.ts b/packages/core/src/modules/credentials/formats/index.ts new file mode 100644 index 0000000000..c33b5cce20 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/index.ts @@ -0,0 +1,2 @@ +export * from './CredentialFormatService' +export * from './indy/IndyCredentialFormatService' diff --git a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts new file mode 100644 index 0000000000..e9c8879d07 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts @@ -0,0 +1,598 @@ +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { Logger } from '../../../../logger' +import type { + NegotiateProposalOptions, + OfferCredentialOptions, + ProposeCredentialOptions, + RequestCredentialOptions, +} from '../../CredentialsModuleOptions' +import type { CredentialPreviewAttribute } from '../../models/CredentialPreviewAttributes' +import type { + ServiceAcceptCredentialOptions, + ServiceAcceptOfferOptions as ServiceOfferOptions, + ServiceAcceptProposalOptions, + ServiceAcceptRequestOptions, + ServiceOfferCredentialOptions, + ServiceRequestCredentialOptions, +} from '../../protocol' +import type { V1CredentialPreview } from '../../protocol/v1/V1CredentialPreview' +import type { CredentialExchangeRecord } from '../../repository/CredentialExchangeRecord' +import type { + FormatServiceCredentialAttachmentFormats, + CredentialFormatSpec, + HandlerAutoAcceptOptions, + FormatServiceOfferAttachmentFormats, + FormatServiceProposeAttachmentFormats, + RevocationRegistry, +} from '../models/CredentialFormatServiceOptions' +import type { Cred, CredDef, CredOffer, CredReq, CredReqMetadata } from 'indy-sdk' + +import { Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../../../agent/AgentConfig' +import { EventEmitter } from '../../../../agent/EventEmitter' +import { AriesFrameworkError } from '../../../../error' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { MessageValidator } from '../../../../utils/MessageValidator' +import { uuid } from '../../../../utils/uuid' +import { IndyHolderService, IndyIssuerService } from '../../../indy' +import { IndyLedgerService } from '../../../ledger' +import { AutoAcceptCredential } from '../../CredentialAutoAcceptType' +import { CredentialResponseCoordinator } from '../../CredentialResponseCoordinator' +import { CredentialUtils } from '../../CredentialUtils' +import { CredentialFormatType } from '../../CredentialsModuleOptions' +import { CredentialProblemReportError, CredentialProblemReportReason } from '../../errors' +import { V2CredentialPreview } from '../../protocol/v2/V2CredentialPreview' +import { CredentialMetadataKeys } from '../../repository/CredentialMetadataTypes' +import { CredentialRepository } from '../../repository/CredentialRepository' +import { CredentialFormatService } from '../CredentialFormatService' +import { CredPropose } from '../models/CredPropose' + +@scoped(Lifecycle.ContainerScoped) +export class IndyCredentialFormatService extends CredentialFormatService { + private indyIssuerService: IndyIssuerService + private indyLedgerService: IndyLedgerService + private indyHolderService: IndyHolderService + protected credentialRepository: CredentialRepository // protected as in base class + private logger: Logger + + public constructor( + credentialRepository: CredentialRepository, + eventEmitter: EventEmitter, + indyIssuerService: IndyIssuerService, + indyLedgerService: IndyLedgerService, + indyHolderService: IndyHolderService, + agentConfig: AgentConfig + ) { + super(credentialRepository, eventEmitter) + this.credentialRepository = credentialRepository + this.indyIssuerService = indyIssuerService + this.indyLedgerService = indyLedgerService + this.indyHolderService = indyHolderService + this.logger = agentConfig.logger + } + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the proposed credential + * @param messageType the type of message which can be Indy, JsonLd etc eg "CRED_20_PROPOSAL" + * @returns object containing associated attachment, formats and filtersAttach elements + * + */ + public async createProposal(options: ProposeCredentialOptions): Promise { + const formats: CredentialFormatSpec = { + attachId: this.generateId(), + format: 'hlindy/cred-filter@v2.0', + } + if (!options.credentialFormats.indy?.payload) { + throw new AriesFrameworkError('Missing payload in createProposal') + } + + // Use class instance instead of interface, otherwise this causes interoperability problems + let proposal = new CredPropose(options.credentialFormats.indy?.payload) + + try { + await MessageValidator.validate(proposal) + } catch (error) { + throw new AriesFrameworkError(`Invalid credPropose class instance: ${proposal} in Indy Format Service`) + } + + proposal = JsonTransformer.toJSON(proposal) + + const attachment = this.getFormatData(proposal, formats.attachId) + + const { previewWithAttachments } = this.getCredentialLinkedAttachments(options) + + return { format: formats, attachment, preview: previewWithAttachments } + } + + public async processProposal( + options: ServiceAcceptProposalOptions, + credentialRecord: CredentialExchangeRecord + ): Promise { + let credPropose = options.proposalAttachment?.getDataAsJson() + credPropose = JsonTransformer.fromJSON(credPropose, CredPropose) + + if (!credPropose) { + throw new AriesFrameworkError('Missing indy credential proposal data payload') + } + await MessageValidator.validate(credPropose) + + if (credPropose.credentialDefinitionId) { + options.credentialFormats = { + indy: { + credentialDefinitionId: credPropose?.credentialDefinitionId, + attributes: [], + }, + } + } + + credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, { + schemaId: credPropose.schemaId, + credentialDefinitionId: credPropose.credentialDefinitionId, + }) + } + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param options The object containing all the options for the credential offer + * @param messageType the type of message which can be Indy, JsonLd etc eg "CRED_20_OFFER" + * @returns object containing associated attachment, formats and offersAttach elements + * + */ + public async createOffer(options: ServiceOfferCredentialOptions): Promise { + const formats: CredentialFormatSpec = { + attachId: this.generateId(), + format: 'hlindy/cred-abstract@v2.0', + } + const offer = await this.createCredentialOffer(options) + + let preview: V2CredentialPreview | undefined + + if (options?.credentialFormats.indy?.attributes) { + preview = new V2CredentialPreview({ + attributes: options?.credentialFormats.indy?.attributes, + }) + } + + // if the proposal has an attachment Id use that, otherwise the generated id of the formats object + const attachmentId = options.attachId ? options.attachId : formats.attachId + + const offersAttach: Attachment = this.getFormatData(offer, attachmentId) + + // with credential preview now being a required field (as per spec) + // attributes could be empty + if (preview && preview.attributes.length > 0) { + await this.checkPreviewAttributesMatchSchemaAttributes(offersAttach, preview) + } + + return { format: formats, attachment: offersAttach, preview } + } + public async processOffer(attachment: Attachment, credentialRecord: CredentialExchangeRecord) { + if (!attachment) { + throw new AriesFrameworkError('Missing offer attachment in processOffer') + } + this.logger.debug(`Save metadata for credential record ${credentialRecord.id}`) + + const credOffer: CredOffer = attachment.getDataAsJson() + + credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, { + schemaId: credOffer.schema_id, + credentialDefinitionId: credOffer.cred_def_id, + }) + } + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param requestOptions The object containing all the options for the credential request + * @param credentialRecord the credential record containing the offer from which this request + * is derived + * @returns object containing associated attachment, formats and requestAttach elements + * + */ + public async createRequest( + options: ServiceRequestCredentialOptions, + credentialRecord: CredentialExchangeRecord, + holderDid: string + ): Promise { + if (!options.offerAttachment) { + throw new AriesFrameworkError( + `Missing attachment from offer message, credential record id = ${credentialRecord.id}` + ) + } + const offer = options.offerAttachment.getDataAsJson() + const credDef = await this.getCredentialDefinition(offer) + + const { credReq, credReqMetadata } = await this.createIndyCredentialRequest(offer, credDef, holderDid) + credentialRecord.metadata.set(CredentialMetadataKeys.IndyRequest, credReqMetadata) + + const formats: CredentialFormatSpec = { + attachId: this.generateId(), + format: 'hlindy/cred-req@v2.0', + } + + const attachmentId = options.attachId ?? formats.attachId + const requestAttach: Attachment = this.getFormatData(credReq, attachmentId) + return { format: formats, attachment: requestAttach } + } + + /** + * Not implemented; there for future versions + */ + public async processRequest( + /* eslint-disable @typescript-eslint/no-unused-vars */ + _options: RequestCredentialOptions, + _credentialRecord: CredentialExchangeRecord + /* eslint-enable @typescript-eslint/no-unused-vars */ + ): Promise { + // not needed for Indy + } + + private async getCredentialDefinition(credOffer: CredOffer): Promise { + const indyCredDef = await this.indyLedgerService.getCredentialDefinition(credOffer.cred_def_id) + return indyCredDef + } + + /** + * Get linked attachments for indy format from a proposal message. This allows attachments + * to be copied across to old style credential records + * + * @param options ProposeCredentialOptions object containing (optionally) the linked attachments + * @return array of linked attachments or undefined if none present + */ + private getCredentialLinkedAttachments(options: ProposeCredentialOptions): { + attachments: Attachment[] | undefined + previewWithAttachments: V2CredentialPreview + } { + // Add the linked attachments to the credentialProposal + if (!options.credentialFormats.indy?.payload) { + throw new AriesFrameworkError('Missing payload in getCredentialLinkedAttachments') + } + + let attachments: Attachment[] | undefined + let previewWithAttachments: V2CredentialPreview | undefined + if (options.credentialFormats.indy.attributes) { + previewWithAttachments = new V2CredentialPreview({ + attributes: options.credentialFormats.indy.attributes, + }) + } + + if (!options.credentialFormats.indy.attributes) { + throw new AriesFrameworkError('Missing attributes from credential proposal') + } + + if (options.credentialFormats.indy && options.credentialFormats.indy.linkedAttachments) { + // there are linked attachments so transform into the attribute field of the CredentialPreview object for + // this proposal + previewWithAttachments = CredentialUtils.createAndLinkAttachmentsToPreview( + options.credentialFormats.indy.linkedAttachments, + new V2CredentialPreview({ + attributes: options.credentialFormats.indy.attributes, + }) + ) + + attachments = options.credentialFormats.indy.linkedAttachments.map( + (linkedAttachment) => linkedAttachment.attachment + ) + } + if (!previewWithAttachments) { + throw new AriesFrameworkError('No previewWithAttachments') + } + return { attachments, previewWithAttachments } + } + + /** + * Gets the attachment object for a given attachId. We need to get out the correct attachId for + * indy and then find the corresponding attachment (if there is one) + * @param formats the formats object containing the attachid + * @param messageAttachment the attachment containing the payload + * @returns The Attachment if found or undefined + */ + + public getAttachment(formats: CredentialFormatSpec[], messageAttachment: Attachment[]): Attachment | undefined { + const formatId = formats.find((f) => f.format.includes('indy')) + const attachment = messageAttachment?.find((attachment) => attachment.id === formatId?.attachId) + return attachment + } + /** + * Create a credential offer for the given credential definition id. + * + * @param credentialDefinitionId The credential definition to create an offer for + * @returns The created credential offer + */ + private async createCredentialOffer( + proposal: ServiceOfferOptions | NegotiateProposalOptions | OfferCredentialOptions + ): Promise { + if (!proposal.credentialFormats?.indy?.credentialDefinitionId) { + throw new AriesFrameworkError('Missing Credential Definition id') + } + const credOffer: CredOffer = await this.indyIssuerService.createCredentialOffer( + proposal.credentialFormats.indy.credentialDefinitionId + ) + return credOffer + } + + /** + * Create a credential offer for the given credential definition id. + * + * @param options RequestCredentialOptions the config options for the credential request + * @throws Error if unable to create the request + * @returns The created credential offer + */ + private async createIndyCredentialRequest( + offer: CredOffer, + credentialDefinition: CredDef, + holderDid: string + ): Promise<{ credReq: CredReq; credReqMetadata: CredReqMetadata }> { + const [credReq, credReqMetadata] = await this.indyHolderService.createCredentialRequest({ + holderDid: holderDid, + credentialOffer: offer, + credentialDefinition, + }) + return { credReq, credReqMetadata } + } + + /** + * Create a {@link AttachmentFormats} object dependent on the message type. + * + * @param requestOptions The object containing all the options for the credential request + * @param credentialRecord the credential record containing the offer from which this request + * is derived + * @returns object containing associated attachment, formats and requestAttach elements + * + */ + public async createCredential( + options: ServiceAcceptRequestOptions, + record: CredentialExchangeRecord, + requestAttachment: Attachment, + offerAttachment?: Attachment + ): Promise { + // Assert credential attributes + const credentialAttributes = record.credentialAttributes + if (!credentialAttributes) { + throw new CredentialProblemReportError( + `Missing required credential attribute values on credential record with id ${record.id}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + + const credOffer = offerAttachment?.getDataAsJson() + const credRequest = requestAttachment?.getDataAsJson() + + if (!credOffer || !credRequest) { + throw new AriesFrameworkError('Missing CredOffer or CredReq in createCredential') + } + if (!this.indyIssuerService) { + throw new AriesFrameworkError('Missing indyIssuerService in createCredential') + } + + const [credential] = await this.indyIssuerService.createCredential({ + credentialOffer: credOffer, + credentialRequest: credRequest, + credentialValues: CredentialUtils.convertAttributesToValues(credentialAttributes), + }) + + const formats: CredentialFormatSpec = { + attachId: this.generateId(), + format: 'hlindy/cred-abstract@v2.0', + } + + const attachmentId = options.attachId ? options.attachId : formats.attachId + const issueAttachment = this.getFormatData(credential, attachmentId) + return { format: formats, attachment: issueAttachment } + } + /** + * Processes an incoming credential - retrieve metadata, retrieve payload and store it in the Indy wallet + * @param message the issue credential message + */ + + /** + * Processes an incoming credential - retrieve metadata, retrieve payload and store it in the Indy wallet + * @param options the issue credential message wrapped inside this object + * @param credentialRecord the credential exchange record for this credential + */ + public async processCredential( + options: ServiceAcceptCredentialOptions, + credentialRecord: CredentialExchangeRecord + ): Promise { + const credentialRequestMetadata = credentialRecord.metadata.get(CredentialMetadataKeys.IndyRequest) + + if (!credentialRequestMetadata) { + throw new CredentialProblemReportError( + `Missing required request metadata for credential with id ${credentialRecord.id}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + if (!options.credentialAttachment) { + throw new AriesFrameworkError(`Missing credential for record id ${credentialRecord.id}`) + } + const indyCredential: Cred = options.credentialAttachment.getDataAsJson() + + const credentialDefinition = await this.indyLedgerService.getCredentialDefinition(indyCredential.cred_def_id) + + if (!options.credentialAttachment) { + throw new AriesFrameworkError('Missing credential attachment in processCredential') + } + const revocationRegistry = await this.getRevocationRegistry(options.credentialAttachment) + const credentialId = await this.indyHolderService.storeCredential({ + credentialId: this.generateId(), + credentialRequestMetadata, + credential: indyCredential, + credentialDefinition, + revocationRegistryDefinition: revocationRegistry?.indy?.revocationRegistryDefinition, + }) + credentialRecord.credentials.push({ + credentialRecordType: CredentialFormatType.Indy, + credentialRecordId: credentialId, + }) + } + + /** + * Checks whether it should automatically respond to a proposal. Moved from CredentialResponseCoordinator + * as this contains format-specific logic + * @param credentialRecord The credential record for which we are testing whether or not to auto respond + * @param agentConfig config object for the agent, used to hold auto accept state for the agent + * @returns true if we should auto respond, false otherwise + */ + + public shouldAutoRespondToProposal(handlerOptions: HandlerAutoAcceptOptions): boolean { + const autoAccept = CredentialResponseCoordinator.composeAutoAccept( + handlerOptions.credentialRecord.autoAcceptCredential, + handlerOptions.autoAcceptType + ) + + if (autoAccept === AutoAcceptCredential.ContentApproved) { + return ( + this.areProposalValuesValid(handlerOptions.credentialRecord, handlerOptions.messageAttributes) && + this.areProposalAndOfferDefinitionIdEqual(handlerOptions.proposalAttachment, handlerOptions.offerAttachment) + ) + } + return false + } + + /** + * Checks whether it should automatically respond to a request. Moved from CredentialResponseCoordinator + * as this contains format-specific logic + * @param credentialRecord The credential record for which we are testing whether or not to auto respond + * @param autoAcceptType auto accept type for this credential exchange - normal auto or content approved + * @returns true if we should auto respond, false otherwise + + */ + + public shouldAutoRespondToRequest(options: HandlerAutoAcceptOptions): boolean { + const autoAccept = CredentialResponseCoordinator.composeAutoAccept( + options.credentialRecord.autoAcceptCredential, + options.autoAcceptType + ) + + if (!options.requestAttachment) { + throw new AriesFrameworkError(`Missing Request Attachment for Credential Record ${options.credentialRecord.id}`) + } + if (autoAccept === AutoAcceptCredential.ContentApproved) { + return this.isRequestDefinitionIdValid( + options.requestAttachment, + options.offerAttachment, + options.proposalAttachment + ) + } + return false + } + /** + * Checks whether it should automatically respond to a request. Moved from CredentialResponseCoordinator + * as this contains format-specific logic + * @param credentialRecord The credential record for which we are testing whether or not to auto respond + * @param autoAcceptType auto accept type for this credential exchange - normal auto or content approved + * @returns true if we should auto respond, false otherwise + */ + + public shouldAutoRespondToCredential(options: HandlerAutoAcceptOptions): boolean { + const autoAccept = CredentialResponseCoordinator.composeAutoAccept( + options.credentialRecord.autoAcceptCredential, + options.autoAcceptType + ) + + if (autoAccept === AutoAcceptCredential.ContentApproved) { + if (options.credentialAttachment) { + return this.areCredentialValuesValid(options.credentialRecord, options.credentialAttachment) + } + } + return false + } + private areProposalValuesValid( + credentialRecord: CredentialExchangeRecord, + proposeMessageAttributes?: CredentialPreviewAttribute[] + ) { + const { credentialAttributes } = credentialRecord + + if (proposeMessageAttributes && credentialAttributes) { + const proposeValues = CredentialUtils.convertAttributesToValues(proposeMessageAttributes) + const defaultValues = CredentialUtils.convertAttributesToValues(credentialAttributes) + if (CredentialUtils.checkValuesMatch(proposeValues, defaultValues)) { + return true + } + } + return false + } + + private areProposalAndOfferDefinitionIdEqual(proposalAttachment?: Attachment, offerAttachment?: Attachment) { + const credOffer = offerAttachment?.getDataAsJson() + let credPropose = proposalAttachment?.getDataAsJson() + credPropose = JsonTransformer.fromJSON(credPropose, CredPropose) + + const proposalCredentialDefinitionId = credPropose?.credentialDefinitionId + const offerCredentialDefinitionId = credOffer?.cred_def_id + return proposalCredentialDefinitionId === offerCredentialDefinitionId + } + + private areCredentialValuesValid(credentialRecord: CredentialExchangeRecord, credentialAttachment: Attachment) { + const indyCredential = credentialAttachment.getDataAsJson() + + if (!indyCredential) { + new AriesFrameworkError(`Missing required base64 encoded attachment data for credential`) + return false + } + + const credentialMessageValues = indyCredential.values + + if (credentialRecord.credentialAttributes) { + const defaultValues = CredentialUtils.convertAttributesToValues(credentialRecord.credentialAttributes) + + if (CredentialUtils.checkValuesMatch(credentialMessageValues, defaultValues)) { + return true + } + } + return false + } + public async deleteCredentialById(credentialRecordId: string): Promise { + await this.indyHolderService.deleteCredential(credentialRecordId) + } + + public async checkPreviewAttributesMatchSchemaAttributes( + offerAttachment: Attachment, + preview: V1CredentialPreview | V2CredentialPreview + ): Promise { + const credOffer = offerAttachment?.getDataAsJson() + + const schema = await this.indyLedgerService.getSchema(credOffer.schema_id) + + CredentialUtils.checkAttributesMatch(schema, preview) + } + + private isRequestDefinitionIdValid( + requestAttachment: Attachment, + offerAttachment?: Attachment, + proposeAttachment?: Attachment + ) { + const indyCredentialRequest = requestAttachment?.getDataAsJson() + let indyCredentialProposal = proposeAttachment?.getDataAsJson() + indyCredentialProposal = JsonTransformer.fromJSON(indyCredentialProposal, CredPropose) + + const indyCredentialOffer = offerAttachment?.getDataAsJson() + + if (indyCredentialProposal || indyCredentialOffer) { + const previousCredentialDefinitionId = + indyCredentialOffer?.cred_def_id ?? indyCredentialProposal?.credentialDefinitionId + + if (previousCredentialDefinitionId === indyCredentialRequest.cred_def_id) { + return true + } + return false + } + return false + } + private generateId(): string { + return uuid() + } + + private async getRevocationRegistry(issueAttachment: Attachment): Promise { + const credential: Cred = issueAttachment.getDataAsJson() + let indyRegistry + if (credential.rev_reg_id) { + indyRegistry = await this.indyLedgerService.getRevocationRegistryDefinition(credential.rev_reg_id) + } + return { indy: indyRegistry } + } +} diff --git a/packages/core/src/modules/credentials/formats/models/CredPropose.ts b/packages/core/src/modules/credentials/formats/models/CredPropose.ts new file mode 100644 index 0000000000..0476cce949 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/models/CredPropose.ts @@ -0,0 +1,81 @@ +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +export interface CredProposeOptions { + schemaIssuerDid?: string + schemaId?: string + schemaName?: string + schemaVersion?: string + credentialDefinitionId?: string + issuerDid?: string +} + +/** + * Class providing validation for the V2 credential proposal payload. + * + * The v1 message contains the properties directly in the message, which means they are + * validated using the class validator decorators. In v2 the attachments content is not transformed + * when transforming the message to a class instance so the content is not verified anymore, hence this + * class. + * + */ +export class CredPropose { + public constructor(options: CredProposeOptions) { + if (options) { + this.schemaIssuerDid = options.schemaIssuerDid + this.schemaId = options.schemaId + this.schemaName = options.schemaName + this.schemaVersion = options.schemaVersion + this.credentialDefinitionId = options.credentialDefinitionId + this.issuerDid = options.issuerDid + } + } + + /** + * Filter to request credential based on a particular Schema issuer DID. + */ + @Expose({ name: 'schema_issuer_did' }) + @IsString() + @IsOptional() + public schemaIssuerDid?: string + + /** + * Filter to request credential based on a particular Schema. + */ + @Expose({ name: 'schema_id' }) + @IsString() + @IsOptional() + public schemaId?: string + + /** + * Filter to request credential based on a schema name. + */ + @Expose({ name: 'schema_name' }) + @IsString() + @IsOptional() + public schemaName?: string + + /** + * Filter to request credential based on a schema version. + */ + @Expose({ name: 'schema_version' }) + @IsString() + @IsOptional() + public schemaVersion?: string + + /** + * Filter to request credential based on a particular Credential Definition. + */ + @Expose({ name: 'cred_def_id' }) + @IsString() + @IsOptional() + public credentialDefinitionId?: string + + /** + * Filter to request a credential issued by the owner of a particular DID. + */ + @Expose({ name: 'issuer_did' }) + @IsString() + @IsOptional() + public issuerDid?: string +} diff --git a/packages/core/src/modules/credentials/formats/models/CredentialFormatServiceOptions.ts b/packages/core/src/modules/credentials/formats/models/CredentialFormatServiceOptions.ts new file mode 100644 index 0000000000..3172fa9236 --- /dev/null +++ b/packages/core/src/modules/credentials/formats/models/CredentialFormatServiceOptions.ts @@ -0,0 +1,115 @@ +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { LinkedAttachment } from '../../../../utils/LinkedAttachment' +import type { ParseRevocationRegistryDefinitionTemplate } from '../../../ledger/services' +import type { AutoAcceptCredential } from '../../CredentialAutoAcceptType' +import type { CredentialPreviewAttribute } from '../../models/CredentialPreviewAttributes' +import type { V2CredentialPreview } from '../../protocol/v2/V2CredentialPreview' +import type { CredentialExchangeRecord } from '../../repository/CredentialExchangeRecord' +import type { CredPropose } from './CredPropose' + +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +import { CredentialFormatType } from '../../CredentialsModuleOptions' + +export type CredentialFormats = + | FormatServiceOfferCredentialFormats + | FormatServiceProposeCredentialFormats + | FormatServiceRequestCredentialFormats +export interface IndyCredentialPreview { + credentialDefinitionId?: string + attributes?: CredentialPreviewAttribute[] +} + +export interface IndyProposeCredentialFormat { + attributes?: CredentialPreviewAttribute[] + linkedAttachments?: LinkedAttachment[] + payload?: CredPropose +} +export interface IndyOfferCredentialFormat { + credentialDefinitionId: string + attributes: CredentialPreviewAttribute[] + linkedAttachments?: LinkedAttachment[] +} +export interface IndyRequestCredentialFormat { + credentialDefinitionId?: string + attributes?: CredentialPreviewAttribute[] +} +export interface IndyIssueCredentialFormat { + credentialDefinitionId?: string + attributes?: CredentialPreviewAttribute[] +} + +export class CredentialFormatSpec { + @Expose({ name: 'attach_id' }) + @IsString() + public attachId!: string + + @IsString() + public format!: string +} + +export type FormatKeys = { + [id: string]: CredentialFormatType +} + +export interface FormatServiceCredentialAttachmentFormats { + format: CredentialFormatSpec + attachment: Attachment +} + +export interface FormatServiceProposeAttachmentFormats extends FormatServiceCredentialAttachmentFormats { + preview?: V2CredentialPreview +} + +export interface FormatServiceOfferAttachmentFormats extends FormatServiceCredentialAttachmentFormats { + preview?: V2CredentialPreview +} +export const FORMAT_KEYS: FormatKeys = { + indy: CredentialFormatType.Indy, +} + +export interface FormatServiceOfferCredentialFormats { + indy?: IndyOfferCredentialFormat + jsonld?: undefined +} + +export interface FormatServiceProposeCredentialFormats { + indy?: IndyProposeCredentialFormat + jsonld?: undefined +} + +export interface FormatServiceAcceptProposeCredentialFormats { + indy?: { + credentialDefinitionId?: string + attributes: CredentialPreviewAttribute[] + linkedAttachments?: LinkedAttachment[] + } + jsonld?: undefined +} + +export interface FormatServiceRequestCredentialFormats { + indy?: IndyRequestCredentialFormat + jsonld?: undefined +} + +export interface FormatServiceIssueCredentialFormats { + indy?: IndyIssueCredentialFormat + jsonld?: undefined +} + +export interface HandlerAutoAcceptOptions { + credentialRecord: CredentialExchangeRecord + autoAcceptType: AutoAcceptCredential + messageAttributes?: CredentialPreviewAttribute[] + proposalAttachment?: Attachment + offerAttachment?: Attachment + requestAttachment?: Attachment + credentialAttachment?: Attachment + credentialDefinitionId?: string +} + +export interface RevocationRegistry { + indy?: ParseRevocationRegistryDefinitionTemplate + jsonld?: undefined +} diff --git a/packages/core/src/modules/credentials/handlers/CredentialAckHandler.ts b/packages/core/src/modules/credentials/handlers/CredentialAckHandler.ts deleted file mode 100644 index cfecea6026..0000000000 --- a/packages/core/src/modules/credentials/handlers/CredentialAckHandler.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { CredentialService } from '../services' - -import { CredentialAckMessage } from '../messages' - -export class CredentialAckHandler implements Handler { - private credentialService: CredentialService - public supportedMessages = [CredentialAckMessage] - - public constructor(credentialService: CredentialService) { - this.credentialService = credentialService - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.credentialService.processAck(messageContext) - } -} diff --git a/packages/core/src/modules/credentials/handlers/IssueCredentialHandler.ts b/packages/core/src/modules/credentials/handlers/IssueCredentialHandler.ts deleted file mode 100644 index 293475f96e..0000000000 --- a/packages/core/src/modules/credentials/handlers/IssueCredentialHandler.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { AgentConfig } from '../../../agent/AgentConfig' -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { CredentialResponseCoordinator } from '../CredentialResponseCoordinator' -import type { CredentialRecord } from '../repository/CredentialRecord' -import type { CredentialService } from '../services' - -import { createOutboundMessage, createOutboundServiceMessage } from '../../../agent/helpers' -import { IssueCredentialMessage } from '../messages' - -export class IssueCredentialHandler implements Handler { - private credentialService: CredentialService - private agentConfig: AgentConfig - private credentialResponseCoordinator: CredentialResponseCoordinator - public supportedMessages = [IssueCredentialMessage] - - public constructor( - credentialService: CredentialService, - agentConfig: AgentConfig, - credentialResponseCoordinator: CredentialResponseCoordinator - ) { - this.credentialService = credentialService - this.agentConfig = agentConfig - this.credentialResponseCoordinator = credentialResponseCoordinator - } - - public async handle(messageContext: HandlerInboundMessage) { - const credentialRecord = await this.credentialService.processCredential(messageContext) - if (this.credentialResponseCoordinator.shouldAutoRespondToIssue(credentialRecord)) { - return await this.createAck(credentialRecord, messageContext) - } - } - - private async createAck(record: CredentialRecord, messageContext: HandlerInboundMessage) { - this.agentConfig.logger.info( - `Automatically sending acknowledgement with autoAccept on ${this.agentConfig.autoAcceptCredentials}` - ) - const { message, credentialRecord } = await this.credentialService.createAck(record) - - if (messageContext.connection) { - return createOutboundMessage(messageContext.connection, message) - } else if (credentialRecord.credentialMessage?.service && credentialRecord.requestMessage?.service) { - const recipientService = credentialRecord.credentialMessage.service - const ourService = credentialRecord.requestMessage.service - - return createOutboundServiceMessage({ - payload: message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], - }) - } - - this.agentConfig.logger.error(`Could not automatically create credential ack`) - } -} diff --git a/packages/core/src/modules/credentials/handlers/OfferCredentialHandler.ts b/packages/core/src/modules/credentials/handlers/OfferCredentialHandler.ts deleted file mode 100644 index e00efdf7c7..0000000000 --- a/packages/core/src/modules/credentials/handlers/OfferCredentialHandler.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { AgentConfig } from '../../../agent/AgentConfig' -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { MediationRecipientService } from '../../routing/services/MediationRecipientService' -import type { CredentialResponseCoordinator } from '../CredentialResponseCoordinator' -import type { CredentialRecord } from '../repository/CredentialRecord' -import type { CredentialService } from '../services' - -import { createOutboundMessage, createOutboundServiceMessage } from '../../../agent/helpers' -import { ServiceDecorator } from '../../../decorators/service/ServiceDecorator' -import { OfferCredentialMessage } from '../messages' - -export class OfferCredentialHandler implements Handler { - private credentialService: CredentialService - private agentConfig: AgentConfig - private credentialResponseCoordinator: CredentialResponseCoordinator - private mediationRecipientService: MediationRecipientService - public supportedMessages = [OfferCredentialMessage] - - public constructor( - credentialService: CredentialService, - agentConfig: AgentConfig, - credentialResponseCoordinator: CredentialResponseCoordinator, - mediationRecipientService: MediationRecipientService - ) { - this.credentialService = credentialService - this.agentConfig = agentConfig - this.credentialResponseCoordinator = credentialResponseCoordinator - this.mediationRecipientService = mediationRecipientService - } - - public async handle(messageContext: HandlerInboundMessage) { - const credentialRecord = await this.credentialService.processOffer(messageContext) - - if (this.credentialResponseCoordinator.shouldAutoRespondToOffer(credentialRecord)) { - return await this.createRequest(credentialRecord, messageContext) - } - } - - private async createRequest(record: CredentialRecord, messageContext: HandlerInboundMessage) { - this.agentConfig.logger.info( - `Automatically sending request with autoAccept on ${this.agentConfig.autoAcceptCredentials}` - ) - - if (messageContext.connection) { - const { message } = await this.credentialService.createRequest(record, { - holderDid: messageContext.connection.did, - }) - - return createOutboundMessage(messageContext.connection, message) - } else if (record.offerMessage?.service) { - const routing = await this.mediationRecipientService.getRouting() - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.verkey], - routingKeys: routing.routingKeys, - }) - const recipientService = record.offerMessage.service - - const { message, credentialRecord } = await this.credentialService.createRequest(record, { - holderDid: ourService.recipientKeys[0], - }) - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - credentialRecord.requestMessage = message - await this.credentialService.update(credentialRecord) - - return createOutboundServiceMessage({ - payload: message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], - }) - } - - this.agentConfig.logger.error(`Could not automatically create credential request`) - } -} diff --git a/packages/core/src/modules/credentials/handlers/ProposeCredentialHandler.ts b/packages/core/src/modules/credentials/handlers/ProposeCredentialHandler.ts deleted file mode 100644 index 48eb7dd11e..0000000000 --- a/packages/core/src/modules/credentials/handlers/ProposeCredentialHandler.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { AgentConfig } from '../../../agent/AgentConfig' -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { CredentialResponseCoordinator } from '../CredentialResponseCoordinator' -import type { CredentialRecord } from '../repository/CredentialRecord' -import type { CredentialService } from '../services' - -import { createOutboundMessage } from '../../../agent/helpers' -import { ProposeCredentialMessage } from '../messages' - -export class ProposeCredentialHandler implements Handler { - private credentialService: CredentialService - private agentConfig: AgentConfig - private credentialAutoResponseCoordinator: CredentialResponseCoordinator - public supportedMessages = [ProposeCredentialMessage] - - public constructor( - credentialService: CredentialService, - agentConfig: AgentConfig, - responseCoordinator: CredentialResponseCoordinator - ) { - this.credentialAutoResponseCoordinator = responseCoordinator - this.credentialService = credentialService - this.agentConfig = agentConfig - } - - public async handle(messageContext: HandlerInboundMessage) { - const credentialRecord = await this.credentialService.processProposal(messageContext) - if (this.credentialAutoResponseCoordinator.shouldAutoRespondToProposal(credentialRecord)) { - return await this.createOffer(credentialRecord, messageContext) - } - } - - private async createOffer( - credentialRecord: CredentialRecord, - messageContext: HandlerInboundMessage - ) { - this.agentConfig.logger.info( - `Automatically sending offer with autoAccept on ${this.agentConfig.autoAcceptCredentials}` - ) - - if (!messageContext.connection) { - this.agentConfig.logger.error('No connection on the messageContext, aborting auto accept') - return - } - - if (!credentialRecord.proposalMessage?.credentialProposal) { - this.agentConfig.logger.error( - `Credential record with id ${credentialRecord.id} is missing required credential proposal` - ) - return - } - - if (!credentialRecord.proposalMessage.credentialDefinitionId) { - this.agentConfig.logger.error('Missing required credential definition id') - return - } - - const { message } = await this.credentialService.createOfferAsResponse(credentialRecord, { - credentialDefinitionId: credentialRecord.proposalMessage.credentialDefinitionId, - preview: credentialRecord.proposalMessage.credentialProposal, - }) - - return createOutboundMessage(messageContext.connection, message) - } -} diff --git a/packages/core/src/modules/credentials/handlers/RequestCredentialHandler.ts b/packages/core/src/modules/credentials/handlers/RequestCredentialHandler.ts deleted file mode 100644 index c4a4d449c4..0000000000 --- a/packages/core/src/modules/credentials/handlers/RequestCredentialHandler.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { AgentConfig } from '../../../agent/AgentConfig' -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { CredentialResponseCoordinator } from '../CredentialResponseCoordinator' -import type { CredentialRecord } from '../repository/CredentialRecord' -import type { CredentialService } from '../services' - -import { createOutboundMessage, createOutboundServiceMessage } from '../../../agent/helpers' -import { RequestCredentialMessage } from '../messages' - -export class RequestCredentialHandler implements Handler { - private agentConfig: AgentConfig - private credentialService: CredentialService - private credentialResponseCoordinator: CredentialResponseCoordinator - public supportedMessages = [RequestCredentialMessage] - - public constructor( - credentialService: CredentialService, - agentConfig: AgentConfig, - credentialResponseCoordinator: CredentialResponseCoordinator - ) { - this.credentialService = credentialService - this.agentConfig = agentConfig - this.credentialResponseCoordinator = credentialResponseCoordinator - } - - public async handle(messageContext: HandlerInboundMessage) { - const credentialRecord = await this.credentialService.processRequest(messageContext) - if (this.credentialResponseCoordinator.shouldAutoRespondToRequest(credentialRecord)) { - return await this.createCredential(credentialRecord, messageContext) - } - } - - private async createCredential( - record: CredentialRecord, - messageContext: HandlerInboundMessage - ) { - this.agentConfig.logger.info( - `Automatically sending credential with autoAccept on ${this.agentConfig.autoAcceptCredentials}` - ) - - const { message, credentialRecord } = await this.credentialService.createCredential(record) - - if (messageContext.connection) { - return createOutboundMessage(messageContext.connection, message) - } else if (credentialRecord.requestMessage?.service && credentialRecord.offerMessage?.service) { - const recipientService = credentialRecord.requestMessage.service - const ourService = credentialRecord.offerMessage.service - - // Set ~service, update message in record (for later use) - message.setService(ourService) - credentialRecord.credentialMessage = message - await this.credentialService.update(credentialRecord) - - return createOutboundServiceMessage({ - payload: message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], - }) - } - - this.agentConfig.logger.error(`Could not automatically create credential request`) - } -} diff --git a/packages/core/src/modules/credentials/handlers/index.ts b/packages/core/src/modules/credentials/handlers/index.ts deleted file mode 100644 index fefbf8d9ad..0000000000 --- a/packages/core/src/modules/credentials/handlers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './CredentialAckHandler' -export * from './IssueCredentialHandler' -export * from './OfferCredentialHandler' -export * from './ProposeCredentialHandler' -export * from './RequestCredentialHandler' diff --git a/packages/core/src/modules/credentials/index.ts b/packages/core/src/modules/credentials/index.ts index 890024a880..d7b5b67b3a 100644 --- a/packages/core/src/modules/credentials/index.ts +++ b/packages/core/src/modules/credentials/index.ts @@ -1,9 +1,10 @@ -export * from './messages' -export * from './services' +export * from './CredentialsModule' +export * from './protocol/v1/messages' export * from './CredentialUtils' -export * from './models' +export * from './protocol/v1/models' export * from './repository' export * from './CredentialState' export * from './CredentialEvents' -export * from './CredentialsModule' export * from './CredentialAutoAcceptType' +export * from './CredentialProtocolVersion' +export * from './CredentialResponseCoordinator' diff --git a/packages/core/src/modules/credentials/messages/CredentialAckMessage.ts b/packages/core/src/modules/credentials/messages/CredentialAckMessage.ts deleted file mode 100644 index 1011addde9..0000000000 --- a/packages/core/src/modules/credentials/messages/CredentialAckMessage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { AckMessageOptions } from '../../common' - -import { Equals } from 'class-validator' - -import { AckMessage } from '../../common' - -export type CredentialAckMessageOptions = AckMessageOptions - -/** - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks - */ -export class CredentialAckMessage extends AckMessage { - /** - * Create new CredentialAckMessage instance. - * @param options - */ - public constructor(options: CredentialAckMessageOptions) { - super(options) - } - - @Equals(CredentialAckMessage.type) - public readonly type = CredentialAckMessage.type - public static readonly type = 'https://didcomm.org/issue-credential/1.0/ack' -} diff --git a/packages/core/src/modules/credentials/messages/OfferCredentialMessage.ts b/packages/core/src/modules/credentials/messages/OfferCredentialMessage.ts deleted file mode 100644 index 5736a9a26f..0000000000 --- a/packages/core/src/modules/credentials/messages/OfferCredentialMessage.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { CredOffer } from 'indy-sdk' - -import { Expose, Type } from 'class-transformer' -import { Equals, IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' - -import { AgentMessage } from '../../../agent/AgentMessage' -import { Attachment } from '../../../decorators/attachment/Attachment' -import { JsonEncoder } from '../../../utils/JsonEncoder' - -import { CredentialPreview } from './CredentialPreview' - -export const INDY_CREDENTIAL_OFFER_ATTACHMENT_ID = 'libindy-cred-offer-0' - -export interface OfferCredentialMessageOptions { - id?: string - comment?: string - offerAttachments: Attachment[] - credentialPreview: CredentialPreview - attachments?: Attachment[] -} - -/** - * Message part of Issue Credential Protocol used to continue or initiate credential exchange by issuer. - * - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#offer-credential - */ -export class OfferCredentialMessage extends AgentMessage { - public constructor(options: OfferCredentialMessageOptions) { - super() - - if (options) { - this.id = options.id || this.generateId() - this.comment = options.comment - this.credentialPreview = options.credentialPreview - this.offerAttachments = options.offerAttachments - this.attachments = options.attachments - } - } - - @Equals(OfferCredentialMessage.type) - public readonly type = OfferCredentialMessage.type - public static readonly type = 'https://didcomm.org/issue-credential/1.0/offer-credential' - - @IsString() - @IsOptional() - public comment?: string - - @Expose({ name: 'credential_preview' }) - @Type(() => CredentialPreview) - @ValidateNested() - @IsInstance(CredentialPreview) - public credentialPreview!: CredentialPreview - - @Expose({ name: 'offers~attach' }) - @Type(() => Attachment) - @IsArray() - @ValidateNested({ - each: true, - }) - @IsInstance(Attachment, { each: true }) - public offerAttachments!: Attachment[] - - public get indyCredentialOffer(): CredOffer | null { - const attachment = this.offerAttachments.find((attachment) => attachment.id === INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) - - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null - } - - // Extract credential offer from attachment - const credentialOfferJson = JsonEncoder.fromBase64(attachment.data.base64) - - return credentialOfferJson - } -} diff --git a/packages/core/src/modules/credentials/messages/RequestCredentialMessage.ts b/packages/core/src/modules/credentials/messages/RequestCredentialMessage.ts deleted file mode 100644 index 2600a44cac..0000000000 --- a/packages/core/src/modules/credentials/messages/RequestCredentialMessage.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { CredReq } from 'indy-sdk' - -import { Expose, Type } from 'class-transformer' -import { Equals, IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' - -import { AgentMessage } from '../../../agent/AgentMessage' -import { Attachment } from '../../../decorators/attachment/Attachment' -import { JsonEncoder } from '../../../utils/JsonEncoder' - -export const INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID = 'libindy-cred-request-0' - -interface RequestCredentialMessageOptions { - id?: string - comment?: string - requestAttachments: Attachment[] - attachments?: Attachment[] -} - -export class RequestCredentialMessage extends AgentMessage { - public constructor(options: RequestCredentialMessageOptions) { - super() - - if (options) { - this.id = options.id || this.generateId() - this.comment = options.comment - this.requestAttachments = options.requestAttachments - this.attachments = options.attachments - } - } - - @Equals(RequestCredentialMessage.type) - public readonly type = RequestCredentialMessage.type - public static readonly type = 'https://didcomm.org/issue-credential/1.0/request-credential' - - @IsString() - @IsOptional() - public comment?: string - - @Expose({ name: 'requests~attach' }) - @Type(() => Attachment) - @IsArray() - @ValidateNested({ - each: true, - }) - @IsInstance(Attachment, { each: true }) - public requestAttachments!: Attachment[] - - public get indyCredentialRequest(): CredReq | null { - const attachment = this.requestAttachments.find( - (attachment) => attachment.id === INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID - ) - - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null - } - - // Extract proof request from attachment - const credentialReqJson = JsonEncoder.fromBase64(attachment.data.base64) - - return credentialReqJson - } -} diff --git a/packages/core/src/modules/credentials/messages/index.ts b/packages/core/src/modules/credentials/messages/index.ts deleted file mode 100644 index 2979876a4a..0000000000 --- a/packages/core/src/modules/credentials/messages/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './CredentialAckMessage' -export * from './CredentialPreview' -export * from './RequestCredentialMessage' -export * from './IssueCredentialMessage' -export * from './OfferCredentialMessage' -export * from './ProposeCredentialMessage' diff --git a/packages/core/src/modules/credentials/models/CredentialPreviewAttributes.ts b/packages/core/src/modules/credentials/models/CredentialPreviewAttributes.ts new file mode 100644 index 0000000000..89c3397b09 --- /dev/null +++ b/packages/core/src/modules/credentials/models/CredentialPreviewAttributes.ts @@ -0,0 +1,39 @@ +import { Expose } from 'class-transformer' +import { IsMimeType, IsOptional, IsString } from 'class-validator' + +import { JsonTransformer } from '../../../utils/JsonTransformer' + +export interface CredentialPreviewAttributeOptions { + name: string + mimeType?: string + value: string +} + +export class CredentialPreviewAttribute { + public constructor(options: CredentialPreviewAttributeOptions) { + if (options) { + this.name = options.name + this.mimeType = options.mimeType + this.value = options.value + } + } + + @IsString() + public name!: string + + @Expose({ name: 'mime-type' }) + @IsOptional() + @IsMimeType() + public mimeType?: string = 'text/plain' + + @IsString() + public value!: string + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } +} + +export interface CredentialPreviewOptions { + attributes: CredentialPreviewAttribute[] +} diff --git a/packages/core/src/modules/credentials/models/RevocationNotification.ts b/packages/core/src/modules/credentials/models/RevocationNotification.ts new file mode 100644 index 0000000000..b26a52c7ad --- /dev/null +++ b/packages/core/src/modules/credentials/models/RevocationNotification.ts @@ -0,0 +1,9 @@ +export class RevocationNotification { + public revocationDate: Date + public comment?: string + + public constructor(comment?: string, revocationDate: Date = new Date()) { + this.revocationDate = revocationDate + this.comment = comment + } +} diff --git a/packages/core/src/modules/credentials/models/index.ts b/packages/core/src/modules/credentials/models/index.ts index cae218929d..f6d0750b49 100644 --- a/packages/core/src/modules/credentials/models/index.ts +++ b/packages/core/src/modules/credentials/models/index.ts @@ -1,3 +1,2 @@ -export * from './Credential' -export * from './IndyCredentialInfo' -export * from './RevocationInterval' +export * from './RevocationNotification' +export * from './CredentialPreviewAttributes' diff --git a/packages/core/src/modules/credentials/protocol/index.ts b/packages/core/src/modules/credentials/protocol/index.ts new file mode 100644 index 0000000000..88af3b8591 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/index.ts @@ -0,0 +1,3 @@ +export * from '../CredentialServiceOptions' +export * from './v1/V1CredentialService' +export * from './v2/V2CredentialService' diff --git a/packages/core/src/modules/credentials/messages/CredentialPreview.ts b/packages/core/src/modules/credentials/protocol/v1/V1CredentialPreview.ts similarity index 54% rename from packages/core/src/modules/credentials/messages/CredentialPreview.ts rename to packages/core/src/modules/credentials/protocol/v1/V1CredentialPreview.ts index e4feed3234..7030f9dc67 100644 --- a/packages/core/src/modules/credentials/messages/CredentialPreview.ts +++ b/packages/core/src/modules/credentials/protocol/v1/V1CredentialPreview.ts @@ -1,43 +1,11 @@ -import { Expose, Transform, Type } from 'class-transformer' -import { Equals, IsInstance, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator' - -import { JsonTransformer } from '../../../utils/JsonTransformer' -import { replaceLegacyDidSovPrefix } from '../../../utils/messageType' - -interface CredentialPreviewAttributeOptions { - name: string - mimeType?: string - value: string -} - -export class CredentialPreviewAttribute { - public constructor(options: CredentialPreviewAttributeOptions) { - if (options) { - this.name = options.name - this.mimeType = options.mimeType - this.value = options.value - } - } - - @IsString() - public name!: string +import type { CredentialPreviewOptions } from '../../models/CredentialPreviewAttributes' - @Expose({ name: 'mime-type' }) - @IsOptional() - @IsMimeType() - public mimeType?: string = 'text/plain' - - @IsString() - public value!: string - - public toJSON(): Record { - return JsonTransformer.toJSON(this) - } -} +import { Expose, Transform, Type } from 'class-transformer' +import { IsInstance, ValidateNested } from 'class-validator' -export interface CredentialPreviewOptions { - attributes: CredentialPreviewAttribute[] -} +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../../utils/messageType' +import { CredentialPreviewAttribute } from '../../models/CredentialPreviewAttributes' /** * Credential preview inner message class. @@ -46,7 +14,7 @@ export interface CredentialPreviewOptions { * * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#preview-credential */ -export class CredentialPreview { +export class V1CredentialPreview { public constructor(options: CredentialPreviewOptions) { if (options) { this.attributes = options.attributes @@ -54,12 +22,12 @@ export class CredentialPreview { } @Expose({ name: '@type' }) - @Equals(CredentialPreview.type) + @IsValidMessageType(V1CredentialPreview.type) @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { toClassOnly: true, }) - public readonly type = CredentialPreview.type - public static readonly type = 'https://didcomm.org/issue-credential/1.0/credential-preview' + public readonly type = V1CredentialPreview.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/credential-preview') @Type(() => CredentialPreviewAttribute) @ValidateNested({ each: true }) @@ -89,7 +57,7 @@ export class CredentialPreview { }) ) - return new CredentialPreview({ + return new V1CredentialPreview({ attributes, }) } diff --git a/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts b/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts new file mode 100644 index 0000000000..bcf68b4109 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts @@ -0,0 +1,1251 @@ +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { HandlerInboundMessage } from '../../../../agent/Handler' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { ConnectionRecord } from '../../../connections' +import type { CredentialStateChangedEvent } from '../../CredentialEvents' +import type { + ServiceAcceptCredentialOptions, + CredentialOfferTemplate, + CredentialProposeOptions, + CredentialProtocolMsgReturnType, + ServiceAcceptRequestOptions, + ServiceRequestCredentialOptions, + ServiceOfferCredentialOptions, +} from '../../CredentialServiceOptions' +import type { + AcceptProposalOptions, + NegotiateProposalOptions, + OfferCredentialOptions, + ProposeCredentialOptions, + RequestCredentialOptions, +} from '../../CredentialsModuleOptions' +import type { CredentialFormatService } from '../../formats/CredentialFormatService' +import type { HandlerAutoAcceptOptions } from '../../formats/models/CredentialFormatServiceOptions' +import type { CredentialPreviewAttribute } from '../../models/CredentialPreviewAttributes' +import type { CredOffer } from 'indy-sdk' + +import { Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../../../agent/AgentConfig' +import { Dispatcher } from '../../../../agent/Dispatcher' +import { EventEmitter } from '../../../../agent/EventEmitter' +import { ServiceDecorator } from '../../../../decorators/service/ServiceDecorator' +import { AriesFrameworkError } from '../../../../error' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' +import { isLinkedAttachment } from '../../../../utils/attachment' +import { AckStatus } from '../../../common' +import { ConnectionService } from '../../../connections/services' +import { MediationRecipientService } from '../../../routing' +import { AutoAcceptCredential } from '../../CredentialAutoAcceptType' +import { CredentialEventTypes } from '../../CredentialEvents' +import { CredentialProtocolVersion } from '../../CredentialProtocolVersion' +import { CredentialResponseCoordinator } from '../../CredentialResponseCoordinator' +import { CredentialState } from '../../CredentialState' +import { CredentialUtils } from '../../CredentialUtils' +import { CredentialProblemReportError, CredentialProblemReportReason } from '../../errors' +import { IndyCredentialFormatService } from '../../formats/indy/IndyCredentialFormatService' +import { CredentialRepository, CredentialMetadataKeys, CredentialExchangeRecord } from '../../repository' +import { CredentialService, RevocationService } from '../../services' + +import { V1CredentialPreview } from './V1CredentialPreview' +import { + V1CredentialAckHandler, + V1CredentialProblemReportHandler, + V1IssueCredentialHandler, + V1OfferCredentialHandler, + V1ProposeCredentialHandler, + V1RequestCredentialHandler, + V1RevocationNotificationHandler, +} from './handlers' +import { + INDY_CREDENTIAL_ATTACHMENT_ID, + INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, + INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + V1ProposeCredentialMessage, + V1IssueCredentialMessage, + V1RequestCredentialMessage, + V1OfferCredentialMessage, + V1CredentialAckMessage, +} from './messages' + +@scoped(Lifecycle.ContainerScoped) +export class V1CredentialService extends CredentialService { + private connectionService: ConnectionService + private formatService: IndyCredentialFormatService + + public constructor( + connectionService: ConnectionService, + didCommMessageRepository: DidCommMessageRepository, + agentConfig: AgentConfig, + mediationRecipientService: MediationRecipientService, + dispatcher: Dispatcher, + eventEmitter: EventEmitter, + credentialRepository: CredentialRepository, + formatService: IndyCredentialFormatService, + revocationService: RevocationService + ) { + super( + credentialRepository, + eventEmitter, + dispatcher, + agentConfig, + mediationRecipientService, + didCommMessageRepository, + revocationService + ) + this.connectionService = connectionService + this.formatService = formatService + } + + /** + * Create a {@link ProposeCredentialMessage} not bound to an existing credential exchange. + * To create a proposal as response to an existing credential exchange, use {@link createProposalAsResponse}. + * + * @param proposal The object containing config options + * @returns Object containing proposal message and associated credential record + * + */ + public async createProposal( + proposal: ProposeCredentialOptions + ): Promise> { + const connection = await this.connectionService.getById(proposal.connectionId) + connection.assertReady() + + let credentialProposal: V1CredentialPreview | undefined + + const credPropose = proposal.credentialFormats.indy?.payload + + if (proposal.credentialFormats.indy?.attributes) { + credentialProposal = new V1CredentialPreview({ attributes: proposal.credentialFormats.indy?.attributes }) + } + + const config: CredentialProposeOptions = { + credentialProposal: credentialProposal, + credentialDefinitionId: credPropose?.credentialDefinitionId, + linkedAttachments: proposal.credentialFormats.indy?.linkedAttachments, + schemaId: credPropose?.schemaId, + } + + const options = { ...config } + + const { attachment: filtersAttach } = await this.formatService.createProposal(proposal) + + if (!filtersAttach) { + throw new AriesFrameworkError('Missing filters attach in Proposal') + } + options.attachments = [] + options.attachments?.push(filtersAttach) + + // Create message + const message = new V1ProposeCredentialMessage(options ?? {}) + + // Create record + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connection.id, + threadId: message.threadId, + state: CredentialState.ProposalSent, + linkedAttachments: config?.linkedAttachments?.map((linkedAttachment) => linkedAttachment.attachment), + credentialAttributes: message.credentialProposal?.attributes, + autoAcceptCredential: config?.autoAcceptCredential, + protocolVersion: CredentialProtocolVersion.V1, + credentials: [], + }) + + // Set the metadata + credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, { + schemaId: options.schemaId, + credentialDefinitionId: options.credentialDefinitionId, + }) + await this.credentialRepository.save(credentialRecord) + + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + this.eventEmitter.emit({ + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord, + previousState: null, + }, + }) + + return { credentialRecord, message } + } + + /** + * Processing an incoming credential message and create a credential offer as a response + * @param proposal The object containing config options + * @param credentialRecord the credential exchange record for this proposal + * @returns Object containing proposal message and associated credential record + */ + public async acceptProposal( + options: AcceptProposalOptions, + credentialRecord: CredentialExchangeRecord + ): Promise> { + const proposalCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + }) + + if (!proposalCredentialMessage?.credentialProposal) { + throw new AriesFrameworkError( + `Credential record with id ${options.credentialRecordId} is missing required credential proposal` + ) + } + + if (!options.credentialFormats) { + throw new AriesFrameworkError('Missing credential formats in V1 acceptProposal') + } + + const credentialDefinitionId = + options.credentialFormats.indy?.credentialDefinitionId ?? proposalCredentialMessage.credentialDefinitionId + + if (!credentialDefinitionId) { + throw new AriesFrameworkError( + 'Missing required credential definition id. If credential proposal message contains no credential definition id it must be passed to config.' + ) + } + const { message } = await this.createOfferAsResponse(credentialRecord, { + preview: proposalCredentialMessage.credentialProposal, + credentialDefinitionId, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + attachments: credentialRecord.linkedAttachments, + }) + + return { credentialRecord, message } + } + + /** + * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection + * associated with the credential record. + * + * @param credentialOptions configuration for the offer see {@link NegotiateProposalOptions} + * @param credentialRecord the credential exchange record for this proposal + * @returns Credential record associated with the credential offer and the corresponding new offer message + * + */ + public async negotiateProposal( + credentialOptions: NegotiateProposalOptions, + credentialRecord: CredentialExchangeRecord + ): Promise> { + if (!credentialRecord.connectionId) { + throw new AriesFrameworkError( + `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` + ) + } + + const credentialProposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + }) + + if (!credentialProposalMessage?.credentialProposal) { + throw new AriesFrameworkError( + `Credential record with id ${credentialOptions.credentialRecordId} is missing required credential proposal` + ) + } + + const credentialDefinitionId = + credentialOptions.credentialFormats.indy?.credentialDefinitionId ?? + credentialProposalMessage.credentialDefinitionId + + if (!credentialDefinitionId) { + throw new AriesFrameworkError( + 'Missing required credential definition id. If credential proposal message contains no credential definition id it must be passed to config.' + ) + } + + if (!credentialOptions?.credentialFormats.indy?.attributes) { + throw new AriesFrameworkError('No proposal attributes in the negotiation options!') + } + const newCredentialProposal = new V1CredentialPreview({ + attributes: credentialOptions?.credentialFormats.indy?.attributes, + }) + + const { message } = await this.createOfferAsResponse(credentialRecord, { + preview: newCredentialProposal, + credentialDefinitionId, + comment: credentialOptions.comment, + autoAcceptCredential: credentialOptions.autoAcceptCredential, + attachments: credentialRecord.linkedAttachments, + }) + return { credentialRecord, message } + } + /** + * Process a received {@link ProposeCredentialMessage}. This will not accept the credential proposal + * or send a credential offer. It will only create a new, or update the existing credential record with + * the information from the credential proposal message. Use {@link createOfferAsResponse} + * after calling this method to create a credential offer. + * + * @param messageContext The message context containing a credential proposal message + * @returns credential record associated with the credential proposal message + * + */ + public async processProposal( + messageContext: InboundMessageContext + ): Promise { + let credentialRecord: CredentialExchangeRecord + const { message: proposalMessage, connection } = messageContext + + this.logger.debug(`Processing credential proposal with id ${proposalMessage.id}`) + + try { + // Credential record already exists + credentialRecord = await this.getByThreadAndConnectionId(proposalMessage.threadId, connection?.id) + // Assert + credentialRecord.assertState(CredentialState.OfferSent) + + const proposalCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + }) + const offerCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + }) + + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: proposalCredentialMessage ?? undefined, + previousSentMessage: offerCredentialMessage ?? undefined, + }) + + // Update record + await this.updateState(credentialRecord, CredentialState.ProposalReceived) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: proposalMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } catch { + // No credential record exists with thread id + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + credentialAttributes: proposalMessage.credentialProposal?.attributes, + state: CredentialState.ProposalReceived, + protocolVersion: CredentialProtocolVersion.V1, + credentials: [], + }) + + credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, { + schemaId: proposalMessage.schemaId, + credentialDefinitionId: proposalMessage.credentialDefinitionId, + }) + + // Assert + this.connectionService.assertConnectionOrServiceDecorator(messageContext) + + // Save record + await this.credentialRepository.save(credentialRecord) + this.eventEmitter.emit({ + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord, + previousState: null, + }, + }) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: proposalMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } + return credentialRecord + } + + /** + * Create a {@link OfferCredentialMessage} as response to a received credential proposal. + * To create an offer not bound to an existing credential exchange, use {@link createOffer}. + * + * @param credentialRecord The credential record for which to create the credential offer + * @param credentialTemplate The credential template to use for the offer + * @returns Object containing offer message and associated credential record + * + */ + public async createOfferAsResponse( + credentialRecord: CredentialExchangeRecord, + credentialTemplate: CredentialOfferTemplate + ): Promise> { + // Assert + credentialRecord.assertState(CredentialState.ProposalReceived) + + // Create message + const { credentialDefinitionId, comment, preview, attachments } = credentialTemplate + + const options: ServiceOfferCredentialOptions = { + attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + credentialFormats: { + indy: { + credentialDefinitionId, + attributes: preview.attributes, + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + } + + const { attachment: offersAttach } = await this.formatService.createOffer(options) + + if (!offersAttach) { + throw new AriesFrameworkError('No offer attachment for credential') + } + + const credOffer = offersAttach.getDataAsJson() + + if (!offersAttach) { + throw new AriesFrameworkError('Missing offers attach in Offer') + } + + const offerMessage = new V1OfferCredentialMessage({ + comment, + offerAttachments: [offersAttach], + credentialPreview: preview, + attachments, + }) + + offerMessage.setThread({ + threadId: credentialRecord.threadId, + }) + + credentialRecord.credentialAttributes = preview.attributes + credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, { + schemaId: credOffer.schema_id, + credentialDefinitionId: credOffer.cred_def_id, + }) + credentialRecord.linkedAttachments = attachments?.filter((attachment) => isLinkedAttachment(attachment)) + credentialRecord.autoAcceptCredential = + credentialTemplate.autoAcceptCredential ?? credentialRecord.autoAcceptCredential + + await this.updateState(credentialRecord, CredentialState.OfferSent) + + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: offerMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + return { message: offerMessage, credentialRecord } + } + /** + * Process a received {@link RequestCredentialMessage}. This will not accept the credential request + * or send a credential. It will only update the existing credential record with + * the information from the credential request message. Use {@link createCredential} + * after calling this method to create a credential. + * + * @param messageContext The message context containing a credential request message + * @returns credential record associated with the credential request message + * + */ + + public async negotiateOffer( + credentialOptions: ProposeCredentialOptions, + credentialRecord: CredentialExchangeRecord + ): Promise> { + if (!credentialRecord.connectionId) { + throw new AriesFrameworkError( + `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` + ) + } + if (!credentialOptions.credentialFormats.indy?.attributes) { + throw new AriesFrameworkError('Missing attributes in V1 Negotiate Offer Options') + } + const credentialPreview = new V1CredentialPreview({ + attributes: credentialOptions.credentialFormats.indy?.attributes, + }) + const options: CredentialProposeOptions = { + credentialProposal: credentialPreview, + } + + credentialRecord.assertState(CredentialState.OfferReceived) + + // Create message + const message = new V1ProposeCredentialMessage(options ?? {}) + + message.setThread({ threadId: credentialRecord.threadId }) + + // Update record + credentialRecord.credentialAttributes = message.credentialProposal?.attributes + await this.updateState(credentialRecord, CredentialState.ProposalSent) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return { credentialRecord, message } + } + + /** + * Create a {@link OfferCredentialMessage} not bound to an existing credential exchange. + * To create an offer as response to an existing credential exchange, use {@link V1CredentialService#createOfferAsResponse}. + * + * @param credentialOptions The options containing config params for creating the credential offer + * @returns Object containing offer message and associated credential record + * + */ + public async createOffer( + credentialOptions: OfferCredentialOptions + ): Promise> { + if (!credentialOptions.connectionId) { + throw new AriesFrameworkError('Connection id missing from offer credential options') + } + const connection = await this.connectionService.getById(credentialOptions.connectionId) + + if ( + !credentialOptions?.credentialFormats.indy?.attributes || + !credentialOptions?.credentialFormats.indy?.credentialDefinitionId + ) { + throw new AriesFrameworkError('Missing properties from OfferCredentialOptions object: cannot create Offer!') + } + const preview: V1CredentialPreview = new V1CredentialPreview({ + attributes: credentialOptions.credentialFormats.indy?.attributes, + }) + + const linkedAttachments = credentialOptions.credentialFormats.indy?.linkedAttachments + + const template: CredentialOfferTemplate = { + ...credentialOptions, + preview: preview, + credentialDefinitionId: credentialOptions?.credentialFormats.indy?.credentialDefinitionId, + linkedAttachments, + } + + const { credentialRecord, message } = await this.createOfferProcessing(template, connection) + + await this.credentialRepository.save(credentialRecord) + this.eventEmitter.emit({ + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord, + previousState: null, + }, + }) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + return { credentialRecord, message } + } + /** + * Process a received {@link OfferCredentialMessage}. This will not accept the credential offer + * or send a credential request. It will only create a new credential record with + * the information from the credential offer message. Use {@link createRequest} + * after calling this method to create a credential request. + * + * @param messageContext The message context containing a credential request message + * @returns credential record associated with the credential offer message + * + */ + public async processOffer( + messageContext: HandlerInboundMessage + ): Promise { + let credentialRecord: CredentialExchangeRecord + const { message: offerMessage, connection } = messageContext + + this.logger.debug(`Processing credential offer with id ${offerMessage.id}`) + + const indyCredentialOffer = offerMessage.indyCredentialOffer + + if (!indyCredentialOffer) { + throw new CredentialProblemReportError( + `Missing required base64 or json encoded attachment data for credential offer with thread id ${offerMessage.threadId}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + + try { + // Credential record already exists + credentialRecord = await this.getByThreadAndConnectionId(offerMessage.threadId, connection?.id) + + const proposalCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + }) + const offerCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + }) + + // Assert + credentialRecord.assertState(CredentialState.ProposalSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: offerCredentialMessage ?? undefined, + previousSentMessage: proposalCredentialMessage ?? undefined, + }) + + credentialRecord.linkedAttachments = offerMessage.appendedAttachments?.filter(isLinkedAttachment) + + const attachment = offerCredentialMessage + ? offerCredentialMessage.getAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + : undefined + if (attachment) { + await this.formatService.processOffer(attachment, credentialRecord) + } + + credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, { + schemaId: indyCredentialOffer.schema_id, + credentialDefinitionId: indyCredentialOffer.cred_def_id, + }) + + await this.updateState(credentialRecord, CredentialState.OfferReceived) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: offerMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } catch { + // No credential record exists with thread id + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: offerMessage.id, + credentialAttributes: offerMessage.credentialPreview.attributes, + state: CredentialState.OfferReceived, + protocolVersion: CredentialProtocolVersion.V1, + credentials: [], + }) + + credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, { + schemaId: indyCredentialOffer.schema_id, + credentialDefinitionId: indyCredentialOffer.cred_def_id, + }) + // Assert + this.connectionService.assertConnectionOrServiceDecorator(messageContext) + + // Save in repository + await this.credentialRepository.save(credentialRecord) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: offerMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + this.eventEmitter.emit({ + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord, + previousState: null, + }, + }) + } + + return credentialRecord + } + + private async createOfferProcessing( + credentialTemplate: CredentialOfferTemplate, + connectionRecord?: ConnectionRecord + ): Promise> { + // Assert + connectionRecord?.assertReady() + + // Create message + const { credentialDefinitionId, comment, preview, linkedAttachments } = credentialTemplate + + // Create and link credential to attachment + const credentialPreview = linkedAttachments + ? CredentialUtils.createAndLinkAttachmentsToPreview(linkedAttachments, preview) + : preview + + const options: ServiceOfferCredentialOptions = { + attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + credentialFormats: { + indy: { + credentialDefinitionId, + attributes: credentialPreview.attributes, + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + } + + const { attachment: offersAttach } = await this.formatService.createOffer(options) + + if (!offersAttach) { + throw new AriesFrameworkError('Missing offers attach in Offer') + } + + // Construct offer message + const offerMessage = new V1OfferCredentialMessage({ + comment, + offerAttachments: [offersAttach], + credentialPreview, + attachments: linkedAttachments?.map((linkedAttachment) => linkedAttachment.attachment), + }) + + // Create record + const credentialRecord = new CredentialExchangeRecord({ + connectionId: connectionRecord?.id, + threadId: offerMessage.id, + credentialAttributes: credentialPreview.attributes, + linkedAttachments: linkedAttachments?.map((linkedAttachments) => linkedAttachments.attachment), + state: CredentialState.OfferSent, + autoAcceptCredential: credentialTemplate.autoAcceptCredential, + protocolVersion: CredentialProtocolVersion.V1, + credentials: [], + }) + + const offer = offersAttach.getDataAsJson() + credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, { + credentialDefinitionId: credentialDefinitionId, + schemaId: offer.schema_id, + }) + + return { message: offerMessage, credentialRecord } + } + + public async createOutOfBandOffer( + credentialOptions: OfferCredentialOptions + ): Promise> { + if (!credentialOptions.credentialFormats.indy?.credentialDefinitionId) { + throw new AriesFrameworkError('Missing credential definition id for out of band credential') + } + const v1Preview = new V1CredentialPreview({ + attributes: credentialOptions.credentialFormats.indy?.attributes, + }) + const template: CredentialOfferTemplate = { + credentialDefinitionId: credentialOptions.credentialFormats.indy?.credentialDefinitionId, + comment: credentialOptions.comment, + preview: v1Preview, + autoAcceptCredential: credentialOptions.autoAcceptCredential, + } + + const { credentialRecord, message } = await this.createOfferProcessing(template) + + // Create and set ~service decorator + const routing = await this.mediationRecipientService.getRouting() + message.service = new ServiceDecorator({ + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.verkey], + routingKeys: routing.routingKeys, + }) + await this.credentialRepository.save(credentialRecord) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + this.eventEmitter.emit({ + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord, + previousState: null, + }, + }) + return { credentialRecord, message } + } + /** + * Create a {@link RequestCredentialMessage} as response to a received credential offer. + * + * @param record The credential record for which to create the credential request + * @param options Additional configuration to use for the credential request + * @returns Object containing request message and associated credential record + * + */ + public async createRequest( + record: CredentialExchangeRecord, + options: ServiceRequestCredentialOptions, + holderDid: string + ): Promise> { + // Assert credential + record.assertState(CredentialState.OfferReceived) + + const offerCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: record.id, + messageClass: V1OfferCredentialMessage, + }) + + // remove + if (!offerCredentialMessage) { + throw new CredentialProblemReportError(`Missing required credential offer with thread id ${record.threadId}`, { + problemCode: CredentialProblemReportReason.IssuanceAbandoned, + }) + } + + const attachment = offerCredentialMessage.getAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + if (attachment) { + options.offerAttachment = attachment + } else { + throw new AriesFrameworkError(`Missing data payload in attachment in credential Record ${record.id}`) + } + options.attachId = INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID + const { attachment: requestAttach } = await this.formatService.createRequest(options, record, holderDid) + if (!requestAttach) { + throw new AriesFrameworkError(`Failed to create attachment for request; credential record = ${record.id}`) + } + + const requestMessage = new V1RequestCredentialMessage({ + comment: options?.comment, + requestAttachments: [requestAttach], + attachments: offerCredentialMessage?.appendedAttachments?.filter((attachment) => isLinkedAttachment(attachment)), + }) + requestMessage.setThread({ threadId: record.threadId }) + + record.autoAcceptCredential = options?.autoAcceptCredential ?? record.autoAcceptCredential + + record.linkedAttachments = offerCredentialMessage?.appendedAttachments?.filter((attachment) => + isLinkedAttachment(attachment) + ) + await this.updateState(record, CredentialState.RequestSent) + + return { message: requestMessage, credentialRecord: record } + } + /** + * Process a received {@link IssueCredentialMessage}. This will not accept the credential + * or send a credential acknowledgement. It will only update the existing credential record with + * the information from the issue credential message. Use {@link createAck} + * after calling this method to create a credential acknowledgement. + * + * @param messageContext The message context containing an issue credential message + * + * @returns credential record associated with the issue credential message + * + */ + + public async processRequest( + messageContext: InboundMessageContext + ): Promise { + const { message: requestMessage, connection } = messageContext + + this.logger.debug(`Processing credential request with id ${requestMessage.id}`) + + const credentialRecord = await this.getByThreadAndConnectionId(requestMessage.threadId, connection?.id) + + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + }) + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + }) + + // Assert + credentialRecord.assertState(CredentialState.OfferSent) + + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: proposalMessage ?? undefined, + previousSentMessage: offerMessage ?? undefined, + }) + + const requestOptions: RequestCredentialOptions = { + connectionId: messageContext.connection?.id, + } + await this.formatService.processRequest(requestOptions, credentialRecord) + + this.logger.trace('Credential record found when processing credential request', credentialRecord) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: requestMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + await this.updateState(credentialRecord, CredentialState.RequestReceived) + return credentialRecord + } + /** + * Create a {@link IssueCredentialMessage} as response to a received credential request. + * + * @param record The credential record for which to create the credential + * @param options Additional configuration to use for the credential + * @returns Object containing issue credential message and associated credential record + * + */ + public async createCredential( + record: CredentialExchangeRecord, + options: ServiceAcceptRequestOptions + ): Promise> { + // Assert + record.assertState(CredentialState.RequestReceived) + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: record.id, + messageClass: V1OfferCredentialMessage, + }) + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: record.id, + messageClass: V1RequestCredentialMessage, + }) + // Assert offer message + if (!offerMessage) { + throw new AriesFrameworkError( + `Missing credential offer for credential exchange with thread id ${record.threadId}` + ) + } + + if (!requestMessage) { + throw new AriesFrameworkError(`Missing request message in credential Record ${record.id}`) + } + let offerAttachment: Attachment | undefined + + if (offerMessage) { + offerAttachment = offerMessage.getAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + } else { + throw new AriesFrameworkError(`Missing data payload in attachment in credential Record ${record.id}`) + } + const requestAttachment = requestMessage.getAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) + + if (!requestAttachment) { + throw new AriesFrameworkError('Missing requestAttachment in v1 createCredential') + } + options.attachId = INDY_CREDENTIAL_ATTACHMENT_ID + + // Assert credential attributes + const credentialAttributes = record.credentialAttributes + if (!credentialAttributes) { + throw new CredentialProblemReportError( + `Missing required credential attribute values on credential record with id ${record.id}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + + const { attachment: credentialsAttach } = await this.formatService.createCredential( + options, + record, + requestAttachment, + offerAttachment + ) + if (!credentialsAttach) { + throw new AriesFrameworkError(`Failed to create attachment for request; credential record = ${record.id}`) + } + + const issueMessage = new V1IssueCredentialMessage({ + comment: options?.comment, + credentialAttachments: [credentialsAttach], + attachments: + offerMessage?.appendedAttachments?.filter((attachment) => isLinkedAttachment(attachment)) || + requestMessage?.appendedAttachments?.filter((attachment: Attachment) => isLinkedAttachment(attachment)), + }) + issueMessage.setThread({ + threadId: record.threadId, + }) + issueMessage.setPleaseAck() + + record.autoAcceptCredential = options?.autoAcceptCredential ?? record.autoAcceptCredential + + await this.updateState(record, CredentialState.CredentialIssued) + return { message: issueMessage, credentialRecord: record } + } + + /** + * Process an incoming {@link IssueCredentialMessage} + * + * @param messageContext The message context containing a credential acknowledgement message + * @returns credential record associated with the credential acknowledgement message + * + */ + public async processCredential( + messageContext: InboundMessageContext + ): Promise { + const { message: issueMessage, connection } = messageContext + + this.logger.debug(`Processing credential with id ${issueMessage.id}`) + + const credentialRecord = await this.getByThreadAndConnectionId(issueMessage.threadId, connection?.id) + + const requestCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1RequestCredentialMessage, + }) + const offerCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + }) + // Assert + credentialRecord.assertState(CredentialState.RequestSent) + + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: offerCredentialMessage ?? undefined, + previousSentMessage: requestCredentialMessage ?? undefined, + }) + + const credentialRequestMetadata = credentialRecord.metadata.get(CredentialMetadataKeys.IndyRequest) + + if (!credentialRequestMetadata) { + throw new CredentialProblemReportError( + `Missing required request metadata for credential with id ${credentialRecord.id}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + + const indyCredential = issueMessage.indyCredential + if (!indyCredential) { + throw new CredentialProblemReportError( + `Missing required base64 or json encoded attachment data for credential with thread id ${issueMessage.threadId}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + + // get the revocation registry and pass it to the process (store) credential method + const issueAttachment = issueMessage.getAttachmentById(INDY_CREDENTIAL_ATTACHMENT_ID) + if (!issueAttachment) { + throw new AriesFrameworkError('Missing credential attachment in processCredential') + } + const options: ServiceAcceptCredentialOptions = { + credentialAttachment: issueAttachment, + } + + await this.formatService.processCredential(options, credentialRecord) + + await this.updateState(credentialRecord, CredentialState.CredentialReceived) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: issueMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + return credentialRecord + } + /** + * Process a received {@link CredentialAckMessage}. + * + * @param messageContext The message context containing a credential acknowledgement message + * @returns credential record associated with the credential acknowledgement message + * + */ + + /** + * Create a {@link CredentialAckMessage} as response to a received credential. + * + * @param credentialRecord The credential record for which to create the credential acknowledgement + * @returns Object containing credential acknowledgement message and associated credential record + * + */ + public async createAck( + credentialRecord: CredentialExchangeRecord + ): Promise> { + credentialRecord.assertState(CredentialState.CredentialReceived) + + // Create message + const ackMessage = new V1CredentialAckMessage({ + status: AckStatus.OK, + threadId: credentialRecord.threadId, + }) + + await this.updateState(credentialRecord, CredentialState.Done) + + return { message: ackMessage, credentialRecord } + } + public async processAck( + messageContext: InboundMessageContext + ): Promise { + const { message: credentialAckMessage, connection } = messageContext + + this.logger.debug(`Processing credential ack with id ${credentialAckMessage.id}`) + + const credentialRecord = await this.getByThreadAndConnectionId(credentialAckMessage.threadId, connection?.id) + + const requestCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1RequestCredentialMessage, + }) + const issueCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1IssueCredentialMessage, + }) + // Assert + credentialRecord.assertState(CredentialState.CredentialIssued) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: requestCredentialMessage ?? undefined, + previousSentMessage: issueCredentialMessage ?? undefined, + }) + + // Update record + await this.updateState(credentialRecord, CredentialState.Done) + + return credentialRecord + } + + public registerHandlers() { + this.dispatcher.registerHandler( + new V1ProposeCredentialHandler(this, this.agentConfig, this.didCommMessageRepository) + ) + this.dispatcher.registerHandler( + new V1OfferCredentialHandler( + this, + this.agentConfig, + this.mediationRecipientService, + this.didCommMessageRepository + ) + ) + this.dispatcher.registerHandler( + new V1RequestCredentialHandler(this, this.agentConfig, this.didCommMessageRepository) + ) + this.dispatcher.registerHandler(new V1IssueCredentialHandler(this, this.agentConfig, this.didCommMessageRepository)) + this.dispatcher.registerHandler(new V1CredentialAckHandler(this)) + this.dispatcher.registerHandler(new V1CredentialProblemReportHandler(this)) + + this.dispatcher.registerHandler(new V1RevocationNotificationHandler(this.revocationService)) + } + + /** + * + * Get the version of Issue Credentials according to AIP1.0 or AIP2.0 + * @returns the version of this credential service + */ + public getVersion(): CredentialProtocolVersion { + return CredentialProtocolVersion.V1 + } + + /** + * Negotiate a credential offer as holder (by sending a credential proposal message) to the connection + * associated with the credential record. + * + * @param credentialOptions configuration for the offer see {@link NegotiateProposalOptions} + * @param credentialRecord the credential exchange record for this proposal + * @returns Credential record associated with the credential offer and the corresponding new offer message + * + */ + + // AUTO RESPOND METHODS + public shouldAutoRespondToCredential( + credentialRecord: CredentialExchangeRecord, + credentialMessage: V1IssueCredentialMessage + ): boolean { + const formatService: CredentialFormatService = this.getFormatService() + + let credentialAttachment: Attachment | undefined + if (credentialMessage) { + credentialAttachment = credentialMessage.getAttachmentById(INDY_CREDENTIAL_ATTACHMENT_ID) + } + const handlerOptions: HandlerAutoAcceptOptions = { + credentialRecord, + autoAcceptType: this.agentConfig.autoAcceptCredentials, + credentialAttachment, + } + + const shouldAutoReturn = + this.agentConfig.autoAcceptCredentials === AutoAcceptCredential.Always || + credentialRecord.autoAcceptCredential === AutoAcceptCredential.Always || + formatService.shouldAutoRespondToCredential(handlerOptions) + + return shouldAutoReturn + } + + public async shouldAutoRespondToProposal(handlerOptions: HandlerAutoAcceptOptions): Promise { + const autoAccept = CredentialResponseCoordinator.composeAutoAccept( + handlerOptions.credentialRecord.autoAcceptCredential, + handlerOptions.autoAcceptType + ) + + if (autoAccept === AutoAcceptCredential.ContentApproved) { + return ( + this.areProposalValuesValid(handlerOptions.credentialRecord, handlerOptions.messageAttributes) && + this.areProposalAndOfferDefinitionIdEqual(handlerOptions.credentialDefinitionId, handlerOptions.offerAttachment) + ) + } + return false + } + private areProposalValuesValid( + credentialRecord: CredentialExchangeRecord, + proposeMessageAttributes?: CredentialPreviewAttribute[] + ) { + const { credentialAttributes } = credentialRecord + + if (proposeMessageAttributes && credentialAttributes) { + const proposeValues = CredentialUtils.convertAttributesToValues(proposeMessageAttributes) + const defaultValues = CredentialUtils.convertAttributesToValues(credentialAttributes) + if (CredentialUtils.checkValuesMatch(proposeValues, defaultValues)) { + return true + } + } + return false + } + private areProposalAndOfferDefinitionIdEqual(proposalCredentialDefinitionId?: string, offerAttachment?: Attachment) { + let credOffer: CredOffer | undefined + + if (offerAttachment) { + credOffer = offerAttachment.getDataAsJson() + } + const offerCredentialDefinitionId = credOffer?.cred_def_id + return proposalCredentialDefinitionId === offerCredentialDefinitionId + } + public shouldAutoRespondToRequest( + credentialRecord: CredentialExchangeRecord, + requestMessage: V1RequestCredentialMessage, + proposeMessage?: V1ProposeCredentialMessage, + offerMessage?: V1OfferCredentialMessage + ): boolean { + const formatService: CredentialFormatService = this.getFormatService() + + let proposalAttachment, offerAttachment, requestAttachment: Attachment | undefined + + if (offerMessage) { + offerAttachment = offerMessage.getAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + } + if (requestMessage) { + requestAttachment = requestMessage.getAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) + } + const handlerOptions: HandlerAutoAcceptOptions = { + credentialRecord, + autoAcceptType: this.agentConfig.autoAcceptCredentials, + proposalAttachment, + offerAttachment, + requestAttachment, + } + const shouldAutoReturn = + this.agentConfig.autoAcceptCredentials === AutoAcceptCredential.Always || + credentialRecord.autoAcceptCredential === AutoAcceptCredential.Always || + formatService.shouldAutoRespondToRequest(handlerOptions) + + return shouldAutoReturn + } + + public shouldAutoRespondToOffer( + credentialRecord: CredentialExchangeRecord, + offerMessage: V1OfferCredentialMessage, + proposeMessage?: V1ProposeCredentialMessage + ): boolean { + const formatService: CredentialFormatService = this.getFormatService() + let proposalAttachment: Attachment | undefined + + const offerAttachment = offerMessage.getAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + if (proposeMessage && proposeMessage.appendedAttachments) { + proposalAttachment = proposeMessage.getAttachment() + } + const offerValues = offerMessage.credentialPreview?.attributes + + const handlerOptions: HandlerAutoAcceptOptions = { + credentialRecord, + autoAcceptType: this.agentConfig.autoAcceptCredentials, + messageAttributes: offerValues, + proposalAttachment, + offerAttachment, + } + const shouldAutoReturn = + this.agentConfig.autoAcceptCredentials === AutoAcceptCredential.Always || + credentialRecord.autoAcceptCredential === AutoAcceptCredential.Always || + formatService.shouldAutoRespondToProposal(handlerOptions) + + return shouldAutoReturn + } + + // REPOSITORY METHODS + + public async getOfferMessage(id: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: id, + messageClass: V1OfferCredentialMessage, + }) + } + + public async getRequestMessage(id: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: id, + messageClass: V1RequestCredentialMessage, + }) + } + + public async getCredentialMessage(id: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: id, + messageClass: V1IssueCredentialMessage, + }) + } + + public getFormats(): CredentialFormatService[] { + throw new Error('Method not implemented.') + } + + public getFormatService(): CredentialFormatService { + return this.formatService + } +} diff --git a/packages/core/src/modules/credentials/protocol/v1/handlers/V1CredentialAckHandler.ts b/packages/core/src/modules/credentials/protocol/v1/handlers/V1CredentialAckHandler.ts new file mode 100644 index 0000000000..9bcd934ad9 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/handlers/V1CredentialAckHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { V1CredentialService } from '../V1CredentialService' + +import { V1CredentialAckMessage } from '../messages' + +export class V1CredentialAckHandler implements Handler { + private credentialService: V1CredentialService + public supportedMessages = [V1CredentialAckMessage] + + public constructor(credentialService: V1CredentialService) { + this.credentialService = credentialService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.credentialService.processAck(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v1/handlers/V1CredentialProblemReportHandler.ts b/packages/core/src/modules/credentials/protocol/v1/handlers/V1CredentialProblemReportHandler.ts new file mode 100644 index 0000000000..184be10163 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/handlers/V1CredentialProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { V1CredentialService } from '../V1CredentialService' + +import { V1CredentialProblemReportMessage } from '../messages' + +export class V1CredentialProblemReportHandler implements Handler { + private credentialService: V1CredentialService + public supportedMessages = [V1CredentialProblemReportMessage] + + public constructor(credentialService: V1CredentialService) { + this.credentialService = credentialService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.credentialService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v1/handlers/V1IssueCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v1/handlers/V1IssueCredentialHandler.ts new file mode 100644 index 0000000000..d5475e3a8b --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/handlers/V1IssueCredentialHandler.ts @@ -0,0 +1,70 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V1CredentialService } from '../V1CredentialService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { V1IssueCredentialMessage, V1RequestCredentialMessage } from '../messages' + +export class V1IssueCredentialHandler implements Handler { + private credentialService: V1CredentialService + private agentConfig: AgentConfig + private didCommMessageRepository: DidCommMessageRepository + public supportedMessages = [V1IssueCredentialMessage] + + public constructor( + credentialService: V1CredentialService, + agentConfig: AgentConfig, + didCommMessageRepository: DidCommMessageRepository + ) { + this.credentialService = credentialService + this.agentConfig = agentConfig + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const credentialRecord = await this.credentialService.processCredential(messageContext) + const credentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1IssueCredentialMessage, + }) + if (!credentialMessage) { + throw new AriesFrameworkError('Missing credential message in V2RequestCredentialHandler') + } + if (this.credentialService.shouldAutoRespondToCredential(credentialRecord, credentialMessage)) { + return await this.createAck(credentialRecord, credentialMessage, messageContext) + } + } + + private async createAck( + record: CredentialExchangeRecord, + credentialMessage: V1IssueCredentialMessage | null, + messageContext: HandlerInboundMessage + ) { + this.agentConfig.logger.info( + `Automatically sending acknowledgement with autoAccept on ${this.agentConfig.autoAcceptCredentials}` + ) + const { message, credentialRecord } = await this.credentialService.createAck(record) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1RequestCredentialMessage, + }) + if (messageContext.connection) { + return createOutboundMessage(messageContext.connection, message) + } else if (credentialMessage?.service && requestMessage?.service) { + const recipientService = credentialMessage.service + const ourService = requestMessage.service + + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + + this.agentConfig.logger.error(`Could not automatically create credential ack`) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v1/handlers/V1OfferCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v1/handlers/V1OfferCredentialHandler.ts new file mode 100644 index 0000000000..a19b60a03f --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/handlers/V1OfferCredentialHandler.ts @@ -0,0 +1,111 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { MediationRecipientService } from '../../../../routing/services/MediationRecipientService' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V1CredentialService } from '../V1CredentialService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { ServiceDecorator } from '../../../../../decorators/service/ServiceDecorator' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { DidCommMessageRole } from '../../../../../storage' +import { V1OfferCredentialMessage, V1ProposeCredentialMessage } from '../messages' + +export class V1OfferCredentialHandler implements Handler { + private credentialService: V1CredentialService + private agentConfig: AgentConfig + private mediationRecipientService: MediationRecipientService + private didCommMessageRepository: DidCommMessageRepository + public supportedMessages = [V1OfferCredentialMessage] + + public constructor( + credentialService: V1CredentialService, + agentConfig: AgentConfig, + mediationRecipientService: MediationRecipientService, + didCommMessageRepository: DidCommMessageRepository + ) { + this.credentialService = credentialService + this.agentConfig = agentConfig + this.mediationRecipientService = mediationRecipientService + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const credentialRecord = await this.credentialService.processOffer(messageContext) + + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + }) + if (!offerMessage) { + throw new AriesFrameworkError('Missing offerMessage in V1OfferCredentialHandler') + } + const proposeMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + }) + + const shouldAutoRespond = this.credentialService.shouldAutoRespondToOffer( + credentialRecord, + offerMessage, + proposeMessage ?? undefined + ) + if (shouldAutoRespond) { + return await this.createRequest(credentialRecord, messageContext, offerMessage) + } + } + + private async createRequest( + record: CredentialExchangeRecord, + messageContext: HandlerInboundMessage, + offerMessage?: V1OfferCredentialMessage + ) { + this.agentConfig.logger.info( + `Automatically sending request with autoAccept on ${this.agentConfig.autoAcceptCredentials}` + ) + if (messageContext.connection) { + const { message, credentialRecord } = await this.credentialService.createRequest( + record, + {}, + messageContext.connection.did + ) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return createOutboundMessage(messageContext.connection, message) + } else if (offerMessage?.service) { + const routing = await this.mediationRecipientService.getRouting() + const ourService = new ServiceDecorator({ + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.verkey], + routingKeys: routing.routingKeys, + }) + const recipientService = offerMessage.service + + const { message, credentialRecord } = await this.credentialService.createRequest( + record, + {}, + ourService.recipientKeys[0] + ) + + // Set and save ~service decorator to record (to remember our verkey) + message.service = ourService + await this.credentialService.update(credentialRecord) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + + this.agentConfig.logger.error(`Could not automatically create credential request`) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v1/handlers/V1ProposeCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v1/handlers/V1ProposeCredentialHandler.ts new file mode 100644 index 0000000000..3ecc0763f2 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/handlers/V1ProposeCredentialHandler.ts @@ -0,0 +1,105 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { Attachment } from '../../../../../decorators/attachment/Attachment' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { HandlerAutoAcceptOptions } from '../../../formats/models/CredentialFormatServiceOptions' +import type { CredentialPreviewAttribute } from '../../../models/CredentialPreviewAttributes' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V1CredentialService } from '../V1CredentialService' + +import { createOutboundMessage } from '../../../../../agent/helpers' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { AutoAcceptCredential } from '../../../CredentialAutoAcceptType' +import { V1OfferCredentialMessage, V1ProposeCredentialMessage } from '../messages' + +export class V1ProposeCredentialHandler implements Handler { + private credentialService: V1CredentialService + private agentConfig: AgentConfig + private didCommMessageRepository: DidCommMessageRepository + public supportedMessages = [V1ProposeCredentialMessage] + + public constructor( + credentialService: V1CredentialService, + agentConfig: AgentConfig, + didCommMessageRepository: DidCommMessageRepository + ) { + this.credentialService = credentialService + this.agentConfig = agentConfig + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const credentialRecord = await this.credentialService.processProposal(messageContext) + + // note that these two messages can be present (or not) and there is no + // guarantee which one is present so we need two try-catch blocks + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + }) + + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + }) + + let proposalValues: CredentialPreviewAttribute[] | undefined + + if (!proposalMessage || !proposalMessage.credentialProposal || !proposalMessage.credentialProposal.attributes) { + throw new AriesFrameworkError('Missing attributes in proposal message') + } + let proposalAttachment, offerAttachment: Attachment | undefined + if (proposalMessage) { + proposalValues = proposalMessage.credentialProposal.attributes + } + if (offerMessage) { + offerAttachment = offerMessage.getAttachmentById('indy') + } + const handlerOptions: HandlerAutoAcceptOptions = { + credentialRecord, + autoAcceptType: this.agentConfig.autoAcceptCredentials, + messageAttributes: proposalValues, + proposalAttachment, + offerAttachment, + credentialDefinitionId: proposalMessage.credentialDefinitionId, + } + if ( + this.agentConfig.autoAcceptCredentials === AutoAcceptCredential.Always || + credentialRecord.autoAcceptCredential === AutoAcceptCredential.Always || + (await this.credentialService.shouldAutoRespondToProposal(handlerOptions)) + ) { + return await this.createOffer(credentialRecord, messageContext, proposalMessage) + } + } + private async createOffer( + credentialRecord: CredentialExchangeRecord, + messageContext: HandlerInboundMessage, + proposalMessage?: V1ProposeCredentialMessage + ) { + this.agentConfig.logger.info( + `Automatically sending offer with autoAccept on ${this.agentConfig.autoAcceptCredentials}` + ) + + if (!messageContext.connection) { + this.agentConfig.logger.error('No connection on the messageContext, aborting auto accept') + return + } + if (!proposalMessage?.credentialProposal) { + this.agentConfig.logger.error( + `Proposal message with id ${credentialRecord.id} is missing required credential proposal` + ) + return + } + + if (!proposalMessage.credentialDefinitionId) { + this.agentConfig.logger.error('Missing required credential definition id') + return + } + + const { message } = await this.credentialService.createOfferAsResponse(credentialRecord, { + credentialDefinitionId: proposalMessage.credentialDefinitionId, + preview: proposalMessage.credentialProposal, + }) + return createOutboundMessage(messageContext.connection, message) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v1/handlers/V1RequestCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v1/handlers/V1RequestCredentialHandler.ts new file mode 100644 index 0000000000..fe2a8dc623 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/handlers/V1RequestCredentialHandler.ts @@ -0,0 +1,130 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { Attachment } from '../../../../../decorators/attachment/Attachment' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { ServiceAcceptRequestOptions } from '../../../CredentialServiceOptions' +import type { CredentialFormatService } from '../../../formats/CredentialFormatService' +import type { HandlerAutoAcceptOptions } from '../../../formats/models/CredentialFormatServiceOptions' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V1CredentialService } from '../V1CredentialService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { DidCommMessageRole } from '../../../../../storage' +import { AutoAcceptCredential } from '../../../CredentialAutoAcceptType' +import { + INDY_CREDENTIAL_ATTACHMENT_ID, + V1ProposeCredentialMessage, + V1RequestCredentialMessage, + V1OfferCredentialMessage, + INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, + INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, +} from '../messages' + +export class V1RequestCredentialHandler implements Handler { + private agentConfig: AgentConfig + private credentialService: V1CredentialService + private didCommMessageRepository: DidCommMessageRepository + public supportedMessages = [V1RequestCredentialMessage] + + public constructor( + credentialService: V1CredentialService, + agentConfig: AgentConfig, + didCommMessageRepository: DidCommMessageRepository + ) { + this.credentialService = credentialService + this.agentConfig = agentConfig + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const credentialRecord = await this.credentialService.processRequest(messageContext) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1RequestCredentialMessage, + }) + + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1OfferCredentialMessage, + }) + + const proposeMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V1ProposeCredentialMessage, + }) + + const formatService: CredentialFormatService = this.credentialService.getFormatService() + + let proposalAttachment, offerAttachment, requestAttachment: Attachment | undefined + if (proposeMessage && proposeMessage.appendedAttachments) { + proposalAttachment = proposeMessage.appendedAttachments[0] + } + if (offerMessage) { + offerAttachment = offerMessage.getAttachmentById(INDY_CREDENTIAL_OFFER_ATTACHMENT_ID) + } + if (requestMessage) { + requestAttachment = requestMessage.getAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) + } + const handlerOptions: HandlerAutoAcceptOptions = { + credentialRecord, + autoAcceptType: this.agentConfig.autoAcceptCredentials, + proposalAttachment, + offerAttachment, + requestAttachment, + } + if ( + this.agentConfig.autoAcceptCredentials === AutoAcceptCredential.Always || + credentialRecord.autoAcceptCredential === AutoAcceptCredential.Always || + formatService.shouldAutoRespondToRequest(handlerOptions) + ) { + return await this.createCredential(credentialRecord, messageContext, offerMessage, requestMessage) + } + } + + private async createCredential( + record: CredentialExchangeRecord, + messageContext: HandlerInboundMessage, + offerMessage?: V1OfferCredentialMessage | null, + requestMessage?: V1RequestCredentialMessage | null + ) { + this.agentConfig.logger.info( + `Automatically sending credential with autoAccept on ${this.agentConfig.autoAcceptCredentials}` + ) + + const options: ServiceAcceptRequestOptions = { + attachId: INDY_CREDENTIAL_ATTACHMENT_ID, + credentialRecordId: record.id, + comment: 'V1 Indy Credential', + } + const { message, credentialRecord } = await this.credentialService.createCredential(record, options) + + if (messageContext.connection) { + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + return createOutboundMessage(messageContext.connection, message) + } else if (requestMessage?.service && offerMessage?.service) { + const recipientService = requestMessage.service + const ourService = offerMessage.service + + // Set ~service, update message in record (for later use) + message.setService(ourService) + + await this.credentialService.update(credentialRecord) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + this.agentConfig.logger.error(`Could not automatically create credential request`) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v1/handlers/V1RevocationNotificationHandler.ts b/packages/core/src/modules/credentials/protocol/v1/handlers/V1RevocationNotificationHandler.ts new file mode 100644 index 0000000000..263ccf4976 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/handlers/V1RevocationNotificationHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { RevocationService } from '../../../services' + +import { V1RevocationNotificationMessage } from '../messages/V1RevocationNotificationMessage' + +export class V1RevocationNotificationHandler implements Handler { + private revocationService: RevocationService + public supportedMessages = [V1RevocationNotificationMessage] + + public constructor(revocationService: RevocationService) { + this.revocationService = revocationService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.revocationService.v1ProcessRevocationNotification(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v1/handlers/index.ts b/packages/core/src/modules/credentials/protocol/v1/handlers/index.ts new file mode 100644 index 0000000000..bd6a99e42c --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/handlers/index.ts @@ -0,0 +1,7 @@ +export * from './V1CredentialAckHandler' +export * from './V1IssueCredentialHandler' +export * from './V1OfferCredentialHandler' +export * from './V1ProposeCredentialHandler' +export * from './V1RequestCredentialHandler' +export * from './V1CredentialProblemReportHandler' +export * from './V1RevocationNotificationHandler' diff --git a/packages/core/src/modules/credentials/protocol/v1/messages/V1CredentialAckMessage.ts b/packages/core/src/modules/credentials/protocol/v1/messages/V1CredentialAckMessage.ts new file mode 100644 index 0000000000..71ac6b1432 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/messages/V1CredentialAckMessage.ts @@ -0,0 +1,23 @@ +import type { AckMessageOptions } from '../../../../common' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { AckMessage } from '../../../../common' + +export type CredentialAckMessageOptions = AckMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks + */ +export class V1CredentialAckMessage extends AckMessage { + /** + * Create new CredentialAckMessage instance. + * @param options + */ + public constructor(options: CredentialAckMessageOptions) { + super(options) + } + + @IsValidMessageType(V1CredentialAckMessage.type) + public readonly type = V1CredentialAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/ack') +} diff --git a/packages/core/src/modules/credentials/protocol/v1/messages/V1CredentialProblemReportMessage.ts b/packages/core/src/modules/credentials/protocol/v1/messages/V1CredentialProblemReportMessage.ts new file mode 100644 index 0000000000..316d9487a6 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/messages/V1CredentialProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '../../../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { ProblemReportMessage } from '../../../../problem-reports/messages/ProblemReportMessage' + +export type CredentialProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V1CredentialProblemReportMessage extends ProblemReportMessage { + /** + * Create new CredentialProblemReportMessage instance. + * @param options + */ + public constructor(options: CredentialProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(V1CredentialProblemReportMessage.type) + public readonly type = V1CredentialProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/problem-report') +} diff --git a/packages/core/src/modules/credentials/messages/IssueCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v1/messages/V1IssueCredentialMessage.ts similarity index 54% rename from packages/core/src/modules/credentials/messages/IssueCredentialMessage.ts rename to packages/core/src/modules/credentials/protocol/v1/messages/V1IssueCredentialMessage.ts index aab5a37e84..4a6aa2158c 100644 --- a/packages/core/src/modules/credentials/messages/IssueCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v1/messages/V1IssueCredentialMessage.ts @@ -1,11 +1,11 @@ import type { Cred } from 'indy-sdk' import { Expose, Type } from 'class-transformer' -import { Equals, IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' -import { AgentMessage } from '../../../agent/AgentMessage' -import { Attachment } from '../../../decorators/attachment/Attachment' -import { JsonEncoder } from '../../../utils/JsonEncoder' +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' export const INDY_CREDENTIAL_ATTACHMENT_ID = 'libindy-cred-0' @@ -16,7 +16,7 @@ interface IssueCredentialMessageOptions { attachments?: Attachment[] } -export class IssueCredentialMessage extends AgentMessage { +export class V1IssueCredentialMessage extends AgentMessage { public constructor(options: IssueCredentialMessageOptions) { super() @@ -24,13 +24,13 @@ export class IssueCredentialMessage extends AgentMessage { this.id = options.id ?? this.generateId() this.comment = options.comment this.credentialAttachments = options.credentialAttachments - this.attachments = options.attachments + this.appendedAttachments = options.attachments } } - @Equals(IssueCredentialMessage.type) - public readonly type = IssueCredentialMessage.type - public static readonly type = 'https://didcomm.org/issue-credential/1.0/issue-credential' + @IsValidMessageType(V1IssueCredentialMessage.type) + public readonly type = V1IssueCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/issue-credential') @IsString() @IsOptional() @@ -48,14 +48,13 @@ export class IssueCredentialMessage extends AgentMessage { public get indyCredential(): Cred | null { const attachment = this.credentialAttachments.find((attachment) => attachment.id === INDY_CREDENTIAL_ATTACHMENT_ID) - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null - } - // Extract credential from attachment - const credentialJson = JsonEncoder.fromBase64(attachment.data.base64) + const credentialJson = attachment?.getDataAsJson() ?? null return credentialJson } + + public getAttachmentById(id: string): Attachment | undefined { + return this.credentialAttachments?.find((attachment) => attachment.id == id) + } } diff --git a/packages/core/src/modules/credentials/protocol/v1/messages/V1OfferCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v1/messages/V1OfferCredentialMessage.ts new file mode 100644 index 0000000000..7a04f72094 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/messages/V1OfferCredentialMessage.ts @@ -0,0 +1,76 @@ +import type { CredOffer } from 'indy-sdk' + +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { V1CredentialPreview } from '../V1CredentialPreview' + +export const INDY_CREDENTIAL_OFFER_ATTACHMENT_ID = 'libindy-cred-offer-0' + +export interface OfferCredentialMessageOptions { + id?: string + comment?: string + offerAttachments: Attachment[] + credentialPreview: V1CredentialPreview + attachments?: Attachment[] +} + +/** + * Message part of Issue Credential Protocol used to continue or initiate credential exchange by issuer. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#offer-credential + */ +export class V1OfferCredentialMessage extends AgentMessage { + public constructor(options: OfferCredentialMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.comment = options.comment + this.credentialPreview = options.credentialPreview + this.messageAttachment = options.offerAttachments + this.appendedAttachments = options.attachments + } + } + + @IsValidMessageType(V1OfferCredentialMessage.type) + public readonly type = V1OfferCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/offer-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'credential_preview' }) + @Type(() => V1CredentialPreview) + @ValidateNested() + @IsInstance(V1CredentialPreview) + public credentialPreview!: V1CredentialPreview + + @Expose({ name: 'offers~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public messageAttachment!: Attachment[] + + public get indyCredentialOffer(): CredOffer | null { + const attachment = this.messageAttachment.find( + (attachment) => attachment.id === INDY_CREDENTIAL_OFFER_ATTACHMENT_ID + ) + + // Extract credential offer from attachment + const credentialOfferJson = attachment?.getDataAsJson() ?? null + + return credentialOfferJson + } + + public getAttachmentById(id: string): Attachment | undefined { + return this.messageAttachment?.find((attachment) => attachment.id == id) + } +} diff --git a/packages/core/src/modules/credentials/messages/ProposeCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v1/messages/V1ProposeCredentialMessage.ts similarity index 64% rename from packages/core/src/modules/credentials/messages/ProposeCredentialMessage.ts rename to packages/core/src/modules/credentials/protocol/v1/messages/V1ProposeCredentialMessage.ts index ab7043b971..81b71cc5a5 100644 --- a/packages/core/src/modules/credentials/messages/ProposeCredentialMessage.ts +++ b/packages/core/src/modules/credentials/protocol/v1/messages/V1ProposeCredentialMessage.ts @@ -1,16 +1,17 @@ -import type { Attachment } from '../../../decorators/attachment/Attachment' +import type { Attachment } from '../../../../../decorators/attachment/Attachment' import { Expose, Type } from 'class-transformer' -import { Equals, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsInstance, IsOptional, IsString, Matches, ValidateNested } from 'class-validator' -import { AgentMessage } from '../../../agent/AgentMessage' - -import { CredentialPreview } from './CredentialPreview' +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { indyDidRegex, schemaIdRegex, schemaVersionRegex, credDefIdRegex } from '../../../../../utils' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { V1CredentialPreview } from '../V1CredentialPreview' export interface ProposeCredentialMessageOptions { id?: string comment?: string - credentialProposal?: CredentialPreview + credentialProposal?: V1CredentialPreview schemaIssuerDid?: string schemaId?: string schemaName?: string @@ -25,7 +26,7 @@ export interface ProposeCredentialMessageOptions { * * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md#propose-credential */ -export class ProposeCredentialMessage extends AgentMessage { +export class V1ProposeCredentialMessage extends AgentMessage { public constructor(options: ProposeCredentialMessageOptions) { super() @@ -39,13 +40,13 @@ export class ProposeCredentialMessage extends AgentMessage { this.schemaVersion = options.schemaVersion this.credentialDefinitionId = options.credentialDefinitionId this.issuerDid = options.issuerDid - this.attachments = options.attachments + this.appendedAttachments = options.attachments } } - @Equals(ProposeCredentialMessage.type) - public readonly type = ProposeCredentialMessage.type - public static readonly type = 'https://didcomm.org/issue-credential/1.0/propose-credential' + @IsValidMessageType(V1ProposeCredentialMessage.type) + public readonly type = V1ProposeCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/propose-credential') /** * Human readable information about this Credential Proposal, @@ -59,11 +60,11 @@ export class ProposeCredentialMessage extends AgentMessage { * Represents the credential data that Prover wants to receive. */ @Expose({ name: 'credential_proposal' }) - @Type(() => CredentialPreview) + @Type(() => V1CredentialPreview) @ValidateNested() @IsOptional() - @IsInstance(CredentialPreview) - public credentialProposal?: CredentialPreview + @IsInstance(V1CredentialPreview) + public credentialProposal?: V1CredentialPreview /** * Filter to request credential based on a particular Schema issuer DID. @@ -71,6 +72,7 @@ export class ProposeCredentialMessage extends AgentMessage { @Expose({ name: 'schema_issuer_did' }) @IsString() @IsOptional() + @Matches(indyDidRegex) public schemaIssuerDid?: string /** @@ -79,6 +81,7 @@ export class ProposeCredentialMessage extends AgentMessage { @Expose({ name: 'schema_id' }) @IsString() @IsOptional() + @Matches(schemaIdRegex) public schemaId?: string /** @@ -95,6 +98,9 @@ export class ProposeCredentialMessage extends AgentMessage { @Expose({ name: 'schema_version' }) @IsString() @IsOptional() + @Matches(schemaVersionRegex, { + message: 'Version must be X.X or X.X.X', + }) public schemaVersion?: string /** @@ -103,6 +109,7 @@ export class ProposeCredentialMessage extends AgentMessage { @Expose({ name: 'cred_def_id' }) @IsString() @IsOptional() + @Matches(credDefIdRegex) public credentialDefinitionId?: string /** @@ -111,5 +118,14 @@ export class ProposeCredentialMessage extends AgentMessage { @Expose({ name: 'issuer_did' }) @IsString() @IsOptional() + @Matches(indyDidRegex) public issuerDid?: string + + public getAttachment(): Attachment | undefined { + if (this.appendedAttachments) { + return this.appendedAttachments[0] + } else { + return undefined + } + } } diff --git a/packages/core/src/modules/credentials/protocol/v1/messages/V1RequestCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v1/messages/V1RequestCredentialMessage.ts new file mode 100644 index 0000000000..d43207ae13 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/messages/V1RequestCredentialMessage.ts @@ -0,0 +1,61 @@ +import type { CredReq } from 'indy-sdk' + +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export const INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID = 'libindy-cred-request-0' + +interface RequestCredentialMessageOptions { + id?: string + comment?: string + requestAttachments: Attachment[] + attachments?: Attachment[] +} + +export class V1RequestCredentialMessage extends AgentMessage { + public constructor(options: RequestCredentialMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.comment = options.comment + this.messageAttachment = options.requestAttachments + this.appendedAttachments = options.attachments + } + } + + @IsValidMessageType(V1RequestCredentialMessage.type) + public readonly type = V1RequestCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/1.0/request-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'requests~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public messageAttachment!: Attachment[] + + public get indyCredentialRequest(): CredReq | null { + const attachment = this.messageAttachment.find( + (attachment) => attachment.id === INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID + ) + // Extract proof request from attachment + const credentialReqJson = attachment?.getDataAsJson() ?? null + + return credentialReqJson + } + + public getAttachmentById(id: string): Attachment | undefined { + return this.messageAttachment?.find((attachment) => attachment.id === id) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v1/messages/V1RevocationNotificationMessage.ts b/packages/core/src/modules/credentials/protocol/v1/messages/V1RevocationNotificationMessage.ts new file mode 100644 index 0000000000..c0af69539d --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/messages/V1RevocationNotificationMessage.ts @@ -0,0 +1,38 @@ +import type { AckDecorator } from '../../../../../decorators/ack/AckDecorator' + +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface RevocationNotificationMessageV1Options { + issueThread: string + id?: string + comment?: string + pleaseAck?: AckDecorator +} + +export class V1RevocationNotificationMessage extends AgentMessage { + public constructor(options: RevocationNotificationMessageV1Options) { + super() + if (options) { + this.issueThread = options.issueThread + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.pleaseAck = options.pleaseAck + } + } + + @IsValidMessageType(V1RevocationNotificationMessage.type) + public readonly type = V1RevocationNotificationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/revocation_notification/1.0/revoke') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'thread_id' }) + @IsString() + public issueThread!: string +} diff --git a/packages/core/src/modules/credentials/protocol/v1/messages/index.ts b/packages/core/src/modules/credentials/protocol/v1/messages/index.ts new file mode 100644 index 0000000000..4b39feb0b1 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/messages/index.ts @@ -0,0 +1,7 @@ +export * from './V1CredentialAckMessage' +export * from '../V1CredentialPreview' +export * from './V1RequestCredentialMessage' +export * from './V1IssueCredentialMessage' +export * from './V1OfferCredentialMessage' +export * from './V1ProposeCredentialMessage' +export * from './V1CredentialProblemReportMessage' diff --git a/packages/core/src/modules/credentials/models/Credential.ts b/packages/core/src/modules/credentials/protocol/v1/models/Credential.ts similarity index 92% rename from packages/core/src/modules/credentials/models/Credential.ts rename to packages/core/src/modules/credentials/protocol/v1/models/Credential.ts index 75dbc9ff87..7b0046d7bb 100644 --- a/packages/core/src/modules/credentials/models/Credential.ts +++ b/packages/core/src/modules/credentials/protocol/v1/models/Credential.ts @@ -3,7 +3,7 @@ import type { IndyCredential } from 'indy-sdk' import { Expose, Type } from 'class-transformer' import { IsInstance, IsOptional, ValidateNested } from 'class-validator' -import { JsonTransformer } from '../../../utils/JsonTransformer' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' import { IndyCredentialInfo } from './IndyCredentialInfo' import { RevocationInterval } from './RevocationInterval' diff --git a/packages/core/src/modules/credentials/models/CredentialInfo.ts b/packages/core/src/modules/credentials/protocol/v1/models/CredentialInfo.ts similarity index 87% rename from packages/core/src/modules/credentials/models/CredentialInfo.ts rename to packages/core/src/modules/credentials/protocol/v1/models/CredentialInfo.ts index 98c69f1836..b0e40666e7 100644 --- a/packages/core/src/modules/credentials/models/CredentialInfo.ts +++ b/packages/core/src/modules/credentials/protocol/v1/models/CredentialInfo.ts @@ -1,4 +1,4 @@ -import type { Attachment } from '../../../decorators/attachment/Attachment' +import type { Attachment } from '../../../../../decorators/attachment/Attachment' export interface CredentialInfoOptions { metadata?: IndyCredentialMetadata | null diff --git a/packages/core/src/modules/credentials/models/IndyCredentialInfo.ts b/packages/core/src/modules/credentials/protocol/v1/models/IndyCredentialInfo.ts similarity index 94% rename from packages/core/src/modules/credentials/models/IndyCredentialInfo.ts rename to packages/core/src/modules/credentials/protocol/v1/models/IndyCredentialInfo.ts index e1c2d21d61..cf8b594ebe 100644 --- a/packages/core/src/modules/credentials/models/IndyCredentialInfo.ts +++ b/packages/core/src/modules/credentials/protocol/v1/models/IndyCredentialInfo.ts @@ -3,7 +3,7 @@ import type { IndyCredentialInfo as IndySDKCredentialInfo } from 'indy-sdk' import { Expose } from 'class-transformer' import { IsOptional, IsString } from 'class-validator' -import { JsonTransformer } from '../../../utils/JsonTransformer' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' export class IndyCredentialInfo { public constructor(options: IndyCredentialInfo) { diff --git a/packages/core/src/modules/credentials/models/RevocationInterval.ts b/packages/core/src/modules/credentials/protocol/v1/models/RevocationInterval.ts similarity index 100% rename from packages/core/src/modules/credentials/models/RevocationInterval.ts rename to packages/core/src/modules/credentials/protocol/v1/models/RevocationInterval.ts diff --git a/packages/core/src/modules/credentials/protocol/v1/models/index.ts b/packages/core/src/modules/credentials/protocol/v1/models/index.ts new file mode 100644 index 0000000000..cae218929d --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v1/models/index.ts @@ -0,0 +1,3 @@ +export * from './Credential' +export * from './IndyCredentialInfo' +export * from './RevocationInterval' diff --git a/packages/core/src/modules/credentials/protocol/v2/CredentialMessageBuilder.ts b/packages/core/src/modules/credentials/protocol/v2/CredentialMessageBuilder.ts new file mode 100644 index 0000000000..1dce4cb9b8 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/CredentialMessageBuilder.ts @@ -0,0 +1,350 @@ +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { + CredentialProtocolMsgReturnType, + ServiceAcceptRequestOptions, + ServiceOfferCredentialOptions, + ServiceRequestCredentialOptions, +} from '../../CredentialServiceOptions' +import type { ProposeCredentialOptions } from '../../CredentialsModuleOptions' +import type { CredentialFormatService } from '../../formats/CredentialFormatService' +import type { CredentialFormatSpec } from '../../formats/models/CredentialFormatServiceOptions' +import type { CredentialExchangeRecordProps } from '../../repository/CredentialExchangeRecord' +import type { V2IssueCredentialMessageProps } from './messages/V2IssueCredentialMessage' +import type { V2OfferCredentialMessageOptions } from './messages/V2OfferCredentialMessage' +import type { V2ProposeCredentialMessageProps } from './messages/V2ProposeCredentialMessage' +import type { V2RequestCredentialMessageOptions } from './messages/V2RequestCredentialMessage' + +import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' +import { uuid } from '../../../../utils/uuid' +import { CredentialProtocolVersion } from '../../CredentialProtocolVersion' +import { CredentialState } from '../../CredentialState' +import { CredentialExchangeRecord } from '../../repository/CredentialExchangeRecord' + +import { V2CredentialPreview } from './V2CredentialPreview' +import { V2IssueCredentialMessage } from './messages/V2IssueCredentialMessage' +import { V2OfferCredentialMessage } from './messages/V2OfferCredentialMessage' +import { V2ProposeCredentialMessage } from './messages/V2ProposeCredentialMessage' +import { V2RequestCredentialMessage } from './messages/V2RequestCredentialMessage' + +export interface CreateRequestOptions { + formatServices: CredentialFormatService[] + record: CredentialExchangeRecord + requestOptions: ServiceRequestCredentialOptions + offerMessage: V2OfferCredentialMessage + holderDid?: string +} + +export class CredentialMessageBuilder { + /** + * Create a v2 credential proposal message according to the logic contained in the format service. The format services + * contain specific logic related to indy, jsonld etc. with others to come. + * + * @param formats {@link CredentialFormatService} array of format service objects each containing format-specific logic + * @param proposal {@link ProposeCredentialOptions} object containing (optionally) the linked attachments + * @param _threadId optional thread id for this message service + * @return a version 2.0 credential propose message see {@link V2ProposeCredentialMessage} + */ + public async createProposal( + formatServices: CredentialFormatService[], + proposal: ProposeCredentialOptions + ): Promise> { + if (formatServices.length === 0) { + throw new AriesFrameworkError('no format services provided to createProposal') + } + + // create message + // there are two arrays in each message, one for formats the other for attachments + const formatsArray: CredentialFormatSpec[] = [] + const filtersAttachArray: Attachment[] | undefined = [] + let previewAttachments: V2CredentialPreview | undefined + for (const formatService of formatServices) { + const { format: formats, attachment, preview } = await formatService.createProposal(proposal) + if (attachment) { + filtersAttachArray.push(attachment) + } else { + throw new AriesFrameworkError('attachment not initialized for credential proposal') + } + if (preview) { + previewAttachments = preview + } + formatsArray.push(formats) + } + const options: V2ProposeCredentialMessageProps = { + id: this.generateId(), + formats: formatsArray, + filtersAttach: filtersAttachArray, + comment: proposal.comment, + credentialProposal: previewAttachments, + } + + const message: V2ProposeCredentialMessage = new V2ProposeCredentialMessage(options) + + const props: CredentialExchangeRecordProps = { + connectionId: proposal.connectionId, + threadId: message.threadId, + state: CredentialState.ProposalSent, + autoAcceptCredential: proposal?.autoAcceptCredential, + protocolVersion: CredentialProtocolVersion.V2, + credentials: [], + } + + // Create the v2 record + const credentialRecord = new CredentialExchangeRecord(props) + + return { message, credentialRecord } + } + + /** + * accept a v2 credential proposal message according to the logic contained in the format service. The format services + * contain specific logic related to indy, jsonld etc. with others to come. + * + * @param message {@link V2ProposeCredentialMessage} object containing (optionally) the linked attachments + * @param connectionId optional connection id for the agent to agent connection + * @return a version 2.0 credential record object see {@link CredentialRecord} + */ + public processProposal(message: V2ProposeCredentialMessage, connectionId?: string): CredentialExchangeRecord { + const props: CredentialExchangeRecordProps = { + connectionId: connectionId, + threadId: message.threadId, + state: CredentialState.ProposalReceived, + credentialAttributes: message.credentialProposal?.attributes, + protocolVersion: CredentialProtocolVersion.V2, + credentials: [], + } + return new CredentialExchangeRecord(props) + } + + public async createOfferAsResponse( + formatServices: CredentialFormatService[], + credentialRecord: CredentialExchangeRecord, + options: ServiceOfferCredentialOptions + ): Promise { + if (formatServices.length === 0) { + throw new AriesFrameworkError('no format services provided to createProposal') + } + // create message + // there are two arrays in each message, one for formats the other for attachments + const formatsArray: CredentialFormatSpec[] = [] + const offersAttachArray: Attachment[] | undefined = [] + let previewAttachments: V2CredentialPreview = new V2CredentialPreview({ + attributes: [], + }) + + for (const formatService of formatServices) { + const { attachment: offersAttach, preview, format } = await formatService.createOffer(options) + if (offersAttach === undefined) { + throw new AriesFrameworkError('offersAttach not initialized for credential offer') + } + if (offersAttach) { + offersAttachArray.push(offersAttach) + } else { + throw new AriesFrameworkError('offersAttach not initialized for credential proposal') + } + if (preview && preview.attributes.length > 0) { + previewAttachments = preview + } + formatsArray.push(format) + + await formatService.processOffer(offersAttach, credentialRecord) + } + + const messageProps: V2OfferCredentialMessageOptions = { + id: this.generateId(), + formats: formatsArray, + comment: options.comment, + offerAttachments: offersAttachArray, + credentialPreview: previewAttachments, + } + const credentialOfferMessage: V2OfferCredentialMessage = new V2OfferCredentialMessage(messageProps) + + credentialOfferMessage.setThread({ + threadId: credentialRecord.threadId, + }) + + credentialRecord.credentialAttributes = previewAttachments?.attributes + + return credentialOfferMessage + } + + /** + * Create a {@link V2RequestCredentialMessage} + * + * @param formatService correct service for format, indy, w3c etc. + * @param record The credential record for which to create the credential request + * @param offer Additional configuration for the offer if present (might not be for W3C) + * @returns Object containing request message and associated credential record + * + */ + public async createRequest( + options: CreateRequestOptions + ): Promise> { + if (options.formatServices.length === 0) { + throw new AriesFrameworkError('no format services provided to createProposal') + } + + const formatsArray: CredentialFormatSpec[] = [] + const requestAttachArray: Attachment[] | undefined = [] + for (const format of options.formatServices) { + // use the attach id in the formats object to find the correct attachment + const attachment = format.getAttachment(options.offerMessage.formats, options.offerMessage.messageAttachment) + + if (attachment) { + options.requestOptions.offerAttachment = attachment + } else { + throw new AriesFrameworkError(`Missing data payload in attachment in credential Record ${options.record.id}`) + } + const { format: formats, attachment: requestAttach } = await format.createRequest( + options.requestOptions, + options.record, + options.holderDid + ) + + options.requestOptions.requestAttachment = requestAttach + if (formats && requestAttach) { + formatsArray.push(formats) + requestAttachArray.push(requestAttach) + } + } + const messageOptions: V2RequestCredentialMessageOptions = { + id: this.generateId(), + formats: formatsArray, + requestsAttach: requestAttachArray, + comment: options.requestOptions.comment, + } + const credentialRequestMessage = new V2RequestCredentialMessage(messageOptions) + credentialRequestMessage.setThread({ threadId: options.record.threadId }) + + options.record.autoAcceptCredential = + options.requestOptions.autoAcceptCredential ?? options.record.autoAcceptCredential + + return { message: credentialRequestMessage, credentialRecord: options.record } + } + + /** + * Create a {@link V2OfferCredentialMessage} as beginning of protocol process. + * + * @param formatService {@link CredentialFormatService} the format service object containing format-specific logic + * @param options attributes of the original offer + * @returns Object containing offer message and associated credential record + * + */ + public async createOffer( + formatServices: CredentialFormatService[], + options: ServiceOfferCredentialOptions + ): Promise<{ credentialRecord: CredentialExchangeRecord; message: V2OfferCredentialMessage }> { + if (formatServices.length === 0) { + throw new AriesFrameworkError('no format services provided to createProposal') + } + const formatsArray: CredentialFormatSpec[] = [] + const offersAttachArray: Attachment[] | undefined = [] + let previewAttachments: V2CredentialPreview = new V2CredentialPreview({ + attributes: [], + }) + + const offerMap = new Map() + for (const formatService of formatServices) { + const { attachment: offersAttach, preview, format } = await formatService.createOffer(options) + + if (offersAttach) { + offersAttachArray.push(offersAttach) + offerMap.set(offersAttach, formatService) + } else { + throw new AriesFrameworkError('offersAttach not initialized for credential proposal') + } + if (preview) { + previewAttachments = preview + } + formatsArray.push(format) + } + + const messageProps: V2OfferCredentialMessageOptions = { + id: this.generateId(), + formats: formatsArray, + comment: options.comment, + offerAttachments: offersAttachArray, + replacementId: undefined, + credentialPreview: previewAttachments, + } + + // Construct v2 offer message + const credentialOfferMessage: V2OfferCredentialMessage = new V2OfferCredentialMessage(messageProps) + + const recordProps: CredentialExchangeRecordProps = { + connectionId: options.connectionId, + threadId: credentialOfferMessage.threadId, + autoAcceptCredential: options?.autoAcceptCredential, + state: CredentialState.OfferSent, + credentialAttributes: previewAttachments?.attributes, + protocolVersion: CredentialProtocolVersion.V2, + credentials: [], + } + + const credentialRecord = new CredentialExchangeRecord(recordProps) + + for (const offersAttach of offerMap.keys()) { + const service = offerMap.get(offersAttach) + if (!service) { + throw new AriesFrameworkError(`No service found for attachment: ${offersAttach.id}`) + } + await service.processOffer(offersAttach, credentialRecord) + } + return { credentialRecord, message: credentialOfferMessage } + } + + /** + * Create a {@link V2IssueCredentialMessage} - we issue the credentials to the holder with this message + * + * @param formatService {@link CredentialFormatService} the format service object containing format-specific logic + * @param offerMessage the original offer message + * @returns Object containing offer message and associated credential record + * + */ + public async createCredential( + credentialFormats: CredentialFormatService[], + record: CredentialExchangeRecord, + serviceOptions: ServiceAcceptRequestOptions, + requestMessage: V2RequestCredentialMessage, + offerMessage: V2OfferCredentialMessage + ): Promise> { + const formatsArray: CredentialFormatSpec[] = [] + const credAttachArray: Attachment[] | undefined = [] + + for (const formatService of credentialFormats) { + const offerAttachment = formatService.getAttachment(offerMessage.formats, offerMessage.messageAttachment) + const requestAttachment = formatService.getAttachment(requestMessage.formats, requestMessage.messageAttachment) + + if (!requestAttachment) { + throw new Error(`Missing request attachment in createCredential`) + } + + const { format: formats, attachment: credentialsAttach } = await formatService.createCredential( + serviceOptions, + record, + requestAttachment, + offerAttachment + ) + + if (!formats) { + throw new AriesFrameworkError('formats not initialized for credential') + } + formatsArray.push(formats) + if (!credentialsAttach) { + throw new AriesFrameworkError('credentialsAttach not initialized for credential') + } + credAttachArray.push(credentialsAttach) + } + const messageOptions: V2IssueCredentialMessageProps = { + id: this.generateId(), + formats: formatsArray, + credentialsAttach: credAttachArray, + comment: serviceOptions.comment, + } + + const message: V2IssueCredentialMessage = new V2IssueCredentialMessage(messageOptions) + + return { message, credentialRecord: record } + } + public generateId(): string { + return uuid() + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/V2CredentialPreview.ts b/packages/core/src/modules/credentials/protocol/v2/V2CredentialPreview.ts new file mode 100644 index 0000000000..8202ec5542 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/V2CredentialPreview.ts @@ -0,0 +1,59 @@ +import type { CredentialPreviewOptions } from '../../models/CredentialPreviewAttributes' + +import { Expose, Type } from 'class-transformer' +import { Equals, IsInstance, ValidateNested } from 'class-validator' + +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { CredentialPreviewAttribute } from '../../models/CredentialPreviewAttributes' + +/** + * Credential preview inner message class. + * + * This is not a message but an inner object for other messages in this protocol. It is used construct a preview of the data for the credential. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2#preview-credential + */ +export class V2CredentialPreview { + public constructor(options: CredentialPreviewOptions) { + if (options) { + this.attributes = options.attributes + } + } + + @Expose({ name: '@type' }) + @Equals(V2CredentialPreview.type) + public type = V2CredentialPreview.type + public static type = 'https://didcomm.org/issue-credential/2.0/credential-preview' + + @Type(() => CredentialPreviewAttribute) + @ValidateNested({ each: true }) + @IsInstance(CredentialPreviewAttribute, { each: true }) + public attributes!: CredentialPreviewAttribute[] + + public toJSON(): Record { + return JsonTransformer.toJSON(this) + } + + /** + * Create a credential preview from a record with name and value entries. + * + * @example + * const preview = CredentialPreview.fromRecord({ + * name: "Bob", + * age: "20" + * }) + */ + public static fromRecord(record: Record) { + const attributes = Object.entries(record).map( + ([name, value]) => + new CredentialPreviewAttribute({ + name, + mimeType: 'text/plain', + value, + }) + ) + return new V2CredentialPreview({ + attributes, + }) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts b/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts new file mode 100644 index 0000000000..9636d8dd39 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts @@ -0,0 +1,1108 @@ +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { HandlerInboundMessage } from '../../../../agent/Handler' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { CredentialStateChangedEvent } from '../../CredentialEvents' +import type { + ServiceAcceptCredentialOptions, + CredentialProtocolMsgReturnType, + ServiceAcceptProposalOptions, + ServiceOfferCredentialOptions, +} from '../../CredentialServiceOptions' +import type { + AcceptProposalOptions, + AcceptRequestOptions, + NegotiateOfferOptions, + NegotiateProposalOptions, + OfferCredentialOptions, + ProposeCredentialOptions, + RequestCredentialOptions, +} from '../../CredentialsModuleOptions' +import type { CredentialFormatService } from '../../formats/CredentialFormatService' +import type { + CredentialFormats, + CredentialFormatSpec, + HandlerAutoAcceptOptions, +} from '../../formats/models/CredentialFormatServiceOptions' +import type { CredentialPreviewAttribute } from '../../models/CredentialPreviewAttributes' +import type { CreateRequestOptions } from './CredentialMessageBuilder' + +import { Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../../../agent/AgentConfig' +import { Dispatcher } from '../../../../agent/Dispatcher' +import { EventEmitter } from '../../../../agent/EventEmitter' +import { ServiceDecorator } from '../../../../decorators/service/ServiceDecorator' +import { AriesFrameworkError } from '../../../../error' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' +import { AckStatus } from '../../../common' +import { ConnectionService } from '../../../connections/services/ConnectionService' +import { MediationRecipientService } from '../../../routing' +import { AutoAcceptCredential } from '../../CredentialAutoAcceptType' +import { CredentialEventTypes } from '../../CredentialEvents' +import { CredentialProtocolVersion } from '../../CredentialProtocolVersion' +import { CredentialState } from '../../CredentialState' +import { CredentialFormatType } from '../../CredentialsModuleOptions' +import { CredentialProblemReportError, CredentialProblemReportReason } from '../../errors' +import { IndyCredentialFormatService } from '../../formats/indy/IndyCredentialFormatService' +import { FORMAT_KEYS } from '../../formats/models/CredentialFormatServiceOptions' +import { CredentialRepository, CredentialExchangeRecord } from '../../repository' +import { RevocationService } from '../../services' +import { CredentialService } from '../../services/CredentialService' + +import { CredentialMessageBuilder } from './CredentialMessageBuilder' +import { V2CredentialAckHandler } from './handlers/V2CredentialAckHandler' +import { V2CredentialProblemReportHandler } from './handlers/V2CredentialProblemReportHandler' +import { V2IssueCredentialHandler } from './handlers/V2IssueCredentialHandler' +import { V2OfferCredentialHandler } from './handlers/V2OfferCredentialHandler' +import { V2ProposeCredentialHandler } from './handlers/V2ProposeCredentialHandler' +import { V2RequestCredentialHandler } from './handlers/V2RequestCredentialHandler' +import { V2CredentialAckMessage } from './messages/V2CredentialAckMessage' +import { V2IssueCredentialMessage } from './messages/V2IssueCredentialMessage' +import { V2OfferCredentialMessage } from './messages/V2OfferCredentialMessage' +import { V2ProposeCredentialMessage } from './messages/V2ProposeCredentialMessage' +import { V2RequestCredentialMessage } from './messages/V2RequestCredentialMessage' + +@scoped(Lifecycle.ContainerScoped) +export class V2CredentialService extends CredentialService { + private connectionService: ConnectionService + private credentialMessageBuilder: CredentialMessageBuilder + private indyCredentialFormatService: IndyCredentialFormatService + private serviceFormatMap: { Indy: IndyCredentialFormatService } // jsonld todo + + public constructor( + connectionService: ConnectionService, + credentialRepository: CredentialRepository, + eventEmitter: EventEmitter, + dispatcher: Dispatcher, + agentConfig: AgentConfig, + mediationRecipientService: MediationRecipientService, + didCommMessageRepository: DidCommMessageRepository, + indyCredentialFormatService: IndyCredentialFormatService, + revocationService: RevocationService + ) { + super( + credentialRepository, + eventEmitter, + dispatcher, + agentConfig, + mediationRecipientService, + didCommMessageRepository, + revocationService + ) + this.connectionService = connectionService + this.indyCredentialFormatService = indyCredentialFormatService + this.credentialMessageBuilder = new CredentialMessageBuilder() + this.serviceFormatMap = { + [CredentialFormatType.Indy]: this.indyCredentialFormatService, + } + } + + /** + * Create a {@link V2ProposeCredentialMessage} not bound to an existing credential exchange. + * + * @param proposal The ProposeCredentialOptions object containing the important fields for the credential message + * @returns Object containing proposal message and associated credential record + * + */ + public async createProposal( + proposal: ProposeCredentialOptions + ): Promise> { + this.logger.debug('Get the Format Service and Create Proposal Message') + + const formats: CredentialFormatService[] = this.getFormats(proposal.credentialFormats) + + if (!formats || formats.length === 0) { + throw new AriesFrameworkError(`Unable to create proposal. No supported formats`) + } + const { message: proposalMessage, credentialRecord } = await this.credentialMessageBuilder.createProposal( + formats, + proposal + ) + + credentialRecord.credentialAttributes = proposalMessage.credentialProposal?.attributes + credentialRecord.connectionId = proposal.connectionId + + this.logger.debug('Save meta data and emit state change event') + + await this.credentialRepository.save(credentialRecord) + + this.eventEmitter.emit({ + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord, + previousState: null, + }, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + for (const format of formats) { + const options: ServiceAcceptProposalOptions = { + credentialRecordId: credentialRecord.id, + credentialFormats: {}, + protocolVersion: CredentialProtocolVersion.V2, + } + options.proposalAttachment = format.getAttachment(proposalMessage.formats, proposalMessage.messageAttachment) + await format.processProposal(options, credentialRecord) + } + return { credentialRecord, message: proposalMessage } + } + + /** + * Method called by {@link V2ProposeCredentialHandler} on reception of a propose credential message + * We do the necessary processing here to accept the proposal and do the state change, emit event etc. + * @param messageContext the inbound propose credential message + * @returns credential record appropriate for this incoming message (once accepted) + */ + public async processProposal( + messageContext: HandlerInboundMessage + ): Promise { + let credentialRecord: CredentialExchangeRecord + const { message: proposalMessage, connection } = messageContext + + this.logger.debug(`Processing credential proposal with id ${proposalMessage.id}`) + + try { + // Credential record already exists + credentialRecord = await this.getByThreadAndConnectionId(proposalMessage.threadId, connection?.id) + + // this may not be the first proposal message... + // let proposalCredentialMessage, offerCredentialMessage + // try { + const proposalCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + }) + const offerCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + credentialRecord.assertState(CredentialState.OfferSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: proposalCredentialMessage ?? undefined, + previousSentMessage: offerCredentialMessage ?? undefined, + }) + + // Update record + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + await this.updateState(credentialRecord, CredentialState.ProposalReceived) + } catch { + // No credential record exists with thread id + // get the format service objects for the formats found in the message + + credentialRecord = this.credentialMessageBuilder.processProposal(proposalMessage, connection?.id) + + // Save record and emit event + this.connectionService.assertConnectionOrServiceDecorator(messageContext) + + await this.credentialRepository.save(credentialRecord) + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + await this.emitEvent(credentialRecord) + } + return credentialRecord + } + + public async acceptProposal( + proposal: AcceptProposalOptions, + credentialRecord: CredentialExchangeRecord + ): Promise> { + const options: ServiceOfferCredentialOptions = { + connectionId: proposal.connectionId ?? undefined, + protocolVersion: proposal.protocolVersion, + credentialFormats: proposal.credentialFormats, + comment: proposal.comment, + } + const message = await this.createOfferAsResponse(credentialRecord, options) + + return { credentialRecord, message } + } + + /** + * Create a {@link AcceptProposalOptions} object used by handler + * + * @param credentialRecord {@link CredentialRecord} the record containing the proposal + * @return options attributes of the proposal + * + */ + private async createAcceptProposalOptions( + credentialRecord: CredentialExchangeRecord + ): Promise { + const proposalMessage: V2ProposeCredentialMessage | null = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + }) + + if (!proposalMessage) { + throw new AriesFrameworkError(`Missing proposal message for credential record ${credentialRecord.id}`) + } + const formats: CredentialFormatService[] = this.getFormatsFromMessage(proposalMessage.formats) + + if (!formats || formats.length === 0) { + throw new AriesFrameworkError(`Unable to create accept proposal options. No supported formats`) + } + const options: ServiceAcceptProposalOptions = { + credentialRecordId: credentialRecord.id, + credentialFormats: {}, + protocolVersion: CredentialProtocolVersion.V2, + } + + for (const formatService of formats) { + options.proposalAttachment = formatService.getAttachment( + proposalMessage.formats, + proposalMessage.messageAttachment + ) + // should fill in the credential formats + await formatService.processProposal(options, credentialRecord) + } + return options + } + + /** + * Negotiate a credential proposal as issuer (by sending a credential offer message) to the connection + * associated with the credential record. + * + * @param options configuration for the offer see {@link NegotiateProposalOptions} + * @returns Credential exchange record associated with the credential offer + * + */ + public async negotiateProposal( + options: NegotiateProposalOptions, + credentialRecord: CredentialExchangeRecord + ): Promise> { + if (!credentialRecord.connectionId) { + throw new AriesFrameworkError( + `No connectionId found for credential record '${credentialRecord.id}'. Connection-less issuance does not support negotiation.` + ) + } + + const message = await this.createOfferAsResponse(credentialRecord, options) + + return { credentialRecord, message } + } + + /** + * Create a {@link ProposePresentationMessage} as response to a received credential offer. + * To create a proposal not bound to an existing credential exchange, use {@link createProposal}. + * + * @param credentialRecord The credential record for which to create the credential proposal + * @param config Additional configuration to use for the proposal + * @returns Object containing proposal message and associated credential record + * + */ + public async negotiateOffer( + options: NegotiateOfferOptions, + credentialRecord: CredentialExchangeRecord + ): Promise> { + // Assert + credentialRecord.assertState(CredentialState.OfferReceived) + + // Create message + + const formats: CredentialFormatService[] = this.getFormats(options.credentialFormats) + + if (!formats || formats.length === 0) { + throw new AriesFrameworkError(`Unable to negotiate offer. No supported formats`) + } + const { message: credentialProposalMessage } = await this.credentialMessageBuilder.createProposal(formats, options) + credentialProposalMessage.setThread({ threadId: credentialRecord.threadId }) + + // Update record + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: credentialProposalMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + credentialRecord.credentialAttributes = credentialProposalMessage.credentialProposal?.attributes + await this.updateState(credentialRecord, CredentialState.ProposalSent) + + return { message: credentialProposalMessage, credentialRecord } + } + /** + * Create a {@link V2OfferCredentialMessage} as beginning of protocol process. + * + * @param formatService {@link CredentialFormatService} the format service object containing format-specific logic + * @param options attributes of the original offer + * @returns Object containing offer message and associated credential record + * + */ + public async createOffer( + options: OfferCredentialOptions + ): Promise> { + if (!options.connectionId) { + throw new AriesFrameworkError('Connection id missing from offer credential options') + } + const connection = await this.connectionService.getById(options.connectionId) + + connection?.assertReady() + + const formats: CredentialFormatService[] = this.getFormats(options.credentialFormats) + + if (!formats || formats.length === 0) { + throw new AriesFrameworkError(`Unable to create offer. No supported formats`) + } + // Create message + const { credentialRecord, message: credentialOfferMessage } = await this.credentialMessageBuilder.createOffer( + formats, + options + ) + credentialRecord.connectionId = options.connectionId + + await this.credentialRepository.save(credentialRecord) + await this.emitEvent(credentialRecord) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: credentialOfferMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return { credentialRecord, message: credentialOfferMessage } + } + + /** + * Create an offer message for an out-of-band (connectionless) credential + * @param credentialOptions the options (parameters) object for the offer + * @returns the credential record and the offer message + */ + public async createOutOfBandOffer( + credentialOptions: OfferCredentialOptions + ): Promise> { + const formats: CredentialFormatService[] = this.getFormats(credentialOptions.credentialFormats) + + if (!formats || formats.length === 0) { + throw new AriesFrameworkError(`Unable to create out of band offer. No supported formats`) + } + // Create message + const { credentialRecord, message: offerCredentialMessage } = await this.credentialMessageBuilder.createOffer( + formats, + credentialOptions + ) + + // Create and set ~service decorator + const routing = await this.mediationRecipientService.getRouting() + offerCredentialMessage.service = new ServiceDecorator({ + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.verkey], + routingKeys: routing.routingKeys, + }) + await this.credentialRepository.save(credentialRecord) + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: offerCredentialMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + await this.emitEvent(credentialRecord) + return { credentialRecord, message: offerCredentialMessage } + } + /** + * Create a {@link OfferCredentialMessage} as response to a received credential proposal. + * To create an offer not bound to an existing credential exchange, use {@link V2CredentialService#createOffer}. + * + * @param credentialRecord The credential record for which to create the credential offer + * @param credentialTemplate The credential template to use for the offer + * @returns Object containing offer message and associated credential record + * + */ + public async createOfferAsResponse( + credentialRecord: CredentialExchangeRecord, + proposal?: ServiceOfferCredentialOptions | NegotiateProposalOptions + ): Promise { + // Assert + credentialRecord.assertState(CredentialState.ProposalReceived) + + let options: ServiceOfferCredentialOptions | undefined + if (!proposal) { + const acceptProposalOptions: AcceptProposalOptions = await this.createAcceptProposalOptions(credentialRecord) + + options = { + credentialFormats: acceptProposalOptions.credentialFormats, + protocolVersion: CredentialProtocolVersion.V2, + credentialRecordId: acceptProposalOptions.connectionId, + comment: acceptProposalOptions.comment, + } + } else { + options = proposal + } + const formats: CredentialFormatService[] = this.getFormats(options.credentialFormats as Record) + + if (!formats || formats.length === 0) { + throw new AriesFrameworkError(`Unable to create offer as response. No supported formats`) + } + // Create the offer message + this.logger.debug(`Get the Format Service and Create Offer Message for credential record ${credentialRecord.id}`) + + const proposeCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + }) + + const credentialOfferMessage = await this.credentialMessageBuilder.createOfferAsResponse( + formats, + credentialRecord, + options + ) + + credentialOfferMessage.credentialPreview = proposeCredentialMessage?.credentialProposal + credentialRecord.credentialAttributes = proposeCredentialMessage?.credentialProposal?.attributes + + await this.updateState(credentialRecord, CredentialState.OfferSent) + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: credentialOfferMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + + return credentialOfferMessage + } + /** + * Method called by {@link V2OfferCredentialHandler} on reception of a offer credential message + * We do the necessary processing here to accept the offer and do the state change, emit event etc. + * @param messageContext the inbound offer credential message + * @returns credential record appropriate for this incoming message (once accepted) + */ + public async processOffer( + messageContext: HandlerInboundMessage + ): Promise { + let credentialRecord: CredentialExchangeRecord + const { message: credentialOfferMessage, connection } = messageContext + + this.logger.debug(`Processing credential offer with id ${credentialOfferMessage.id}`) + + const formats: CredentialFormatService[] = this.getFormatsFromMessage(credentialOfferMessage.formats) + if (!formats || formats.length === 0) { + throw new AriesFrameworkError(`Unable to create offer. No supported formats`) + } + try { + // Credential record already exists + credentialRecord = await this.getByThreadAndConnectionId(credentialOfferMessage.threadId, connection?.id) + + const proposeCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + }) + const offerCredentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + credentialRecord.assertState(CredentialState.ProposalSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: offerCredentialMessage ?? undefined, + previousSentMessage: proposeCredentialMessage ?? undefined, + }) + + for (const format of formats) { + const attachment = format.getAttachment( + credentialOfferMessage.formats, + credentialOfferMessage.messageAttachment + ) + + if (!attachment) { + throw new AriesFrameworkError(`Missing offer attachment in credential offer message`) + } + await format.processOffer(attachment, credentialRecord) + } + await this.updateState(credentialRecord, CredentialState.OfferReceived) + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: credentialOfferMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + } catch (error) { + // No credential record exists with thread id + + this.logger.debug('No credential record found for this offer - create a new one') + credentialRecord = new CredentialExchangeRecord({ + connectionId: connection?.id, + threadId: credentialOfferMessage.id, + credentialAttributes: credentialOfferMessage.credentialPreview?.attributes, + state: CredentialState.OfferReceived, + protocolVersion: CredentialProtocolVersion.V2, + credentials: [], + }) + + for (const format of formats) { + const attachment = format.getAttachment( + credentialOfferMessage.formats, + credentialOfferMessage.messageAttachment + ) + + if (!attachment) { + throw new AriesFrameworkError(`Missing offer attachment in credential offer message`) + } + await format.processOffer(attachment, credentialRecord) + } + + // Save in repository + this.logger.debug('Saving credential record and emit offer-received event') + await this.credentialRepository.save(credentialRecord) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: credentialOfferMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + this.eventEmitter.emit({ + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord, + previousState: null, + }, + }) + } + + return credentialRecord + } + + /** + * Create a {@link V2RequestCredentialMessage} + * + * @param credentialRecord The credential record for which to create the credential request + * @param options request options for creating this request + * @returns Object containing request message and associated credential record + * + */ + public async createRequest( + record: CredentialExchangeRecord, + options: RequestCredentialOptions, + holderDid?: string // temporary workaround + ): Promise> { + this.logger.debug('Get the Format Service and Create Request Message') + + record.assertState(CredentialState.OfferReceived) + + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: record.id, + messageClass: V2OfferCredentialMessage, + }) + + if (!offerMessage) { + throw new CredentialProblemReportError( + `Missing required base64 or json encoded attachment data for credential offer with thread id ${record.threadId}`, + { problemCode: CredentialProblemReportReason.IssuanceAbandoned } + ) + } + const formats: CredentialFormatService[] = this.getFormatsFromMessage(offerMessage.formats) + if (!formats || formats.length == 0) { + throw new AriesFrameworkError('No format keys found on the RequestCredentialOptions object') + } + + const optionsForRequest: CreateRequestOptions = { + formatServices: formats, + record, + requestOptions: options, + offerMessage, + holderDid, + } + const { message, credentialRecord } = await this.credentialMessageBuilder.createRequest(optionsForRequest) + + await this.updateState(credentialRecord, CredentialState.RequestSent) + return { message, credentialRecord } + } + + /** + * Process a received {@link RequestCredentialMessage}. This will not accept the credential request + * or send a credential. It will only update the existing credential record with + * the information from the credential request message. Use {@link createCredential} + * after calling this method to create a credential. + * + * @param messageContext The message context containing a v2 credential request message + * @returns credential record associated with the credential request message + * + */ + public async processRequest( + messageContext: InboundMessageContext + ): Promise { + const { message: credentialRequestMessage, connection } = messageContext + + const credentialRecord = await this.getByThreadAndConnectionId(credentialRequestMessage.threadId, connection?.id) + credentialRecord.connectionId = connection?.id + + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + }) + + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + // Assert + credentialRecord.assertState(CredentialState.OfferSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: proposalMessage ?? undefined, + previousSentMessage: offerMessage ?? undefined, + }) + + this.logger.debug('Credential record found when processing credential request', credentialRecord) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: credentialRequestMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + await this.updateState(credentialRecord, CredentialState.RequestReceived) + + return credentialRecord + } + + /** + * Create a {@link IssueCredentialMessage} as response to a received credential request. + * + * @param credentialRecord The credential record for which to create the credential + * @param options Additional configuration to use for the credential + * @returns Object containing issue credential message and associated credential record + * + */ + public async createCredential( + record: CredentialExchangeRecord, + options: AcceptRequestOptions + ): Promise> { + record.assertState(CredentialState.RequestReceived) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: record.id, + messageClass: V2RequestCredentialMessage, + }) + + if (!requestMessage) { + throw new AriesFrameworkError( + `Missing credential request for credential exchange with thread id ${record.threadId}` + ) + } + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: record.id, + messageClass: V2OfferCredentialMessage, + }) + if (!offerMessage) { + throw new AriesFrameworkError('Missing Offer Message in create credential') + } + const credentialFormats: CredentialFormatService[] = this.getFormatsFromMessage(requestMessage.formats) + if (!credentialFormats || credentialFormats.length === 0) { + throw new AriesFrameworkError(`Unable to create credential. No supported formats`) + } + const { message: issueCredentialMessage, credentialRecord } = await this.credentialMessageBuilder.createCredential( + credentialFormats, + record, + options, + requestMessage, + offerMessage + ) + + issueCredentialMessage.setThread({ + threadId: credentialRecord.threadId, + }) + issueCredentialMessage.setPleaseAck() + + credentialRecord.autoAcceptCredential = options?.autoAcceptCredential ?? credentialRecord.autoAcceptCredential + + await this.updateState(credentialRecord, CredentialState.CredentialIssued) + + return { message: issueCredentialMessage, credentialRecord } + } + + /** + * Process a received {@link IssueCredentialMessage}. This will not accept the credential + * or send a credential acknowledgement. It will only update the existing credential record with + * the information from the issue credential message. Use {@link createAck} + * after calling this method to create a credential acknowledgement. + * + * @param messageContext The message context containing an issue credential message + * + * @returns credential record associated with the issue credential message + * + */ + public async processCredential( + messageContext: InboundMessageContext + ): Promise { + const { message: issueCredentialMessage, connection } = messageContext + + this.logger.debug(`Processing credential with id ${issueCredentialMessage.id}`) + + const credentialRecord = await this.getByThreadAndConnectionId(issueCredentialMessage.threadId, connection?.id) + + credentialRecord.connectionId = connection?.id + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2RequestCredentialMessage, + }) + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + // Assert + credentialRecord.assertState(CredentialState.RequestSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: offerMessage ?? undefined, + previousSentMessage: requestMessage ?? undefined, + }) + + const formatServices: CredentialFormatService[] = this.getFormatsFromMessage(issueCredentialMessage.formats) + + for (const formatService of formatServices) { + // get the revocation registry and pass it to the process (store) credential method + const issueAttachment = formatService.getAttachment( + issueCredentialMessage.formats, + issueCredentialMessage.messageAttachment + ) + + if (!issueAttachment) { + throw new AriesFrameworkError('Missing credential attachment in processCredential') + } + const options: ServiceAcceptCredentialOptions = { + credentialAttachment: issueAttachment, + } + await formatService.processCredential(options, credentialRecord) + } + + await this.updateState(credentialRecord, CredentialState.CredentialReceived) + + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: issueCredentialMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + return credentialRecord + } + /** + * Create a {@link V2CredentialAckMessage} as response to a received credential. + * + * @param credentialRecord The credential record for which to create the credential acknowledgement + * @returns Object containing credential acknowledgement message and associated credential record + * + */ + public async createAck( + credentialRecord: CredentialExchangeRecord + ): Promise> { + credentialRecord.assertState(CredentialState.CredentialReceived) + + // Create message + const ackMessage = new V2CredentialAckMessage({ + status: AckStatus.OK, + threadId: credentialRecord.threadId, + }) + + await this.updateState(credentialRecord, CredentialState.Done) + + return { message: ackMessage, credentialRecord } + } + + /** + * Process a received {@link CredentialAckMessage}. + * + * @param messageContext The message context containing a credential acknowledgement message + * @returns credential record associated with the credential acknowledgement message + * + */ + public async processAck( + messageContext: InboundMessageContext + ): Promise { + const { message: credentialAckMessage, connection } = messageContext + + this.logger.debug(`Processing credential ack with id ${credentialAckMessage.id}`) + + const credentialRecord = await this.getByThreadAndConnectionId(credentialAckMessage.threadId, connection?.id) + credentialRecord.connectionId = connection?.id + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2RequestCredentialMessage, + }) + + const credentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2IssueCredentialMessage, + }) + + // Assert + credentialRecord.assertState(CredentialState.CredentialIssued) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: requestMessage ?? undefined, + previousSentMessage: credentialMessage ?? undefined, + }) + + // Update record + await this.updateState(credentialRecord, CredentialState.Done) + + return credentialRecord + } + /** + * Register the v2 handlers. These handlers supplement, ie are created in addition to, the existing + * v1 handlers. + */ + public registerHandlers() { + this.logger.debug('Registering V2 handlers') + + this.dispatcher.registerHandler( + new V2ProposeCredentialHandler(this, this.agentConfig, this.didCommMessageRepository) + ) + + this.dispatcher.registerHandler( + new V2OfferCredentialHandler( + this, + this.agentConfig, + this.mediationRecipientService, + this.didCommMessageRepository + ) + ) + + this.dispatcher.registerHandler( + new V2RequestCredentialHandler(this, this.agentConfig, this.didCommMessageRepository) + ) + + this.dispatcher.registerHandler(new V2IssueCredentialHandler(this, this.agentConfig, this.didCommMessageRepository)) + this.dispatcher.registerHandler(new V2CredentialAckHandler(this)) + this.dispatcher.registerHandler(new V2CredentialProblemReportHandler(this)) + } + + // AUTO ACCEPT METHODS + public async shouldAutoRespondToProposal(options: HandlerAutoAcceptOptions): Promise { + if (this.agentConfig.autoAcceptCredentials === AutoAcceptCredential.Never) { + return false + } + if (options.credentialRecord.autoAcceptCredential === AutoAcceptCredential.Never) { + return false + } + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: options.credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + }) + if (!proposalMessage) { + throw new AriesFrameworkError('Missing proposal message in V2ProposeCredentialHandler') + } + const formatServices: CredentialFormatService[] = this.getFormatsFromMessage(proposalMessage.formats) + let shouldAutoRespond = true + for (const formatService of formatServices) { + const formatShouldAutoRespond = + this.agentConfig.autoAcceptCredentials == AutoAcceptCredential.Always || + formatService.shouldAutoRespondToProposal(options) + + shouldAutoRespond = shouldAutoRespond && formatShouldAutoRespond + } + return shouldAutoRespond + } + + public shouldAutoRespondToOffer( + credentialRecord: CredentialExchangeRecord, + offerMessage: V2OfferCredentialMessage, + proposeMessage?: V2ProposeCredentialMessage + ): boolean { + if (this.agentConfig.autoAcceptCredentials === AutoAcceptCredential.Never) { + return false + } + let offerValues: CredentialPreviewAttribute[] | undefined + let shouldAutoRespond = true + const formatServices: CredentialFormatService[] = this.getFormatsFromMessage(offerMessage.formats) + for (const formatService of formatServices) { + let proposalAttachment: Attachment | undefined + + if (proposeMessage) { + proposalAttachment = formatService.getAttachment(proposeMessage.formats, proposeMessage.messageAttachment) + } + const offerAttachment = formatService.getAttachment(offerMessage.formats, offerMessage.messageAttachment) + + offerValues = offerMessage.credentialPreview?.attributes + + const handlerOptions: HandlerAutoAcceptOptions = { + credentialRecord, + autoAcceptType: this.agentConfig.autoAcceptCredentials, + messageAttributes: offerValues, + proposalAttachment, + offerAttachment, + } + const formatShouldAutoRespond = + this.agentConfig.autoAcceptCredentials == AutoAcceptCredential.Always || + formatService.shouldAutoRespondToProposal(handlerOptions) + + shouldAutoRespond = shouldAutoRespond && formatShouldAutoRespond + } + + return shouldAutoRespond + } + + public shouldAutoRespondToRequest( + credentialRecord: CredentialExchangeRecord, + requestMessage: V2RequestCredentialMessage, + proposeMessage?: V2ProposeCredentialMessage, + offerMessage?: V2OfferCredentialMessage + ): boolean { + const formatServices: CredentialFormatService[] = this.getFormatsFromMessage(requestMessage.formats) + let shouldAutoRespond = true + + for (const formatService of formatServices) { + let proposalAttachment, offerAttachment, requestAttachment: Attachment | undefined + if (proposeMessage) { + proposalAttachment = formatService.getAttachment(proposeMessage.formats, proposeMessage.messageAttachment) + } + if (offerMessage) { + offerAttachment = formatService.getAttachment(offerMessage.formats, offerMessage.messageAttachment) + } + if (requestMessage) { + requestAttachment = formatService.getAttachment(requestMessage.formats, requestMessage.messageAttachment) + } + const handlerOptions: HandlerAutoAcceptOptions = { + credentialRecord, + autoAcceptType: this.agentConfig.autoAcceptCredentials, + proposalAttachment, + offerAttachment, + requestAttachment, + } + const formatShouldAutoRespond = + this.agentConfig.autoAcceptCredentials == AutoAcceptCredential.Always || + formatService.shouldAutoRespondToRequest(handlerOptions) + + shouldAutoRespond = shouldAutoRespond && formatShouldAutoRespond + } + return shouldAutoRespond + } + + public shouldAutoRespondToCredential( + credentialRecord: CredentialExchangeRecord, + credentialMessage: V2IssueCredentialMessage + ): boolean { + // 1. Get all formats for this message + const formatServices: CredentialFormatService[] = this.getFormatsFromMessage(credentialMessage.formats) + + // 2. loop through found formats + let shouldAutoRespond = true + let credentialAttachment: Attachment | undefined + + for (const formatService of formatServices) { + if (credentialMessage) { + credentialAttachment = formatService.getAttachment( + credentialMessage.formats, + credentialMessage.messageAttachment + ) + } + const handlerOptions: HandlerAutoAcceptOptions = { + credentialRecord, + autoAcceptType: this.agentConfig.autoAcceptCredentials, + credentialAttachment, + } + // 3. Call format.shouldRespondToProposal for each one + + const formatShouldAutoRespond = + this.agentConfig.autoAcceptCredentials == AutoAcceptCredential.Always || + formatService.shouldAutoRespondToCredential(handlerOptions) + + shouldAutoRespond = shouldAutoRespond && formatShouldAutoRespond + } + return shouldAutoRespond + } + public async getOfferMessage(id: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: id, + messageClass: V2OfferCredentialMessage, + }) + } + public async getRequestMessage(id: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: id, + messageClass: V2RequestCredentialMessage, + }) + } + + public async getCredentialMessage(id: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: id, + messageClass: V2IssueCredentialMessage, + }) + } + + public update(credentialRecord: CredentialExchangeRecord) { + return this.credentialRepository.update(credentialRecord) + } + + /** + * Returns the protocol version for this credential service + * @returns v2 as this is the v2 service + */ + public getVersion(): CredentialProtocolVersion { + return CredentialProtocolVersion.V2 + } + + /** + * Gets the correct formatting service for this credential record type, eg indy or jsonld. Others may be + * added in the future. + * Each formatting service knows how to format the message structure for the specific record type + * @param credentialFormatType the format type, indy, jsonld, jwt etc. + * @returns the formatting service. + */ + public getFormatService(credentialFormatType: CredentialFormatType): CredentialFormatService { + return this.serviceFormatMap[credentialFormatType] + } + + private async emitEvent(credentialRecord: CredentialExchangeRecord) { + this.eventEmitter.emit({ + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord, + previousState: null, + }, + }) + } + /** + * Retrieve a credential record by connection id and thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The credential record + */ + public getByThreadAndConnectionId(threadId: string, connectionId?: string): Promise { + return this.credentialRepository.getSingleByQuery({ + connectionId, + threadId, + }) + } + + /** + * Get all the format service objects for a given credential format from an incoming message + * @param messageFormats the format objects containing the format name (eg indy) + * @return the credential format service objects in an array - derived from format object keys + */ + public getFormatsFromMessage(messageFormats: CredentialFormatSpec[]): CredentialFormatService[] { + const formats: CredentialFormatService[] = [] + for (const msg of messageFormats) { + if (msg.format.includes('indy')) { + formats.push(this.getFormatService(CredentialFormatType.Indy)) + } else if (msg.format.includes('aries')) { + // todo + } else { + throw new AriesFrameworkError(`Unknown Message Format: ${msg.format}`) + } + } + return formats + } + /** + * Get all the format service objects for a given credential format + * @param credentialFormats the format object containing various optional parameters + * @return the credential format service objects in an array - derived from format object keys + */ + public getFormats(credentialFormats: CredentialFormats): CredentialFormatService[] { + const formats: CredentialFormatService[] = [] + const formatKeys = Object.keys(credentialFormats) + + for (const key of formatKeys) { + const credentialFormatType: CredentialFormatType = FORMAT_KEYS[key] + const formatService: CredentialFormatService = this.getFormatService(credentialFormatType) + formats.push(formatService) + } + return formats + } +} diff --git a/packages/core/tests/connectionless-credentials.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v1connectionless-credentials.test.ts similarity index 60% rename from packages/core/tests/connectionless-credentials.test.ts rename to packages/core/src/modules/credentials/protocol/v2/__tests__/v1connectionless-credentials.test.ts index 73f4559cfc..cb0890b56c 100644 --- a/packages/core/tests/connectionless-credentials.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v1connectionless-credentials.test.ts @@ -1,21 +1,24 @@ -import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' -import type { CredentialStateChangedEvent } from '../src/modules/credentials' +import type { SubjectMessage } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import type { CredentialStateChangedEvent } from '../../../CredentialEvents' +import type { + AcceptOfferOptions, + AcceptRequestOptions, + OfferCredentialOptions, +} from '../../../CredentialsModuleOptions' import { ReplaySubject, Subject } from 'rxjs' -import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' -import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { Agent } from '../src/agent/Agent' -import { - CredentialPreview, - AutoAcceptCredential, - CredentialEventTypes, - CredentialRecord, - CredentialState, -} from '../src/modules/credentials' - -import { getBaseConfig, prepareForIssuance, waitForCredentialRecordSubject } from './helpers' -import testLogger from './logger' +import { SubjectInboundTransport } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../../../tests/transport/SubjectOutboundTransport' +import { prepareForIssuance, waitForCredentialRecordSubject, getBaseConfig } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { Agent } from '../../../../../agent/Agent' +import { AutoAcceptCredential } from '../../../CredentialAutoAcceptType' +import { CredentialEventTypes } from '../../../CredentialEvents' +import { CredentialProtocolVersion } from '../../../CredentialProtocolVersion' +import { CredentialState } from '../../../CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { V1CredentialPreview } from '../../v1/V1CredentialPreview' const faberConfig = getBaseConfig('Faber connection-less Credentials', { endpoints: ['rxjs:faber'], @@ -25,7 +28,7 @@ const aliceConfig = getBaseConfig('Alice connection-less Credentials', { endpoints: ['rxjs:alice'], }) -const credentialPreview = CredentialPreview.fromRecord({ +const credentialPreview = V1CredentialPreview.fromRecord({ name: 'John', age: '99', }) @@ -47,12 +50,12 @@ describe('credentials', () => { } faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) - faberAgent.registerOutboundTransport(new SubjectOutboundTransport(aliceMessages, subjectMap)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await faberAgent.initialize() aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) - aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(faberMessages, subjectMap)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() const { definition } = await prepareForIssuance(faberAgent, ['name', 'age']) @@ -70,20 +73,32 @@ describe('credentials', () => { }) afterEach(async () => { - await faberAgent.shutdown({ deleteWallet: true }) - await aliceAgent.shutdown({ deleteWallet: true }) + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() }) test('Faber starts with connection-less credential offer to Alice', async () => { testLogger.test('Faber sends credential offer to Alice') + + const offerOptions: OfferCredentialOptions = { + comment: 'V1 Out of Band offer', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + connectionId: '', + } // eslint-disable-next-line prefer-const - let { offerMessage, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer({ - preview: credentialPreview, - credentialDefinitionId: credDefId, - comment: 'some comment about credential', - }) + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer( + offerOptions + ) - await aliceAgent.receiveMessage(offerMessage.toJSON()) + await aliceAgent.receiveMessage(message.toJSON()) let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { threadId: faberCredentialRecord.threadId, @@ -91,16 +106,23 @@ describe('credentials', () => { }) testLogger.test('Alice sends credential request to Faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptOffer(aliceCredentialRecord.id) + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + } + const credentialRecord = await aliceAgent.credentials.acceptOffer(acceptOfferOptions) testLogger.test('Faber waits for credential request from Alice') faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { - threadId: aliceCredentialRecord.threadId, + threadId: credentialRecord.threadId, state: CredentialState.RequestReceived, }) testLogger.test('Faber sends credential to Alice') - faberCredentialRecord = await faberAgent.credentials.acceptRequest(faberCredentialRecord.id) + const options: AcceptRequestOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V1 Indy Credential', + } + faberCredentialRecord = await faberAgent.credentials.acceptRequest(options) testLogger.test('Alice waits for credential from Faber') aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { @@ -118,11 +140,9 @@ describe('credentials', () => { }) expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, + type: CredentialExchangeRecord.type, id: expect.any(String), createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), metadata: { data: { '_internal/indyCredential': { @@ -130,17 +150,20 @@ describe('credentials', () => { }, }, }, - credentialId: expect.any(String), + credentials: [ + { + credentialRecordType: 'Indy', + credentialRecordId: expect.any(String), + }, + ], state: CredentialState.Done, threadId: expect.any(String), }) expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, + type: CredentialExchangeRecord.type, id: expect.any(String), createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), metadata: { data: { '_internal/indyCredential': { @@ -154,16 +177,25 @@ describe('credentials', () => { }) test('Faber starts with connection-less credential offer to Alice with auto-accept enabled', async () => { - // eslint-disable-next-line prefer-const - let { offerMessage, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer({ - preview: credentialPreview, - credentialDefinitionId: credDefId, - comment: 'some comment about credential', + const offerOptions: OfferCredentialOptions = { + comment: 'V1 Out of Band offer', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V1, autoAcceptCredential: AutoAcceptCredential.ContentApproved, - }) + connectionId: '', + } + // eslint-disable-next-line prefer-const + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer( + offerOptions + ) // Receive Message - await aliceAgent.receiveMessage(offerMessage.toJSON()) + await aliceAgent.receiveMessage(message.toJSON()) // Wait for it to be processed let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { @@ -171,9 +203,12 @@ describe('credentials', () => { state: CredentialState.OfferReceived, }) - await aliceAgent.credentials.acceptOffer(aliceCredentialRecord.id, { + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, autoAcceptCredential: AutoAcceptCredential.ContentApproved, - }) + } + + await aliceAgent.credentials.acceptOffer(acceptOfferOptions) aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { threadId: faberCredentialRecord.threadId, @@ -186,11 +221,9 @@ describe('credentials', () => { }) expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, + type: CredentialExchangeRecord.type, id: expect.any(String), createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), metadata: { data: { '_internal/indyCredential': { @@ -198,17 +231,20 @@ describe('credentials', () => { }, }, }, - credentialId: expect.any(String), + credentials: [ + { + credentialRecordType: 'Indy', + credentialRecordId: expect.any(String), + }, + ], state: CredentialState.Done, threadId: expect.any(String), }) expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, + type: CredentialExchangeRecord.type, id: expect.any(String), createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), state: CredentialState.Done, threadId: expect.any(String), }) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v1credentials-auto-accept.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v1credentials-auto-accept.test.ts new file mode 100644 index 0000000000..231f9532ad --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v1credentials-auto-accept.test.ts @@ -0,0 +1,484 @@ +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections' +import type { + AcceptOfferOptions, + AcceptProposalOptions, + NegotiateOfferOptions, + NegotiateProposalOptions, + OfferCredentialOptions, + ProposeCredentialOptions, +} from '../../../CredentialsModuleOptions' +import type { Schema } from 'indy-sdk' + +import { AriesFrameworkError } from '../../../../../../src/error/AriesFrameworkError' +import { setupCredentialTests, waitForCredentialRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { sleep } from '../../../../../utils/sleep' +import { AutoAcceptCredential } from '../../../CredentialAutoAcceptType' +import { CredentialProtocolVersion } from '../../../CredentialProtocolVersion' +import { CredentialState } from '../../../CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { V1CredentialPreview } from '../../v1/V1CredentialPreview' + +describe('credentials', () => { + let faberAgent: Agent + let aliceAgent: Agent + let credDefId: string + let schema: Schema + let faberConnection: ConnectionRecord + let aliceConnection: ConnectionRecord + const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + const newCredentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', + }) + + describe('Auto accept on `always`', () => { + beforeAll(async () => { + ;({ faberAgent, aliceAgent, credDefId, schema, faberConnection, aliceConnection } = await setupCredentialTests( + 'faber agent: always', + 'alice agent: always', + AutoAcceptCredential.Always + )) + }) + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + // ============================== + // TESTS v1 BEGIN + // ========================== + test('Alice starts with V1 credential proposal to Faber, both with autoAcceptCredential on `always`', async () => { + testLogger.test('Alice sends credential proposal to Faber') + let aliceCredentialRecord: CredentialExchangeRecord + + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V1, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + payload: { + credentialDefinitionId: credDefId, + }, + }, + }, + comment: 'v1 propose credential test', + } + const schemaId = schema.id + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + testLogger.test('Faber waits for credential ack from Alice') + aliceCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyCredential': { + schemaId, + credentialDefinitionId: credDefId, + }, + }, + }, + state: CredentialState.Done, + }) + }) + test('Faber starts with V1 credential offer to Alice, both with autoAcceptCredential on `always`', async () => { + testLogger.test('Faber sends credential offer to Alice') + const schemaId = schema.id + const offerOptions: OfferCredentialOptions = { + comment: 'some comment about credential', + connectionId: faberConnection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + } + const faberCredentialExchangeRecord: CredentialExchangeRecord = await faberAgent.credentials.offerCredential( + offerOptions + ) + testLogger.test('Alice waits for credential from Faber') + const aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + testLogger.test('Faber waits for credential ack from Alice') + const faberCredentialRecord: CredentialExchangeRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyRequest': expect.any(Object), + '_internal/indyCredential': { + schemaId, + credentialDefinitionId: credDefId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'Indy', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.Done, + }) + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + }) + }) + + describe('Auto accept on `contentApproved`', () => { + beforeAll(async () => { + ;({ faberAgent, aliceAgent, credDefId, schema, faberConnection, aliceConnection } = await setupCredentialTests( + 'faber agent: contentApproved', + 'alice agent: contentApproved', + AutoAcceptCredential.ContentApproved + )) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + // ============================== + // TESTS v1 BEGIN + // ========================== + test('Alice starts with V1 credential proposal to Faber, both with autoAcceptCredential on `contentApproved`', async () => { + testLogger.test('Alice sends credential proposal to Faber') + const schemaId = schema.id + let faberCredentialExchangeRecord: CredentialExchangeRecord + let aliceCredentialExchangeRecord: CredentialExchangeRecord + + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V1, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + payload: { + credentialDefinitionId: credDefId, + }, + }, + }, + } + aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialExchangeRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + const options: AcceptProposalOptions = { + credentialRecordId: faberCredentialExchangeRecord.id, + comment: 'V1 Indy Offer', + credentialFormats: { + indy: { + credentialDefinitionId: credDefId, + attributes: credentialPreview.attributes, + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + } + testLogger.test('Faber sends credential offer to Alice') + options.credentialRecordId = faberCredentialExchangeRecord.id + faberCredentialExchangeRecord = await faberAgent.credentials.acceptProposal(options) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialExchangeRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialExchangeRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialExchangeRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyRequest': expect.any(Object), + '_internal/indyCredential': { + schemaId, + credentialDefinitionId: credDefId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'Indy', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.Done, + }) + + expect(faberCredentialExchangeRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyCredential': { + schemaId, + credentialDefinitionId: credDefId, + }, + }, + }, + state: CredentialState.Done, + }) + }) + + test('Faber starts with V1 credential offer to Alice, both with autoAcceptCredential on `contentApproved`', async () => { + testLogger.test('Faber sends credential offer to Alice') + const schemaId = schema.id + let aliceCredentialExchangeRecord: CredentialExchangeRecord + let faberCredentialExchangeRecord: CredentialExchangeRecord + + const offerOptions: OfferCredentialOptions = { + comment: 'some comment about credential', + connectionId: faberConnection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + } + faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential(offerOptions) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialExchangeRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + expect(JsonTransformer.toJSON(aliceCredentialExchangeRecord)).toMatchObject({ + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(aliceCredentialExchangeRecord.id).not.toBeNull() + expect(aliceCredentialExchangeRecord.getTags()).toEqual({ + threadId: aliceCredentialExchangeRecord.threadId, + state: aliceCredentialExchangeRecord.state, + connectionId: aliceConnection.id, + credentialIds: [], + }) + + if (aliceCredentialExchangeRecord.connectionId) { + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: aliceCredentialExchangeRecord.id, + } + testLogger.test('alice sends credential request to faber') + faberCredentialExchangeRecord = await aliceAgent.credentials.acceptOffer(acceptOfferOptions) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialExchangeRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialExchangeRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialExchangeRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyRequest': expect.any(Object), + '_internal/indyCredential': { + schemaId, + credentialDefinitionId: credDefId, + }, + }, + }, + credentials: [ + { + credentialRecordType: 'Indy', + credentialRecordId: expect.any(String), + }, + ], + state: CredentialState.Done, + }) + + expect(faberCredentialExchangeRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + } else { + throw new AriesFrameworkError('missing alice connection id') + } + }) + + test('Alice starts with V1 credential proposal to Faber, both have autoAcceptCredential on `contentApproved` and attributes did change', async () => { + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V1, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + payload: { + credentialDefinitionId: credDefId, + }, + }, + }, + comment: 'v1 propose credential test', + } + testLogger.test('Alice sends credential proposal to Faber') + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialExchangeRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + const negotiateOptions: NegotiateProposalOptions = { + credentialRecordId: faberCredentialExchangeRecord.id, + credentialFormats: { + indy: { + credentialDefinitionId: credDefId, + attributes: newCredentialPreview.attributes, + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + } + await faberAgent.credentials.negotiateProposal(negotiateOptions) + + testLogger.test('Alice waits for credential offer from Faber') + + const record = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(record.id).not.toBeNull() + expect(record.getTags()).toEqual({ + threadId: record.threadId, + state: record.state, + connectionId: aliceConnection.id, + credentialIds: [], + }) + + // Check if the state of the credential records did not change + faberCredentialExchangeRecord = await faberAgent.credentials.getById(faberCredentialExchangeRecord.id) + faberCredentialExchangeRecord.assertState(CredentialState.OfferSent) + + const aliceRecord = await aliceAgent.credentials.getById(record.id) + aliceRecord.assertState(CredentialState.OfferReceived) + }) + + test('Faber starts with V1 credential offer to Alice, both have autoAcceptCredential on `contentApproved` and attributes did change', async () => { + testLogger.test('Faber sends credential offer to Alice') + const offerOptions: OfferCredentialOptions = { + comment: 'some comment about credential', + connectionId: faberConnection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V1, + } + let faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential(offerOptions) + + testLogger.test('Alice waits for credential offer from Faber') + let aliceCredentialExchangeRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(aliceCredentialExchangeRecord.id).not.toBeNull() + expect(aliceCredentialExchangeRecord.getTags()).toEqual({ + threadId: aliceCredentialExchangeRecord.threadId, + state: aliceCredentialExchangeRecord.state, + connectionId: aliceConnection.id, + credentialIds: [], + }) + + testLogger.test('Alice sends credential request to Faber') + const negotiateOfferOptions: NegotiateOfferOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V1, + credentialRecordId: aliceCredentialExchangeRecord.id, + credentialFormats: { + indy: { + attributes: newCredentialPreview.attributes, + payload: { + credentialDefinitionId: credDefId, + }, + }, + }, + comment: 'v1 propose credential test', + } + const aliceExchangeCredentialRecord = await aliceAgent.credentials.negotiateOffer(negotiateOfferOptions) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialExchangeRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceExchangeCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + await sleep(5000) + + // Check if the state of fabers credential record did not change + const faberRecord = await faberAgent.credentials.getById(faberCredentialExchangeRecord.id) + faberRecord.assertState(CredentialState.ProposalReceived) + + aliceCredentialExchangeRecord = await aliceAgent.credentials.getById(aliceCredentialExchangeRecord.id) + aliceCredentialExchangeRecord.assertState(CredentialState.ProposalSent) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2connectionless-credentials.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2connectionless-credentials.test.ts new file mode 100644 index 0000000000..a72179ddc1 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2connectionless-credentials.test.ts @@ -0,0 +1,243 @@ +import type { SubjectMessage } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import type { CredentialStateChangedEvent } from '../../../CredentialEvents' +import type { + AcceptOfferOptions, + AcceptRequestOptions, + OfferCredentialOptions, +} from '../../../CredentialsModuleOptions' + +import { ReplaySubject, Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../../../../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../../../../../../tests/transport/SubjectOutboundTransport' +import { prepareForIssuance, waitForCredentialRecordSubject, getBaseConfig } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { Agent } from '../../../../../agent/Agent' +import { AutoAcceptCredential } from '../../../CredentialAutoAcceptType' +import { CredentialEventTypes } from '../../../CredentialEvents' +import { CredentialProtocolVersion } from '../../../CredentialProtocolVersion' +import { CredentialState } from '../../../CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { V2CredentialPreview } from '../V2CredentialPreview' + +const faberConfig = getBaseConfig('Faber connection-less Credentials V2', { + endpoints: ['rxjs:faber'], +}) + +const aliceConfig = getBaseConfig('Alice connection-less Credentials V2', { + endpoints: ['rxjs:alice'], +}) + +const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', +}) + +describe('credentials', () => { + let faberAgent: Agent + let aliceAgent: Agent + let faberReplay: ReplaySubject + let aliceReplay: ReplaySubject + let credDefId: string + let credSchemaId: string + + beforeEach(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + } + faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + const { definition, schema } = await prepareForIssuance(faberAgent, ['name', 'age']) + credDefId = definition.id + credSchemaId = schema.id + + faberReplay = new ReplaySubject() + aliceReplay = new ReplaySubject() + + faberAgent.events + .observable(CredentialEventTypes.CredentialStateChanged) + .subscribe(faberReplay) + aliceAgent.events + .observable(CredentialEventTypes.CredentialStateChanged) + .subscribe(aliceReplay) + }) + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Faber starts with V2 Indy connection-less credential offer to Alice', async () => { + testLogger.test('Faber sends credential offer to Alice') + + const offerOptions: OfferCredentialOptions = { + comment: 'V2 Out of Band offer', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + connectionId: '', + } + // eslint-disable-next-line prefer-const + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer( + offerOptions + ) + + await aliceAgent.receiveMessage(message.toJSON()) + + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + testLogger.test('Alice sends credential request to Faber') + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + } + + const credentialRecord = await aliceAgent.credentials.acceptOffer(acceptOfferOptions) + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: credentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + const options: AcceptRequestOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + } + faberCredentialRecord = await faberAgent.credentials.acceptRequest(options) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Alice sends credential ack to Faber') + aliceCredentialRecord = await aliceAgent.credentials.acceptCredential(aliceCredentialRecord.id) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyCredential': { + schemaId: credSchemaId, + }, + }, + }, + state: CredentialState.Done, + threadId: expect.any(String), + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyCredential': { + schemaId: credSchemaId, + }, + }, + }, + state: CredentialState.Done, + threadId: expect.any(String), + }) + }) + + test('Faber starts with V2 Indy connection-less credential offer to Alice with auto-accept enabled', async () => { + const offerOptions: OfferCredentialOptions = { + comment: 'V2 Out of Band offer', + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + connectionId: '', + } + // eslint-disable-next-line prefer-const + let { message, credentialRecord: faberCredentialRecord } = await faberAgent.credentials.createOutOfBandOffer( + offerOptions + ) + + // Receive Message + await aliceAgent.receiveMessage(message.toJSON()) + + // Wait for it to be processed + let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + autoAcceptCredential: AutoAcceptCredential.ContentApproved, + } + + await aliceAgent.credentials.acceptOffer(acceptOfferOptions) + + aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + faberCredentialRecord = await waitForCredentialRecordSubject(faberReplay, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyCredential': { + schemaId: credSchemaId, + }, + }, + }, + state: CredentialState.Done, + threadId: expect.any(String), + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + threadId: expect.any(String), + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-architecture.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-architecture.test.ts new file mode 100644 index 0000000000..e122206d1f --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-architecture.test.ts @@ -0,0 +1,121 @@ +import type { ProposeCredentialOptions } from '../../../CredentialsModuleOptions' +import type { CredentialFormatService } from '../../../formats/CredentialFormatService' +import type { + FormatServiceProposeCredentialFormats, + IndyProposeCredentialFormat, +} from '../../../formats/models/CredentialFormatServiceOptions' +import type { CredentialService } from '../../../services/CredentialService' + +import { getBaseConfig } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { CredentialProtocolVersion } from '../../../CredentialProtocolVersion' +import { CredentialsModule } from '../../../CredentialsModule' +import { CredentialFormatType } from '../../../CredentialsModuleOptions' +import { V1CredentialPreview } from '../../v1/V1CredentialPreview' +import { CredentialMessageBuilder } from '../CredentialMessageBuilder' + +const { config, agentDependencies: dependencies } = getBaseConfig('Format Service Test') + +const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', +}) + +const testAttributes: IndyProposeCredentialFormat = { + attributes: credentialPreview.attributes, + payload: { + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, +} + +const proposal: ProposeCredentialOptions = { + connectionId: '', + protocolVersion: CredentialProtocolVersion.V1, + credentialFormats: { + indy: testAttributes, + }, + comment: 'v2 propose credential test', +} + +const multiFormatProposal: ProposeCredentialOptions = { + connectionId: '', + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: testAttributes, + }, + comment: 'v2 propose credential test', +} + +describe('V2 Credential Architecture', () => { + const agent = new Agent(config, dependencies) + const container = agent.injectionContainer + const api = container.resolve(CredentialsModule) + + describe('Credential Service', () => { + test('returns the correct credential service for a protocol version 1.0', () => { + const version: CredentialProtocolVersion = CredentialProtocolVersion.V1 + expect(container.resolve(CredentialsModule)).toBeInstanceOf(CredentialsModule) + const service: CredentialService = api.getService(version) + expect(service.getVersion()).toEqual(CredentialProtocolVersion.V1) + }) + + test('returns the correct credential service for a protocol version 2.0', () => { + const version: CredentialProtocolVersion = CredentialProtocolVersion.V2 + const service: CredentialService = api.getService(version) + expect(service.getVersion()).toEqual(CredentialProtocolVersion.V2) + }) + }) + + describe('Credential Format Service', () => { + test('returns the correct credential format service for indy', () => { + const version: CredentialProtocolVersion = CredentialProtocolVersion.V2 + const service: CredentialService = api.getService(version) + const formatService: CredentialFormatService = service.getFormatService(CredentialFormatType.Indy) + expect(formatService).not.toBeNull() + const type: string = formatService.constructor.name + expect(type).toEqual('IndyCredentialFormatService') + }) + + test('propose credential format service returns correct format and filters~attach', async () => { + const version: CredentialProtocolVersion = CredentialProtocolVersion.V2 + const service: CredentialService = api.getService(version) + const formatService: CredentialFormatService = service.getFormatService(CredentialFormatType.Indy) + const { format: formats, attachment: filtersAttach } = await formatService.createProposal(proposal) + + expect(formats.attachId.length).toBeGreaterThan(0) + expect(formats.format).toEqual('hlindy/cred-filter@v2.0') + expect(filtersAttach).toBeTruthy() + }) + test('propose credential format service transforms and validates CredPropose payload correctly', async () => { + const version: CredentialProtocolVersion = CredentialProtocolVersion.V2 + const service: CredentialService = api.getService(version) + const formatService: CredentialFormatService = service.getFormatService(CredentialFormatType.Indy) + const { format: formats, attachment: filtersAttach } = await formatService.createProposal(proposal) + + expect(formats.attachId.length).toBeGreaterThan(0) + expect(formats.format).toEqual('hlindy/cred-filter@v2.0') + expect(filtersAttach).toBeTruthy() + }) + test('propose credential format service creates message with multiple formats', async () => { + const version: CredentialProtocolVersion = CredentialProtocolVersion.V2 + const service: CredentialService = api.getService(version) + + const credFormats: FormatServiceProposeCredentialFormats = + multiFormatProposal.credentialFormats as FormatServiceProposeCredentialFormats + const formats: CredentialFormatService[] = service.getFormats(credFormats) + expect(formats.length).toBe(1) // for now will be added to with jsonld + const messageBuilder: CredentialMessageBuilder = new CredentialMessageBuilder() + + const v2Proposal = await messageBuilder.createProposal(formats, multiFormatProposal) + + expect(v2Proposal.message.formats.length).toBe(1) + expect(v2Proposal.message.formats[0].format).toEqual('hlindy/cred-filter@v2.0') + // expect(v2Proposal.message.formats[1].format).toEqual('aries/ld-proof-vc-detail@v1.0') + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-auto-accept.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-auto-accept.test.ts new file mode 100644 index 0000000000..3a7fc9bb7f --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-auto-accept.test.ts @@ -0,0 +1,477 @@ +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections' +import type { + AcceptOfferOptions, + AcceptProposalOptions, + NegotiateOfferOptions, + NegotiateProposalOptions, + OfferCredentialOptions, + ProposeCredentialOptions, +} from '../../../CredentialsModuleOptions' +import type { CredPropose } from '../../../formats/models/CredPropose' +import type { Schema } from 'indy-sdk' + +import { AriesFrameworkError } from '../../../../../../src/error/AriesFrameworkError' +import { setupCredentialTests, waitForCredentialRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { sleep } from '../../../../../utils/sleep' +import { AutoAcceptCredential } from '../../../CredentialAutoAcceptType' +import { CredentialProtocolVersion } from '../../../CredentialProtocolVersion' +import { CredentialState } from '../../../CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { V2CredentialPreview } from '../V2CredentialPreview' + +describe('credentials', () => { + let faberAgent: Agent + let aliceAgent: Agent + let credDefId: string + let schema: Schema + let faberConnection: ConnectionRecord + let aliceConnection: ConnectionRecord + // let faberCredentialRecord: CredentialRecord + let aliceCredentialRecord: CredentialExchangeRecord + const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + const newCredentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', + }) + + describe('Auto accept on `always`', () => { + beforeAll(async () => { + ;({ faberAgent, aliceAgent, credDefId, schema, faberConnection, aliceConnection } = await setupCredentialTests( + 'faber agent: always v2', + 'alice agent: always v2', + AutoAcceptCredential.Always + )) + }) + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + // ============================== + // TESTS v2 BEGIN + // ========================== + test('Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on `always`', async () => { + testLogger.test('Alice sends credential proposal to Faber') + const schemaId = schema.id + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + payload: { + schemaIssuerDid: faberAgent.publicDid?.did, + schemaName: schema.name, + schemaVersion: schema.version, + schemaId: schema.id, + issuerDid: faberAgent.publicDid?.did, + credentialDefinitionId: credDefId, + }, + }, + }, + comment: 'v propose credential test', + } + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + testLogger.test('Faber waits for credential ack from Alice') + aliceCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyCredential': { + schemaId, + }, + }, + }, + state: CredentialState.Done, + }) + }) + test('Faber starts with V2 credential offer to Alice, both with autoAcceptCredential on `always`', async () => { + testLogger.test('Faber sends V2 credential offer to Alice as start of protocol process') + const schemaId = schema.id + const offerOptions: OfferCredentialOptions = { + comment: 'V2 Offer Credential', + connectionId: faberConnection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + const faberCredentialExchangeRecord: CredentialExchangeRecord = await faberAgent.credentials.offerCredential( + offerOptions + ) + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + testLogger.test('Faber waits for credential ack from Alice') + const faberCredentialRecord: CredentialExchangeRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyRequest': expect.any(Object), + '_internal/indyCredential': { + schemaId, + }, + }, + }, + state: CredentialState.Done, + }) + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + }) + }) + + describe('Auto accept on `contentApproved`', () => { + beforeAll(async () => { + ;({ faberAgent, aliceAgent, credDefId, schema, faberConnection, aliceConnection } = await setupCredentialTests( + 'faber agent: contentApproved v2', + 'alice agent: contentApproved v2', + AutoAcceptCredential.ContentApproved + )) + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with V2 credential proposal to Faber, both with autoAcceptCredential on `contentApproved`', async () => { + testLogger.test('Alice sends credential proposal to Faber') + const schemaId = schema.id + + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + payload: { + schemaIssuerDid: faberAgent.publicDid?.did, + schemaName: schema.name, + schemaVersion: schema.version, + schemaId: schema.id, + issuerDid: faberAgent.publicDid?.did, + credentialDefinitionId: credDefId, + }, + }, + }, + comment: 'v2 propose credential test', + } + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + testLogger.test('Faber sends credential offer to Alice') + const options: AcceptProposalOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Offer', + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: { + attributes: [], + credentialDefinitionId: credDefId, + }, + }, + } + const faberCredentialExchangeRecord = await faberAgent.credentials.acceptProposal(options) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyRequest': expect.any(Object), + '_internal/indyCredential': { + schemaId, + }, + }, + }, + state: CredentialState.Done, + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyCredential': { + schemaId, + }, + }, + }, + state: CredentialState.Done, + }) + }) + test('Faber starts with V2 credential offer to Alice, both with autoAcceptCredential on `contentApproved`', async () => { + testLogger.test('Faber sends credential offer to Alice') + const schemaId = schema.id + + const offerOptions: OfferCredentialOptions = { + comment: 'some comment about credential', + connectionId: faberConnection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + const faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential(offerOptions) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: aliceCredentialRecord.threadId, + state: aliceCredentialRecord.state, + connectionId: aliceConnection.id, + credentialIds: [], + }) + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + + if (aliceCredentialRecord.connectionId) { + // we do not need to specify connection id in this object + // it is either connectionless or included in the offer message + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + // connectionId: aliceCredentialRecord.connectionId, + // credentialRecordType: CredentialRecordType.Indy, + // protocolVersion: CredentialProtocolVersion.V2, + } + testLogger.test('Alice sends credential request to faber') + const faberCredentialExchangeRecord: CredentialExchangeRecord = await aliceAgent.credentials.acceptOffer( + acceptOfferOptions + ) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + testLogger.test('Faber waits for credential ack from Alice') + const faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.Done, + }) + + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + metadata: { + data: { + '_internal/indyRequest': expect.any(Object), + '_internal/indyCredential': { + schemaId, + }, + }, + }, + state: CredentialState.Done, + }) + + expect(faberCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + state: CredentialState.Done, + }) + } else { + throw new AriesFrameworkError('missing alice connection id') + } + }) + test('Alice starts with V2 credential proposal to Faber, both have autoAcceptCredential on `contentApproved` and attributes did change', async () => { + const credPropose: CredPropose = { + schemaIssuerDid: faberAgent.publicDid?.did, + schemaName: schema.name, + schemaVersion: schema.version, + schemaId: schema.id, + issuerDid: faberAgent.publicDid?.did, + credentialDefinitionId: credDefId, + } + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: { + payload: credPropose, + attributes: credentialPreview.attributes, + }, + }, + comment: 'v2 propose credential test', + } + testLogger.test('Alice sends credential proposal to Faber') + const aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + const negotiateOptions: NegotiateProposalOptions = { + credentialRecordId: faberCredentialRecord.id, + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: { + credentialDefinitionId: credDefId, + attributes: newCredentialPreview.attributes, + }, + }, + } + await faberAgent.credentials.negotiateProposal(negotiateOptions) + + testLogger.test('Alice waits for credential offer from Faber') + + const record = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(record.id).not.toBeNull() + expect(record.getTags()).toEqual({ + threadId: record.threadId, + state: record.state, + connectionId: aliceConnection.id, + credentialIds: [], + }) + + // Check if the state of the credential records did not change + faberCredentialRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) + faberCredentialRecord.assertState(CredentialState.OfferSent) + + const aliceRecord = await aliceAgent.credentials.getById(record.id) + aliceRecord.assertState(CredentialState.OfferReceived) + }) + test('Faber starts with V2 credential offer to Alice, both have autoAcceptCredential on `contentApproved` and attributes did change', async () => { + testLogger.test('Faber sends credential offer to Alice') + const offerOptions: OfferCredentialOptions = { + comment: 'some comment about credential', + connectionId: faberConnection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + const faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential(offerOptions) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: aliceCredentialRecord.threadId, + state: aliceCredentialRecord.state, + connectionId: aliceConnection.id, + credentialIds: [], + }) + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + + testLogger.test('Alice sends credential request to Faber') + const proposeOptions: NegotiateOfferOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V2, + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + indy: { + attributes: newCredentialPreview.attributes, + payload: { + credentialDefinitionId: credDefId, + }, + }, + }, + comment: 'v2 propose credential test', + } + await sleep(5000) + + const aliceExchangeCredentialRecord = await aliceAgent.credentials.negotiateOffer(proposeOptions) + + testLogger.test('Faber waits for credential proposal from Alice') + const faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceExchangeCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + // Check if the state of fabers credential record did not change + const faberRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) + faberRecord.assertState(CredentialState.ProposalReceived) + + aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id) + aliceCredentialRecord.assertState(CredentialState.ProposalSent) + }) + }) +}) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials.propose-offer.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials.propose-offer.test.ts new file mode 100644 index 0000000000..0a2ccd221f --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials.propose-offer.test.ts @@ -0,0 +1,758 @@ +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections' +import type { ServiceAcceptOfferOptions } from '../../../CredentialServiceOptions' +import type { + AcceptOfferOptions, + AcceptProposalOptions, + AcceptRequestOptions, + NegotiateOfferOptions, + NegotiateProposalOptions, + OfferCredentialOptions, + ProposeCredentialOptions, +} from '../../../CredentialsModuleOptions' +import type { CredPropose } from '../../../formats/models/CredPropose' + +import { issueCredential, setupCredentialTests, waitForCredentialRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { IndyHolderService } from '../../../../../modules/indy/services/IndyHolderService' +import { DidCommMessageRepository } from '../../../../../storage' +import { JsonTransformer } from '../../../../../utils' +import { CredentialProtocolVersion } from '../../../CredentialProtocolVersion' +import { CredentialState } from '../../../CredentialState' +import { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import { V1CredentialPreview } from '../../v1/V1CredentialPreview' +import { V1OfferCredentialMessage } from '../../v1/messages/V1OfferCredentialMessage' +import { V2CredentialPreview } from '../V2CredentialPreview' +import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' + +describe('credentials', () => { + let faberAgent: Agent + let aliceAgent: Agent + let credDefId: string + let faberConnection: ConnectionRecord + let aliceConnection: ConnectionRecord + let aliceCredentialRecord: CredentialExchangeRecord + let faberCredentialRecord: CredentialExchangeRecord + let credPropose: CredPropose + + const newCredentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'another x-ray value', + profile_picture: 'another profile picture', + }) + + let didCommMessageRepository: DidCommMessageRepository + beforeAll(async () => { + ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection } = await setupCredentialTests( + 'Faber Agent Credentials', + 'Alice Agent Credential' + )) + credPropose = { + credentialDefinitionId: credDefId, + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + } + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + // ============================== + // TEST v1 BEGIN + // ========================== + test('Alice starts with V1 credential proposal to Faber', async () => { + const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + + const testAttributes = { + attributes: credentialPreview.attributes, + payload: { + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + } + testLogger.test('Alice sends (v1) credential proposal to Faber') + // set the propose options + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V1, + credentialFormats: { + indy: testAttributes, + }, + comment: 'v1 propose credential test', + } + + const credentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + + expect(credentialExchangeRecord.connectionId).toEqual(proposeOptions.connectionId) + expect(credentialExchangeRecord.protocolVersion).toEqual(CredentialProtocolVersion.V1) + expect(credentialExchangeRecord.state).toEqual(CredentialState.ProposalSent) + expect(credentialExchangeRecord.threadId).not.toBeNull() + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + const options: AcceptProposalOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V1 Indy Proposal', + credentialFormats: { + indy: { + credentialDefinitionId: credDefId, + attributes: credentialPreview.attributes, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal(options) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const offerMessage = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberCredentialRecord.id, + messageClass: V1OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', + comment: 'V1 Indy Proposal', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + 'offers~attach': expect.any(Array), + }) + // below values are not in json object + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: faberCredentialRecord.threadId, + connectionId: aliceCredentialRecord.connectionId, + state: aliceCredentialRecord.state, + credentialIds: [], + }) + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + if (aliceCredentialRecord.connectionId) { + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + } + const offerCredentialExchangeRecord: CredentialExchangeRecord = await aliceAgent.credentials.acceptOffer( + acceptOfferOptions + ) + + expect(offerCredentialExchangeRecord.connectionId).toEqual(proposeOptions.connectionId) + expect(offerCredentialExchangeRecord.protocolVersion).toEqual(CredentialProtocolVersion.V1) + expect(offerCredentialExchangeRecord.state).toEqual(CredentialState.RequestSent) + expect(offerCredentialExchangeRecord.threadId).not.toBeNull() + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + const options: AcceptRequestOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V1 Indy Credential', + } + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest(options) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + } else { + throw new AriesFrameworkError('Missing Connection Id') + } + }) + // ============================== + // TEST v1 END + // ========================== + + // -------------------------- V2 TEST BEGIN -------------------------------------------- + + test('Alice starts with V2 (Indy format) credential proposal to Faber', async () => { + const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + const testAttributes = { + attributes: credentialPreview.attributes, + payload: { + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + } + testLogger.test('Alice sends (v2) credential proposal to Faber') + // set the propose options + // we should set the version to V1.0 and V2.0 in separate tests, one as a regression test + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: testAttributes, + }, + comment: 'v2 propose credential test', + } + testLogger.test('Alice sends (v2, Indy) credential proposal to Faber') + + const credentialExchangeRecord: CredentialExchangeRecord = await aliceAgent.credentials.proposeCredential( + proposeOptions + ) + + expect(credentialExchangeRecord.connectionId).toEqual(proposeOptions.connectionId) + expect(credentialExchangeRecord.protocolVersion).toEqual(CredentialProtocolVersion.V2) + expect(credentialExchangeRecord.state).toEqual(CredentialState.ProposalSent) + expect(credentialExchangeRecord.threadId).not.toBeNull() + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: credentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + const options: AcceptProposalOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Offer', + credentialFormats: { + indy: { + credentialDefinitionId: credDefId, + attributes: credentialPreview.attributes, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal(options) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const offerMessage = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberCredentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + expect(JsonTransformer.toJSON(offerMessage)).toMatchObject({ + '@type': 'https://didcomm.org/issue-credential/2.0/offer-credential', + comment: 'V2 Indy Offer', + credential_preview: { + '@type': 'https://didcomm.org/issue-credential/2.0/credential-preview', + attributes: [ + { + name: 'name', + 'mime-type': 'text/plain', + value: 'John', + }, + { + name: 'age', + 'mime-type': 'text/plain', + value: '99', + }, + { + name: 'x-ray', + 'mime-type': 'text/plain', + value: 'some x-ray', + }, + { + name: 'profile_picture', + 'mime-type': 'text/plain', + value: 'profile picture', + }, + ], + }, + }) + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: faberCredentialRecord.threadId, + credentialIds: [], + connectionId: aliceCredentialRecord.connectionId, + state: aliceCredentialRecord.state, + }) + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + + if (aliceCredentialRecord.connectionId) { + const acceptOfferOptions: ServiceAcceptOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + indy: undefined, + }, + } + const offerCredentialExchangeRecord: CredentialExchangeRecord = await aliceAgent.credentials.acceptOffer( + acceptOfferOptions + ) + + expect(offerCredentialExchangeRecord.connectionId).toEqual(proposeOptions.connectionId) + expect(offerCredentialExchangeRecord.protocolVersion).toEqual(CredentialProtocolVersion.V2) + expect(offerCredentialExchangeRecord.state).toEqual(CredentialState.RequestSent) + expect(offerCredentialExchangeRecord.threadId).not.toBeNull() + + testLogger.test('Faber waits for credential request from Alice') + await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + testLogger.test('Faber sends credential to Alice') + + const options: AcceptRequestOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + } + await faberAgent.credentials.acceptRequest(options) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + await aliceAgent.credentials.acceptCredential(aliceCredentialRecord.id) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + } else { + throw new AriesFrameworkError('Missing Connection Id') + } + }) + + test('Ensure missing attributes are caught if absent from in V2 (Indy) Proposal Message', async () => { + // Note missing attributes... + const testAttributes = { + payload: { + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + } + testLogger.test('Alice sends (v2) credential proposal to Faber') + // set the propose options + // we should set the version to V1.0 and V2.0 in separate tests, one as a regression test + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: testAttributes, + }, + comment: 'v2 propose credential test', + } + testLogger.test('Alice sends (v2, Indy) credential proposal to Faber') + + await expect(aliceAgent.credentials.proposeCredential(proposeOptions)).rejects.toThrow( + 'Missing attributes from credential proposal' + ) + }) + + test('Faber Issues Credential which is then deleted from Alice`s wallet', async () => { + const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + + const { holderCredential } = await issueCredential({ + issuerAgent: faberAgent, + issuerConnectionId: faberConnection.id, + holderAgent: aliceAgent, + credentialTemplate: { + credentialDefinitionId: credDefId, + comment: 'some comment about credential', + preview: credentialPreview, + }, + }) + // test that delete credential removes from both repository and wallet + // latter is tested by spying on holder service (Indy) to + // see if deleteCredential is called + const holderService = aliceAgent.injectionContainer.resolve(IndyHolderService) + + const deleteCredentialSpy = jest.spyOn(holderService, 'deleteCredential') + await aliceAgent.credentials.deleteById(holderCredential.id, { deleteAssociatedCredentials: true }) + expect(deleteCredentialSpy).toHaveBeenNthCalledWith(1, holderCredential.credentials[0].credentialRecordId) + + return expect(aliceAgent.credentials.getById(holderCredential.id)).rejects.toThrowError( + `CredentialRecord: record with id ${holderCredential.id} not found.` + ) + }) + + test('Alice starts with propose - Faber counter offer - Alice second proposal- Faber sends second offer', async () => { + // proposeCredential -> negotiateProposal -> negotiateOffer -> negotiateProposal -> acceptOffer -> acceptRequest -> DONE (credential issued) + const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: { + payload: credPropose, + attributes: credentialPreview.attributes, + }, + }, + comment: 'v2 propose credential test', + } + testLogger.test('Alice sends credential proposal to Faber') + let aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + const negotiateOptions: NegotiateProposalOptions = { + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + indy: { + credentialDefinitionId: credDefId, + attributes: newCredentialPreview.attributes, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + faberCredentialRecord = await faberAgent.credentials.negotiateProposal(negotiateOptions) + + testLogger.test('Alice waits for credential offer from Faber') + + let record = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + // below values are not in json object + expect(record.id).not.toBeNull() + expect(record.getTags()).toEqual({ + threadId: record.threadId, + state: record.state, + connectionId: aliceConnection.id, + credentialIds: [], + }) + + // // Check if the state of the credential records did not change + faberCredentialRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) + faberCredentialRecord.assertState(CredentialState.OfferSent) + + const aliceRecord = await aliceAgent.credentials.getById(record.id) + aliceRecord.assertState(CredentialState.OfferReceived) + + // // second proposal + const negotiateOfferOptions: NegotiateOfferOptions = { + credentialRecordId: aliceRecord.id, + credentialFormats: { + indy: { + payload: credPropose, + attributes: newCredentialPreview.attributes, + }, + }, + connectionId: aliceConnection.id, + } + aliceCredentialExchangeRecord = await aliceAgent.credentials.negotiateOffer(negotiateOfferOptions) + + // aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + expect(aliceCredentialExchangeRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + faberCredentialRecord = await faberAgent.credentials.negotiateProposal(negotiateOptions) + + testLogger.test('Alice waits for credential offer from Faber') + + record = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: aliceCredentialExchangeRecord.id, + } + const offerCredentialExchangeRecord: CredentialExchangeRecord = await aliceAgent.credentials.acceptOffer( + acceptOfferOptions + ) + + expect(offerCredentialExchangeRecord.connectionId).toEqual(proposeOptions.connectionId) + expect(offerCredentialExchangeRecord.protocolVersion).toEqual(CredentialProtocolVersion.V2) + expect(offerCredentialExchangeRecord.state).toEqual(CredentialState.RequestSent) + expect(offerCredentialExchangeRecord.threadId).not.toBeNull() + + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialExchangeRecord.threadId, + state: CredentialState.RequestReceived, + }) + testLogger.test('Faber sends credential to Alice') + + const options: AcceptRequestOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + } + await faberAgent.credentials.acceptRequest(options) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + + // testLogger.test('Alice sends credential ack to Faber') + await aliceAgent.credentials.acceptCredential(aliceCredentialRecord.id) + + testLogger.test('Faber waits for credential ack from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.Done, + }) + expect(aliceCredentialRecord).toMatchObject({ + type: CredentialExchangeRecord.type, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: expect.any(String), + connectionId: expect.any(String), + state: CredentialState.CredentialReceived, + }) + }) + + test('Faber starts with offer - Alice counter proposal - Faber second offer - Alice sends second proposal', async () => { + testLogger.test('Faber sends credential offer to Alice') + const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + const offerOptions: OfferCredentialOptions = { + comment: 'some comment about credential', + connectionId: faberConnection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + const faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential(offerOptions) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const negotiateOfferOptions: NegotiateOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + credentialFormats: { + indy: { + payload: credPropose, + attributes: newCredentialPreview.attributes, + }, + }, + connectionId: aliceConnection.id, + } + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer(negotiateOfferOptions) + + // aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + const negotiateOptions: NegotiateProposalOptions = { + credentialRecordId: faberCredentialRecord.id, + credentialFormats: { + indy: { + credentialDefinitionId: credDefId, + attributes: newCredentialPreview.attributes, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + faberCredentialRecord = await faberAgent.credentials.negotiateProposal(negotiateOptions) + + testLogger.test('Alice waits for credential offer from Faber') + + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer(negotiateOfferOptions) + + // aliceCredentialExchangeRecord = await aliceAgent.credentials.proposeCredential(proposeOptions) + expect(aliceCredentialRecord.state).toBe(CredentialState.ProposalSent) + + testLogger.test('Faber waits for credential proposal from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.ProposalReceived, + }) + + const options: AcceptProposalOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Proposal', + credentialFormats: { + indy: { + credentialDefinitionId: credDefId, + attributes: credentialPreview.attributes, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + + testLogger.test('Faber sends credential offer to Alice') + await faberAgent.credentials.acceptProposal(options) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.OfferReceived, + }) + + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: aliceCredentialRecord.id, + } + const offerCredentialExchangeRecord: CredentialExchangeRecord = await aliceAgent.credentials.acceptOffer( + acceptOfferOptions + ) + + expect(offerCredentialExchangeRecord.protocolVersion).toEqual(CredentialProtocolVersion.V2) + expect(offerCredentialExchangeRecord.state).toEqual(CredentialState.RequestSent) + expect(offerCredentialExchangeRecord.threadId).not.toBeNull() + testLogger.test('Faber waits for credential request from Alice') + faberCredentialRecord = await waitForCredentialRecord(faberAgent, { + threadId: aliceCredentialRecord.threadId, + state: CredentialState.RequestReceived, + }) + + const acceptRequestOptions: AcceptRequestOptions = { + credentialRecordId: faberCredentialRecord.id, + comment: 'V2 Indy Credential', + } + testLogger.test('Faber sends credential to Alice') + await faberAgent.credentials.acceptRequest(acceptRequestOptions) + + testLogger.test('Alice waits for credential from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialRecord.threadId, + state: CredentialState.CredentialReceived, + }) + }) + + test('Faber starts with V2 offer; Alice declines', async () => { + testLogger.test('Faber sends credential offer to Alice') + const credentialPreview = V2CredentialPreview.fromRecord({ + name: 'John', + age: '99', + 'x-ray': 'some x-ray', + profile_picture: 'profile picture', + }) + const offerOptions: OfferCredentialOptions = { + comment: 'some comment about credential', + connectionId: faberConnection.id, + credentialFormats: { + indy: { + attributes: credentialPreview.attributes, + credentialDefinitionId: credDefId, + }, + }, + protocolVersion: CredentialProtocolVersion.V2, + } + const faberCredentialExchangeRecord = await faberAgent.credentials.offerCredential(offerOptions) + + testLogger.test('Alice waits for credential offer from Faber') + aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { + threadId: faberCredentialExchangeRecord.threadId, + state: CredentialState.OfferReceived, + }) + // below values are not in json object + expect(aliceCredentialRecord.id).not.toBeNull() + expect(aliceCredentialRecord.getTags()).toEqual({ + threadId: aliceCredentialRecord.threadId, + state: aliceCredentialRecord.state, + connectionId: aliceConnection.id, + credentialIds: [], + }) + expect(aliceCredentialRecord.type).toBe(CredentialExchangeRecord.type) + testLogger.test('Alice declines offer') + if (aliceCredentialRecord.id) { + await aliceAgent.credentials.declineOffer(aliceCredentialRecord.id) + } else { + throw new AriesFrameworkError('Missing credential record id') + } + }) +}) +// -------------------------- V2 TEST END -------------------------------------------- diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialAckHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialAckHandler.ts new file mode 100644 index 0000000000..3794e260ba --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialAckHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { V2CredentialService } from '../V2CredentialService' + +import { V2CredentialAckMessage } from '../messages/V2CredentialAckMessage' + +export class V2CredentialAckHandler implements Handler { + private credentialService: V2CredentialService + public supportedMessages = [V2CredentialAckMessage] + + public constructor(credentialService: V2CredentialService) { + this.credentialService = credentialService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.credentialService.processAck(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialProblemReportHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialProblemReportHandler.ts new file mode 100644 index 0000000000..914c902691 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2CredentialProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { V2CredentialService } from '../V2CredentialService' + +import { V2CredentialProblemReportMessage } from '../messages/V2CredentialProblemReportMessage' + +export class V2CredentialProblemReportHandler implements Handler { + private credentialService: V2CredentialService + public supportedMessages = [V2CredentialProblemReportMessage] + + public constructor(credentialService: V2CredentialService) { + this.credentialService = credentialService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.credentialService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts new file mode 100644 index 0000000000..dc820e3843 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2IssueCredentialHandler.ts @@ -0,0 +1,77 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V2CredentialService } from '../V2CredentialService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { V2IssueCredentialMessage } from '../messages/V2IssueCredentialMessage' +import { V2RequestCredentialMessage } from '../messages/V2RequestCredentialMessage' + +export class V2IssueCredentialHandler implements Handler { + private credentialService: V2CredentialService + private agentConfig: AgentConfig + private didCommMessageRepository: DidCommMessageRepository + + public supportedMessages = [V2IssueCredentialMessage] + + public constructor( + credentialService: V2CredentialService, + agentConfig: AgentConfig, + didCommMessageRepository: DidCommMessageRepository + ) { + this.credentialService = credentialService + this.agentConfig = agentConfig + this.didCommMessageRepository = didCommMessageRepository + } + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialService.processCredential(messageContext) + const credentialMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2IssueCredentialMessage, + }) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2RequestCredentialMessage, + }) + + if (!credentialMessage) { + throw new AriesFrameworkError(`Missing credential message from credential record ${credentialRecord.id}`) + } + + const shouldAutoRespond = this.credentialService.shouldAutoRespondToCredential(credentialRecord, credentialMessage) + if (shouldAutoRespond) { + return await this.createAck(credentialRecord, messageContext, requestMessage ?? undefined, credentialMessage) + } + } + + private async createAck( + record: CredentialExchangeRecord, + messageContext: HandlerInboundMessage, + requestMessage?: V2RequestCredentialMessage, + credentialMessage?: V2IssueCredentialMessage + ) { + this.agentConfig.logger.info( + `Automatically sending acknowledgement with autoAccept on ${this.agentConfig.autoAcceptCredentials}` + ) + const { message } = await this.credentialService.createAck(record) + + if (messageContext.connection) { + return createOutboundMessage(messageContext.connection, message) + } else if (requestMessage?.service && credentialMessage?.service) { + const recipientService = credentialMessage.service + const ourService = requestMessage.service + + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + + this.agentConfig.logger.error(`Could not automatically create credential ack`) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts new file mode 100644 index 0000000000..0e6fee4af7 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts @@ -0,0 +1,118 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { MediationRecipientService } from '../../../../routing/services/MediationRecipientService' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V2CredentialService } from '../V2CredentialService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { ServiceDecorator } from '../../../../../decorators/service/ServiceDecorator' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { DidCommMessageRole } from '../../../../../storage' +import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' +import { V2ProposeCredentialMessage } from '../messages/V2ProposeCredentialMessage' + +export class V2OfferCredentialHandler implements Handler { + private credentialService: V2CredentialService + private agentConfig: AgentConfig + private mediationRecipientService: MediationRecipientService + public supportedMessages = [V2OfferCredentialMessage] + + private didCommMessageRepository: DidCommMessageRepository + + public constructor( + credentialService: V2CredentialService, + agentConfig: AgentConfig, + mediationRecipientService: MediationRecipientService, + didCommMessageRepository: DidCommMessageRepository + ) { + this.credentialService = credentialService + this.agentConfig = agentConfig + this.mediationRecipientService = mediationRecipientService + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialService.processOffer(messageContext) + + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + const proposeMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + }) + + if (!offerMessage) { + throw new AriesFrameworkError('Missing offer message in V2OfferCredentialHandler') + } + + const shouldAutoRespond = this.credentialService.shouldAutoRespondToOffer( + credentialRecord, + offerMessage, + proposeMessage ?? undefined + ) + + if (shouldAutoRespond) { + return await this.createRequest(credentialRecord, messageContext, offerMessage) + } + } + + private async createRequest( + record: CredentialExchangeRecord, + messageContext: HandlerInboundMessage, + offerMessage?: V2OfferCredentialMessage + ) { + this.agentConfig.logger.info( + `Automatically sending request with autoAccept on ${this.agentConfig.autoAcceptCredentials}` + ) + + if (messageContext.connection) { + const { message, credentialRecord } = await this.credentialService.createRequest( + record, + {}, + messageContext.connection.did + ) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Receiver, + associatedRecordId: credentialRecord.id, + }) + return createOutboundMessage(messageContext.connection, message) + } else if (offerMessage?.service) { + const routing = await this.mediationRecipientService.getRouting() + const ourService = new ServiceDecorator({ + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.verkey], + routingKeys: routing.routingKeys, + }) + const recipientService = offerMessage.service + + const { message, credentialRecord } = await this.credentialService.createRequest( + record, + {}, + ourService.recipientKeys[0] + ) + + // Set and save ~service decorator to record (to remember our verkey) + message.service = ourService + + await this.credentialService.update(credentialRecord) + await this.didCommMessageRepository.saveAgentMessage({ + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: credentialRecord.id, + }) + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + + this.agentConfig.logger.error(`Could not automatically create credential request`) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2ProposeCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2ProposeCredentialHandler.ts new file mode 100644 index 0000000000..4e97bc36cf --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2ProposeCredentialHandler.ts @@ -0,0 +1,60 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { HandlerAutoAcceptOptions } from '../../../formats/models/CredentialFormatServiceOptions' +import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' +import type { V2CredentialService } from '../V2CredentialService' + +import { createOutboundMessage } from '../../../../../agent/helpers' +import { V2ProposeCredentialMessage } from '../messages/V2ProposeCredentialMessage' + +export class V2ProposeCredentialHandler implements Handler { + private credentialService: V2CredentialService + private agentConfig: AgentConfig + private didCommMessageRepository: DidCommMessageRepository + + public supportedMessages = [V2ProposeCredentialMessage] + + public constructor( + credentialService: V2CredentialService, + agentConfig: AgentConfig, + didCommMessageRepository: DidCommMessageRepository + ) { + this.credentialService = credentialService + this.agentConfig = agentConfig + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialService.processProposal(messageContext) + + const handlerOptions: HandlerAutoAcceptOptions = { + credentialRecord, + autoAcceptType: this.agentConfig.autoAcceptCredentials, + } + + const shouldAutoRespond = await this.credentialService.shouldAutoRespondToProposal(handlerOptions) + if (shouldAutoRespond) { + return await this.createOffer(credentialRecord, messageContext) + } + } + + private async createOffer( + credentialRecord: CredentialExchangeRecord, + messageContext: HandlerInboundMessage + ) { + this.agentConfig.logger.info( + `Automatically sending offer with autoAccept on ${this.agentConfig.autoAcceptCredentials}` + ) + + if (!messageContext.connection) { + this.agentConfig.logger.error('No connection on the messageContext, aborting auto accept') + return + } + + const message = await this.credentialService.createOfferAsResponse(credentialRecord) + + return createOutboundMessage(messageContext.connection, message) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts new file mode 100644 index 0000000000..a94fac4c91 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts @@ -0,0 +1,96 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler } from '../../../../../agent/Handler' +import type { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { AcceptRequestOptions } from '../../../CredentialsModuleOptions' +import type { CredentialExchangeRecord } from '../../../repository' +import type { V2CredentialService } from '../V2CredentialService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' +import { V2ProposeCredentialMessage } from '../messages/V2ProposeCredentialMessage' +import { V2RequestCredentialMessage } from '../messages/V2RequestCredentialMessage' + +export class V2RequestCredentialHandler implements Handler { + private credentialService: V2CredentialService + private agentConfig: AgentConfig + private didCommMessageRepository: DidCommMessageRepository + public supportedMessages = [V2RequestCredentialMessage] + + public constructor( + credentialService: V2CredentialService, + agentConfig: AgentConfig, + didCommMessageRepository: DidCommMessageRepository + ) { + this.credentialService = credentialService + this.agentConfig = agentConfig + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: InboundMessageContext) { + const credentialRecord = await this.credentialService.processRequest(messageContext) + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2RequestCredentialMessage, + }) + + if (!requestMessage) { + throw new AriesFrameworkError('Missing request message in V2RequestCredentialHandler') + } + const offerMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2OfferCredentialMessage, + }) + + const proposeMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: credentialRecord.id, + messageClass: V2ProposeCredentialMessage, + }) + + const shouldAutoRespond = this.credentialService.shouldAutoRespondToRequest( + credentialRecord, + requestMessage, + proposeMessage ?? undefined, + offerMessage ?? undefined + ) + if (shouldAutoRespond) { + return await this.createCredential(credentialRecord, messageContext, requestMessage, offerMessage) + } + } + + private async createCredential( + record: CredentialExchangeRecord, + messageContext: InboundMessageContext, + requestMessage: V2RequestCredentialMessage, + offerMessage?: V2OfferCredentialMessage | null + ) { + this.agentConfig.logger.info( + `Automatically sending credential with autoAccept on ${this.agentConfig.autoAcceptCredentials}` + ) + const options: AcceptRequestOptions = { + comment: requestMessage.comment, + autoAcceptCredential: record.autoAcceptCredential, + credentialRecordId: record.id, + } + + const { message, credentialRecord } = await this.credentialService.createCredential(record, options) + if (messageContext.connection) { + return createOutboundMessage(messageContext.connection, message) + } else if (requestMessage.service && offerMessage?.service) { + const recipientService = requestMessage.service + const ourService = offerMessage.service + + // Set ~service, update message in record (for later use) + message.setService(ourService) + await this.credentialService.update(credentialRecord) + + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + this.agentConfig.logger.error(`Could not automatically create credential request`) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2RevocationNotificationHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2RevocationNotificationHandler.ts new file mode 100644 index 0000000000..54fcdfbc44 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2RevocationNotificationHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { RevocationService } from '../../../services' + +import { V2RevocationNotificationMessage } from '../messages/V2RevocationNotificationMessage' + +export class V2RevocationNotificationHandler implements Handler { + private revocationService: RevocationService + public supportedMessages = [V2RevocationNotificationMessage] + + public constructor(revocationService: RevocationService) { + this.revocationService = revocationService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.revocationService.v2ProcessRevocationNotification(messageContext) + } +} diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/index.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/index.ts new file mode 100644 index 0000000000..9a291bf883 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/index.ts @@ -0,0 +1,5 @@ +export * from './V2CredentialAckHandler' +export * from './V2IssueCredentialHandler' +export * from './V2OfferCredentialHandler' +export * from './V2ProposeCredentialHandler' +export * from './V2RequestCredentialHandler' diff --git a/packages/core/src/modules/credentials/protocol/v2/index.ts b/packages/core/src/modules/credentials/protocol/v2/index.ts new file mode 100644 index 0000000000..4a587629e5 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/index.ts @@ -0,0 +1,2 @@ +export * from '../../CredentialServiceOptions' +export * from './V2CredentialService' diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialAckMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialAckMessage.ts new file mode 100644 index 0000000000..06b6b9020e --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialAckMessage.ts @@ -0,0 +1,23 @@ +import type { AckMessageOptions } from '../../../../common' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { AckMessage } from '../../../../common' + +export type CredentialAckMessageOptions = AckMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks + */ +export class V2CredentialAckMessage extends AckMessage { + /** + * Create new CredentialAckMessage instance. + * @param options + */ + public constructor(options: CredentialAckMessageOptions) { + super(options) + } + + @IsValidMessageType(V2CredentialAckMessage.type) + public readonly type = V2CredentialAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/ack') +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialProblemReportMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialProblemReportMessage.ts new file mode 100644 index 0000000000..d4b9c817ee --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2CredentialProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '../../../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { ProblemReportMessage } from '../../../../problem-reports/messages/ProblemReportMessage' + +export type CredentialProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V2CredentialProblemReportMessage extends ProblemReportMessage { + /** + * Create new CredentialProblemReportMessage instance. + * @param options + */ + public constructor(options: CredentialProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(V2CredentialProblemReportMessage.type) + public readonly type = V2CredentialProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/problem-report') +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts new file mode 100644 index 0000000000..9005fff339 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2IssueCredentialMessage.ts @@ -0,0 +1,49 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../formats/models/CredentialFormatServiceOptions' + +export interface V2IssueCredentialMessageProps { + id?: string + comment?: string + formats: CredentialFormatSpec[] + credentialsAttach: Attachment[] +} + +export class V2IssueCredentialMessage extends AgentMessage { + public constructor(options: V2IssueCredentialMessageProps) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.formats = options.formats + this.messageAttachment = options.credentialsAttach + } + } + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + // @IsInstance(CredentialFormatSpec, { each: true }) -> this causes message validation to fail + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V2IssueCredentialMessage.type) + public readonly type = V2IssueCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/issue-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'credentials~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public messageAttachment!: Attachment[] +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2OfferCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2OfferCredentialMessage.ts new file mode 100644 index 0000000000..d8c19fcdb6 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2OfferCredentialMessage.ts @@ -0,0 +1,64 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../formats/models/CredentialFormatServiceOptions' +import { V2CredentialPreview } from '../V2CredentialPreview' + +export interface V2OfferCredentialMessageOptions { + id?: string + formats: CredentialFormatSpec[] + offerAttachments: Attachment[] + credentialPreview: V2CredentialPreview + replacementId?: string + comment?: string +} + +export class V2OfferCredentialMessage extends AgentMessage { + public constructor(options: V2OfferCredentialMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.formats = options.formats + this.credentialPreview = options.credentialPreview + this.messageAttachment = options.offerAttachments + } + } + + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + // @IsInstance(CredentialFormatSpec, { each: true }) -> this causes message validation to fail + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V2OfferCredentialMessage.type) + public readonly type = V2OfferCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/offer-credential') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'credential_preview' }) + @Type(() => V2CredentialPreview) + @ValidateNested() + @IsInstance(V2CredentialPreview) + public credentialPreview?: V2CredentialPreview + + @Expose({ name: 'offers~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public messageAttachment!: Attachment[] + + @Expose({ name: 'replacement_id' }) + @IsString() + @IsOptional() + public replacementId?: string +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts new file mode 100644 index 0000000000..87d4cece24 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2ProposeCredentialMessage.ts @@ -0,0 +1,64 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../formats/models/CredentialFormatServiceOptions' +import { V2CredentialPreview } from '../V2CredentialPreview' + +export interface V2ProposeCredentialMessageProps { + id?: string + formats: CredentialFormatSpec[] + filtersAttach: Attachment[] + comment?: string + credentialProposal?: V2CredentialPreview + attachments?: Attachment[] +} + +export class V2ProposeCredentialMessage extends AgentMessage { + public constructor(props: V2ProposeCredentialMessageProps) { + super() + if (props) { + this.id = props.id ?? this.generateId() + this.comment = props.comment + this.credentialProposal = props.credentialProposal + this.formats = props.formats + this.messageAttachment = props.filtersAttach + this.appendedAttachments = props.attachments + } + } + + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V2ProposeCredentialMessage.type) + public readonly type = V2ProposeCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/propose-credential') + + @Expose({ name: 'credential_proposal' }) + @Type(() => V2CredentialPreview) + @ValidateNested() + @IsOptional() + @IsInstance(V2CredentialPreview) + public credentialProposal?: V2CredentialPreview + + @Expose({ name: 'filters~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public messageAttachment!: Attachment[] + + /** + * Human readable information about this Credential Proposal, + * so the proposal can be evaluated by human judgment. + */ + @IsOptional() + @IsString() + public comment?: string +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts new file mode 100644 index 0000000000..f9e080922b --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2RequestCredentialMessage.ts @@ -0,0 +1,53 @@ +import { Expose, Type } from 'class-transformer' +import { IsArray, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { CredentialFormatSpec } from '../../../formats/models/CredentialFormatServiceOptions' + +export interface V2RequestCredentialMessageOptions { + id?: string + formats: CredentialFormatSpec[] + requestsAttach: Attachment[] + comment?: string +} + +export class V2RequestCredentialMessage extends AgentMessage { + public constructor(options: V2RequestCredentialMessageOptions) { + super() + if (options) { + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.formats = options.formats + this.messageAttachment = options.requestsAttach + } + } + + @Type(() => CredentialFormatSpec) + @ValidateNested() + @IsArray() + // @IsInstance(CredentialFormatSpec, { each: true }) -> this causes message validation to fail + public formats!: CredentialFormatSpec[] + + @IsValidMessageType(V2RequestCredentialMessage.type) + public readonly type = V2RequestCredentialMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/issue-credential/2.0/request-credential') + + @Expose({ name: 'requests~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + public messageAttachment!: Attachment[] + + /** + * Human readable information about this Credential Request, + * so the proposal can be evaluated by human judgment. + */ + @IsOptional() + @IsString() + public comment?: string +} diff --git a/packages/core/src/modules/credentials/protocol/v2/messages/V2RevocationNotificationMessage.ts b/packages/core/src/modules/credentials/protocol/v2/messages/V2RevocationNotificationMessage.ts new file mode 100644 index 0000000000..a0d19ba5a3 --- /dev/null +++ b/packages/core/src/modules/credentials/protocol/v2/messages/V2RevocationNotificationMessage.ts @@ -0,0 +1,44 @@ +import type { AckDecorator } from '../../../../../decorators/ack/AckDecorator' + +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +export interface RevocationNotificationMessageV2Options { + revocationFormat: string + credentialId: string + id?: string + comment?: string + pleaseAck?: AckDecorator +} + +export class V2RevocationNotificationMessage extends AgentMessage { + public constructor(options: RevocationNotificationMessageV2Options) { + super() + if (options) { + this.revocationFormat = options.revocationFormat + this.credentialId = options.credentialId + this.id = options.id ?? this.generateId() + this.comment = options.comment + this.pleaseAck = options.pleaseAck + } + } + + @IsValidMessageType(V2RevocationNotificationMessage.type) + public readonly type = V2RevocationNotificationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/revocation_notification/2.0/revoke') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'revocation_format' }) + @IsString() + public revocationFormat!: string + + @Expose({ name: 'credential_id' }) + @IsString() + public credentialId!: string +} diff --git a/packages/core/src/modules/credentials/repository/CredentialRecord.ts b/packages/core/src/modules/credentials/repository/CredentialExchangeRecord.ts similarity index 56% rename from packages/core/src/modules/credentials/repository/CredentialRecord.ts rename to packages/core/src/modules/credentials/repository/CredentialExchangeRecord.ts index 159a854469..dd9da3ec51 100644 --- a/packages/core/src/modules/credentials/repository/CredentialRecord.ts +++ b/packages/core/src/modules/credentials/repository/CredentialExchangeRecord.ts @@ -1,6 +1,10 @@ import type { TagsBase } from '../../../storage/BaseRecord' import type { AutoAcceptCredential } from '../CredentialAutoAcceptType' +import type { CredentialProtocolVersion } from '../CredentialProtocolVersion' import type { CredentialState } from '../CredentialState' +import type { CredentialFormatType } from '../CredentialsModuleOptions' +import type { RevocationNotification } from '../models/RevocationNotification' +import type { CredentialMetadata } from './CredentialMetadataTypes' import { Type } from 'class-transformer' @@ -8,31 +12,26 @@ import { Attachment } from '../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../error' import { BaseRecord } from '../../../storage/BaseRecord' import { uuid } from '../../../utils/uuid' -import { - CredentialPreviewAttribute, - IssueCredentialMessage, - OfferCredentialMessage, - ProposeCredentialMessage, - RequestCredentialMessage, -} from '../messages' -import { CredentialInfo } from '../models/CredentialInfo' - -export interface CredentialRecordProps { +import { CredentialPreviewAttribute } from '../models/CredentialPreviewAttributes' +import { CredentialInfo } from '../protocol/v1/models/CredentialInfo' + +import { CredentialMetadataKeys } from './CredentialMetadataTypes' + +export interface CredentialExchangeRecordProps { id?: string createdAt?: Date state: CredentialState connectionId?: string threadId: string + protocolVersion: CredentialProtocolVersion - credentialId?: string tags?: CustomCredentialTags - proposalMessage?: ProposeCredentialMessage - offerMessage?: OfferCredentialMessage - requestMessage?: RequestCredentialMessage - credentialMessage?: IssueCredentialMessage credentialAttributes?: CredentialPreviewAttribute[] autoAcceptCredential?: AutoAcceptCredential linkedAttachments?: Attachment[] + revocationNotification?: RevocationNotification + errorMessage?: string + credentials?: CredentialRecordBinding[] } export type CustomCredentialTags = TagsBase @@ -40,25 +39,30 @@ export type DefaultCredentialTags = { threadId: string connectionId?: string state: CredentialState + credentialIds: string[] + indyRevocationRegistryId?: string + indyCredentialRevocationId?: string +} + +export interface CredentialRecordBinding { + credentialRecordType: CredentialFormatType + credentialRecordId: string credentialId?: string } -export class CredentialRecord extends BaseRecord { +export class CredentialExchangeRecord extends BaseRecord< + DefaultCredentialTags, + CustomCredentialTags, + CredentialMetadata +> { public connectionId?: string public threadId!: string - public credentialId?: string public state!: CredentialState public autoAcceptCredential?: AutoAcceptCredential - - // message data - @Type(() => ProposeCredentialMessage) - public proposalMessage?: ProposeCredentialMessage - @Type(() => OfferCredentialMessage) - public offerMessage?: OfferCredentialMessage - @Type(() => RequestCredentialMessage) - public requestMessage?: RequestCredentialMessage - @Type(() => IssueCredentialMessage) - public credentialMessage?: IssueCredentialMessage + public revocationNotification?: RevocationNotification + public errorMessage?: string + public protocolVersion!: CredentialProtocolVersion + public credentials!: CredentialRecordBinding[] @Type(() => CredentialPreviewAttribute) public credentialAttributes?: CredentialPreviewAttribute[] @@ -66,10 +70,11 @@ export class CredentialRecord extends BaseRecord Attachment) public linkedAttachments?: Attachment[] + // Type is CredentialRecord on purpose (without Exchange) as this is how the record was initially called. public static readonly type = 'CredentialRecord' - public readonly type = CredentialRecord.type + public readonly type = CredentialExchangeRecord.type - public constructor(props: CredentialRecordProps) { + public constructor(props: CredentialExchangeRecordProps) { super() if (props) { @@ -77,27 +82,35 @@ export class CredentialRecord extends BaseRecord c.credentialRecordId) + } + return { ...this._tags, threadId: this.threadId, connectionId: this.connectionId, state: this.state, - credentialId: this.credentialId, + credentialIds: credentialIds, + indyRevocationRegistryId: metadata?.indyRevocationRegistryId, + indyCredentialRevocationId: metadata?.indyCredentialRevocationId, } } @@ -115,10 +128,18 @@ export class CredentialRecord extends BaseRecord { - public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { - super(CredentialRecord, storageService) +export class CredentialRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService + ) { + super(CredentialExchangeRecord, storageService) } } diff --git a/packages/core/src/modules/credentials/repository/index.ts b/packages/core/src/modules/credentials/repository/index.ts index e4bfece5f8..b7b986ad3e 100644 --- a/packages/core/src/modules/credentials/repository/index.ts +++ b/packages/core/src/modules/credentials/repository/index.ts @@ -1,2 +1,3 @@ -export * from './CredentialRecord' +export * from './CredentialExchangeRecord' export * from './CredentialRepository' +export * from './CredentialMetadataTypes' diff --git a/packages/core/src/modules/credentials/services/CredentialService.ts b/packages/core/src/modules/credentials/services/CredentialService.ts index 8270a33bae..1bb7df046c 100644 --- a/packages/core/src/modules/credentials/services/CredentialService.ts +++ b/packages/core/src/modules/credentials/services/CredentialService.ts @@ -1,716 +1,231 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' import type { AgentMessage } from '../../../agent/AgentMessage' +import type { Dispatcher } from '../../../agent/Dispatcher' +import type { EventEmitter } from '../../../agent/EventEmitter' +import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' import type { Logger } from '../../../logger' -import type { LinkedAttachment } from '../../../utils/LinkedAttachment' -import type { ConnectionRecord } from '../../connections' -import type { AutoAcceptCredential } from '../CredentialAutoAcceptType' -import type { CredentialStateChangedEvent } from '../CredentialEvents' -import type { ProposeCredentialMessageOptions } from '../messages' -import type { CredReqMetadata } from 'indy-sdk' - -import { scoped, Lifecycle } from 'tsyringe' - -import { AgentConfig } from '../../../agent/AgentConfig' -import { EventEmitter } from '../../../agent/EventEmitter' -import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' -import { AriesFrameworkError } from '../../../error' -import { JsonEncoder } from '../../../utils/JsonEncoder' -import { isLinkedAttachment } from '../../../utils/attachment' -import { uuid } from '../../../utils/uuid' -import { AckStatus } from '../../common' -import { ConnectionService } from '../../connections/services/ConnectionService' -import { IndyIssuerService, IndyHolderService } from '../../indy' -import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' -import { CredentialEventTypes } from '../CredentialEvents' -import { CredentialState } from '../CredentialState' -import { CredentialUtils } from '../CredentialUtils' -import { - INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, - INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, - IssueCredentialMessage, - OfferCredentialMessage, - ProposeCredentialMessage, - CredentialPreview, - RequestCredentialMessage, - CredentialAckMessage, - INDY_CREDENTIAL_ATTACHMENT_ID, -} from '../messages' -import { CredentialRepository } from '../repository' -import { CredentialRecord } from '../repository/CredentialRecord' - -@scoped(Lifecycle.ContainerScoped) -export class CredentialService { - private credentialRepository: CredentialRepository - private connectionService: ConnectionService - private ledgerService: IndyLedgerService - private logger: Logger - private indyIssuerService: IndyIssuerService - private indyHolderService: IndyHolderService - private eventEmitter: EventEmitter +import type { DidCommMessageRepository } from '../../../storage' +import type { MediationRecipientService } from '../../routing' +import type { CredentialStateChangedEvent } from './../CredentialEvents' +import type { CredentialProtocolVersion } from './../CredentialProtocolVersion' +import type { + CredentialProtocolMsgReturnType, + DeleteCredentialOptions, + ServiceRequestCredentialOptions, +} from './../CredentialServiceOptions' +import type { + AcceptProposalOptions, + AcceptRequestOptions, + CredentialFormatType, + NegotiateOfferOptions, + NegotiateProposalOptions, + OfferCredentialOptions, + ProposeCredentialOptions, +} from './../CredentialsModuleOptions' +import type { CredentialFormatService } from './../formats/CredentialFormatService' +import type { CredentialFormats, HandlerAutoAcceptOptions } from './../formats/models/CredentialFormatServiceOptions' +import type { + V1CredentialProblemReportMessage, + V1IssueCredentialMessage, + V1OfferCredentialMessage, + V1ProposeCredentialMessage, + V1RequestCredentialMessage, +} from './../protocol/v1/messages' +import type { V2CredentialProblemReportMessage } from './../protocol/v2/messages/V2CredentialProblemReportMessage' +import type { V2IssueCredentialMessage } from './../protocol/v2/messages/V2IssueCredentialMessage' +import type { V2OfferCredentialMessage } from './../protocol/v2/messages/V2OfferCredentialMessage' +import type { V2ProposeCredentialMessage } from './../protocol/v2/messages/V2ProposeCredentialMessage' +import type { V2RequestCredentialMessage } from './../protocol/v2/messages/V2RequestCredentialMessage' +import type { CredentialExchangeRecord, CredentialRepository } from './../repository' +import type { RevocationService } from './RevocationService' + +import { CredentialEventTypes } from './../CredentialEvents' +import { CredentialState } from './../CredentialState' + +export abstract class CredentialService { + protected credentialRepository: CredentialRepository + protected eventEmitter: EventEmitter + protected dispatcher: Dispatcher + protected agentConfig: AgentConfig + protected mediationRecipientService: MediationRecipientService + protected didCommMessageRepository: DidCommMessageRepository + protected logger: Logger + protected revocationService: RevocationService public constructor( credentialRepository: CredentialRepository, - connectionService: ConnectionService, - ledgerService: IndyLedgerService, + eventEmitter: EventEmitter, + dispatcher: Dispatcher, agentConfig: AgentConfig, - indyIssuerService: IndyIssuerService, - indyHolderService: IndyHolderService, - eventEmitter: EventEmitter + mediationRecipientService: MediationRecipientService, + didCommMessageRepository: DidCommMessageRepository, + revocationService: RevocationService ) { this.credentialRepository = credentialRepository - this.connectionService = connectionService - this.ledgerService = ledgerService - this.logger = agentConfig.logger - this.indyIssuerService = indyIssuerService - this.indyHolderService = indyHolderService this.eventEmitter = eventEmitter + this.dispatcher = dispatcher + this.agentConfig = agentConfig + this.mediationRecipientService = mediationRecipientService + this.didCommMessageRepository = didCommMessageRepository + this.logger = this.agentConfig.logger + this.revocationService = revocationService + + this.registerHandlers() } - /** - * Create a {@link ProposeCredentialMessage} not bound to an existing credential exchange. - * To create a proposal as response to an existing credential exchange, use {@link CredentialService#createProposalAsResponse}. - * - * @param connectionRecord The connection for which to create the credential proposal - * @param config Additional configuration to use for the proposal - * @returns Object containing proposal message and associated credential record - * - */ - public async createProposal( - connectionRecord: ConnectionRecord, - config?: CredentialProposeOptions - ): Promise> { - // Assert - connectionRecord.assertReady() - - const options = { ...config } - - // Add the linked attachments to the credentialProposal - if (config?.linkedAttachments) { - options.credentialProposal = CredentialUtils.createAndLinkAttachmentsToPreview( - config.linkedAttachments, - config.credentialProposal ?? new CredentialPreview({ attributes: [] }) - ) - options.attachments = config.linkedAttachments.map((linkedAttachment) => linkedAttachment.attachment) - } - - // Create message - const proposalMessage = new ProposeCredentialMessage(options ?? {}) - - // Create record - const credentialRecord = new CredentialRecord({ - connectionId: connectionRecord.id, - threadId: proposalMessage.threadId, - state: CredentialState.ProposalSent, - proposalMessage, - linkedAttachments: config?.linkedAttachments?.map((linkedAttachment) => linkedAttachment.attachment), - credentialAttributes: proposalMessage.credentialProposal?.attributes, - autoAcceptCredential: config?.autoAcceptCredential, - }) + abstract getVersion(): CredentialProtocolVersion - // Set the metadata - credentialRecord.metadata.set('_internal/indyCredential', { - schemaId: options.schemaId, - credentialDefinintionId: options.credentialDefinitionId, - }) - - await this.credentialRepository.save(credentialRecord) - this.eventEmitter.emit({ - type: CredentialEventTypes.CredentialStateChanged, - payload: { - credentialRecord, - previousState: null, - }, - }) + abstract getFormats(cred: CredentialFormats): CredentialFormatService[] - return { message: proposalMessage, credentialRecord } - } + // methods for proposal + abstract createProposal(proposal: ProposeCredentialOptions): Promise> + abstract processProposal(messageContext: HandlerInboundMessage): Promise + abstract acceptProposal( + proposal: AcceptProposalOptions, + credentialRecord: CredentialExchangeRecord + ): Promise> + abstract negotiateProposal( + options: NegotiateProposalOptions, + credentialRecord: CredentialExchangeRecord + ): Promise> - /** - * Create a {@link ProposePresentationMessage} as response to a received credential offer. - * To create a proposal not bound to an existing credential exchange, use {@link CredentialService#createProposal}. - * - * @param credentialRecord The credential record for which to create the credential proposal - * @param config Additional configuration to use for the proposal - * @returns Object containing proposal message and associated credential record - * - */ - public async createProposalAsResponse( - credentialRecord: CredentialRecord, - config?: CredentialProposeOptions - ): Promise> { - // Assert - credentialRecord.assertState(CredentialState.OfferReceived) + // methods for offer + abstract createOffer(options: OfferCredentialOptions): Promise> + abstract processOffer(messageContext: HandlerInboundMessage): Promise - // Create message - const proposalMessage = new ProposeCredentialMessage(config ?? {}) - proposalMessage.setThread({ threadId: credentialRecord.threadId }) + abstract createOutOfBandOffer(options: OfferCredentialOptions): Promise> - // Update record - credentialRecord.proposalMessage = proposalMessage - credentialRecord.credentialAttributes = proposalMessage.credentialProposal?.attributes - this.updateState(credentialRecord, CredentialState.ProposalSent) + // methods for request + abstract createRequest( + credentialRecord: CredentialExchangeRecord, + options: ServiceRequestCredentialOptions, + holderDid: string + ): Promise> - return { message: proposalMessage, credentialRecord } - } + abstract processAck(messageContext: InboundMessageContext): Promise - /** - * Process a received {@link ProposeCredentialMessage}. This will not accept the credential proposal - * or send a credential offer. It will only create a new, or update the existing credential record with - * the information from the credential proposal message. Use {@link CredentialService#createOfferAsResponse} - * after calling this method to create a credential offer. - * - * @param messageContext The message context containing a credential proposal message - * @returns credential record associated with the credential proposal message - * - */ - public async processProposal( - messageContext: InboundMessageContext - ): Promise { - let credentialRecord: CredentialRecord - const { message: proposalMessage, connection } = messageContext - - this.logger.debug(`Processing credential proposal with id ${proposalMessage.id}`) - - try { - // Credential record already exists - credentialRecord = await this.getByThreadAndConnectionId(proposalMessage.threadId, connection?.id) - - // Assert - credentialRecord.assertState(CredentialState.OfferSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: credentialRecord.proposalMessage, - previousSentMessage: credentialRecord.offerMessage, - }) - - // Update record - credentialRecord.proposalMessage = proposalMessage - await this.updateState(credentialRecord, CredentialState.ProposalReceived) - } catch { - // No credential record exists with thread id - credentialRecord = new CredentialRecord({ - connectionId: connection?.id, - threadId: proposalMessage.threadId, - proposalMessage, - credentialAttributes: proposalMessage.credentialProposal?.attributes, - state: CredentialState.ProposalReceived, - }) - - credentialRecord.metadata.set('_internal/indyCredential', { - schemaId: proposalMessage.schemaId, - credentialDefinitionId: proposalMessage.credentialDefinitionId, - }) - - // Assert - this.connectionService.assertConnectionOrServiceDecorator(messageContext) - - // Save record - await this.credentialRepository.save(credentialRecord) - this.eventEmitter.emit({ - type: CredentialEventTypes.CredentialStateChanged, - payload: { - credentialRecord, - previousState: null, - }, - }) - } + abstract negotiateOffer( + options: NegotiateOfferOptions, + credentialRecord: CredentialExchangeRecord + ): Promise> - return credentialRecord - } + // methods for issue - /** - * Create a {@link OfferCredentialMessage} as response to a received credential proposal. - * To create an offer not bound to an existing credential exchange, use {@link CredentialService#createOffer}. - * - * @param credentialRecord The credential record for which to create the credential offer - * @param credentialTemplate The credential template to use for the offer - * @returns Object containing offer message and associated credential record - * - */ - public async createOfferAsResponse( - credentialRecord: CredentialRecord, - credentialTemplate: CredentialOfferTemplate - ): Promise> { - // Assert - credentialRecord.assertState(CredentialState.ProposalReceived) - - // Create message - const { credentialDefinitionId, comment, preview, attachments } = credentialTemplate - const credOffer = await this.indyIssuerService.createCredentialOffer(credentialDefinitionId) - const offerAttachment = new Attachment({ - id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credOffer), - }), - }) + abstract processRequest( + messageContext: InboundMessageContext + ): Promise - const credentialOfferMessage = new OfferCredentialMessage({ - comment, - offerAttachments: [offerAttachment], - credentialPreview: preview, - attachments, - }) + // methods for issue + abstract createCredential( + credentialRecord: CredentialExchangeRecord, + options?: AcceptRequestOptions + ): Promise> - credentialOfferMessage.setThread({ - threadId: credentialRecord.threadId, - }) + abstract processCredential( + messageContext: InboundMessageContext + ): Promise - credentialRecord.offerMessage = credentialOfferMessage - credentialRecord.credentialAttributes = preview.attributes - credentialRecord.metadata.set('_internal/indyCredential', { - schemaId: credOffer.schema_id, - credentialDefinitionId: credOffer.cred_def_id, - }) - credentialRecord.linkedAttachments = attachments?.filter((attachment) => isLinkedAttachment(attachment)) - credentialRecord.autoAcceptCredential = - credentialTemplate.autoAcceptCredential ?? credentialRecord.autoAcceptCredential + abstract createAck(credentialRecord: CredentialExchangeRecord): Promise> - await this.updateState(credentialRecord, CredentialState.OfferSent) + abstract registerHandlers(): void - return { message: credentialOfferMessage, credentialRecord } - } + abstract getFormatService(credentialFormatType?: CredentialFormatType): CredentialFormatService /** - * Create a {@link OfferCredentialMessage} not bound to an existing credential exchange. - * To create an offer as response to an existing credential exchange, use {@link CredentialService#createOfferAsResponse}. - * - * @param connectionRecord The connection for which to create the credential offer - * @param credentialTemplate The credential template to use for the offer - * @returns Object containing offer message and associated credential record - * - */ - public async createOffer( - credentialTemplate: CredentialOfferTemplate, - connectionRecord?: ConnectionRecord - ): Promise> { - // Assert - connectionRecord?.assertReady() - - // Create message - const { credentialDefinitionId, comment, preview, linkedAttachments } = credentialTemplate - const credOffer = await this.indyIssuerService.createCredentialOffer(credentialDefinitionId) - const offerAttachment = new Attachment({ - id: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credOffer), - }), - }) - - // Create and link credential to attacment - const credentialPreview = linkedAttachments - ? CredentialUtils.createAndLinkAttachmentsToPreview(linkedAttachments, preview) - : preview - - // Construct offer message - const credentialOfferMessage = new OfferCredentialMessage({ - comment, - offerAttachments: [offerAttachment], - credentialPreview, - attachments: linkedAttachments?.map((linkedAttachment) => linkedAttachment.attachment), - }) - - // Create record - const credentialRecord = new CredentialRecord({ - connectionId: connectionRecord?.id, - threadId: credentialOfferMessage.id, - offerMessage: credentialOfferMessage, - credentialAttributes: credentialPreview.attributes, - linkedAttachments: linkedAttachments?.map((linkedAttachments) => linkedAttachments.attachment), - state: CredentialState.OfferSent, - autoAcceptCredential: credentialTemplate.autoAcceptCredential, - }) - - credentialRecord.metadata.set('_internal/indyCredential', { - credentialDefinitionId: credOffer.cred_def_id, - schemaId: credOffer.schema_id, - }) - - await this.credentialRepository.save(credentialRecord) - this.eventEmitter.emit({ - type: CredentialEventTypes.CredentialStateChanged, - payload: { - credentialRecord, - previousState: null, - }, - }) - - return { message: credentialOfferMessage, credentialRecord } - } - - /** - * Process a received {@link OfferCredentialMessage}. This will not accept the credential offer - * or send a credential request. It will only create a new credential record with - * the information from the credential offer message. Use {@link CredentialService#createRequest} - * after calling this method to create a credential request. - * - * @param messageContext The message context containing a credential request message - * @returns credential record associated with the credential offer message - * + * Decline a credential offer + * @param credentialRecord The credential to be declined */ - public async processOffer(messageContext: InboundMessageContext): Promise { - let credentialRecord: CredentialRecord - const { message: credentialOfferMessage, connection } = messageContext - - this.logger.debug(`Processing credential offer with id ${credentialOfferMessage.id}`) - - const indyCredentialOffer = credentialOfferMessage.indyCredentialOffer - if (!indyCredentialOffer) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for credential offer with thread id ${credentialOfferMessage.threadId}` - ) - } + public async declineOffer(credentialRecord: CredentialExchangeRecord): Promise { + credentialRecord.assertState(CredentialState.OfferReceived) - try { - // Credential record already exists - credentialRecord = await this.getByThreadAndConnectionId(credentialOfferMessage.threadId, connection?.id) - - // Assert - credentialRecord.assertState(CredentialState.ProposalSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: credentialRecord.offerMessage, - previousSentMessage: credentialRecord.proposalMessage, - }) - - credentialRecord.offerMessage = credentialOfferMessage - credentialRecord.linkedAttachments = credentialOfferMessage.attachments?.filter(isLinkedAttachment) - - credentialRecord.metadata.set('_internal/indyCredential', { - schemaId: indyCredentialOffer.schema_id, - credentialDefinitionId: indyCredentialOffer.cred_def_id, - }) - - await this.updateState(credentialRecord, CredentialState.OfferReceived) - } catch { - // No credential record exists with thread id - credentialRecord = new CredentialRecord({ - connectionId: connection?.id, - threadId: credentialOfferMessage.id, - offerMessage: credentialOfferMessage, - credentialAttributes: credentialOfferMessage.credentialPreview.attributes, - state: CredentialState.OfferReceived, - }) - - credentialRecord.metadata.set('_internal/indyCredential', { - credentialDefinitionId: indyCredentialOffer.cred_def_id, - schemaId: indyCredentialOffer.schema_id, - }) - - // Assert - this.connectionService.assertConnectionOrServiceDecorator(messageContext) - - // Save in repository - await this.credentialRepository.save(credentialRecord) - this.eventEmitter.emit({ - type: CredentialEventTypes.CredentialStateChanged, - payload: { - credentialRecord, - previousState: null, - }, - }) - } + await this.updateState(credentialRecord, CredentialState.Declined) return credentialRecord } - /** - * Create a {@link RequestCredentialMessage} as response to a received credential offer. + * Process a received {@link ProblemReportMessage}. * - * @param credentialRecord The credential record for which to create the credential request - * @param options Additional configuration to use for the credential request - * @returns Object containing request message and associated credential record + * @param messageContext The message context containing a credential problem report message + * @returns credential record associated with the credential problem report message * */ - public async createRequest( - credentialRecord: CredentialRecord, - options: CredentialRequestOptions - ): Promise> { - // Assert credential - credentialRecord.assertState(CredentialState.OfferReceived) - - const credentialOffer = credentialRecord.offerMessage?.indyCredentialOffer - - if (!credentialOffer) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for credential offer with thread id ${credentialRecord.threadId}` - ) - } - - const credentialDefinition = await this.ledgerService.getCredentialDefinition(credentialOffer.cred_def_id) - - const [credReq, credReqMetadata] = await this.indyHolderService.createCredentialRequest({ - holderDid: options.holderDid, - credentialOffer, - credentialDefinition, - }) - - const requestAttachment = new Attachment({ - id: INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credReq), - }), - }) + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message: credentialProblemReportMessage } = messageContext - const credentialRequest = new RequestCredentialMessage({ - comment: options?.comment, - requestAttachments: [requestAttachment], - attachments: credentialRecord.offerMessage?.attachments?.filter((attachment) => isLinkedAttachment(attachment)), - }) - credentialRequest.setThread({ threadId: credentialRecord.threadId }) + const connection = messageContext.assertReadyConnection() - credentialRecord.metadata.set('_internal/indyRequest', credReqMetadata) - credentialRecord.requestMessage = credentialRequest - credentialRecord.autoAcceptCredential = options?.autoAcceptCredential ?? credentialRecord.autoAcceptCredential + this.logger.debug(`Processing problem report with id ${credentialProblemReportMessage.id}`) - credentialRecord.linkedAttachments = credentialRecord.offerMessage?.attachments?.filter((attachment) => - isLinkedAttachment(attachment) + const credentialRecord = await this.getByThreadAndConnectionId( + credentialProblemReportMessage.threadId, + connection.id ) - await this.updateState(credentialRecord, CredentialState.RequestSent) - - return { message: credentialRequest, credentialRecord } - } - - /** - * Process a received {@link RequestCredentialMessage}. This will not accept the credential request - * or send a credential. It will only update the existing credential record with - * the information from the credential request message. Use {@link CredentialService#createCredential} - * after calling this method to create a credential. - * - * @param messageContext The message context containing a credential request message - * @returns credential record associated with the credential request message - * - */ - public async processRequest( - messageContext: InboundMessageContext - ): Promise { - const { message: credentialRequestMessage, connection } = messageContext - - this.logger.debug(`Processing credential request with id ${credentialRequestMessage.id}`) - - const indyCredentialRequest = credentialRequestMessage?.indyCredentialRequest - - if (!indyCredentialRequest) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for credential request with thread id ${credentialRequestMessage.threadId}` - ) - } - - const credentialRecord = await this.getByThreadAndConnectionId(credentialRequestMessage.threadId, connection?.id) - - // Assert - credentialRecord.assertState(CredentialState.OfferSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: credentialRecord.proposalMessage, - previousSentMessage: credentialRecord.offerMessage, - }) - - this.logger.debug('Credential record found when processing credential request', credentialRecord) - - credentialRecord.requestMessage = credentialRequestMessage - await this.updateState(credentialRecord, CredentialState.RequestReceived) + // Update record + credentialRecord.errorMessage = `${credentialProblemReportMessage.description.code}: ${credentialProblemReportMessage.description.en}` + await this.update(credentialRecord) return credentialRecord } + abstract shouldAutoRespondToProposal(options: HandlerAutoAcceptOptions): Promise - /** - * Create a {@link IssueCredentialMessage} as response to a received credential request. - * - * @param credentialRecord The credential record for which to create the credential - * @param options Additional configuration to use for the credential - * @returns Object containing issue credential message and associated credential record - * - */ - public async createCredential( - credentialRecord: CredentialRecord, - options?: CredentialResponseOptions - ): Promise> { - // Assert - credentialRecord.assertState(CredentialState.RequestReceived) - - const requestMessage = credentialRecord.requestMessage - const offerMessage = credentialRecord.offerMessage - - // Assert offer message - if (!offerMessage) { - throw new AriesFrameworkError( - `Missing credential offer for credential exchange with thread id ${credentialRecord.threadId}` - ) - } - - // Assert credential attributes - const credentialAttributes = credentialRecord.credentialAttributes - if (!credentialAttributes) { - throw new Error( - `Missing required credential attribute values on credential record with id ${credentialRecord.id}` - ) - } - - // Assert Indy offer - const indyCredentialOffer = offerMessage?.indyCredentialOffer - if (!indyCredentialOffer) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for credential offer with thread id ${credentialRecord.threadId}` - ) - } - - // Assert Indy request - const indyCredentialRequest = requestMessage?.indyCredentialRequest - if (!indyCredentialRequest) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for credential request with thread id ${credentialRecord.threadId}` - ) - } - - const [credential] = await this.indyIssuerService.createCredential({ - credentialOffer: indyCredentialOffer, - credentialRequest: indyCredentialRequest, - credentialValues: CredentialUtils.convertAttributesToValues(credentialAttributes), - }) + abstract shouldAutoRespondToOffer( + credentialRecord: CredentialExchangeRecord, + offerMessage: V1OfferCredentialMessage | V2OfferCredentialMessage, + proposeMessage?: V1ProposeCredentialMessage | V2ProposeCredentialMessage + ): boolean - const credentialAttachment = new Attachment({ - id: INDY_CREDENTIAL_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(credential), - }), - }) + abstract shouldAutoRespondToRequest( + credentialRecord: CredentialExchangeRecord, + requestMessage: V1RequestCredentialMessage | V2RequestCredentialMessage, + proposeMessage?: V1ProposeCredentialMessage | V2ProposeCredentialMessage, + offerMessage?: V1OfferCredentialMessage | V2OfferCredentialMessage + ): boolean - const issueCredentialMessage = new IssueCredentialMessage({ - comment: options?.comment, - credentialAttachments: [credentialAttachment], - attachments: - offerMessage?.attachments?.filter((attachment) => isLinkedAttachment(attachment)) || - requestMessage?.attachments?.filter((attachment) => isLinkedAttachment(attachment)), - }) - issueCredentialMessage.setThread({ - threadId: credentialRecord.threadId, - }) - issueCredentialMessage.setPleaseAck() + abstract shouldAutoRespondToCredential( + credentialRecord: CredentialExchangeRecord, + credentialMessage: V1IssueCredentialMessage | V2IssueCredentialMessage + ): boolean - credentialRecord.credentialMessage = issueCredentialMessage - credentialRecord.autoAcceptCredential = options?.autoAcceptCredential ?? credentialRecord.autoAcceptCredential + abstract getOfferMessage(id: string): Promise - await this.updateState(credentialRecord, CredentialState.CredentialIssued) + abstract getRequestMessage(id: string): Promise - return { message: issueCredentialMessage, credentialRecord } - } + abstract getCredentialMessage(id: string): Promise /** - * Process a received {@link IssueCredentialMessage}. This will not accept the credential - * or send a credential acknowledgement. It will only update the existing credential record with - * the information from the issue credential message. Use {@link CredentialService#createAck} - * after calling this method to create a credential acknowledgement. - * - * @param messageContext The message context containing an issue credential message + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. * - * @returns credential record associated with the issue credential message + * @param credentialRecord The credential record to update the state for + * @param newState The state to update to * */ - public async processCredential( - messageContext: InboundMessageContext - ): Promise { - const { message: issueCredentialMessage, connection } = messageContext - - this.logger.debug(`Processing credential with id ${issueCredentialMessage.id}`) - - const credentialRecord = await this.getByThreadAndConnectionId(issueCredentialMessage.threadId, connection?.id) - - // Assert - credentialRecord.assertState(CredentialState.RequestSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: credentialRecord.offerMessage, - previousSentMessage: credentialRecord.requestMessage, - }) - - const credentialRequestMetadata = credentialRecord.metadata.get('_internal/indyRequest') - - if (!credentialRequestMetadata) { - throw new AriesFrameworkError(`Missing required request metadata for credential with id ${credentialRecord.id}`) - } - - const indyCredential = issueCredentialMessage.indyCredential - if (!indyCredential) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for credential with thread id ${issueCredentialMessage.threadId}` - ) - } - - const credentialDefinition = await this.ledgerService.getCredentialDefinition(indyCredential.cred_def_id) - - const credentialId = await this.indyHolderService.storeCredential({ - credentialId: uuid(), - credentialRequestMetadata, - credential: indyCredential, - credentialDefinition, - }) - credentialRecord.credentialId = credentialId - credentialRecord.credentialMessage = issueCredentialMessage - await this.updateState(credentialRecord, CredentialState.CredentialReceived) - - return credentialRecord - } + public async updateState(credentialRecord: CredentialExchangeRecord, newState: CredentialState) { + const previousState = credentialRecord.state + credentialRecord.state = newState + await this.credentialRepository.update(credentialRecord) - /** - * Create a {@link CredentialAckMessage} as response to a received credential. - * - * @param credentialRecord The credential record for which to create the credential acknowledgement - * @returns Object containing credential acknowledgement message and associated credential record - * - */ - public async createAck( - credentialRecord: CredentialRecord - ): Promise> { - credentialRecord.assertState(CredentialState.CredentialReceived) - - // Create message - const ackMessage = new CredentialAckMessage({ - status: AckStatus.OK, - threadId: credentialRecord.threadId, + this.eventEmitter.emit({ + type: CredentialEventTypes.CredentialStateChanged, + payload: { + credentialRecord, + previousState: previousState, + }, }) - - await this.updateState(credentialRecord, CredentialState.Done) - - return { message: ackMessage, credentialRecord } - } - - /** - * Decline a credential offer - * @param credentialRecord The credential to be declined - */ - public async declineOffer(credentialRecord: CredentialRecord): Promise { - credentialRecord.assertState(CredentialState.OfferReceived) - - await this.updateState(credentialRecord, CredentialState.Declined) - - return credentialRecord } - /** - * Process a received {@link CredentialAckMessage}. + * Retrieve a credential record by id * - * @param messageContext The message context containing a credential acknowledgement message - * @returns credential record associated with the credential acknowledgement message + * @param credentialRecordId The credential record id + * @throws {RecordNotFoundError} If no record is found + * @return The credential record * */ - public async processAck(messageContext: InboundMessageContext): Promise { - const { message: credentialAckMessage, connection } = messageContext - - this.logger.debug(`Processing credential ack with id ${credentialAckMessage.id}`) - - const credentialRecord = await this.getByThreadAndConnectionId(credentialAckMessage.threadId, connection?.id) - - // Assert - credentialRecord.assertState(CredentialState.CredentialIssued) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: credentialRecord.requestMessage, - previousSentMessage: credentialRecord.credentialMessage, - }) - - // Update record - await this.updateState(credentialRecord, CredentialState.Done) - - return credentialRecord + public getById(credentialRecordId: string): Promise { + return this.credentialRepository.getById(credentialRecordId) } /** @@ -718,40 +233,29 @@ export class CredentialService { * * @returns List containing all credential records */ - public getAll(): Promise { + public getAll(): Promise { return this.credentialRepository.getAll() } - /** - * Retrieve a credential record by id - * - * @param credentialRecordId The credential record id - * @throws {RecordNotFoundError} If no record is found - * @return The credential record - * - */ - public getById(credentialRecordId: string): Promise { - return this.credentialRepository.getById(credentialRecordId) - } - /** * Find a credential record by id * * @param credentialRecordId the credential record id * @returns The credential record or null if not found */ - public findById(connectionId: string): Promise { + public findById(connectionId: string): Promise { return this.credentialRepository.findById(connectionId) } - /** - * Delete a credential record by id - * - * @param credentialId the credential record id - */ - public async deleteById(credentialId: string) { - const credentialRecord = await this.getById(credentialId) - return this.credentialRepository.delete(credentialRecord) + public async delete(credentialRecord: CredentialExchangeRecord, options?: DeleteCredentialOptions): Promise { + await this.credentialRepository.delete(credentialRecord) + + if (options?.deleteAssociatedCredentials) { + for (const credential of credentialRecord.credentials) { + const formatService: CredentialFormatService = this.getFormatService(credential.credentialRecordType) + await formatService.deleteCredentialById(credential.credentialRecordId) + } + } } /** @@ -763,66 +267,14 @@ export class CredentialService { * @throws {RecordDuplicateError} If multiple records are found * @returns The credential record */ - public getByThreadAndConnectionId(threadId: string, connectionId?: string): Promise { + public getByThreadAndConnectionId(threadId: string, connectionId?: string): Promise { return this.credentialRepository.getSingleByQuery({ connectionId, threadId, }) } - public update(credentialRecord: CredentialRecord) { - return this.credentialRepository.update(credentialRecord) + public async update(credentialRecord: CredentialExchangeRecord) { + return await this.credentialRepository.update(credentialRecord) } - - /** - * Update the record to a new state and emit an state changed event. Also updates the record - * in storage. - * - * @param credentialRecord The credential record to update the state for - * @param newState The state to update to - * - */ - private async updateState(credentialRecord: CredentialRecord, newState: CredentialState) { - const previousState = credentialRecord.state - credentialRecord.state = newState - await this.credentialRepository.update(credentialRecord) - - this.eventEmitter.emit({ - type: CredentialEventTypes.CredentialStateChanged, - payload: { - credentialRecord, - previousState: previousState, - }, - }) - } -} - -export interface CredentialProtocolMsgReturnType { - message: MessageType - credentialRecord: CredentialRecord -} - -export interface CredentialOfferTemplate { - credentialDefinitionId: string - comment?: string - preview: CredentialPreview - autoAcceptCredential?: AutoAcceptCredential - attachments?: Attachment[] - linkedAttachments?: LinkedAttachment[] -} - -export interface CredentialRequestOptions { - holderDid: string - comment?: string - autoAcceptCredential?: AutoAcceptCredential -} - -export interface CredentialResponseOptions { - comment?: string - autoAcceptCredential?: AutoAcceptCredential -} - -export type CredentialProposeOptions = Omit & { - linkedAttachments?: LinkedAttachment[] - autoAcceptCredential?: AutoAcceptCredential } diff --git a/packages/core/src/modules/credentials/services/RevocationService.ts b/packages/core/src/modules/credentials/services/RevocationService.ts new file mode 100644 index 0000000000..99ba4dfa53 --- /dev/null +++ b/packages/core/src/modules/credentials/services/RevocationService.ts @@ -0,0 +1,127 @@ +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { Logger } from '../../../logger' +import type { ConnectionRecord } from '../../connections' +import type { RevocationNotificationReceivedEvent } from '../CredentialEvents' +import type { V1RevocationNotificationMessage } from '../protocol/v1/messages/V1RevocationNotificationMessage' +import type { V2RevocationNotificationMessage } from '../protocol/v2/messages/V2RevocationNotificationMessage' + +import { scoped, Lifecycle } from 'tsyringe' + +import { AgentConfig } from '../../../agent/AgentConfig' +import { EventEmitter } from '../../../agent/EventEmitter' +import { AriesFrameworkError } from '../../../error/AriesFrameworkError' +import { CredentialEventTypes } from '../CredentialEvents' +import { RevocationNotification } from '../models/RevocationNotification' +import { CredentialRepository } from '../repository' + +@scoped(Lifecycle.ContainerScoped) +export class RevocationService { + private credentialRepository: CredentialRepository + private eventEmitter: EventEmitter + private logger: Logger + + public constructor(credentialRepository: CredentialRepository, eventEmitter: EventEmitter, agentConfig: AgentConfig) { + this.credentialRepository = credentialRepository + this.eventEmitter = eventEmitter + this.logger = agentConfig.logger + } + + private async processRevocationNotification( + indyRevocationRegistryId: string, + indyCredentialRevocationId: string, + connection: ConnectionRecord, + comment?: string + ) { + const query = { indyRevocationRegistryId, indyCredentialRevocationId } + + this.logger.trace(`Getting record by query for revocation notification:`, query) + const credentialRecord = await this.credentialRepository.getSingleByQuery(query) + + credentialRecord.assertConnection(connection.id) + + credentialRecord.revocationNotification = new RevocationNotification(comment) + await this.credentialRepository.update(credentialRecord) + + this.logger.trace('Emitting RevocationNotificationReceivedEvent') + this.eventEmitter.emit({ + type: CredentialEventTypes.RevocationNotificationReceived, + payload: { + credentialRecord, + }, + }) + } + + /** + * Process a received {@link V1RevocationNotificationMessage}. This will create a + * {@link RevocationNotification} and store it in the corresponding {@link CredentialRecord} + * + * @param messageContext message context of RevocationNotificationMessageV1 + */ + public async v1ProcessRevocationNotification( + messageContext: InboundMessageContext + ): Promise { + this.logger.info('Processing revocation notification v1', { message: messageContext.message }) + // ThreadID = indy:::: + const threadRegex = + /(indy)::((?:[\dA-z]{21,22}):4:(?:[\dA-z]{21,22}):3:[Cc][Ll]:(?:(?:[1-9][0-9]*)|(?:[\dA-z]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(?:[\dA-z-]+))::(\d+)$/ + const threadId = messageContext.message.issueThread + try { + const threadIdGroups = threadId.match(threadRegex) + if (threadIdGroups) { + const [, , indyRevocationRegistryId, indyCredentialRevocationId] = threadIdGroups + const comment = messageContext.message.comment + const connection = messageContext.assertReadyConnection() + + await this.processRevocationNotification( + indyRevocationRegistryId, + indyCredentialRevocationId, + connection, + comment + ) + } else { + throw new AriesFrameworkError( + `Incorrect revocation notification threadId format: \n${threadId}\ndoes not match\n"indy::::"` + ) + } + } catch (error) { + this.logger.warn('Failed to process revocation notification message', { error, threadId }) + } + } + + /** + * Process a received {@link V2RevocationNotificationMessage}. This will create a + * {@link RevocationNotification} and store it in the corresponding {@link CredentialRecord} + * + * @param messageContext message context of RevocationNotificationMessageV2 + */ + public async v2ProcessRevocationNotification( + messageContext: InboundMessageContext + ): Promise { + this.logger.info('Processing revocation notification v2', { message: messageContext.message }) + + // CredentialId = :: + const credentialIdRegex = + /((?:[\dA-z]{21,22}):4:(?:[\dA-z]{21,22}):3:[Cc][Ll]:(?:(?:[1-9][0-9]*)|(?:[\dA-z]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(?:[\dA-z-]+))::(\d+)$/ + const credentialId = messageContext.message.credentialId + try { + const credentialIdGroups = credentialId.match(credentialIdRegex) + if (credentialIdGroups) { + const [, indyRevocationRegistryId, indyCredentialRevocationId] = credentialIdGroups + const comment = messageContext.message.comment + const connection = messageContext.assertReadyConnection() + await this.processRevocationNotification( + indyRevocationRegistryId, + indyCredentialRevocationId, + connection, + comment + ) + } else { + throw new AriesFrameworkError( + `Incorrect revocation notification credentialId format: \n${credentialId}\ndoes not match\n"::"` + ) + } + } catch (error) { + this.logger.warn('Failed to process revocation notification message', { error, credentialId }) + } + } +} diff --git a/packages/core/src/modules/credentials/services/index.ts b/packages/core/src/modules/credentials/services/index.ts index 3ef45ad8eb..0c38ea1a3c 100644 --- a/packages/core/src/modules/credentials/services/index.ts +++ b/packages/core/src/modules/credentials/services/index.ts @@ -1 +1,2 @@ export * from './CredentialService' +export * from './RevocationService' diff --git a/packages/core/src/modules/dids/DidsModule.ts b/packages/core/src/modules/dids/DidsModule.ts new file mode 100644 index 0000000000..28c9946e2e --- /dev/null +++ b/packages/core/src/modules/dids/DidsModule.ts @@ -0,0 +1,34 @@ +import type { Key } from '../../crypto' +import type { DidResolutionOptions } from './types' + +import { Lifecycle, scoped } from 'tsyringe' + +import { DidRepository } from './repository' +import { DidResolverService } from './services/DidResolverService' + +@scoped(Lifecycle.ContainerScoped) +export class DidsModule { + private resolverService: DidResolverService + private didRepository: DidRepository + + public constructor(resolverService: DidResolverService, didRepository: DidRepository) { + this.resolverService = resolverService + this.didRepository = didRepository + } + + public resolve(didUrl: string, options?: DidResolutionOptions) { + return this.resolverService.resolve(didUrl, options) + } + + public resolveDidDocument(didUrl: string) { + return this.resolverService.resolveDidDocument(didUrl) + } + + public findByRecipientKey(recipientKey: Key) { + return this.didRepository.findByRecipientKey(recipientKey) + } + + public findAllByRecipientKey(recipientKey: Key) { + return this.didRepository.findAllByRecipientKey(recipientKey) + } +} diff --git a/packages/core/src/modules/dids/__tests__/DidResolverService.test.ts b/packages/core/src/modules/dids/__tests__/DidResolverService.test.ts new file mode 100644 index 0000000000..785c30d00c --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/DidResolverService.test.ts @@ -0,0 +1,69 @@ +import type { IndyLedgerService } from '../../ledger' +import type { DidRepository } from '../repository' + +import { getAgentConfig, mockProperty } from '../../../../tests/helpers' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { DidDocument } from '../domain' +import { parseDid } from '../domain/parse' +import { KeyDidResolver } from '../methods/key/KeyDidResolver' +import { DidResolverService } from '../services/DidResolverService' + +import didKeyEd25519Fixture from './__fixtures__/didKeyEd25519.json' + +jest.mock('../methods/key/KeyDidResolver') + +const agentConfig = getAgentConfig('DidResolverService') + +describe('DidResolverService', () => { + const indyLedgerServiceMock = jest.fn() as unknown as IndyLedgerService + const didDocumentRepositoryMock = jest.fn() as unknown as DidRepository + const didResolverService = new DidResolverService(agentConfig, indyLedgerServiceMock, didDocumentRepositoryMock) + + it('should correctly find and call the correct resolver for a specified did', async () => { + const didKeyResolveSpy = jest.spyOn(KeyDidResolver.prototype, 'resolve') + mockProperty(KeyDidResolver.prototype, 'supportedMethods', ['key']) + + const returnValue = { + didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument), + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + } + didKeyResolveSpy.mockResolvedValue(returnValue) + + const result = await didResolverService.resolve('did:key:xxxx', { someKey: 'string' }) + expect(result).toEqual(returnValue) + + expect(didKeyResolveSpy).toHaveBeenCalledTimes(1) + expect(didKeyResolveSpy).toHaveBeenCalledWith('did:key:xxxx', parseDid('did:key:xxxx'), { someKey: 'string' }) + }) + + it("should return an error with 'invalidDid' if the did string couldn't be parsed", async () => { + const did = 'did:__Asd:asdfa' + + const result = await didResolverService.resolve(did) + + expect(result).toEqual({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'invalidDid', + }, + }) + }) + + it("should return an error with 'unsupportedDidMethod' if the did has no resolver", async () => { + const did = 'did:example:asdfa' + + const result = await didResolverService.resolve(did) + + expect(result).toEqual({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'unsupportedDidMethod', + }, + }) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123.json new file mode 100644 index 0000000000..32532f721a --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample123.json @@ -0,0 +1,94 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:example:123", + "alsoKnownAs": ["did:example:456"], + "controller": ["did:example:456"], + "verificationMethod": [ + { + "id": "did:example:123#key-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC X..." + }, + { + "id": "did:example:123#key-2", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:example:123#key-3", + "type": "Secp256k1VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:example:123#service-1", + "type": "Mediator", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h" + }, + { + "id": "did:example:123#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:example:123#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + "did:example:123#key-1", + { + "id": "did:example:123#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:example:123#key-1", + { + "id": "did:example:123#assertionMethod-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:example:123#key-1", + { + "id": "did:example:123#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didExample456Invalid.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample456Invalid.json new file mode 100644 index 0000000000..17f6c5c251 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didExample456Invalid.json @@ -0,0 +1,86 @@ +{ + "@context": "https://w3id.org/did/v1", + "id": "did:example:456", + "alsoKnownAs": "did:example:123", + "controller": "did:example:123", + "verificationMethod": [ + "did:example:456#key-1", + { + "id": "did:example:456#key-2", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "-----BEGIN PUBLIC 9..." + }, + { + "id": "did:example:456#key-3", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyHex": "-----BEGIN PUBLIC A..." + } + ], + "service": [ + { + "id": "did:example:123#service-1", + "type": "Mediator" + }, + { + "id": "did:example:123#service-2", + "type": "IndyAgent", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", + "recipientKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "routingKeys": ["Q4zqM7aXqm7gDQkUVLng9h"], + "priority": 5 + }, + { + "id": "did:example:123#service-3", + "type": "did-communication", + "serviceEndpoint": "https://agent.com/did-comm", + "recipientKeys": ["DADEajsDSaksLng9h"], + "routingKeys": ["DADEajsDSaksLng9h"], + "priority": 10 + } + ], + "authentication": [ + "did:example:123#key-1", + { + "id": "did:example:123#authentication-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "assertionMethod": [ + "did:example:123#key-1", + { + "id": "did:example:123#assertionMethod-1", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityDelegation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityDelegation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "capabilityInvocation": [ + "did:example:123#key-1", + { + "id": "did:example:123#capabilityInvocation-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ], + "keyAgreement": [ + "did:example:123#key-1", + { + "id": "did:example:123#keyAgreement-1", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A..." + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1.json new file mode 100644 index 0000000000..64ea24fb7e --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1.json @@ -0,0 +1,24 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/bbs/v1"], + "id": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA", + "verificationMethod": [ + { + "id": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA", + "type": "Bls12381G1Key2020", + "controller": "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA", + "publicKeyBase58": "6FywSzB5BPd7xehCo1G4nYHAoZPMMP3gd4PLnvgA6SsTsogtz8K7RDznqLpFPLZXAE" + } + ], + "authentication": [ + "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" + ], + "assertionMethod": [ + "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" + ], + "capabilityDelegation": [ + "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" + ], + "capabilityInvocation": [ + "did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA#z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1g2.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1g2.json new file mode 100644 index 0000000000..898bf59d77 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g1g2.json @@ -0,0 +1,34 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/bbs/v1"], + "id": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s", + "verificationMethod": [ + { + "id": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "type": "Bls12381G1Key2020", + "controller": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s", + "publicKeyBase58": "7BVES4h78wzabPAfMhchXyH5d8EX78S5TtzePH2YkftWcE6by9yj3NTAv9nsyCeYch" + }, + { + "id": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM", + "type": "Bls12381G2Key2020", + "controller": "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s", + "publicKeyBase58": "26d2BdqELsXg7ZHCWKL2D5Y2S7mYrpkdhJemSEEvokd4qy4TULJeeU44hYPGKo4x4DbBp5ARzkv1D6xuB3bmhpdpKAXuXtode67wzh9PCtW8kTqQhH19VSiFZkLNkhe9rtf3" + } + ], + "authentication": [ + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" + ], + "assertionMethod": [ + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" + ], + "capabilityDelegation": [ + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" + ], + "capabilityInvocation": [ + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd", + "did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s#zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g2.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g2.json new file mode 100644 index 0000000000..29724406d1 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyBls12381g2.json @@ -0,0 +1,24 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/bbs/v1"], + "id": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT", + "verificationMethod": [ + { + "id": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT", + "type": "Bls12381G2Key2020", + "controller": "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT", + "publicKeyBase58": "mxE4sHTpbPcmxNviRVR9r7D2taXcNyVJmf9TBUFS1gRt3j3Ej9Seo59GQeCzYwbQgDrfWCwEJvmBwjLvheAky5N2NqFVzk4kuq3S8g4Fmekai4P622vHqWjFrsioYYDqhf9" + } + ], + "authentication": [ + "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" + ], + "assertionMethod": [ + "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" + ], + "capabilityDelegation": [ + "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" + ], + "capabilityInvocation": [ + "did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT#zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyEd25519.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyEd25519.json new file mode 100644 index 0000000000..8cfad8b6d1 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyEd25519.json @@ -0,0 +1,36 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th", + "verificationMethod": [ + { + "id": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th", + "type": "Ed25519VerificationKey2018", + "controller": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th", + "publicKeyBase58": "8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K" + } + ], + "assertionMethod": [ + "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th" + ], + "authentication": [ + "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th" + ], + "capabilityInvocation": [ + "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th" + ], + "capabilityDelegation": [ + "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th" + ], + "keyAgreement": [ + { + "id": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th#z6LShpNhGwSupbB7zjuivH156vhLJBDDzmQtA4BY9S94pe1K", + "type": "X25519KeyAgreementKey2019", + "controller": "did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th", + "publicKeyBase58": "79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ" + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyX25519.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyX25519.json new file mode 100644 index 0000000000..ad660d24f3 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyX25519.json @@ -0,0 +1,12 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/x25519-2019/v1"], + "id": "did:key:z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE", + "keyAgreement": [ + { + "id": "did:key:z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE#z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE", + "type": "X25519KeyAgreementKey2019", + "controller": "did:key:z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE", + "publicKeyBase58": "6fUMuABnqSDsaGKojbUF3P7ZkEL3wi2njsDdUWZGNgCU" + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didPeer1zQmY.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didPeer1zQmY.json new file mode 100644 index 0000000000..4a33648df6 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didPeer1zQmY.json @@ -0,0 +1,33 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmchWGXSsHohSMrgts5oxG76zAfG49RkMZbhrYqPJeVXc1", + "service": [ + { + "id": "#service-0", + "serviceEndpoint": "https://example.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#d0d32199-851f-48e3-b178-6122bd4216a4"], + "routingKeys": [ + "did:key:z6Mkh66d8nyf6EGUaeN2oWFAxv4qxppwUwnmy9crnZoseN7h#z6LSdgnNCDyjAvZHRHfA9rUfrcEk2vndbPsBo85BuZpc1hFC" + ], + "accept": ["didcomm/aip2;env=rfc19"] + } + ], + "authentication": [ + { + "id": "#d0d32199-851f-48e3-b178-6122bd4216a4", + "type": "Ed25519VerificationKey2018", + "controller": "#id", + "publicKeyBase58": "CQZzRfoJMRzoESU2VtWrgx3rTsk9yjrjqXL2UdxWjX2q" + } + ], + "keyAgreement": [ + { + "id": "#08673492-3c44-47fe-baa4-a1780c585d75", + "type": "X25519KeyAgreementKey2019", + "controller": "#id", + "publicKeyBase58": "7SbWSgJgjSvSTc7ZAKHJiaZbTBwNM9TdFUAU1UyZfJn8" + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json new file mode 100644 index 0000000000..6a6e4ed706 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json @@ -0,0 +1,51 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-1", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:sov:R1xKJw17sUoXhejEpugMYJ", + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1", + "publicKeyBase58": "Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt" + } + ], + "authentication": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], + "assertionMethod": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-1"], + "keyAgreement": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "service": [ + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://ssi.com" + }, + { + "accept": ["didcomm/aip2;env=rfc19"], + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#did-communication", + "priority": 0, + "recipientKeys": ["did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "routingKeys": [], + "serviceEndpoint": "https://ssi.com", + "type": "did-communication" + }, + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#profile", + "serviceEndpoint": "https://profile.com", + "type": "profile" + }, + { + "id": "did:sov:R1xKJw17sUoXhejEpugMYJ#hub", + "serviceEndpoint": "https://hub.com", + "type": "hub" + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json new file mode 100644 index 0000000000..7b74e0587f --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1", + "https://didcomm.org/messaging/contexts/v2" + ], + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-1", + "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:sov:WJz9mHyW9BZksioQnRsrAo", + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1", + "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" + } + ], + "authentication": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], + "assertionMethod": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-1"], + "keyAgreement": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "service": [ + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#did-communication", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["did:sov:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "routingKeys": ["routingKey1", "routingKey2"], + "accept": ["didcomm/aip2;env=rfc19"], + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:sov:WJz9mHyW9BZksioQnRsrAo#didcomm-1", + "type": "DIDComm", + "serviceEndpoint": "https://agent.com", + "accept": ["didcomm/v2"], + "routingKeys": ["routingKey1", "routingKey2"] + } + ] +} diff --git a/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts b/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts new file mode 100644 index 0000000000..387d2f5ec3 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/keyDidDocument.test.ts @@ -0,0 +1,52 @@ +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { getDidDocumentForKey } from '../domain/keyDidDocument' +import { DidKey } from '../methods/key' + +import didKeyBls12381g1Fixture from './__fixtures__/didKeyBls12381g1.json' +import didKeyBls12381g1g2Fixture from './__fixtures__/didKeyBls12381g1g2.json' +import didKeyBls12381g2Fixture from './__fixtures__/didKeyBls12381g2.json' +import didKeyEd25519Fixture from './__fixtures__/didKeyEd25519.json' +import didKeyX25519Fixture from './__fixtures__/didKeyX25519.json' + +const TEST_X25519_DID = 'did:key:z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE' +const TEST_ED25519_DID = `did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th` +const TEST_BLS12381G1_DID = `did:key:z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA` +const TEST_BLS12381G2_DID = `did:key:zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT` +const TEST_BLS12381G1G2_DID = `did:key:z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s` + +describe('getDidDocumentForKey', () => { + it('should return a valid did:key did document for and x25519 key', () => { + const didKey = DidKey.fromDid(TEST_X25519_DID) + const didDocument = getDidDocumentForKey(TEST_X25519_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyX25519Fixture) + }) + + it('should return a valid did:key did document for and ed25519 key', () => { + const didKey = DidKey.fromDid(TEST_ED25519_DID) + const didDocument = getDidDocumentForKey(TEST_ED25519_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyEd25519Fixture) + }) + + it('should return a valid did:key did document for and bls12381g1 key', () => { + const didKey = DidKey.fromDid(TEST_BLS12381G1_DID) + const didDocument = getDidDocumentForKey(TEST_BLS12381G1_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyBls12381g1Fixture) + }) + + it('should return a valid did:key did document for and bls12381g2 key', () => { + const didKey = DidKey.fromDid(TEST_BLS12381G2_DID) + const didDocument = getDidDocumentForKey(TEST_BLS12381G2_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyBls12381g2Fixture) + }) + + it('should return a valid did:key did document for and bls12381g1g2 key', () => { + const didKey = DidKey.fromDid(TEST_BLS12381G1G2_DID) + const didDocument = getDidDocumentForKey(TEST_BLS12381G1G2_DID, didKey.key) + + expect(JsonTransformer.toJSON(didDocument)).toMatchObject(didKeyBls12381g1g2Fixture) + }) +}) diff --git a/packages/core/src/modules/dids/__tests__/peer-did.test.ts b/packages/core/src/modules/dids/__tests__/peer-did.test.ts new file mode 100644 index 0000000000..2d523cf4c2 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/peer-did.test.ts @@ -0,0 +1,171 @@ +import type { IndyLedgerService } from '../../ledger' + +import { getAgentConfig } from '../../../../tests/helpers' +import { Key, KeyType } from '../../../crypto' +import { IndyStorageService } from '../../../storage/IndyStorageService' +import { JsonTransformer } from '../../../utils' +import { IndyWallet } from '../../../wallet/IndyWallet' +import { DidCommV1Service, DidDocument, DidDocumentBuilder } from '../domain' +import { DidDocumentRole } from '../domain/DidDocumentRole' +import { convertPublicKeyToX25519, getEd25519VerificationMethod } from '../domain/key-type/ed25519' +import { getX25519VerificationMethod } from '../domain/key-type/x25519' +import { DidKey } from '../methods/key' +import { getNumAlgoFromPeerDid, PeerDidNumAlgo } from '../methods/peer/didPeer' +import { didDocumentJsonToNumAlgo1Did } from '../methods/peer/peerDidNumAlgo1' +import { DidRecord, DidRepository } from '../repository' +import { DidResolverService } from '../services' + +import didPeer1zQmY from './__fixtures__/didPeer1zQmY.json' + +describe('peer dids', () => { + const config = getAgentConfig('Peer DIDs Lifecycle') + + let didRepository: DidRepository + let didResolverService: DidResolverService + let wallet: IndyWallet + + beforeEach(async () => { + wallet = new IndyWallet(config) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(config.walletConfig!) + + const storageService = new IndyStorageService(wallet, config) + didRepository = new DidRepository(storageService) + + // Mocking IndyLedgerService as we're only interested in the did:peer resolver + didResolverService = new DidResolverService(config, {} as unknown as IndyLedgerService, didRepository) + }) + + afterEach(async () => { + await wallet.delete() + }) + + test('create a peer did method 1 document from ed25519 keys with a service', async () => { + // The following scenario show how we could create a key and create a did document from it for DID Exchange + + const { verkey: publicKeyBase58 } = await wallet.createDid({ seed: 'astringoftotalin32characterslong' }) + const { verkey: mediatorPublicKeyBase58 } = await wallet.createDid({ seed: 'anotherstringof32characterslong1' }) + + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519) + const x25519Key = Key.fromPublicKey(convertPublicKeyToX25519(ed25519Key.publicKey), KeyType.X25519) + + const ed25519VerificationMethod = getEd25519VerificationMethod({ + // The id can either be the first 8 characters of the key data (for ed25519 it's publicKeyBase58) + // uuid is easier as it is consistent between different key types. Normally you would dynamically + // generate the uuid, but static for testing purposes + id: `#d0d32199-851f-48e3-b178-6122bd4216a4`, + key: ed25519Key, + // For peer dids generated with method 1, the controller MUST be #id as we don't know the did yet + controller: '#id', + }) + const x25519VerificationMethod = getX25519VerificationMethod({ + // The id can either be the first 8 characters of the key data (for ed25519 it's publicKeyBase58) + // uuid is easier as it is consistent between different key types. Normally you would dynamically + // generate the uuid, but static for testing purposes + id: `#08673492-3c44-47fe-baa4-a1780c585d75`, + key: x25519Key, + // For peer dids generated with method 1, the controller MUST be #id as we don't know the did yet + controller: '#id', + }) + + const mediatorEd25519Key = Key.fromPublicKeyBase58(mediatorPublicKeyBase58, KeyType.Ed25519) + const mediatorEd25519DidKey = new DidKey(mediatorEd25519Key) + + const mediatorX25519Key = Key.fromPublicKey(convertPublicKeyToX25519(mediatorEd25519Key.publicKey), KeyType.X25519) + // Use ed25519 did:key, which also includes the x25519 key used for didcomm + const mediatorRoutingKey = `${mediatorEd25519DidKey.did}#${mediatorX25519Key.fingerprint}` + + const service = new DidCommV1Service({ + id: '#service-0', + // Fixme: can we use relative reference (#id) instead of absolute reference here (did:example:123#id)? + // We don't know the did yet + recipientKeys: [ed25519VerificationMethod.id], + serviceEndpoint: 'https://example.com', + accept: ['didcomm/aip2;env=rfc19'], + // It is important that we encode the routing keys as key references. + // So instead of using plain verkeys, we should encode them as did:key dids + routingKeys: [mediatorRoutingKey], + }) + + const didDocument = + // placeholder did, as it is generated from the did document + new DidDocumentBuilder('') + // ed25519 authentication method for signatures + .addAuthentication(ed25519VerificationMethod) + // x25519 for key agreement + .addKeyAgreement(x25519VerificationMethod) + .addService(service) + .build() + + const didDocumentJson = didDocument.toJSON() + const did = didDocumentJsonToNumAlgo1Did(didDocumentJson) + + expect(did).toBe(didPeer1zQmY.id) + + // Set did after generating it + didDocument.id = did + + expect(didDocument.toJSON()).toMatchObject(didPeer1zQmY) + + // Save the record to storage + const didDocumentRecord = new DidRecord({ + id: didPeer1zQmY.id, + role: DidDocumentRole.Created, + // It is important to take the did document from the PeerDid class + // as it will have the id property + didDocument: didDocument, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + + await didRepository.save(didDocumentRecord) + }) + + test('receive a did and did document', async () => { + // This flow assumes peer dids. When implementing for did exchange other did methods could be used + + // We receive the did and did document from the did exchange message (request or response) + // It is important to not parse the did document to a DidDocument class yet as we need the raw json + // to consistently verify the hash of the did document + const did = didPeer1zQmY.id + const numAlgo = getNumAlgoFromPeerDid(did) + + // Note that the did document could be undefined (if inlined did:peer or public did) + const didDocument = JsonTransformer.fromJSON(didPeer1zQmY, DidDocument) + + // make sure the dids are valid by matching them against our encoded variants + expect(didDocumentJsonToNumAlgo1Did(didPeer1zQmY)).toBe(did) + + // If a did document was provided, we match it against the did document of the peer did + // This validates whether we get the same did document + if (didDocument) { + expect(didDocument.toJSON()).toMatchObject(didPeer1zQmY) + } + + const didDocumentRecord = new DidRecord({ + id: did, + role: DidDocumentRole.Received, + // If the method is a genesis doc (did:peer:1) we should store the document + // Otherwise we only need to store the did itself (as the did can be generated) + didDocument: numAlgo === PeerDidNumAlgo.GenesisDoc ? didDocument : undefined, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + + await didRepository.save(didDocumentRecord) + + // Then we save the did (not the did document) in the connection record + // connectionRecord.theirDid = didPeer.did + + // Then when we want to send a message we can resolve the did document + const { didDocument: resolvedDidDocument } = await didResolverService.resolve(did) + expect(resolvedDidDocument).toBeInstanceOf(DidDocument) + expect(resolvedDidDocument?.toJSON()).toMatchObject(didPeer1zQmY) + }) +}) diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts new file mode 100644 index 0000000000..19b5b6efb6 --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -0,0 +1,215 @@ +import type { DidDocumentService } from './service' + +import { Expose, Type } from 'class-transformer' +import { IsArray, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { KeyType, Key } from '../../../crypto' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { IsStringOrStringArray } from '../../../utils/transformers' + +import { getKeyDidMappingByVerificationMethod } from './key-type' +import { IndyAgentService, ServiceTransformer, DidCommV1Service } from './service' +import { VerificationMethodTransformer, VerificationMethod, IsStringOrVerificationMethod } from './verificationMethod' + +type DidPurpose = + | 'authentication' + | 'keyAgreement' + | 'assertionMethod' + | 'capabilityInvocation' + | 'capabilityDelegation' + +interface DidDocumentOptions { + context?: string | string[] + id: string + alsoKnownAs?: string[] + controller?: string[] + verificationMethod?: VerificationMethod[] + service?: DidDocumentService[] + authentication?: Array + assertionMethod?: Array + keyAgreement?: Array + capabilityInvocation?: Array + capabilityDelegation?: Array +} + +export class DidDocument { + @Expose({ name: '@context' }) + @IsStringOrStringArray() + public context: string | string[] = ['https://w3id.org/did/v1'] + + @IsString() + public id!: string + + @IsArray() + @IsString({ each: true }) + @IsOptional() + public alsoKnownAs?: string[] + + @IsStringOrStringArray() + @IsOptional() + public controller?: string | string[] + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => VerificationMethod) + @IsOptional() + public verificationMethod?: VerificationMethod[] + + @IsArray() + @ServiceTransformer() + @IsOptional() + public service?: DidDocumentService[] + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public authentication?: Array + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public assertionMethod?: Array + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public keyAgreement?: Array + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public capabilityInvocation?: Array + + @IsArray() + @VerificationMethodTransformer() + @IsStringOrVerificationMethod({ each: true }) + @IsOptional() + public capabilityDelegation?: Array + + public constructor(options: DidDocumentOptions) { + if (options) { + this.context = options.context ?? this.context + this.id = options.id + this.alsoKnownAs = options.alsoKnownAs + this.controller = options.controller + this.verificationMethod = options.verificationMethod + this.service = options.service + this.authentication = options.authentication + this.assertionMethod = options.assertionMethod + this.keyAgreement = options.keyAgreement + this.capabilityInvocation = options.capabilityInvocation + this.capabilityDelegation = options.capabilityDelegation + } + } + + public dereferenceVerificationMethod(keyId: string) { + // TODO: once we use JSON-LD we should use that to resolve references in did documents. + // for now we check whether the key id ends with the keyId. + // so if looking for #123 and key.id is did:key:123#123, it is valid. But #123 as key.id is also valid + const verificationMethod = this.verificationMethod?.find((key) => key.id.endsWith(keyId)) + + if (!verificationMethod) { + throw new Error(`Unable to locate verification method with id '${keyId}'`) + } + + return verificationMethod + } + + public dereferenceKey(keyId: string, allowedPurposes?: DidPurpose[]) { + const allPurposes: DidPurpose[] = [ + 'authentication', + 'keyAgreement', + 'assertionMethod', + 'capabilityInvocation', + 'capabilityDelegation', + ] + + const purposes = allowedPurposes ?? allPurposes + + for (const purpose of purposes) { + for (const key of this[purpose] ?? []) { + if (typeof key === 'string' && key.endsWith(keyId)) { + return this.dereferenceVerificationMethod(key) + } else if (typeof key !== 'string' && key.id.endsWith(keyId)) { + return key + } + } + } + + throw new Error(`Unable to locate verification method with id '${keyId}' in purposes ${purposes}`) + } + + /** + * Returns all of the service endpoints matching the given type. + * + * @param type The type of service(s) to query. + */ + public getServicesByType(type: string): S[] { + return (this.service?.filter((service) => service.type === type) ?? []) as S[] + } + + /** + * Returns all of the service endpoints matching the given class + * + * @param classType The class to query services. + */ + public getServicesByClassType( + classType: new (...args: never[]) => S + ): S[] { + return (this.service?.filter((service) => service instanceof classType) ?? []) as S[] + } + + /** + * Get all DIDComm services ordered by priority descending. This means the highest + * priority will be the first entry. + */ + public get didCommServices(): Array { + const didCommServiceTypes = [IndyAgentService.type, DidCommV1Service.type] + const services = (this.service?.filter((service) => didCommServiceTypes.includes(service.type)) ?? []) as Array< + IndyAgentService | DidCommV1Service + > + + // Sort services based on indicated priority + return services.sort((a, b) => b.priority - a.priority) + } + + // TODO: it would probably be easier if we add a utility to each service so we don't have to handle logic for all service types here + public get recipientKeys(): Key[] { + let recipientKeys: Key[] = [] + + for (const service of this.didCommServices) { + if (service instanceof IndyAgentService) { + recipientKeys = [ + ...recipientKeys, + ...service.recipientKeys.map((publicKeyBase58) => Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519)), + ] + } else if (service instanceof DidCommV1Service) { + recipientKeys = [ + ...recipientKeys, + ...service.recipientKeys.map((recipientKey) => keyReferenceToKey(this, recipientKey)), + ] + } + } + + return recipientKeys + } + + public toJSON() { + return JsonTransformer.toJSON(this) + } +} + +export function keyReferenceToKey(didDocument: DidDocument, keyId: string) { + // FIXME: we allow authentication keys as historically ed25519 keys have been used in did documents + // for didcomm. In the future we should update this to only be allowed for IndyAgent and DidCommV1 services + // as didcomm v2 doesn't have this issue anymore + const verificationMethod = didDocument.dereferenceKey(keyId, ['authentication', 'keyAgreement']) + const { getKeyFromVerificationMethod } = getKeyDidMappingByVerificationMethod(verificationMethod) + const key = getKeyFromVerificationMethod(verificationMethod) + + return key +} diff --git a/packages/core/src/modules/dids/domain/DidDocumentBuilder.ts b/packages/core/src/modules/dids/domain/DidDocumentBuilder.ts new file mode 100644 index 0000000000..503a1f2759 --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidDocumentBuilder.ts @@ -0,0 +1,124 @@ +import type { DidDocumentService } from './service' + +import { DidDocument } from './DidDocument' +import { VerificationMethod } from './verificationMethod' + +export class DidDocumentBuilder { + private didDocument: DidDocument + + public constructor(id: string) { + this.didDocument = new DidDocument({ + id, + }) + } + + public addContext(context: string) { + if (typeof this.didDocument.context === 'string') { + this.didDocument.context = [this.didDocument.context, context] + } else { + this.didDocument.context.push(context) + } + + return this + } + + public addService(service: DidDocumentService) { + if (!this.didDocument.service) { + this.didDocument.service = [] + } + + this.didDocument.service.push(service) + + return this + } + + public addVerificationMethod(verificationMethod: VerificationMethod) { + if (!this.didDocument.verificationMethod) { + this.didDocument.verificationMethod = [] + } + + this.didDocument.verificationMethod.push( + verificationMethod instanceof VerificationMethod ? verificationMethod : new VerificationMethod(verificationMethod) + ) + + return this + } + + public addAuthentication(authentication: string | VerificationMethod) { + if (!this.didDocument.authentication) { + this.didDocument.authentication = [] + } + + const verificationMethod = + authentication instanceof VerificationMethod || typeof authentication === 'string' + ? authentication + : new VerificationMethod(authentication) + + this.didDocument.authentication.push(verificationMethod) + + return this + } + + public addAssertionMethod(assertionMethod: string | VerificationMethod) { + if (!this.didDocument.assertionMethod) { + this.didDocument.assertionMethod = [] + } + + const verificationMethod = + assertionMethod instanceof VerificationMethod || typeof assertionMethod === 'string' + ? assertionMethod + : new VerificationMethod(assertionMethod) + + this.didDocument.assertionMethod.push(verificationMethod) + + return this + } + + public addCapabilityDelegation(capabilityDelegation: string | VerificationMethod) { + if (!this.didDocument.capabilityDelegation) { + this.didDocument.capabilityDelegation = [] + } + + const verificationMethod = + capabilityDelegation instanceof VerificationMethod || typeof capabilityDelegation === 'string' + ? capabilityDelegation + : new VerificationMethod(capabilityDelegation) + + this.didDocument.capabilityDelegation.push(verificationMethod) + + return this + } + public addCapabilityInvocation(capabilityInvocation: string | VerificationMethod) { + if (!this.didDocument.capabilityInvocation) { + this.didDocument.capabilityInvocation = [] + } + + const verificationMethod = + capabilityInvocation instanceof VerificationMethod || typeof capabilityInvocation === 'string' + ? capabilityInvocation + : new VerificationMethod(capabilityInvocation) + + this.didDocument.capabilityInvocation.push(verificationMethod) + + return this + } + + public addKeyAgreement(keyAgreement: string | VerificationMethod) { + if (!this.didDocument.keyAgreement) { + this.didDocument.keyAgreement = [] + } + + const verificationMethod = + keyAgreement instanceof VerificationMethod || typeof keyAgreement === 'string' + ? keyAgreement + : new VerificationMethod(keyAgreement) + + this.didDocument.keyAgreement.push(verificationMethod) + + return this + } + + public build(): DidDocument { + return this.didDocument + } +} diff --git a/packages/core/src/modules/dids/domain/DidDocumentRole.ts b/packages/core/src/modules/dids/domain/DidDocumentRole.ts new file mode 100644 index 0000000000..66ba66e488 --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidDocumentRole.ts @@ -0,0 +1,4 @@ +export enum DidDocumentRole { + Created = 'created', + Received = 'received', +} diff --git a/packages/core/src/modules/dids/domain/DidResolver.ts b/packages/core/src/modules/dids/domain/DidResolver.ts new file mode 100644 index 0000000000..6e0a98537f --- /dev/null +++ b/packages/core/src/modules/dids/domain/DidResolver.ts @@ -0,0 +1,6 @@ +import type { ParsedDid, DidResolutionResult, DidResolutionOptions } from '../types' + +export interface DidResolver { + readonly supportedMethods: string[] + resolve(did: string, parsed: ParsedDid, didResolutionOptions: DidResolutionOptions): Promise +} diff --git a/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts new file mode 100644 index 0000000000..7bcd45e1dc --- /dev/null +++ b/packages/core/src/modules/dids/domain/__tests__/DidDocument.test.ts @@ -0,0 +1,281 @@ +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { MessageValidator } from '../../../../utils/MessageValidator' +import didExample123Fixture from '../../__tests__/__fixtures__/didExample123.json' +import didExample456Invalid from '../../__tests__/__fixtures__/didExample456Invalid.json' +import { DidDocument } from '../DidDocument' +import { DidDocumentService, IndyAgentService, DidCommV1Service } from '../service' +import { VerificationMethod } from '../verificationMethod' + +const didDocumentInstance = new DidDocument({ + id: 'did:example:123', + alsoKnownAs: ['did:example:456'], + controller: ['did:example:456'], + verificationMethod: [ + new VerificationMethod({ + id: 'did:example:123#key-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC X...', + }), + new VerificationMethod({ + id: 'did:example:123#key-2', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: '-----BEGIN PUBLIC 9...', + }), + new VerificationMethod({ + id: 'did:example:123#key-3', + type: 'Secp256k1VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }), + ], + service: [ + new DidDocumentService({ + id: 'did:example:123#service-1', + type: 'Mediator', + serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + }), + new IndyAgentService({ + id: 'did:example:123#service-2', + serviceEndpoint: 'did:sov:Q4zqM7aXqm7gDQkUVLng9h', + recipientKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + routingKeys: ['Q4zqM7aXqm7gDQkUVLng9h'], + priority: 5, + }), + new DidCommV1Service({ + id: 'did:example:123#service-3', + serviceEndpoint: 'https://agent.com/did-comm', + recipientKeys: ['DADEajsDSaksLng9h'], + routingKeys: ['DADEajsDSaksLng9h'], + priority: 10, + }), + ], + authentication: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#authentication-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], + assertionMethod: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#assertionMethod-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], + capabilityDelegation: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#capabilityDelegation-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], + capabilityInvocation: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#capabilityInvocation-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], + keyAgreement: [ + 'did:example:123#key-1', + new VerificationMethod({ + id: 'did:example:123#keyAgreement-1', + type: 'RsaVerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyPem: '-----BEGIN PUBLIC A...', + }), + ], +}) + +describe('Did | DidDocument', () => { + it('should correctly transforms Json to DidDocument class', () => { + const didDocument = JsonTransformer.fromJSON(didExample123Fixture, DidDocument) + + // Check other properties + expect(didDocument.id).toBe(didExample123Fixture.id) + expect(didDocument.alsoKnownAs).toEqual(didExample123Fixture.alsoKnownAs) + expect(didDocument.context).toEqual(didExample123Fixture['@context']) + expect(didDocument.controller).toEqual(didExample123Fixture.controller) + + // Check verification method + const verificationMethods = didDocument.verificationMethod ?? [] + expect(verificationMethods[0]).toBeInstanceOf(VerificationMethod) + expect(verificationMethods[1]).toBeInstanceOf(VerificationMethod) + expect(verificationMethods[2]).toBeInstanceOf(VerificationMethod) + + // Check Service + const services = didDocument.service ?? [] + expect(services[0]).toBeInstanceOf(DidDocumentService) + expect(services[1]).toBeInstanceOf(IndyAgentService) + expect(services[2]).toBeInstanceOf(DidCommV1Service) + + // Check Authentication + const authentication = didDocument.authentication ?? [] + expect(typeof authentication[0]).toBe('string') + expect(authentication[1]).toBeInstanceOf(VerificationMethod) + + // Check assertionMethod + const assertionMethod = didDocument.assertionMethod ?? [] + expect(typeof assertionMethod[0]).toBe('string') + expect(assertionMethod[1]).toBeInstanceOf(VerificationMethod) + + // Check capabilityDelegation + const capabilityDelegation = didDocument.capabilityDelegation ?? [] + expect(typeof capabilityDelegation[0]).toBe('string') + expect(capabilityDelegation[1]).toBeInstanceOf(VerificationMethod) + + // Check capabilityInvocation + const capabilityInvocation = didDocument.capabilityInvocation ?? [] + expect(typeof capabilityInvocation[0]).toBe('string') + expect(capabilityInvocation[1]).toBeInstanceOf(VerificationMethod) + + // Check keyAgreement + const keyAgreement = didDocument.keyAgreement ?? [] + expect(typeof keyAgreement[0]).toBe('string') + expect(keyAgreement[1]).toBeInstanceOf(VerificationMethod) + }) + + it('validation should throw an error if the did document is invalid', async () => { + const didDocument = JsonTransformer.fromJSON(didExample456Invalid, DidDocument) + + try { + await MessageValidator.validate(didDocument) + } catch (error) { + expect(error).toMatchObject([ + { + value: 'did:example:123', + property: 'alsoKnownAs', + children: [], + constraints: { isArray: 'alsoKnownAs must be an array' }, + }, + { + value: [ + 'did:example:456#key-1', + { + id: 'did:example:456#key-2', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: '-----BEGIN PUBLIC 9...', + }, + { + id: 'did:example:456#key-3', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }, + ], + property: 'verificationMethod', + children: [ + { + target: [ + 'did:example:456#key-1', + { + id: 'did:example:456#key-2', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: '-----BEGIN PUBLIC 9...', + }, + { + id: 'did:example:456#key-3', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }, + ], + value: 'did:example:456#key-1', + property: '0', + children: [ + { + value: 'did:example:456#key-1', + property: 'verificationMethod', + constraints: { + nestedValidation: 'each value in nested property verificationMethod must be either object or array', + }, + }, + ], + }, + { + target: [ + 'did:example:456#key-1', + { + id: 'did:example:456#key-2', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyBase58: '-----BEGIN PUBLIC 9...', + }, + { + id: 'did:example:456#key-3', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }, + ], + value: { + id: 'did:example:456#key-3', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }, + property: '2', + children: [ + { + target: { + id: 'did:example:456#key-3', + controller: 'did:sov:LjgpST2rjsoxYegQDRm7EL', + publicKeyHex: '-----BEGIN PUBLIC A...', + }, + property: 'type', + children: [], + constraints: { isString: 'type must be a string' }, + }, + ], + }, + ], + }, + ]) + } + }) + + it('should correctly transforms DidDoc class to Json', () => { + const didDocumentJson = JsonTransformer.toJSON(didDocumentInstance) + + expect(didDocumentJson).toMatchObject(didExample123Fixture) + }) + + describe('getServicesByType', () => { + it('returns all services with specified type', async () => { + expect(didDocumentInstance.getServicesByType('IndyAgent')).toEqual( + didDocumentInstance.service?.filter((service) => service.type === 'IndyAgent') + ) + }) + }) + + describe('getServicesByClassType', () => { + it('returns all services with specified class', async () => { + expect(didDocumentInstance.getServicesByClassType(IndyAgentService)).toEqual( + didDocumentInstance.service?.filter((service) => service instanceof IndyAgentService) + ) + }) + }) + + describe('didCommServices', () => { + it('returns all IndyAgentService and DidCommService instances', async () => { + const services = didDocumentInstance.service ?? [] + + expect(didDocumentInstance.didCommServices).toEqual(expect.arrayContaining([services[1], services[2]])) + }) + + it('returns all IndyAgentService and DidCommService instances sorted by priority', async () => { + const services = didDocumentInstance.service ?? [] + + expect(didDocumentInstance.didCommServices).toEqual([services[2], services[1]]) + }) + }) +}) diff --git a/packages/core/src/modules/dids/domain/createPeerDidFromServices.ts b/packages/core/src/modules/dids/domain/createPeerDidFromServices.ts new file mode 100644 index 0000000000..6f4dfe6a00 --- /dev/null +++ b/packages/core/src/modules/dids/domain/createPeerDidFromServices.ts @@ -0,0 +1,72 @@ +import type { ResolvedDidCommService } from '../../../agent/MessageSender' + +import { convertPublicKeyToX25519 } from '@stablelib/ed25519' + +import { KeyType, Key } from '../../../crypto' +import { AriesFrameworkError } from '../../../error' +import { uuid } from '../../../utils/uuid' +import { DidKey } from '../methods/key' + +import { DidDocumentBuilder } from './DidDocumentBuilder' +import { getEd25519VerificationMethod } from './key-type/ed25519' +import { getX25519VerificationMethod } from './key-type/x25519' +import { DidCommV1Service } from './service/DidCommV1Service' + +export function createDidDocumentFromServices(services: ResolvedDidCommService[]) { + const didDocumentBuilder = new DidDocumentBuilder('') + + // Keep track off all added key id based on the fingerprint so we can add them to the recipientKeys as references + const recipientKeyIdMapping: { [fingerprint: string]: string } = {} + + services.forEach((service, index) => { + // Get the local key reference for each of the recipient keys + const recipientKeys = service.recipientKeys.map((recipientKey) => { + // Key already added to the did document + if (recipientKeyIdMapping[recipientKey.fingerprint]) return recipientKeyIdMapping[recipientKey.fingerprint] + + if (recipientKey.keyType !== KeyType.Ed25519) { + throw new AriesFrameworkError( + `Unable to create did document from services. recipient key type ${recipientKey.keyType} is not supported. Supported key types are ${KeyType.Ed25519}` + ) + } + const x25519Key = Key.fromPublicKey(convertPublicKeyToX25519(recipientKey.publicKey), KeyType.X25519) + + const ed25519VerificationMethod = getEd25519VerificationMethod({ + id: `#${uuid()}`, + key: recipientKey, + controller: '#id', + }) + const x25519VerificationMethod = getX25519VerificationMethod({ + id: `#${uuid()}`, + key: x25519Key, + controller: '#id', + }) + + recipientKeyIdMapping[recipientKey.fingerprint] = ed25519VerificationMethod.id + + // We should not add duplicated keys for services + didDocumentBuilder.addAuthentication(ed25519VerificationMethod).addKeyAgreement(x25519VerificationMethod) + + return recipientKeyIdMapping[recipientKey.fingerprint] + }) + + // Transform all routing keys into did:key:xxx#key-id references. This will probably change for didcomm v2 + const routingKeys = service.routingKeys?.map((key) => { + const didKey = new DidKey(key) + + return `${didKey.did}#${key.fingerprint}` + }) + + didDocumentBuilder.addService( + new DidCommV1Service({ + id: service.id, + priority: index, + serviceEndpoint: service.serviceEndpoint, + recipientKeys, + routingKeys, + }) + ) + }) + + return didDocumentBuilder.build() +} diff --git a/packages/core/src/modules/dids/domain/index.ts b/packages/core/src/modules/dids/domain/index.ts new file mode 100644 index 0000000000..bf0ff1c854 --- /dev/null +++ b/packages/core/src/modules/dids/domain/index.ts @@ -0,0 +1,4 @@ +export * from './service' +export * from './verificationMethod' +export * from './DidDocument' +export * from './DidDocumentBuilder' diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1.test.ts new file mode 100644 index 0000000000..7fdbf067ab --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1.test.ts @@ -0,0 +1,75 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import keyBls12381g1Fixture from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' +import { VerificationMethod } from '../../verificationMethod' +import { keyDidBls12381g1 } from '../bls12381g1' + +const TEST_BLS12381G1_BASE58_KEY = '6FywSzB5BPd7xehCo1G4nYHAoZPMMP3gd4PLnvgA6SsTsogtz8K7RDznqLpFPLZXAE' +const TEST_BLS12381G1_FINGERPRINT = 'z3tEFALUKUzzCAvytMHX8X4SnsNsq6T5tC5Zb18oQEt1FqNcJXqJ3AA9umgzA9yoqPBeWA' +const TEST_BLS12381G1_DID = `did:key:${TEST_BLS12381G1_FINGERPRINT}` +const TEST_BLS12381G1_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([234, 1]), + TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY), +]) + +describe('bls12381g1', () => { + it('creates a Key instance from public key bytes and bls12381g1 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY) + + const key = Key.fromPublicKey(publicKeyBytes, KeyType.Bls12381g1) + + expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and bls12381g1 key type', async () => { + const key = Key.fromPublicKeyBase58(TEST_BLS12381G1_BASE58_KEY, KeyType.Bls12381g1) + + expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1_FINGERPRINT) + + expect(key.publicKeyBase58).toBe(TEST_BLS12381G1_BASE58_KEY) + }) + + it('should correctly calculate the getter properties', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1_FINGERPRINT) + + expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + expect(key.publicKeyBase58).toBe(TEST_BLS12381G1_BASE58_KEY) + expect(key.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY)) + expect(key.keyType).toBe(KeyType.Bls12381g1) + expect(key.prefixedPublicKey.equals(TEST_BLS12381G1_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1_FINGERPRINT) + const verificationMethods = keyDidBls12381g1.getVerificationMethods(TEST_BLS12381G1_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([keyBls12381g1Fixture.verificationMethod[0]]) + }) + + it('supports Bls12381G1Key2020 verification method type', () => { + expect(keyDidBls12381g1.supportedVerificationMethodTypes).toMatchObject(['Bls12381G1Key2020']) + }) + + it('returns key for Bls12381G1Key2020 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(keyBls12381g1Fixture.verificationMethod[0], VerificationMethod) + + const key = keyDidBls12381g1.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(keyBls12381g1Fixture.verificationMethod[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidBls12381g1.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + 'Invalid verification method passed' + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1g2.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1g2.test.ts new file mode 100644 index 0000000000..442422f2cb --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g1g2.test.ts @@ -0,0 +1,104 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import keyBls12381g1g2Fixture from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' +import { VerificationMethod } from '../../verificationMethod' +import { keyDidBls12381g1g2 } from '../bls12381g1g2' + +const TEST_BLS12381G1G2_BASE58_KEY = + 'AQ4MiG1JKHmM5N4CgkF9uQ484PHN7gXB3ctF4ayL8hT6FdD6rcfFS3ZnMNntYsyJBckfNPf3HL8VU8jzgyT3qX88Yg3TeF2NkG2aZnJDNnXH1jkJStWMxjLw22LdphqAj1rSorsDhHjE8Rtz61bD6FP9aPokQUDVpZ4zXqsXVcxJ7YEc66TTLTTPwQPS7uNM4u2Fs' +const TEST_BLS12381G1G2_FINGERPRINT = + 'z5TcESXuYUE9aZWYwSdrUEGK1HNQFHyTt4aVpaCTVZcDXQmUheFwfNZmRksaAbBneNm5KyE52SdJeRCN1g6PJmF31GsHWwFiqUDujvasK3wTiDr3vvkYwEJHt7H5RGEKYEp1ErtQtcEBgsgY2DA9JZkHj1J9HZ8MRDTguAhoFtR4aTBQhgnkP4SwVbxDYMEZoF2TMYn3s' +const TEST_BLS12381G1G2_DID = `did:key:${TEST_BLS12381G1G2_FINGERPRINT}` + +const TEST_BLS12381G1_BASE58_KEY = '7BVES4h78wzabPAfMhchXyH5d8EX78S5TtzePH2YkftWcE6by9yj3NTAv9nsyCeYch' +const TEST_BLS12381G1_FINGERPRINT = 'z3tEG5qmJZX29jJSX5kyhDR5YJNnefJFdwTxRqk6zbEPv4Pf2xF12BpmXv9NExxSRFGfxd' + +const TEST_BLS12381G2_BASE58_KEY = + '26d2BdqELsXg7ZHCWKL2D5Y2S7mYrpkdhJemSEEvokd4qy4TULJeeU44hYPGKo4x4DbBp5ARzkv1D6xuB3bmhpdpKAXuXtode67wzh9PCtW8kTqQhH19VSiFZkLNkhe9rtf3' +const TEST_BLS12381G2_FINGERPRINT = + 'zUC7LTa4hWtaE9YKyDsMVGiRNqPMN3s4rjBdB3MFi6PcVWReNfR72y3oGW2NhNcaKNVhMobh7aHp8oZB3qdJCs7RebM2xsodrSm8MmePbN25NTGcpjkJMwKbcWfYDX7eHCJjPGM' + +const TEST_BLS12381G1G2_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([238, 1]), + TypedArrayEncoder.fromBase58(TEST_BLS12381G1G2_BASE58_KEY), +]) + +describe('bls12381g1g2', () => { + it('creates a Key instance from public key bytes and bls12381g1g2 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_BLS12381G1G2_BASE58_KEY) + + const key = Key.fromPublicKey(publicKeyBytes, KeyType.Bls12381g1g2) + + expect(key.fingerprint).toBe(TEST_BLS12381G1G2_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and bls12381g1g2 key type', async () => { + const key = Key.fromPublicKeyBase58(TEST_BLS12381G1G2_BASE58_KEY, KeyType.Bls12381g1g2) + + expect(key.fingerprint).toBe(TEST_BLS12381G1G2_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + + expect(key.publicKeyBase58).toBe(TEST_BLS12381G1G2_BASE58_KEY) + }) + + it('should correctly calculate the getter properties', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + + expect(key.fingerprint).toBe(TEST_BLS12381G1G2_FINGERPRINT) + expect(key.publicKeyBase58).toBe(TEST_BLS12381G1G2_BASE58_KEY) + expect(key.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G1G2_BASE58_KEY)) + expect(key.keyType).toBe(KeyType.Bls12381g1g2) + expect(key.prefixedPublicKey.equals(TEST_BLS12381G1G2_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + const verificationMethods = keyDidBls12381g1g2.getVerificationMethods(TEST_BLS12381G1G2_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject(keyBls12381g1g2Fixture.verificationMethod) + }) + + it('supports no verification method type', () => { + // Verification methods can be handled by g1 or g2 key types. No reason to do it in here + expect(keyDidBls12381g1g2.supportedVerificationMethodTypes).toMatchObject([]) + }) + + it('throws an error for getKeyFromVerificationMethod as it is not supported for bls12381g1g2 key types', () => { + const verificationMethod = JsonTransformer.fromJSON( + keyBls12381g1g2Fixture.verificationMethod[0], + VerificationMethod + ) + + expect(() => keyDidBls12381g1g2.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + 'Not supported for bls12381g1g2 key' + ) + }) + + it('should correctly go from g1g2 to g1', async () => { + const g1g2Key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + + const g1PublicKey = g1g2Key.publicKey.slice(0, 48) + const g1DidKey = Key.fromPublicKey(g1PublicKey, KeyType.Bls12381g1) + + expect(g1DidKey.fingerprint).toBe(TEST_BLS12381G1_FINGERPRINT) + expect(g1DidKey.publicKeyBase58).toBe(TEST_BLS12381G1_BASE58_KEY) + expect(g1DidKey.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G1_BASE58_KEY)) + expect(g1DidKey.keyType).toBe(KeyType.Bls12381g1) + }) + + it('should correctly go from g1g2 to g2', async () => { + const g1g2Key = Key.fromFingerprint(TEST_BLS12381G1G2_FINGERPRINT) + + const g2PublicKey = g1g2Key.publicKey.slice(48) + const g2DidKey = Key.fromPublicKey(g2PublicKey, KeyType.Bls12381g2) + + expect(g2DidKey.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + expect(g2DidKey.publicKeyBase58).toBe(TEST_BLS12381G2_BASE58_KEY) + expect(g2DidKey.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY)) + expect(g2DidKey.keyType).toBe(KeyType.Bls12381g2) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g2.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g2.test.ts new file mode 100644 index 0000000000..5b326f1f3b --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/bls12381g2.test.ts @@ -0,0 +1,77 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import keyBls12381g2Fixture from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' +import { VerificationMethod } from '../../verificationMethod' +import { keyDidBls12381g2 } from '../bls12381g2' + +const TEST_BLS12381G2_BASE58_KEY = + 'mxE4sHTpbPcmxNviRVR9r7D2taXcNyVJmf9TBUFS1gRt3j3Ej9Seo59GQeCzYwbQgDrfWCwEJvmBwjLvheAky5N2NqFVzk4kuq3S8g4Fmekai4P622vHqWjFrsioYYDqhf9' +const TEST_BLS12381G2_FINGERPRINT = + 'zUC71nmwvy83x1UzNKbZbS7N9QZx8rqpQx3Ee3jGfKiEkZngTKzsRoqobX6wZdZF5F93pSGYYco3gpK9tc53ruWUo2tkBB9bxPCFBUjq2th8FbtT4xih6y6Q1K9EL4Th86NiCGT' +const TEST_BLS12381G2_DID = `did:key:${TEST_BLS12381G2_FINGERPRINT}` +const TEST_BLS12381G2_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([235, 1]), + TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY), +]) + +describe('bls12381g2', () => { + it('creates a Key instance from public key bytes and bls12381g2 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY) + + const key = Key.fromPublicKey(publicKeyBytes, KeyType.Bls12381g2) + + expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and bls12381g2 key type', async () => { + const key = Key.fromPublicKeyBase58(TEST_BLS12381G2_BASE58_KEY, KeyType.Bls12381g2) + + expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G2_FINGERPRINT) + + expect(key.publicKeyBase58).toBe(TEST_BLS12381G2_BASE58_KEY) + }) + + it('should correctly calculate the getter properties', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G2_FINGERPRINT) + + expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + expect(key.publicKeyBase58).toBe(TEST_BLS12381G2_BASE58_KEY) + expect(key.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_BLS12381G2_BASE58_KEY)) + expect(key.keyType).toBe(KeyType.Bls12381g2) + expect(key.prefixedPublicKey.equals(TEST_BLS12381G2_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_BLS12381G2_FINGERPRINT) + const verificationMethods = keyDidBls12381g2.getVerificationMethods(TEST_BLS12381G2_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([keyBls12381g2Fixture.verificationMethod[0]]) + }) + + it('supports Bls12381G2Key2020 verification method type', () => { + expect(keyDidBls12381g2.supportedVerificationMethodTypes).toMatchObject(['Bls12381G2Key2020']) + }) + + it('returns key for Bls12381G2Key2020 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(keyBls12381g2Fixture.verificationMethod[0], VerificationMethod) + + const key = keyDidBls12381g2.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_BLS12381G2_FINGERPRINT) + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(keyBls12381g2Fixture.verificationMethod[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidBls12381g2.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + 'Invalid verification method passed' + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts new file mode 100644 index 0000000000..cd93ada9cd --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts @@ -0,0 +1,75 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import didKeyEd25519Fixture from '../../../__tests__/__fixtures__//didKeyEd25519.json' +import { VerificationMethod } from '../../../domain/verificationMethod' +import { keyDidEd25519 } from '../ed25519' + +const TEST_ED25519_BASE58_KEY = '8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K' +const TEST_ED25519_FINGERPRINT = 'z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th' +const TEST_ED25519_DID = `did:key:${TEST_ED25519_FINGERPRINT}` +const TEST_ED25519_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([237, 1]), + TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY), +]) + +describe('ed25519', () => { + it('creates a Key instance from public key bytes and ed25519 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY) + + const didKey = Key.fromPublicKey(publicKeyBytes, KeyType.Ed25519) + + expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and ed25519 key type', async () => { + const didKey = Key.fromPublicKeyBase58(TEST_ED25519_BASE58_KEY, KeyType.Ed25519) + + expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const didKey = Key.fromFingerprint(TEST_ED25519_FINGERPRINT) + + expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('should correctly calculate the getter properties', async () => { + const didKey = Key.fromFingerprint(TEST_ED25519_FINGERPRINT) + + expect(didKey.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + expect(didKey.publicKeyBase58).toBe(TEST_ED25519_BASE58_KEY) + expect(didKey.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_ED25519_BASE58_KEY)) + expect(didKey.keyType).toBe(KeyType.Ed25519) + expect(didKey.prefixedPublicKey.equals(TEST_ED25519_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_ED25519_FINGERPRINT) + const verificationMethods = keyDidEd25519.getVerificationMethods(TEST_ED25519_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([didKeyEd25519Fixture.verificationMethod[0]]) + }) + + it('supports Ed25519VerificationKey2018 verification method type', () => { + expect(keyDidEd25519.supportedVerificationMethodTypes).toMatchObject(['Ed25519VerificationKey2018']) + }) + + it('returns key for Ed25519VerificationKey2018 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyEd25519Fixture.verificationMethod[0], VerificationMethod) + + const key = keyDidEd25519.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_ED25519_FINGERPRINT) + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyEd25519Fixture.verificationMethod[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidEd25519.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + 'Invalid verification method passed' + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts new file mode 100644 index 0000000000..9562434057 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/x25519.test.ts @@ -0,0 +1,75 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { Buffer, JsonTransformer, TypedArrayEncoder } from '../../../../../utils' +import didKeyX25519Fixture from '../../../__tests__/__fixtures__/didKeyX25519.json' +import { VerificationMethod } from '../../verificationMethod' +import { keyDidX25519 } from '../x25519' + +const TEST_X25519_BASE58_KEY = '6fUMuABnqSDsaGKojbUF3P7ZkEL3wi2njsDdUWZGNgCU' +const TEST_X25519_FINGERPRINT = 'z6LShLeXRTzevtwcfehaGEzCMyL3bNsAeKCwcqwJxyCo63yE' +const TEST_X25519_DID = `did:key:${TEST_X25519_FINGERPRINT}` +const TEST_X25519_PREFIX_BYTES = Buffer.concat([ + new Uint8Array([236, 1]), + TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY), +]) + +describe('x25519', () => { + it('creates a Key instance from public key bytes and x25519 key type', async () => { + const publicKeyBytes = TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY) + + const didKey = Key.fromPublicKey(publicKeyBytes, KeyType.X25519) + + expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('creates a Key instance from a base58 encoded public key and x25519 key type', async () => { + const didKey = Key.fromPublicKeyBase58(TEST_X25519_BASE58_KEY, KeyType.X25519) + + expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('creates a Key instance from a fingerprint', async () => { + const didKey = Key.fromFingerprint(TEST_X25519_FINGERPRINT) + + expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('should correctly calculate the getter properties', async () => { + const didKey = Key.fromFingerprint(TEST_X25519_FINGERPRINT) + + expect(didKey.fingerprint).toBe(TEST_X25519_FINGERPRINT) + expect(didKey.publicKeyBase58).toBe(TEST_X25519_BASE58_KEY) + expect(didKey.publicKey).toEqual(TypedArrayEncoder.fromBase58(TEST_X25519_BASE58_KEY)) + expect(didKey.keyType).toBe(KeyType.X25519) + expect(didKey.prefixedPublicKey.equals(TEST_X25519_PREFIX_BYTES)).toBe(true) + }) + + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_X25519_FINGERPRINT) + const verificationMethods = keyDidX25519.getVerificationMethods(TEST_X25519_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([didKeyX25519Fixture.keyAgreement[0]]) + }) + + it('supports X25519KeyAgreementKey2019 verification method type', () => { + expect(keyDidX25519.supportedVerificationMethodTypes).toMatchObject(['X25519KeyAgreementKey2019']) + }) + + it('returns key for X25519KeyAgreementKey2019 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyX25519Fixture.keyAgreement[0], VerificationMethod) + + const key = keyDidX25519.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_X25519_FINGERPRINT) + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyX25519Fixture.keyAgreement[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidX25519.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + 'Invalid verification method passed' + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/bls12381g1.ts b/packages/core/src/modules/dids/domain/key-type/bls12381g1.ts new file mode 100644 index 0000000000..6ac241f5d9 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/bls12381g1.ts @@ -0,0 +1,32 @@ +import type { VerificationMethod } from '../verificationMethod' +import type { KeyDidMapping } from './keyDidMapping' + +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' + +const VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020 = 'Bls12381G1Key2020' + +export function getBls12381g1VerificationMethod(did: string, key: Key) { + return { + id: `${did}#${key.fingerprint}`, + type: VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020, + controller: did, + publicKeyBase58: key.publicKeyBase58, + } +} + +export const keyDidBls12381g1: KeyDidMapping = { + supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020], + + getVerificationMethods: (did, key) => [getBls12381g1VerificationMethod(did, key)], + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if ( + verificationMethod.type !== VERIFICATION_METHOD_TYPE_BLS12381G1_KEY_2020 || + !verificationMethod.publicKeyBase58 + ) { + throw new Error('Invalid verification method passed') + } + + return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Bls12381g1) + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/bls12381g1g2.ts b/packages/core/src/modules/dids/domain/key-type/bls12381g1g2.ts new file mode 100644 index 0000000000..55b0d8c949 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/bls12381g1g2.ts @@ -0,0 +1,29 @@ +import type { KeyDidMapping } from './keyDidMapping' + +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' + +import { getBls12381g1VerificationMethod } from './bls12381g1' +import { getBls12381g2VerificationMethod } from './bls12381g2' + +export function getBls12381g1g2VerificationMethod(did: string, key: Key) { + const g1PublicKey = key.publicKey.slice(0, 48) + const g2PublicKey = key.publicKey.slice(48) + + const bls12381g1Key = Key.fromPublicKey(g1PublicKey, KeyType.Bls12381g1) + const bls12381g2Key = Key.fromPublicKey(g2PublicKey, KeyType.Bls12381g2) + + const bls12381g1VerificationMethod = getBls12381g1VerificationMethod(did, bls12381g1Key) + const bls12381g2VerificationMethod = getBls12381g2VerificationMethod(did, bls12381g2Key) + + return [bls12381g1VerificationMethod, bls12381g2VerificationMethod] +} + +export const keyDidBls12381g1g2: KeyDidMapping = { + supportedVerificationMethodTypes: [], + getVerificationMethods: getBls12381g1g2VerificationMethod, + + getKeyFromVerificationMethod: () => { + throw new Error('Not supported for bls12381g1g2 key') + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/bls12381g2.ts b/packages/core/src/modules/dids/domain/key-type/bls12381g2.ts new file mode 100644 index 0000000000..a17d20130a --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/bls12381g2.ts @@ -0,0 +1,33 @@ +import type { VerificationMethod } from '../verificationMethod' +import type { KeyDidMapping } from './keyDidMapping' + +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' + +const VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020 = 'Bls12381G2Key2020' + +export function getBls12381g2VerificationMethod(did: string, key: Key) { + return { + id: `${did}#${key.fingerprint}`, + type: VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020, + controller: did, + publicKeyBase58: key.publicKeyBase58, + } +} + +export const keyDidBls12381g2: KeyDidMapping = { + supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020], + + getVerificationMethods: (did, key) => [getBls12381g2VerificationMethod(did, key)], + + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if ( + verificationMethod.type !== VERIFICATION_METHOD_TYPE_BLS12381G2_KEY_2020 || + !verificationMethod.publicKeyBase58 + ) { + throw new Error('Invalid verification method passed') + } + + return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Bls12381g2) + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/ed25519.ts b/packages/core/src/modules/dids/domain/key-type/ed25519.ts new file mode 100644 index 0000000000..eb360c72fb --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/ed25519.ts @@ -0,0 +1,37 @@ +import type { VerificationMethod } from '../verificationMethod' +import type { KeyDidMapping } from './keyDidMapping' + +import { convertPublicKeyToX25519 } from '@stablelib/ed25519' + +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' + +const VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018 = 'Ed25519VerificationKey2018' + +export function getEd25519VerificationMethod({ key, id, controller }: { id: string; key: Key; controller: string }) { + return { + id, + type: VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, + controller, + publicKeyBase58: key.publicKeyBase58, + } +} + +export const keyDidEd25519: KeyDidMapping = { + supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018], + getVerificationMethods: (did, key) => [ + getEd25519VerificationMethod({ id: `${did}#${key.fingerprint}`, key, controller: did }), + ], + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if ( + verificationMethod.type !== VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018 || + !verificationMethod.publicKeyBase58 + ) { + throw new Error('Invalid verification method passed') + } + + return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Ed25519) + }, +} + +export { convertPublicKeyToX25519 } diff --git a/packages/core/src/modules/dids/domain/key-type/index.ts b/packages/core/src/modules/dids/domain/key-type/index.ts new file mode 100644 index 0000000000..8e0d752102 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/index.ts @@ -0,0 +1 @@ +export { getKeyDidMappingByKeyType, getKeyDidMappingByVerificationMethod } from './keyDidMapping' diff --git a/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts b/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts new file mode 100644 index 0000000000..713817d1bb --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts @@ -0,0 +1,71 @@ +import type { Key } from '../../../../crypto/Key' +import type { VerificationMethod } from '../verificationMethod' + +import { KeyType } from '../../../../crypto' + +import { keyDidBls12381g1 } from './bls12381g1' +import { keyDidBls12381g1g2 } from './bls12381g1g2' +import { keyDidBls12381g2 } from './bls12381g2' +import { keyDidEd25519 } from './ed25519' +import { keyDidX25519 } from './x25519' + +export interface KeyDidMapping { + getVerificationMethods: (did: string, key: Key) => VerificationMethod[] + getKeyFromVerificationMethod(verificationMethod: VerificationMethod): Key + supportedVerificationMethodTypes: string[] +} + +// TODO: Maybe we should make this dynamically? +const keyDidMapping: Record = { + [KeyType.Ed25519]: keyDidEd25519, + [KeyType.X25519]: keyDidX25519, + [KeyType.Bls12381g1]: keyDidBls12381g1, + [KeyType.Bls12381g2]: keyDidBls12381g2, + [KeyType.Bls12381g1g2]: keyDidBls12381g1g2, +} + +/** + * Dynamically creates a mapping from verification method key type to the key Did interface + * for all key types. + * + * { + * "Ed25519VerificationKey2018": KeyDidMapping + * } + */ +const verificationMethodKeyDidMapping = Object.values(KeyType).reduce>( + (mapping, keyType) => { + const supported = keyDidMapping[keyType].supportedVerificationMethodTypes.reduce>( + (accumulator, vMethodKeyType) => ({ + ...accumulator, + [vMethodKeyType]: keyDidMapping[keyType], + }), + {} + ) + + return { + ...mapping, + ...supported, + } + }, + {} +) + +export function getKeyDidMappingByKeyType(keyType: KeyType) { + const keyDid = keyDidMapping[keyType] + + if (!keyDid) { + throw new Error(`Unsupported key did from key type '${keyType}'`) + } + + return keyDid +} + +export function getKeyDidMappingByVerificationMethod(verificationMethod: VerificationMethod) { + const keyDid = verificationMethodKeyDidMapping[verificationMethod.type] + + if (!keyDid) { + throw new Error(`Unsupported key did from verification method type '${verificationMethod.type}'`) + } + + return keyDid +} diff --git a/packages/core/src/modules/dids/domain/key-type/x25519.ts b/packages/core/src/modules/dids/domain/key-type/x25519.ts new file mode 100644 index 0000000000..5ce7ff0683 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/x25519.ts @@ -0,0 +1,34 @@ +import type { VerificationMethod } from '../verificationMethod' +import type { KeyDidMapping } from './keyDidMapping' + +import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' + +const VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019 = 'X25519KeyAgreementKey2019' + +export function getX25519VerificationMethod({ key, id, controller }: { id: string; key: Key; controller: string }) { + return { + id, + type: VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019, + controller, + publicKeyBase58: key.publicKeyBase58, + } +} + +export const keyDidX25519: KeyDidMapping = { + supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019], + + getVerificationMethods: (did, key) => [ + getX25519VerificationMethod({ id: `${did}#${key.fingerprint}`, key, controller: did }), + ], + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if ( + verificationMethod.type !== VERIFICATION_METHOD_TYPE_X25519_KEY_AGREEMENT_KEY_2019 || + !verificationMethod.publicKeyBase58 + ) { + throw new Error('Invalid verification method passed') + } + + return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.X25519) + }, +} diff --git a/packages/core/src/modules/dids/domain/keyDidDocument.ts b/packages/core/src/modules/dids/domain/keyDidDocument.ts new file mode 100644 index 0000000000..ca639ca05b --- /dev/null +++ b/packages/core/src/modules/dids/domain/keyDidDocument.ts @@ -0,0 +1,109 @@ +import type { VerificationMethod } from './verificationMethod/VerificationMethod' + +import { KeyType, Key } from '../../../crypto' + +import { DidDocumentBuilder } from './DidDocumentBuilder' +import { getBls12381g1VerificationMethod } from './key-type/bls12381g1' +import { getBls12381g1g2VerificationMethod } from './key-type/bls12381g1g2' +import { getBls12381g2VerificationMethod } from './key-type/bls12381g2' +import { convertPublicKeyToX25519, getEd25519VerificationMethod } from './key-type/ed25519' +import { getX25519VerificationMethod } from './key-type/x25519' + +const didDocumentKeyTypeMapping = { + [KeyType.Ed25519]: getEd25519DidDoc, + [KeyType.X25519]: getX25519DidDoc, + [KeyType.Bls12381g1]: getBls12381g1DidDoc, + [KeyType.Bls12381g2]: getBls12381g2DidDoc, + [KeyType.Bls12381g1g2]: getBls12381g1g2DidDoc, +} + +export function getDidDocumentForKey(did: string, key: Key) { + const getDidDocument = didDocumentKeyTypeMapping[key.keyType] + + return getDidDocument(did, key) +} + +function getBls12381g1DidDoc(did: string, key: Key) { + const verificationMethod = getBls12381g1VerificationMethod(did, key) + + return getSignatureKeyBase({ + did, + key, + verificationMethod, + }).build() +} + +function getBls12381g1g2DidDoc(did: string, key: Key) { + const verificationMethods = getBls12381g1g2VerificationMethod(did, key) + + const didDocumentBuilder = new DidDocumentBuilder(did) + + for (const verificationMethod of verificationMethods) { + didDocumentBuilder + .addVerificationMethod(verificationMethod) + .addAuthentication(verificationMethod.id) + .addAssertionMethod(verificationMethod.id) + .addCapabilityDelegation(verificationMethod.id) + .addCapabilityInvocation(verificationMethod.id) + } + + return didDocumentBuilder.build() +} + +function getEd25519DidDoc(did: string, key: Key) { + const verificationMethod = getEd25519VerificationMethod({ id: `${did}#${key.fingerprint}`, key, controller: did }) + + const publicKeyX25519 = convertPublicKeyToX25519(key.publicKey) + const didKeyX25519 = Key.fromPublicKey(publicKeyX25519, KeyType.X25519) + const x25519VerificationMethod = getX25519VerificationMethod({ + id: `${did}#${didKeyX25519.fingerprint}`, + key: didKeyX25519, + controller: did, + }) + + const didDocBuilder = getSignatureKeyBase({ did, key, verificationMethod }) + + didDocBuilder + .addContext('https://w3id.org/security/suites/ed25519-2018/v1') + .addContext('https://w3id.org/security/suites/x25519-2019/v1') + .addKeyAgreement(x25519VerificationMethod) + + return didDocBuilder.build() +} + +function getX25519DidDoc(did: string, key: Key) { + const verificationMethod = getX25519VerificationMethod({ id: `${did}#${key.fingerprint}`, key, controller: did }) + + const document = new DidDocumentBuilder(did).addKeyAgreement(verificationMethod).build() + + return document +} + +function getBls12381g2DidDoc(did: string, key: Key) { + const verificationMethod = getBls12381g2VerificationMethod(did, key) + + return getSignatureKeyBase({ + did, + key, + verificationMethod, + }).build() +} + +function getSignatureKeyBase({ + did, + key, + verificationMethod, +}: { + did: string + key: Key + verificationMethod: VerificationMethod +}) { + const keyId = `${did}#${key.fingerprint}` + + return new DidDocumentBuilder(did) + .addVerificationMethod(verificationMethod) + .addAuthentication(keyId) + .addAssertionMethod(keyId) + .addCapabilityDelegation(keyId) + .addCapabilityInvocation(keyId) +} diff --git a/packages/core/src/modules/dids/domain/parse.ts b/packages/core/src/modules/dids/domain/parse.ts new file mode 100644 index 0000000000..aebeccec6f --- /dev/null +++ b/packages/core/src/modules/dids/domain/parse.ts @@ -0,0 +1,13 @@ +import type { ParsedDid } from '../types' + +import { parse } from 'did-resolver' + +export function parseDid(did: string): ParsedDid { + const parsed = parse(did) + + if (!parsed) { + throw new Error(`Error parsing did '${did}'`) + } + + return parsed +} diff --git a/packages/core/src/modules/connections/models/did/service/DidCommService.ts b/packages/core/src/modules/dids/domain/service/DidCommV1Service.ts similarity index 81% rename from packages/core/src/modules/connections/models/did/service/DidCommService.ts rename to packages/core/src/modules/dids/domain/service/DidCommV1Service.ts index 9f9ffe91a3..52b1a84195 100644 --- a/packages/core/src/modules/connections/models/did/service/DidCommService.ts +++ b/packages/core/src/modules/dids/domain/service/DidCommV1Service.ts @@ -1,8 +1,8 @@ import { ArrayNotEmpty, IsOptional, IsString } from 'class-validator' -import { Service } from './Service' +import { DidDocumentService } from './DidDocumentService' -export class DidCommService extends Service { +export class DidCommV1Service extends DidDocumentService { public constructor(options: { id: string serviceEndpoint: string @@ -11,7 +11,7 @@ export class DidCommService extends Service { accept?: string[] priority?: number }) { - super({ ...options, type: DidCommService.type }) + super({ ...options, type: DidCommV1Service.type }) if (options) { this.recipientKeys = options.recipientKeys diff --git a/packages/core/src/modules/dids/domain/service/DidCommV2Service.ts b/packages/core/src/modules/dids/domain/service/DidCommV2Service.ts new file mode 100644 index 0000000000..c8f04d227a --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/DidCommV2Service.ts @@ -0,0 +1,24 @@ +import { IsOptional, IsString } from 'class-validator' + +import { DidDocumentService } from './DidDocumentService' + +export class DidCommV2Service extends DidDocumentService { + public constructor(options: { id: string; serviceEndpoint: string; routingKeys?: string[]; accept?: string[] }) { + super({ ...options, type: DidCommV2Service.type }) + + if (options) { + this.routingKeys = options.routingKeys + this.accept = options.accept + } + } + + public static type = 'DIDComm' + + @IsString({ each: true }) + @IsOptional() + public routingKeys?: string[] + + @IsString({ each: true }) + @IsOptional() + public accept?: string[] +} diff --git a/packages/core/src/modules/connections/models/did/service/Service.ts b/packages/core/src/modules/dids/domain/service/DidDocumentService.ts similarity index 75% rename from packages/core/src/modules/connections/models/did/service/Service.ts rename to packages/core/src/modules/dids/domain/service/DidDocumentService.ts index 3ed46b0aa7..c3fd763ec5 100644 --- a/packages/core/src/modules/connections/models/did/service/Service.ts +++ b/packages/core/src/modules/dids/domain/service/DidDocumentService.ts @@ -1,6 +1,8 @@ import { IsString } from 'class-validator' -export class Service { +import { getProtocolScheme } from '../../../../utils/uri' + +export class DidDocumentService { public constructor(options: { id: string; serviceEndpoint: string; type: string }) { if (options) { this.id = options.id @@ -10,7 +12,7 @@ export class Service { } public get protocolScheme(): string { - return this.serviceEndpoint.split(':')[0] + return getProtocolScheme(this.serviceEndpoint) } @IsString() diff --git a/packages/core/src/modules/connections/models/did/service/IndyAgentService.ts b/packages/core/src/modules/dids/domain/service/IndyAgentService.ts similarity index 85% rename from packages/core/src/modules/connections/models/did/service/IndyAgentService.ts rename to packages/core/src/modules/dids/domain/service/IndyAgentService.ts index fd380af430..588547fda2 100644 --- a/packages/core/src/modules/connections/models/did/service/IndyAgentService.ts +++ b/packages/core/src/modules/dids/domain/service/IndyAgentService.ts @@ -1,8 +1,8 @@ import { ArrayNotEmpty, IsOptional, IsString } from 'class-validator' -import { Service } from './Service' +import { DidDocumentService } from './DidDocumentService' -export class IndyAgentService extends Service { +export class IndyAgentService extends DidDocumentService { public constructor(options: { id: string serviceEndpoint: string diff --git a/packages/core/src/modules/connections/models/did/service/index.ts b/packages/core/src/modules/dids/domain/service/ServiceTransformer.ts similarity index 52% rename from packages/core/src/modules/connections/models/did/service/index.ts rename to packages/core/src/modules/dids/domain/service/ServiceTransformer.ts index cedbbe0170..a47c6173d5 100644 --- a/packages/core/src/modules/connections/models/did/service/index.ts +++ b/packages/core/src/modules/dids/domain/service/ServiceTransformer.ts @@ -2,13 +2,15 @@ import type { ClassConstructor } from 'class-transformer' import { Transform, plainToInstance } from 'class-transformer' -import { DidCommService } from './DidCommService' +import { DidCommV1Service } from './DidCommV1Service' +import { DidCommV2Service } from './DidCommV2Service' +import { DidDocumentService } from './DidDocumentService' import { IndyAgentService } from './IndyAgentService' -import { Service } from './Service' export const serviceTypes: { [key: string]: unknown | undefined } = { [IndyAgentService.type]: IndyAgentService, - [DidCommService.type]: DidCommService, + [DidCommV1Service.type]: DidCommV1Service, + [DidCommV2Service.type]: DidCommV2Service, } /** @@ -22,10 +24,11 @@ export const serviceTypes: { [key: string]: unknown | undefined } = { */ export function ServiceTransformer() { return Transform( - ({ value }: { value: { type: string }[] }) => { - return value.map((serviceJson) => { - const serviceClass = (serviceTypes[serviceJson.type] ?? Service) as ClassConstructor - const service = plainToInstance(serviceClass, serviceJson) + ({ value }: { value?: Array<{ type: string }> }) => { + return value?.map((serviceJson) => { + const serviceClass = (serviceTypes[serviceJson.type] ?? + DidDocumentService) as ClassConstructor + const service = plainToInstance(serviceClass, serviceJson) return service }) @@ -35,5 +38,3 @@ export function ServiceTransformer() { } ) } - -export { IndyAgentService, DidCommService, Service } diff --git a/packages/core/src/modules/dids/domain/service/index.ts b/packages/core/src/modules/dids/domain/service/index.ts new file mode 100644 index 0000000000..51fc9bc8d9 --- /dev/null +++ b/packages/core/src/modules/dids/domain/service/index.ts @@ -0,0 +1,7 @@ +import { DidCommV1Service } from './DidCommV1Service' +import { DidCommV2Service } from './DidCommV2Service' +import { DidDocumentService } from './DidDocumentService' +import { IndyAgentService } from './IndyAgentService' +import { ServiceTransformer, serviceTypes } from './ServiceTransformer' + +export { IndyAgentService, DidCommV1Service, DidDocumentService, DidCommV2Service, ServiceTransformer, serviceTypes } diff --git a/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts new file mode 100644 index 0000000000..a86bd58978 --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts @@ -0,0 +1,73 @@ +import { IsString, IsOptional } from 'class-validator' + +export interface VerificationMethodOptions { + id: string + type: string + controller: string + publicKeyBase58?: string + publicKeyBase64?: string + publicKeyJwk?: Record + publicKeyHex?: string + publicKeyMultibase?: string + publicKeyPem?: string + blockchainAccountId?: string + ethereumAddress?: string +} + +export class VerificationMethod { + public constructor(options: VerificationMethodOptions) { + if (options) { + this.id = options.id + this.type = options.type + this.controller = options.controller + this.publicKeyBase58 = options.publicKeyBase58 + this.publicKeyBase64 = options.publicKeyBase64 + this.publicKeyJwk = options.publicKeyJwk + this.publicKeyHex = options.publicKeyHex + this.publicKeyMultibase = options.publicKeyMultibase + this.publicKeyPem = options.publicKeyPem + this.blockchainAccountId = options.blockchainAccountId + this.ethereumAddress = options.ethereumAddress + } + } + + @IsString() + public id!: string + + @IsString() + public type!: string + + @IsString() + public controller!: string + + @IsOptional() + @IsString() + public publicKeyBase58?: string + + @IsOptional() + @IsString() + public publicKeyBase64?: string + + // TODO: define JWK structure, we don't support JWK yet + public publicKeyJwk?: Record + + @IsOptional() + @IsString() + public publicKeyHex?: string + + @IsOptional() + @IsString() + public publicKeyMultibase?: string + + @IsOptional() + @IsString() + public publicKeyPem?: string + + @IsOptional() + @IsString() + public blockchainAccountId?: string + + @IsOptional() + @IsString() + public ethereumAddress?: string +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethodTransformer.ts b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethodTransformer.ts new file mode 100644 index 0000000000..43dfd2c1c0 --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethodTransformer.ts @@ -0,0 +1,59 @@ +import type { ValidationOptions } from 'class-validator' + +import { Transform, TransformationType } from 'class-transformer' +import { isString, ValidateBy, isInstance, buildMessage } from 'class-validator' + +import { JsonTransformer } from '../../../../utils/JsonTransformer' + +import { VerificationMethod } from './VerificationMethod' + +/** + * Checks if a given value is a real string. + */ +function IsStringOrVerificationMethod(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isStringOrVerificationMethod', + validator: { + validate: (value): boolean => isString(value) || isInstance(value, VerificationMethod), + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be a string or instance of VerificationMethod', + validationOptions + ), + }, + }, + validationOptions + ) +} + +/** + * Decorator that transforms authentication json to corresponding class instances + * + * @example + * class Example { + * VerificationMethodTransformer() + * private authentication: VerificationMethod + * } + */ +function VerificationMethodTransformer() { + return Transform(({ value, type }: { value?: Array; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + return value?.map((auth) => { + // referenced verification method + if (typeof auth === 'string') { + return String(auth) + } + + // embedded verification method + return JsonTransformer.fromJSON(auth, VerificationMethod) + }) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + return value?.map((auth) => (typeof auth === 'string' ? auth : JsonTransformer.toJSON(auth))) + } + + // PLAIN_TO_PLAIN + return value + }) +} + +export { IsStringOrVerificationMethod, VerificationMethodTransformer } diff --git a/packages/core/src/modules/dids/domain/verificationMethod/index.ts b/packages/core/src/modules/dids/domain/verificationMethod/index.ts new file mode 100644 index 0000000000..2bfdad4059 --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/index.ts @@ -0,0 +1,4 @@ +import { VerificationMethod } from './VerificationMethod' +import { VerificationMethodTransformer, IsStringOrVerificationMethod } from './VerificationMethodTransformer' + +export { VerificationMethod, VerificationMethodTransformer, IsStringOrVerificationMethod } diff --git a/packages/core/src/modules/dids/helpers.ts b/packages/core/src/modules/dids/helpers.ts new file mode 100644 index 0000000000..ef3c68ab07 --- /dev/null +++ b/packages/core/src/modules/dids/helpers.ts @@ -0,0 +1,31 @@ +import { KeyType, Key } from '../../crypto' + +import { DidKey } from './methods/key' + +export function didKeyToVerkey(key: string) { + if (key.startsWith('did:key')) { + const publicKeyBase58 = DidKey.fromDid(key).key.publicKeyBase58 + return publicKeyBase58 + } + return key +} + +export function verkeyToDidKey(key: string) { + if (key.startsWith('did:key')) { + return key + } + const publicKeyBase58 = key + const ed25519Key = Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519) + const didKey = new DidKey(ed25519Key) + return didKey.did +} + +export function didKeyToInstanceOfKey(key: string) { + const didKey = DidKey.fromDid(key) + return didKey.key +} + +export function verkeyToInstanceOfKey(verkey: string) { + const ed25519Key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) + return ed25519Key +} diff --git a/packages/core/src/modules/dids/index.ts b/packages/core/src/modules/dids/index.ts new file mode 100644 index 0000000000..573f20b4cd --- /dev/null +++ b/packages/core/src/modules/dids/index.ts @@ -0,0 +1,5 @@ +export * from './types' +export * from './domain' +export * from './DidsModule' +export * from './services' +export { DidKey } from './methods/key/DidKey' diff --git a/packages/core/src/modules/dids/methods/key/DidKey.ts b/packages/core/src/modules/dids/methods/key/DidKey.ts new file mode 100644 index 0000000000..fb377d63c0 --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/DidKey.ts @@ -0,0 +1,26 @@ +import { Key } from '../../../../crypto/Key' +import { getDidDocumentForKey } from '../../domain/keyDidDocument' +import { parseDid } from '../../domain/parse' + +export class DidKey { + public readonly key: Key + + public constructor(key: Key) { + this.key = key + } + + public static fromDid(did: string) { + const parsed = parseDid(did) + + const key = Key.fromFingerprint(parsed.id) + return new DidKey(key) + } + + public get did() { + return `did:key:${this.key.fingerprint}` + } + + public get didDocument() { + return getDidDocumentForKey(this.did, this.key) + } +} diff --git a/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts new file mode 100644 index 0000000000..eb7d4ee5ae --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts @@ -0,0 +1,31 @@ +import type { DidResolver } from '../../domain/DidResolver' +import type { DidResolutionResult } from '../../types' + +import { DidKey } from './DidKey' + +export class KeyDidResolver implements DidResolver { + public readonly supportedMethods = ['key'] + + public async resolve(did: string): Promise { + const didDocumentMetadata = {} + + try { + const didDocument = DidKey.fromDid(did).didDocument + + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } +} diff --git a/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts new file mode 100644 index 0000000000..ce63ed8a83 --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts @@ -0,0 +1,27 @@ +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import didKeyBls12381g1 from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' +import didKeyBls12381g1g2 from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' +import didKeyBls12381g2 from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' +import didKeyEd25519 from '../../../__tests__/__fixtures__/didKeyEd25519.json' +import didKeyX25519 from '../../../__tests__/__fixtures__/didKeyX25519.json' +import { DidKey } from '../DidKey' + +describe('DidKey', () => { + it('creates a DidKey instance from a did', async () => { + const documentTypes = [didKeyX25519, didKeyEd25519, didKeyBls12381g1, didKeyBls12381g2, didKeyBls12381g1g2] + + for (const documentType of documentTypes) { + const didKey = DidKey.fromDid(documentType.id) + expect(didKey.didDocument.toJSON()).toMatchObject(documentType) + } + }) + + it('creates a DidKey instance from a key instance', async () => { + const key = Key.fromPublicKeyBase58(didKeyX25519.keyAgreement[0].publicKeyBase58, KeyType.X25519) + const didKey = new DidKey(key) + + expect(didKey.did).toBe(didKeyX25519.id) + expect(didKey.didDocument.toJSON()).toMatchObject(didKeyX25519) + }) +}) diff --git a/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts new file mode 100644 index 0000000000..7c12e9f110 --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/__tests__/KeyDidResolver.test.ts @@ -0,0 +1,54 @@ +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import didKeyEd25519Fixture from '../../../__tests__/__fixtures__/didKeyEd25519.json' +import { DidKey } from '../DidKey' +import { KeyDidResolver } from '../KeyDidResolver' + +describe('DidResolver', () => { + describe('KeyDidResolver', () => { + let keyDidResolver: KeyDidResolver + + beforeEach(() => { + keyDidResolver = new KeyDidResolver() + }) + + it('should correctly resolve a did:key document', async () => { + const fromDidSpy = jest.spyOn(DidKey, 'fromDid') + const result = await keyDidResolver.resolve('did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didKeyEd25519Fixture, + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + }) + expect(result.didDocument) + expect(fromDidSpy).toHaveBeenCalledTimes(1) + expect(fromDidSpy).toHaveBeenCalledWith('did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') + }) + + it('should return did resolution metadata with error if the did contains an unsupported multibase', async () => { + const result = await keyDidResolver.resolve('did:key:asdfkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') + + expect(result).toEqual({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:key:asdfkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th': Error: No decoder found for multibase prefix 'a'`, + }, + }) + }) + + it('should return did resolution metadata with error if the did contains an unsupported multibase', async () => { + const result = await keyDidResolver.resolve('did:key:z6MkmjYasdfasfd8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th') + + expect(result).toEqual({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:key:z6MkmjYasdfasfd8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th': Error: Unsupported key type from multicodec code '107'`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/key/index.ts b/packages/core/src/modules/dids/methods/key/index.ts new file mode 100644 index 0000000000..c832783193 --- /dev/null +++ b/packages/core/src/modules/dids/methods/key/index.ts @@ -0,0 +1 @@ +export { DidKey } from './DidKey' diff --git a/packages/core/src/modules/dids/methods/peer/DidPeer.ts~73d296f6 (fix: always encode keys according to RFCs (#733)) b/packages/core/src/modules/dids/methods/peer/DidPeer.ts~73d296f6 (fix: always encode keys according to RFCs (#733)) new file mode 100644 index 0000000000..e73554e2a2 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/DidPeer.ts~73d296f6 (fix: always encode keys according to RFCs (#733)) @@ -0,0 +1,134 @@ +import type { DidDocument } from '../../domain' +import type { ParsedDid } from '../../types' + +import { instanceToInstance } from 'class-transformer' + +import { JsonEncoder, MultiBaseEncoder, MultiHashEncoder } from '../../../../utils' +import { Key } from '../../domain/Key' +import { getDidDocumentForKey } from '../../domain/keyDidDocument' +import { parseDid } from '../../domain/parse' + +import { didDocumentToNumAlgo2Did, didToNumAlgo2DidDocument } from './peerDidNumAlgo2' + +const PEER_DID_REGEX = new RegExp( + '^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))+(.(S)[0-9a-zA-Z=]*)?)))$' +) + +export const enum PeerDidNumAlgo { + InceptionKeyWithoutDoc = 0, + GenesisDoc = 1, + MultipleInceptionKeyWithoutDoc = 2, +} + +function getNumAlgoFromPeerDid(did: string) { + return Number(did[9]) +} + +export class DidPeer { + private readonly parsedDid: ParsedDid + + // If numAlgo 1 is used, the did document always has a did document + private readonly _didDocument?: DidDocument + + private constructor({ didDocument, did }: { did: string; didDocument?: DidDocument }) { + const parsed = parseDid(did) + + if (!this.isValidPeerDid(did)) { + throw new Error(`Invalid peer did '${did}'`) + } + + this.parsedDid = parsed + this._didDocument = didDocument + } + + public static fromKey(key: Key) { + const did = `did:peer:0${key.fingerprint}` + return new DidPeer({ did }) + } + + public static fromDid(did: string) { + return new DidPeer({ + did, + }) + } + + public static fromDidDocument( + didDocument: DidDocument, + numAlgo?: PeerDidNumAlgo.GenesisDoc | PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + ): DidPeer { + if (!numAlgo && didDocument.id.startsWith('did:peer:')) { + numAlgo = getNumAlgoFromPeerDid(didDocument.id) + } + + if (!numAlgo) { + throw new Error( + 'Could not determine numAlgo. The did document must either have a full id property containing the numAlgo, or the numAlgo must be provided as a separate property' + ) + } + + if (numAlgo === PeerDidNumAlgo.GenesisDoc) { + // FIXME: We should do this on the JSON value of the did document, as the DidDocument class + // adds a lot of properties and default values that will mess with the hash value + // Remove id from did document as the id should be generated without an id. + const didDocumentBuffer = JsonEncoder.toBuffer({ ...didDocument.toJSON(), id: undefined }) + + const didIdentifier = MultiBaseEncoder.encode(MultiHashEncoder.encode(didDocumentBuffer, 'sha2-256'), 'base58btc') + + const did = `did:peer:1${didIdentifier}` + + return new DidPeer({ did, didDocument }) + } else if (numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) { + const did = didDocumentToNumAlgo2Did(didDocument) + return new DidPeer({ did }) + } else { + throw new Error(`Unsupported numAlgo: ${numAlgo}. Not all peer did methods support parsing a did document`) + } + } + + public get did() { + return this.parsedDid.did + } + + public get numAlgo(): PeerDidNumAlgo { + // numalgo is the first digit of the method specific identifier + return Number(this.parsedDid.id[0]) as PeerDidNumAlgo + } + + private get identifierWithoutNumAlgo() { + return this.parsedDid.id.substring(1) + } + + private isValidPeerDid(did: string): boolean { + const isValid = PEER_DID_REGEX.test(did) + + return isValid + } + + public get didDocument() { + // Method 1 (numAlgo 0) + if (this.numAlgo === PeerDidNumAlgo.InceptionKeyWithoutDoc) { + const key = Key.fromFingerprint(this.identifierWithoutNumAlgo) + return getDidDocumentForKey(this.parsedDid.did, key) + } + // Method 2 (numAlgo 1) + else if (this.numAlgo === PeerDidNumAlgo.GenesisDoc) { + if (!this._didDocument) { + throw new Error('No did document provided for method 1 peer did') + } + + // Clone the document, and set the id + const didDocument = instanceToInstance(this._didDocument) + didDocument.id = this.did + + return didDocument + } + // Method 3 (numAlgo 2) + else if (this.numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) { + const didDocument = didToNumAlgo2DidDocument(this.parsedDid.did) + + return didDocument + } + + throw new Error(`Unsupported numAlgo '${this.numAlgo}'`) + } +} diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts new file mode 100644 index 0000000000..6aebfda5f2 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts @@ -0,0 +1,68 @@ +import type { DidDocument } from '../../domain' +import type { DidResolver } from '../../domain/DidResolver' +import type { DidRepository } from '../../repository' +import type { DidResolutionResult } from '../../types' + +import { AriesFrameworkError } from '../../../../error' + +import { getNumAlgoFromPeerDid, isValidPeerDid, PeerDidNumAlgo } from './didPeer' +import { didToNumAlgo0DidDocument } from './peerDidNumAlgo0' +import { didToNumAlgo2DidDocument } from './peerDidNumAlgo2' + +export class PeerDidResolver implements DidResolver { + public readonly supportedMethods = ['peer'] + + private didRepository: DidRepository + + public constructor(didRepository: DidRepository) { + this.didRepository = didRepository + } + + public async resolve(did: string): Promise { + const didDocumentMetadata = {} + + try { + let didDocument: DidDocument + + if (!isValidPeerDid(did)) { + throw new AriesFrameworkError(`did ${did} is not a valid peer did`) + } + + const numAlgo = getNumAlgoFromPeerDid(did) + + // For method 0, generate from did + if (numAlgo === PeerDidNumAlgo.InceptionKeyWithoutDoc) { + didDocument = didToNumAlgo0DidDocument(did) + } + // For Method 1, retrieve from storage + else if (numAlgo === PeerDidNumAlgo.GenesisDoc) { + const didDocumentRecord = await this.didRepository.getById(did) + + if (!didDocumentRecord.didDocument) { + throw new AriesFrameworkError(`Found did record for method 1 peer did (${did}), but no did document.`) + } + + didDocument = didDocumentRecord.didDocument + } + // For Method 2, generate from did + else { + didDocument = didToNumAlgo2DidDocument(did) + } + + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts~9399a710 (feat: find existing connection based on invitation did (#698)) b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts~9399a710 (feat: find existing connection based on invitation did (#698)) new file mode 100644 index 0000000000..abc697a492 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts~9399a710 (feat: find existing connection based on invitation did (#698)) @@ -0,0 +1,117 @@ +import { JsonTransformer } from '../../../../../utils' +import didKeyBls12381g1 from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' +import didKeyBls12381g1g2 from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' +import didKeyBls12381g2 from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' +import didKeyEd25519 from '../../../__tests__/__fixtures__/didKeyEd25519.json' +import didKeyX25519 from '../../../__tests__/__fixtures__/didKeyX25519.json' +import { DidDocument, Key } from '../../../domain' +import { DidPeer, PeerDidNumAlgo } from '../DidPeer' +import { outOfBandServiceToNumAlgo2Did } from '../peerDidNumAlgo2' + +import didPeer1zQmRDidCommServices from './__fixtures__/didPeer1zQmR-did-comm-service.json' +import didPeer1zQmR from './__fixtures__/didPeer1zQmR.json' +import didPeer1zQmZ from './__fixtures__/didPeer1zQmZ.json' +import didPeer2Ez6L from './__fixtures__/didPeer2Ez6L.json' + +describe('DidPeer', () => { + test('transforms a key correctly into a peer did method 0 did document', async () => { + const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] + + for (const didDocument of didDocuments) { + const key = Key.fromFingerprint(didDocument.id.split(':')[2]) + + const didPeer = DidPeer.fromKey(key) + const expectedDidPeerDocument = JSON.parse( + JSON.stringify(didDocument).replace(new RegExp('did:key:', 'g'), 'did:peer:0') + ) + + expect(didPeer.didDocument.toJSON()).toMatchObject(expectedDidPeerDocument) + } + }) + + test('transforms a method 2 did correctly into a did document', () => { + expect(DidPeer.fromDid(didPeer2Ez6L.id).didDocument.toJSON()).toMatchObject(didPeer2Ez6L) + }) + + test('transforms a method 0 did correctly into a did document', () => { + const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] + + for (const didDocument of didDocuments) { + const didPeer = DidPeer.fromDid(didDocument.id.replace('did:key:', 'did:peer:0')) + const expectedDidPeerDocument = JSON.parse( + JSON.stringify(didDocument).replace(new RegExp('did:key:', 'g'), 'did:peer:0') + ) + + expect(didPeer.didDocument.toJSON()).toMatchObject(expectedDidPeerDocument) + } + }) + + test('transforms a did document into a valid method 2 did', () => { + const didPeer2 = DidPeer.fromDidDocument( + JsonTransformer.fromJSON(didPeer2Ez6L, DidDocument), + PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + ) + + expect(didPeer2.did).toBe(didPeer2Ez6L.id) + }) + + test('transforms a did comm service into a valid method 2 did', () => { + const didDocument = JsonTransformer.fromJSON(didPeer1zQmRDidCommServices, DidDocument) + const peerDid = outOfBandServiceToNumAlgo2Did(didDocument.didCommServices[0]) + const peerDidInstance = DidPeer.fromDid(peerDid) + + // TODO the following `console.log` statement throws an error "TypeError: Cannot read property 'toLowerCase' + // of undefined" because of this: + // + // `service.id = `${did}#${service.type.toLowerCase()}-${serviceIndex++}`` + + // console.log(peerDidInstance.didDocument) + + expect(peerDid).toBe( + 'did:peer:2.Ez6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCJ9' + ) + expect(peerDid).toBe(peerDidInstance.did) + }) + + test('transforms a did document into a valid method 1 did', () => { + const didPeer1 = DidPeer.fromDidDocument( + JsonTransformer.fromJSON(didPeer1zQmR, DidDocument), + PeerDidNumAlgo.GenesisDoc + ) + + expect(didPeer1.did).toBe(didPeer1zQmR.id) + }) + + // FIXME: we need some input data from AFGO for this test to succeed (we create a hash of the document, so any inconsistency is fatal) + xtest('transforms a did document from aries-framework-go into a valid method 1 did', () => { + const didPeer1 = DidPeer.fromDidDocument( + JsonTransformer.fromJSON(didPeer1zQmZ, DidDocument), + PeerDidNumAlgo.GenesisDoc + ) + + expect(didPeer1.did).toBe(didPeer1zQmZ.id) + }) + + test('extracts the numAlgo from the peer did', async () => { + // NumAlgo 0 + const key = Key.fromFingerprint(didKeyEd25519.id.split(':')[2]) + const didPeerNumAlgo0 = DidPeer.fromKey(key) + + expect(didPeerNumAlgo0.numAlgo).toBe(PeerDidNumAlgo.InceptionKeyWithoutDoc) + expect(DidPeer.fromDid('did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL').numAlgo).toBe( + PeerDidNumAlgo.InceptionKeyWithoutDoc + ) + + // NumAlgo 1 + const peerDidNumAlgo1 = 'did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa' + expect(DidPeer.fromDid(peerDidNumAlgo1).numAlgo).toBe(PeerDidNumAlgo.GenesisDoc) + + // NumAlgo 2 + const peerDidNumAlgo2 = + 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + expect(DidPeer.fromDid(peerDidNumAlgo2).numAlgo).toBe(PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) + expect(DidPeer.fromDidDocument(JsonTransformer.fromJSON(didPeer2Ez6L, DidDocument)).numAlgo).toBe( + PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + ) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR-did-comm-service.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR-did-comm-service.json new file mode 100644 index 0000000000..addf924368 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR-did-comm-service.json @@ -0,0 +1,23 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmRYBx1pL86DrsxoJ2ZD3w42d7Ng92ErPgFsCSqg8Q1h4i", + "keyAgreement": [ + { + "id": "#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7" + } + ], + "service": [ + { + "id": "#service-0", + "type": "did-communication", + "serviceEndpoint": "https://example.com/endpoint", + "recipientKeys": ["#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V"], + "routingKeys": [ + "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH" + ], + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc587"] + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR.json new file mode 100644 index 0000000000..f20f5c2aab --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmR.json @@ -0,0 +1,32 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:peer:1zQmXv3d2vqC2Q9JrnrFqqj5h8vzcNAumL1UZbb1TGh58j2c", + "authentication": [ + { + "id": "#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7" + }, + { + "id": "#6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J" + } + ], + "keyAgreement": [ + { + "id": "#6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc", + "type": "X25519KeyAgreementKey2019", + "publicKeyBase58": "JhNWeSVLMYccCk7iopQW4guaSJTojqpMEELgSLhKwRr" + } + ], + "service": [ + { + "id": "#service-0", + "type": "DIDCommMessaging", + "serviceEndpoint": "https://example.com/endpoint", + "routingKeys": ["did:example:somemediator#somekey"], + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc587"] + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmZ.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmZ.json new file mode 100644 index 0000000000..659ccf98d4 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer1zQmZ.json @@ -0,0 +1,27 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/did/v2"], + "id": "did:peer:1zQmZdT2jawCX5T1RKUB7ro83gQuiKbuHwuHi8G1NypB8BTr", + "authentication": [ + { + "id": "did:example:123456789abcdefghs#key3", + "type": "RsaVerificationKey2018", + "controller": "did:example:123456789abcdefghs", + "publicKeyHex": "02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71" + } + ], + "verificationMethod": [ + { + "id": "did:example:123456789abcdefghi#keys-1", + "type": "Secp256k1VerificationKey2018", + "controller": "did:example:123456789abcdefghi", + "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" + }, + { + "id": "did:example:123456789abcdefghw#key2", + "type": "RsaVerificationKey2018", + "controller": "did:example:123456789abcdefghw", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAryQICCl6NZ5gDKrnSztO\n3Hy8PEUcuyvg/ikC+VcIo2SFFSf18a3IMYldIugqqqZCs4/4uVW3sbdLs/6PfgdX\n7O9D22ZiFWHPYA2k2N744MNiCD1UE+tJyllUhSblK48bn+v1oZHCM0nYQ2NqUkvS\nj+hwUU3RiWl7x3D2s9wSdNt7XUtW05a/FXehsPSiJfKvHJJnGOX0BgTvkLnkAOTd\nOrUZ/wK69Dzu4IvrN4vs9Nes8vbwPa/ddZEzGR0cQMt0JBkhk9kU/qwqUseP1QRJ\n5I1jR4g8aYPL/ke9K35PxZWuDp3U0UPAZ3PjFAh+5T+fc7gzCs9dPzSHloruU+gl\nFQIDAQAB\n-----END PUBLIC KEY-----" + } + ], + "created": "0001-01-01 00:00:00 +0000 UTC" +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6L.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6L.json new file mode 100644 index 0000000000..6412d22f52 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6L.json @@ -0,0 +1,34 @@ +{ + "id": "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0", + "authentication": [ + { + "id": "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0", + "publicKeyBase58": "ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7" + }, + { + "id": "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0#6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0", + "publicKeyBase58": "3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J" + } + ], + "keyAgreement": [ + { + "id": "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0#6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc", + "type": "X25519KeyAgreementKey2019", + "controller": "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0", + "publicKeyBase58": "JhNWeSVLMYccCk7iopQW4guaSJTojqpMEELgSLhKwRr" + } + ], + "service": [ + { + "id": "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0#didcommmessaging-0", + "type": "DIDCommMessaging", + "serviceEndpoint": "https://example.com/endpoint", + "routingKeys": ["did:example:somemediator#somekey"], + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc587"] + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LMoreServices.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LMoreServices.json new file mode 100644 index 0000000000..dd6e3d09d4 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer2Ez6LMoreServices.json @@ -0,0 +1,34 @@ +{ + "id": "did:peer:2.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0", + "authentication": [ + { + "id": "did:peer:2.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0#6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V", + "type": "Ed25519VerificationKey2018", + "controller": "did:peer:2.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0", + "publicKeyBase58": "ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7" + } + ], + "keyAgreement": [ + { + "id": "did:peer:2.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0#6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud", + "type": "X25519KeyAgreementKey2019", + "controller": "did:peer:2.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0", + "publicKeyBase58": "DmgBSHMqaZiYqwNMEJJuxWzsGGC8jUYADrfSdBrC6L8s" + } + ], + "service": [ + { + "id": "did:peer:2.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0#didcommmessaging-0", + "type": "DIDCommMessaging", + "serviceEndpoint": "https://example.com/endpoint", + "routingKeys": ["did:example:somemediator#somekey"] + }, + { + "id": "did:peer:2.Ez6LSpSrLxbAhg2SHwKk7kwpsH7DM7QjFS5iK6qP87eViohud.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.SW3sidCI6ImRtIiwicyI6Imh0dHBzOi8vZXhhbXBsZS5jb20vZW5kcG9pbnQiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5Il19LHsidCI6ImV4YW1wbGUiLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludDIiLCJyIjpbImRpZDpleGFtcGxlOnNvbWVtZWRpYXRvciNzb21la2V5MiJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfV0#example-1", + "type": "example", + "serviceEndpoint": "https://example.com/endpoint2", + "routingKeys": ["did:example:somemediator#somekey2"], + "accept": ["didcomm/v2", "didcomm/aip2;env=rfc587"] + } + ] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/didPeer.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/didPeer.test.ts new file mode 100644 index 0000000000..99716995f5 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/didPeer.test.ts @@ -0,0 +1,40 @@ +import { isValidPeerDid, getNumAlgoFromPeerDid, PeerDidNumAlgo } from '../didPeer' + +describe('didPeer', () => { + test('isValidPeerDid', () => { + expect(isValidPeerDid('did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL')).toBe(true) + expect(isValidPeerDid('did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa')).toBe(true) + expect( + isValidPeerDid( + 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + ) + ).toBe(true) + + expect( + isValidPeerDid( + 'did:peer:4.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + ) + ).toBe(false) + }) + + describe('getNumAlgoFromPeerDid', () => { + test('extracts the numAlgo from the peer did', async () => { + // NumAlgo 0 + expect(getNumAlgoFromPeerDid('did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL')).toBe( + PeerDidNumAlgo.InceptionKeyWithoutDoc + ) + + // NumAlgo 1 + expect(getNumAlgoFromPeerDid('did:peer:1zQmZMygzYqNwU6Uhmewx5Xepf2VLp5S4HLSwwgf2aiKZuwa')).toBe( + PeerDidNumAlgo.GenesisDoc + ) + + // NumAlgo 2 + expect( + getNumAlgoFromPeerDid( + 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + ) + ).toBe(PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts new file mode 100644 index 0000000000..efc938ae2d --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo0.test.ts @@ -0,0 +1,41 @@ +import { Key } from '../../../../../crypto' +import didKeyBls12381g1 from '../../../__tests__/__fixtures__/didKeyBls12381g1.json' +import didKeyBls12381g1g2 from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' +import didKeyBls12381g2 from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' +import didKeyEd25519 from '../../../__tests__/__fixtures__/didKeyEd25519.json' +import didKeyX25519 from '../../../__tests__/__fixtures__/didKeyX25519.json' +import { didToNumAlgo0DidDocument, keyToNumAlgo0DidDocument } from '../peerDidNumAlgo0' + +describe('peerDidNumAlgo0', () => { + describe('keyToNumAlgo0DidDocument', () => { + test('transforms a key correctly into a peer did method 0 did document', async () => { + const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] + + for (const didDocument of didDocuments) { + const key = Key.fromFingerprint(didDocument.id.split(':')[2]) + + const didPeerDocument = keyToNumAlgo0DidDocument(key) + const expectedDidPeerDocument = JSON.parse( + JSON.stringify(didDocument).replace(new RegExp('did:key:', 'g'), 'did:peer:0') + ) + + expect(didPeerDocument.toJSON()).toMatchObject(expectedDidPeerDocument) + } + }) + }) + + describe('didToNumAlgo0DidDocument', () => { + test('transforms a method 0 did correctly into a did document', () => { + const didDocuments = [didKeyEd25519, didKeyBls12381g1, didKeyX25519, didKeyBls12381g1g2, didKeyBls12381g2] + + for (const didDocument of didDocuments) { + const didPeer = didToNumAlgo0DidDocument(didDocument.id.replace('did:key:', 'did:peer:0')) + const expectedDidPeerDocument = JSON.parse( + JSON.stringify(didDocument).replace(new RegExp('did:key:', 'g'), 'did:peer:0') + ) + + expect(didPeer.toJSON()).toMatchObject(expectedDidPeerDocument) + } + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo1.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo1.test.ts new file mode 100644 index 0000000000..c4cd88219f --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo1.test.ts @@ -0,0 +1,17 @@ +import { didDocumentJsonToNumAlgo1Did } from '../peerDidNumAlgo1' + +import didPeer1zQmR from './__fixtures__/didPeer1zQmR.json' +import didPeer1zQmZ from './__fixtures__/didPeer1zQmZ.json' + +describe('peerDidNumAlgo1', () => { + describe('didDocumentJsonToNumAlgo1Did', () => { + test('transforms a did document into a valid method 1 did', async () => { + expect(didDocumentJsonToNumAlgo1Did(didPeer1zQmR)).toEqual(didPeer1zQmR.id) + }) + + // FIXME: we need some input data from AFGO for this test to succeed (we create a hash of the document, so any inconsistency is fatal) + xtest('transforms a did document from aries-framework-go into a valid method 1 did', () => { + expect(didDocumentJsonToNumAlgo1Did(didPeer1zQmZ)).toEqual(didPeer1zQmZ.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo2.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo2.test.ts new file mode 100644 index 0000000000..bb1a2e7f52 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo2.test.ts @@ -0,0 +1,46 @@ +import { JsonTransformer } from '../../../../../utils' +import { OutOfBandDidCommService } from '../../../../oob/domain/OutOfBandDidCommService' +import { DidDocument } from '../../../domain' +import { didToNumAlgo2DidDocument, didDocumentToNumAlgo2Did, outOfBandServiceToNumAlgo2Did } from '../peerDidNumAlgo2' + +import didPeer2Ez6L from './__fixtures__/didPeer2Ez6L.json' +import didPeer2Ez6LMoreServices from './__fixtures__/didPeer2Ez6LMoreServices.json' + +describe('peerDidNumAlgo2', () => { + describe('didDocumentToNumAlgo2Did', () => { + test('transforms method 2 peer did to a did document', async () => { + expect(didToNumAlgo2DidDocument(didPeer2Ez6L.id).toJSON()).toMatchObject(didPeer2Ez6L) + + expect(didToNumAlgo2DidDocument(didPeer2Ez6LMoreServices.id).toJSON()).toMatchObject(didPeer2Ez6LMoreServices) + }) + }) + + describe('didDocumentToNumAlgo2Did', () => { + test('transforms method 2 peer did document to a did', async () => { + const expectedDid = didPeer2Ez6L.id + + const didDocument = JsonTransformer.fromJSON(didPeer2Ez6L, DidDocument) + + expect(didDocumentToNumAlgo2Did(didDocument)).toBe(expectedDid) + }) + }) + + describe('outOfBandServiceToNumAlgo2Did', () => { + test('transforms a did comm service into a valid method 2 did', () => { + const service = new OutOfBandDidCommService({ + id: '#service-0', + serviceEndpoint: 'https://example.com/endpoint', + recipientKeys: ['did:key:z6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V'], + routingKeys: ['did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'], + accept: ['didcomm/v2', 'didcomm/aip2;env=rfc587'], + }) + const peerDid = outOfBandServiceToNumAlgo2Did(service) + const peerDidDocument = didToNumAlgo2DidDocument(peerDid) + + expect(peerDid).toBe( + 'did:peer:2.SeyJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3FSWXFRaVNndlpRZG5CeXR3ODZRYnMyWldVa0d2MjJvZDkzNVlGNHM4TTdWI3o2TWtxUllxUWlTZ3ZaUWRuQnl0dzg2UWJzMlpXVWtHdjIyb2Q5MzVZRjRzOE03ViJdLCJyIjpbImRpZDprZXk6ejZNa3BUSFI4Vk5zQnhZQUFXSHV0MkdlYWRkOWpTd3VCVjh4Um9BbndXc2R2a3RII3o2TWtwVEhSOFZOc0J4WUFBV0h1dDJHZWFkZDlqU3d1QlY4eFJvQW53V3Nkdmt0SCJdLCJhIjpbImRpZGNvbW0vdjIiLCJkaWRjb21tL2FpcDI7ZW52PXJmYzU4NyJdfQ' + ) + expect(peerDid).toBe(peerDidDocument.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/didPeer.ts b/packages/core/src/modules/dids/methods/peer/didPeer.ts new file mode 100644 index 0000000000..b21aa77306 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/didPeer.ts @@ -0,0 +1,29 @@ +const PEER_DID_REGEX = new RegExp( + '^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))+(.(S)[0-9a-zA-Z=]*)?)))$' +) + +export function isValidPeerDid(did: string): boolean { + const isValid = PEER_DID_REGEX.test(did) + + return isValid +} + +export const enum PeerDidNumAlgo { + InceptionKeyWithoutDoc = 0, + GenesisDoc = 1, + MultipleInceptionKeyWithoutDoc = 2, +} + +export function getNumAlgoFromPeerDid(did: string) { + const numAlgo = Number(did[9]) + + if ( + numAlgo !== PeerDidNumAlgo.InceptionKeyWithoutDoc && + numAlgo !== PeerDidNumAlgo.GenesisDoc && + numAlgo !== PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + ) { + throw new Error(`Invalid peer did numAlgo: ${numAlgo}`) + } + + return numAlgo as PeerDidNumAlgo +} diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts new file mode 100644 index 0000000000..934156d5d8 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo0.ts @@ -0,0 +1,28 @@ +import { Key } from '../../../../crypto' +import { getDidDocumentForKey } from '../../domain/keyDidDocument' +import { parseDid } from '../../domain/parse' + +import { getNumAlgoFromPeerDid, isValidPeerDid, PeerDidNumAlgo } from './didPeer' + +export function keyToNumAlgo0DidDocument(key: Key) { + const did = `did:peer:0${key.fingerprint}` + + return getDidDocumentForKey(did, key) +} + +export function didToNumAlgo0DidDocument(did: string) { + const parsed = parseDid(did) + const numAlgo = getNumAlgoFromPeerDid(did) + + if (!isValidPeerDid(did)) { + throw new Error(`Invalid peer did '${did}'`) + } + + if (numAlgo !== PeerDidNumAlgo.InceptionKeyWithoutDoc) { + throw new Error(`Invalid numAlgo ${numAlgo}, expected ${PeerDidNumAlgo.InceptionKeyWithoutDoc}`) + } + + const key = Key.fromFingerprint(parsed.id.substring(1)) + + return getDidDocumentForKey(did, key) +} diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo1.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo1.ts new file mode 100644 index 0000000000..bcbb5db2bc --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo1.ts @@ -0,0 +1,12 @@ +import { JsonEncoder, MultiBaseEncoder, MultiHashEncoder } from '../../../../utils' + +export function didDocumentJsonToNumAlgo1Did(didDocumentJson: Record): string { + // We need to remove the id property before hashing + const didDocumentBuffer = JsonEncoder.toBuffer({ ...didDocumentJson, id: undefined }) + + const didIdentifier = MultiBaseEncoder.encode(MultiHashEncoder.encode(didDocumentBuffer, 'sha2-256'), 'base58btc') + + const did = `did:peer:1${didIdentifier}` + + return did +} diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts new file mode 100644 index 0000000000..4b26cd5efa --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo2.ts @@ -0,0 +1,233 @@ +import type { JsonObject } from '../../../../types' +import type { OutOfBandDidCommService } from '../../../oob/domain/OutOfBandDidCommService' +import type { DidDocument, VerificationMethod } from '../../domain' + +import { Key } from '../../../../crypto' +import { JsonEncoder, JsonTransformer } from '../../../../utils' +import { DidCommV1Service, DidDocumentService } from '../../domain' +import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' +import { getKeyDidMappingByKeyType, getKeyDidMappingByVerificationMethod } from '../../domain/key-type' +import { parseDid } from '../../domain/parse' +import { DidKey } from '../key' + +enum DidPeerPurpose { + Assertion = 'A', + Encryption = 'E', + Verification = 'V', + CapabilityInvocation = 'I', + CapabilityDelegation = 'D', + Service = 'S', +} + +function isDidPeerKeyPurpose(purpose: string): purpose is Exclude { + return purpose !== DidPeerPurpose.Service && Object.values(DidPeerPurpose).includes(purpose as DidPeerPurpose) +} + +const didPeerAbbreviations: { [key: string]: string | undefined } = { + type: 't', + DIDCommMessaging: 'dm', + serviceEndpoint: 's', + routingKeys: 'r', + accept: 'a', +} + +const didPeerExpansions: { [key: string]: string | undefined } = { + t: 'type', + dm: 'DIDCommMessaging', + s: 'serviceEndpoint', + r: 'routingKeys', + a: 'accept', +} + +export function didToNumAlgo2DidDocument(did: string) { + const parsed = parseDid(did) + const identifierWithoutNumAlgo = parsed.id.substring(2) + + // Get a list of all did document entries splitted by . + const entries = identifierWithoutNumAlgo.split('.') + const didDocument = new DidDocumentBuilder(did) + let serviceIndex = 0 + + for (const entry of entries) { + // Remove the purpose identifier to get the service or key content + const entryContent = entry.substring(1) + // Get the purpose identifier + const purpose = entry[0] + + // Handle service entry first + if (purpose === DidPeerPurpose.Service) { + let services = JsonEncoder.fromBase64(entryContent) + + // Make sure we have an array of services (can be both json or array) + services = Array.isArray(services) ? services : [services] + + for (let service of services) { + // Expand abbreviations used for service key/values + service = expandServiceAbbreviations(service) + + service.id = `${did}#${service.type.toLowerCase()}-${serviceIndex++}` + + didDocument.addService(JsonTransformer.fromJSON(service, DidDocumentService)) + } + } + // Otherwise we can be sure it is a key + else { + // Decode the fingerprint, and extract the verification method(s) + const key = Key.fromFingerprint(entryContent) + const { getVerificationMethods } = getKeyDidMappingByKeyType(key.keyType) + const verificationMethods = getVerificationMethods(did, key) + + // Add all verification methods to the did document + for (const verificationMethod of verificationMethods) { + // FIXME: the peer did uses key identifiers without the multi base prefix + // However method 0 (and thus did:key) do use the multi base prefix in the + // key identifier. Fixing it like this for now, before making something more complex + verificationMethod.id = verificationMethod.id.replace('#z', '#') + addVerificationMethodToDidDocument(didDocument, verificationMethod, purpose) + } + } + } + + return didDocument.build() +} + +export function didDocumentToNumAlgo2Did(didDocument: DidDocument) { + const purposeMapping = { + [DidPeerPurpose.Assertion]: didDocument.assertionMethod, + [DidPeerPurpose.Encryption]: didDocument.keyAgreement, + // FIXME: should verification be authentication or verificationMethod + // verificationMethod is general so it doesn't make a lot of sense to add + // it to the verificationMethod list + [DidPeerPurpose.Verification]: didDocument.authentication, + [DidPeerPurpose.CapabilityInvocation]: didDocument.capabilityInvocation, + [DidPeerPurpose.CapabilityDelegation]: didDocument.capabilityDelegation, + } + + let did = 'did:peer:2' + + for (const [purpose, entries] of Object.entries(purposeMapping)) { + // Not all entries are required to be defined + if (entries === undefined) continue + + // Dereference all entries to full verification methods + const dereferenced = entries.map((entry) => + typeof entry === 'string' ? didDocument.dereferenceVerificationMethod(entry) : entry + ) + + // Transform als verification methods into a fingerprint (multibase, multicodec) + const encoded = dereferenced.map((entry) => { + const { getKeyFromVerificationMethod } = getKeyDidMappingByVerificationMethod(entry) + const key = getKeyFromVerificationMethod(entry) + + // Encode as '.PurposeFingerprint' + const encoded = `.${purpose}${key.fingerprint}` + + return encoded + }) + + // Add all encoded keys + did += encoded.join('') + } + + if (didDocument.service && didDocument.service.length > 0) { + const abbreviatedServices = didDocument.service.map((service) => { + // Transform to JSON, remove id property + const serviceJson = JsonTransformer.toJSON(service) + delete serviceJson.id + + return abbreviateServiceJson(serviceJson) + }) + + const encodedServices = JsonEncoder.toBase64URL( + // If array length is 1, encode as json object. Otherwise as array + // This is how it's done in the python peer did implementation. + abbreviatedServices.length === 1 ? abbreviatedServices[0] : abbreviatedServices + ) + + did += `.${DidPeerPurpose.Service}${encodedServices}` + } + + return did +} + +export function outOfBandServiceToNumAlgo2Did(service: OutOfBandDidCommService) { + // FIXME: add the key entries for the recipientKeys to the did document. + const didDocument = new DidDocumentBuilder('') + .addService( + new DidCommV1Service({ + id: service.id, + serviceEndpoint: service.serviceEndpoint, + accept: service.accept, + // FIXME: this should actually be local key references, not did:key:123#456 references + recipientKeys: service.recipientKeys.map((recipientKey) => { + const did = DidKey.fromDid(recipientKey) + return `${did.did}#${did.key.fingerprint}` + }), + // Map did:key:xxx to actual did:key:xxx#123 + routingKeys: service.routingKeys?.map((routingKey) => { + const did = DidKey.fromDid(routingKey) + return `${did.did}#${did.key.fingerprint}` + }), + }) + ) + .build() + + const did = didDocumentToNumAlgo2Did(didDocument) + + return did +} + +function expandServiceAbbreviations(service: JsonObject) { + const expand = (abbreviated: string) => didPeerExpansions[abbreviated] ?? abbreviated + + const fullService = Object.entries(service).reduce( + (serviceBody, [key, value]) => ({ + ...serviceBody, + [expand(key)]: expand(value as string), + }), + {} + ) + + return fullService +} + +function abbreviateServiceJson(service: JsonObject) { + const abbreviate = (expanded: string) => didPeerAbbreviations[expanded] ?? expanded + + const abbreviatedService = Object.entries(service).reduce( + (serviceBody, [key, value]) => ({ + ...serviceBody, + [abbreviate(key)]: abbreviate(value as string), + }), + {} + ) + + return abbreviatedService +} + +function addVerificationMethodToDidDocument( + didDocument: DidDocumentBuilder, + verificationMethod: VerificationMethod, + purpose: string +) { + const purposeMapping = { + [DidPeerPurpose.Assertion]: didDocument.addAssertionMethod.bind(didDocument), + [DidPeerPurpose.Encryption]: didDocument.addKeyAgreement.bind(didDocument), + // FIXME: should verification be authentication or verificationMethod + // verificationMethod is general so it doesn't make a lot of sense to add + // it to the verificationMethod list + [DidPeerPurpose.Verification]: didDocument.addAuthentication.bind(didDocument), + [DidPeerPurpose.CapabilityInvocation]: didDocument.addCapabilityInvocation.bind(didDocument), + [DidPeerPurpose.CapabilityDelegation]: didDocument.addCapabilityDelegation.bind(didDocument), + } + + // Verify the purpose is a did peer key purpose (service excluded) + if (isDidPeerKeyPurpose(purpose)) { + const addVerificationMethod = purposeMapping[purpose] + + // Add the verification method based on the method from the mapping + addVerificationMethod(verificationMethod) + } else { + throw new Error(`Unsupported peer did purpose '${purpose}'`) + } +} diff --git a/packages/core/src/modules/dids/methods/sov/SovDidResolver.ts b/packages/core/src/modules/dids/methods/sov/SovDidResolver.ts new file mode 100644 index 0000000000..694987c059 --- /dev/null +++ b/packages/core/src/modules/dids/methods/sov/SovDidResolver.ts @@ -0,0 +1,162 @@ +import type { IndyEndpointAttrib, IndyLedgerService } from '../../../ledger' +import type { DidResolver } from '../../domain/DidResolver' +import type { ParsedDid, DidResolutionResult } from '../../types' + +import { convertPublicKeyToX25519 } from '@stablelib/ed25519' + +import { ED25519_SUITE_CONTEXT_URL_2018 } from '../../../../crypto/signature-suites/ed25519/constants' +import { TypedArrayEncoder } from '../../../../utils/TypedArrayEncoder' +import { getFullVerkey } from '../../../../utils/did' +import { SECURITY_X25519_CONTEXT_URL } from '../../../vc/constants' +import { DidDocumentService } from '../../domain' +import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' +import { DidCommV1Service } from '../../domain/service/DidCommV1Service' +import { DidCommV2Service } from '../../domain/service/DidCommV2Service' + +export class SovDidResolver implements DidResolver { + private indyLedgerService: IndyLedgerService + + public constructor(indyLedgerService: IndyLedgerService) { + this.indyLedgerService = indyLedgerService + } + + public readonly supportedMethods = ['sov'] + + public async resolve(did: string, parsed: ParsedDid): Promise { + const didDocumentMetadata = {} + + try { + const nym = await this.indyLedgerService.getPublicDid(parsed.id) + const endpoints = await this.indyLedgerService.getEndpointsForDid(did) + + const verificationMethodId = `${parsed.did}#key-1` + const keyAgreementId = `${parsed.did}#key-agreement-1` + + const publicKeyBase58 = getFullVerkey(nym.did, nym.verkey) + const publicKeyX25519 = TypedArrayEncoder.toBase58( + convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(publicKeyBase58)) + ) + + const builder = new DidDocumentBuilder(parsed.did) + + .addContext(ED25519_SUITE_CONTEXT_URL_2018) + .addContext(SECURITY_X25519_CONTEXT_URL) + .addVerificationMethod({ + controller: parsed.did, + id: verificationMethodId, + publicKeyBase58: getFullVerkey(nym.did, nym.verkey), + type: 'Ed25519VerificationKey2018', + }) + .addVerificationMethod({ + controller: parsed.did, + id: keyAgreementId, + publicKeyBase58: publicKeyX25519, + type: 'X25519KeyAgreementKey2019', + }) + .addAuthentication(verificationMethodId) + .addAssertionMethod(verificationMethodId) + .addKeyAgreement(keyAgreementId) + + this.addServices(builder, parsed, endpoints, keyAgreementId) + + return { + didDocument: builder.build(), + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } + + // Process Indy Attrib Endpoint Types according to: https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html > Read (Resolve) > DID Service Endpoint + private processEndpointTypes(types?: string[]) { + const expectedTypes = ['endpoint', 'did-communication', 'DIDComm'] + const defaultTypes = ['endpoint', 'did-communication'] + + // Return default types if types "is NOT present [or] empty" + if (!types || types?.length <= 0) { + return defaultTypes + } + + // Return default types if types "contain any other values" + for (const type of types) { + if (!expectedTypes.includes(type)) { + return defaultTypes + } + } + + // Return provided types + return types + } + + private addServices( + builder: DidDocumentBuilder, + parsed: ParsedDid, + endpoints: IndyEndpointAttrib, + keyAgreementId: string + ) { + const { endpoint, routingKeys, types, ...otherEndpoints } = endpoints + + if (endpoint) { + const processedTypes = this.processEndpointTypes(types) + + // If 'endpoint' included in types, add id to the services array + if (processedTypes.includes('endpoint')) { + builder.addService( + new DidDocumentService({ + id: `${parsed.did}#endpoint`, + serviceEndpoint: endpoint, + type: 'endpoint', + }) + ) + } + + // If 'did-communication' included in types, add DIDComm v1 entry + if (processedTypes.includes('did-communication')) { + builder.addService( + new DidCommV1Service({ + id: `${parsed.did}#did-communication`, + serviceEndpoint: endpoint, + priority: 0, + routingKeys: routingKeys ?? [], + recipientKeys: [keyAgreementId], + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + + // If 'DIDComm' included in types, add DIDComm v2 entry + if (processedTypes.includes('DIDComm')) { + builder + .addService( + new DidCommV2Service({ + id: `${parsed.did}#didcomm-1`, + serviceEndpoint: endpoint, + routingKeys: routingKeys ?? [], + accept: ['didcomm/v2'], + }) + ) + .addContext('https://didcomm.org/messaging/contexts/v2') + } + } + } + + // Add other endpoint types + for (const [type, endpoint] of Object.entries(otherEndpoints)) { + builder.addService( + new DidDocumentService({ + id: `${parsed.did}#${type}`, + serviceEndpoint: endpoint as string, + type, + }) + ) + } + } +} diff --git a/packages/core/src/modules/dids/methods/sov/__tests__/SovDidResolver.test.ts b/packages/core/src/modules/dids/methods/sov/__tests__/SovDidResolver.test.ts new file mode 100644 index 0000000000..ec20ac80be --- /dev/null +++ b/packages/core/src/modules/dids/methods/sov/__tests__/SovDidResolver.test.ts @@ -0,0 +1,100 @@ +import type { IndyEndpointAttrib } from '../../../../ledger/services/IndyLedgerService' +import type { GetNymResponse } from 'indy-sdk' + +import { mockFunction } from '../../../../../../tests/helpers' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { IndyLedgerService } from '../../../../ledger/services/IndyLedgerService' +import didSovR1xKJw17sUoXhejEpugMYJFixture from '../../../__tests__/__fixtures__/didSovR1xKJw17sUoXhejEpugMYJ.json' +import didSovWJz9mHyW9BZksioQnRsrAoFixture from '../../../__tests__/__fixtures__/didSovWJz9mHyW9BZksioQnRsrAo.json' +import { parseDid } from '../../../domain/parse' +import { SovDidResolver } from '../SovDidResolver' + +jest.mock('../../../../ledger/services/IndyLedgerService') +const IndyLedgerServiceMock = IndyLedgerService as jest.Mock + +describe('DidResolver', () => { + describe('SovDidResolver', () => { + let ledgerService: IndyLedgerService + let sovDidResolver: SovDidResolver + + beforeEach(() => { + ledgerService = new IndyLedgerServiceMock() + sovDidResolver = new SovDidResolver(ledgerService) + }) + + it('should correctly resolve a did:sov document', async () => { + const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' + + const nymResponse: GetNymResponse = { + did: 'R1xKJw17sUoXhejEpugMYJ', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + role: 'ENDORSER', + } + + const endpoints: IndyEndpointAttrib = { + endpoint: 'https://ssi.com', + profile: 'https://profile.com', + hub: 'https://hub.com', + } + + mockFunction(ledgerService.getPublicDid).mockResolvedValue(nymResponse) + mockFunction(ledgerService.getEndpointsForDid).mockResolvedValue(endpoints) + + const result = await sovDidResolver.resolve(did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovR1xKJw17sUoXhejEpugMYJFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should resolve a did:sov document with routingKeys and types entries in the attrib', async () => { + const did = 'did:sov:WJz9mHyW9BZksioQnRsrAo' + + const nymResponse: GetNymResponse = { + did: 'WJz9mHyW9BZksioQnRsrAo', + verkey: 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8', + role: 'ENDORSER', + } + + const endpoints: IndyEndpointAttrib = { + endpoint: 'https://agent.com', + types: ['endpoint', 'did-communication', 'DIDComm'], + routingKeys: ['routingKey1', 'routingKey2'], + } + + mockFunction(ledgerService.getPublicDid).mockReturnValue(Promise.resolve(nymResponse)) + mockFunction(ledgerService.getEndpointsForDid).mockReturnValue(Promise.resolve(endpoints)) + + const result = await sovDidResolver.resolve(did, parseDid(did)) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didSovWJz9mHyW9BZksioQnRsrAoFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { + const did = 'did:sov:R1xKJw17sUoXhejEpugMYJ' + + mockFunction(ledgerService.getPublicDid).mockRejectedValue(new Error('Error retrieving did')) + + const result = await sovDidResolver.resolve(did, parseDid(did)) + + expect(result).toMatchObject({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:sov:R1xKJw17sUoXhejEpugMYJ': Error: Error retrieving did`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts new file mode 100644 index 0000000000..ee8642326e --- /dev/null +++ b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts @@ -0,0 +1,40 @@ +import type { DidResolver } from '../../domain/DidResolver' +import type { ParsedDid, DidResolutionResult, DidResolutionOptions } from '../../types' + +import { Resolver } from 'did-resolver' +import * as didWeb from 'web-did-resolver' + +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { MessageValidator } from '../../../../utils/MessageValidator' +import { DidDocument } from '../../domain' + +export class WebDidResolver implements DidResolver { + public readonly supportedMethods + + // FIXME: Would be nice if we don't have to provide a did resolver instance + private _resolverInstance = new Resolver() + private resolver = didWeb.getResolver() + + public constructor() { + this.supportedMethods = Object.keys(this.resolver) + } + + public async resolve( + did: string, + parsed: ParsedDid, + didResolutionOptions: DidResolutionOptions + ): Promise { + const result = await this.resolver[parsed.method](did, parsed, this._resolverInstance, didResolutionOptions) + + let didDocument = null + if (result.didDocument) { + didDocument = JsonTransformer.fromJSON(result.didDocument, DidDocument) + await MessageValidator.validate(didDocument) + } + + return { + ...result, + didDocument, + } + } +} diff --git a/packages/core/src/modules/dids/repository/DidRecord.ts b/packages/core/src/modules/dids/repository/DidRecord.ts new file mode 100644 index 0000000000..b72358b02a --- /dev/null +++ b/packages/core/src/modules/dids/repository/DidRecord.ts @@ -0,0 +1,60 @@ +import type { TagsBase } from '../../../storage/BaseRecord' + +import { Type } from 'class-transformer' +import { IsEnum, ValidateNested } from 'class-validator' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { DidDocument } from '../domain' +import { DidDocumentRole } from '../domain/DidDocumentRole' +import { parseDid } from '../domain/parse' + +export interface DidRecordProps { + id: string + role: DidDocumentRole + didDocument?: DidDocument + createdAt?: Date + tags?: CustomDidTags +} + +interface CustomDidTags extends TagsBase { + recipientKeyFingerprints?: string[] +} + +type DefaultDidTags = { + role: DidDocumentRole + method: string +} + +export class DidRecord extends BaseRecord implements DidRecordProps { + @Type(() => DidDocument) + @ValidateNested() + public didDocument?: DidDocument + + @IsEnum(DidDocumentRole) + public role!: DidDocumentRole + + public static readonly type = 'DidDocumentRecord' + public readonly type = DidRecord.type + + public constructor(props: DidRecordProps) { + super() + + if (props) { + this.id = props.id + this.role = props.role + this.didDocument = props.didDocument + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + } + } + + public getTags() { + const did = parseDid(this.id) + + return { + ...this._tags, + role: this.role, + method: did.method, + } + } +} diff --git a/packages/core/src/modules/dids/repository/DidRepository.ts b/packages/core/src/modules/dids/repository/DidRepository.ts new file mode 100644 index 0000000000..b8cf4649e6 --- /dev/null +++ b/packages/core/src/modules/dids/repository/DidRepository.ts @@ -0,0 +1,24 @@ +import type { Key } from '../../../crypto' + +import { inject, scoped, Lifecycle } from 'tsyringe' + +import { InjectionSymbols } from '../../../constants' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { DidRecord } from './DidRecord' + +@scoped(Lifecycle.ContainerScoped) +export class DidRepository extends Repository { + public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { + super(DidRecord, storageService) + } + + public findByRecipientKey(recipientKey: Key) { + return this.findSingleByQuery({ recipientKeyFingerprints: [recipientKey.fingerprint] }) + } + + public findAllByRecipientKey(recipientKey: Key) { + return this.findByQuery({ recipientKeyFingerprints: [recipientKey.fingerprint] }) + } +} diff --git a/packages/core/src/modules/dids/repository/index.ts b/packages/core/src/modules/dids/repository/index.ts new file mode 100644 index 0000000000..2d9119d818 --- /dev/null +++ b/packages/core/src/modules/dids/repository/index.ts @@ -0,0 +1,2 @@ +export * from './DidRepository' +export * from './DidRecord' diff --git a/packages/core/src/modules/dids/services/DidResolverService.ts b/packages/core/src/modules/dids/services/DidResolverService.ts new file mode 100644 index 0000000000..efab110883 --- /dev/null +++ b/packages/core/src/modules/dids/services/DidResolverService.ts @@ -0,0 +1,78 @@ +import type { Logger } from '../../../logger' +import type { DidResolver } from '../domain/DidResolver' +import type { DidResolutionOptions, DidResolutionResult, ParsedDid } from '../types' + +import { Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../../agent/AgentConfig' +import { AriesFrameworkError } from '../../../error' +import { IndyLedgerService } from '../../ledger' +import { parseDid } from '../domain/parse' +import { KeyDidResolver } from '../methods/key/KeyDidResolver' +import { PeerDidResolver } from '../methods/peer/PeerDidResolver' +import { SovDidResolver } from '../methods/sov/SovDidResolver' +import { WebDidResolver } from '../methods/web/WebDidResolver' +import { DidRepository } from '../repository' + +@scoped(Lifecycle.ContainerScoped) +export class DidResolverService { + private logger: Logger + private resolvers: DidResolver[] + + public constructor(agentConfig: AgentConfig, indyLedgerService: IndyLedgerService, didRepository: DidRepository) { + this.logger = agentConfig.logger + + this.resolvers = [ + new SovDidResolver(indyLedgerService), + new WebDidResolver(), + new KeyDidResolver(), + new PeerDidResolver(didRepository), + ] + } + + public async resolve(didUrl: string, options: DidResolutionOptions = {}): Promise { + this.logger.debug(`resolving didUrl ${didUrl}`) + + const result = { + didResolutionMetadata: {}, + didDocument: null, + didDocumentMetadata: {}, + } + + let parsed: ParsedDid + try { + parsed = parseDid(didUrl) + } catch (error) { + return { + ...result, + didResolutionMetadata: { error: 'invalidDid' }, + } + } + + const resolver = this.findResolver(parsed) + if (!resolver) { + return { + ...result, + didResolutionMetadata: { error: 'unsupportedDidMethod' }, + } + } + + return resolver.resolve(parsed.did, parsed, options) + } + + public async resolveDidDocument(did: string) { + const { + didDocument, + didResolutionMetadata: { error, message }, + } = await this.resolve(did) + + if (!didDocument) { + throw new AriesFrameworkError(`Unable to resolve did document for did '${did}': ${error} ${message}`) + } + return didDocument + } + + private findResolver(parsed: ParsedDid): DidResolver | null { + return this.resolvers.find((r) => r.supportedMethods.includes(parsed.method)) ?? null + } +} diff --git a/packages/core/src/modules/dids/services/index.ts b/packages/core/src/modules/dids/services/index.ts new file mode 100644 index 0000000000..1b4265132d --- /dev/null +++ b/packages/core/src/modules/dids/services/index.ts @@ -0,0 +1 @@ +export * from './DidResolverService' diff --git a/packages/core/src/modules/dids/types.ts b/packages/core/src/modules/dids/types.ts new file mode 100644 index 0000000000..8c5231aa6e --- /dev/null +++ b/packages/core/src/modules/dids/types.ts @@ -0,0 +1,16 @@ +import type { DidDocument } from './domain' +import type { DIDResolutionOptions, ParsedDID, DIDDocumentMetadata, DIDResolutionMetadata } from 'did-resolver' + +export type ParsedDid = ParsedDID +export type DidResolutionOptions = DIDResolutionOptions +export type DidDocumentMetadata = DIDDocumentMetadata + +export interface DidResolutionMetadata extends DIDResolutionMetadata { + message?: string +} + +export interface DidResolutionResult { + didResolutionMetadata: DidResolutionMetadata + didDocument: DidDocument | null + didDocumentMetadata: DidDocumentMetadata +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts index 1b498f9adc..7dc21438fe 100644 --- a/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesModule.ts @@ -1,11 +1,21 @@ +import type { AgentMessageProcessedEvent } from '../../agent/Events' +import type { ParsedMessageType } from '../../utils/messageType' + +import { firstValueFrom, of, ReplaySubject } from 'rxjs' +import { filter, takeUntil, timeout, catchError, map } from 'rxjs/operators' import { Lifecycle, scoped } from 'tsyringe' +import { AgentConfig } from '../../agent/AgentConfig' import { Dispatcher } from '../../agent/Dispatcher' +import { EventEmitter } from '../../agent/EventEmitter' +import { AgentEventTypes } from '../../agent/Events' import { MessageSender } from '../../agent/MessageSender' import { createOutboundMessage } from '../../agent/helpers' +import { canHandleMessageType, parseMessageType } from '../../utils/messageType' import { ConnectionService } from '../connections/services' import { DiscloseMessageHandler, QueryMessageHandler } from './handlers' +import { DiscloseMessage } from './messages' import { DiscoverFeaturesService } from './services' @scoped(Lifecycle.ContainerScoped) @@ -13,17 +23,61 @@ export class DiscoverFeaturesModule { private connectionService: ConnectionService private messageSender: MessageSender private discoverFeaturesService: DiscoverFeaturesService + private eventEmitter: EventEmitter + private agentConfig: AgentConfig public constructor( dispatcher: Dispatcher, connectionService: ConnectionService, messageSender: MessageSender, - discoverFeaturesService: DiscoverFeaturesService + discoverFeaturesService: DiscoverFeaturesService, + eventEmitter: EventEmitter, + agentConfig: AgentConfig ) { this.connectionService = connectionService this.messageSender = messageSender this.discoverFeaturesService = discoverFeaturesService this.registerHandlers(dispatcher) + this.eventEmitter = eventEmitter + this.agentConfig = agentConfig + } + + public async isProtocolSupported(connectionId: string, message: { type: ParsedMessageType }) { + const { protocolUri } = message.type + + // Listen for response to our feature query + const replaySubject = new ReplaySubject(1) + this.eventEmitter + .observable(AgentEventTypes.AgentMessageProcessed) + .pipe( + // Stop when the agent shuts down + takeUntil(this.agentConfig.stop$), + // filter by connection id and query disclose message type + filter( + (e) => + e.payload.connection?.id === connectionId && + canHandleMessageType(DiscloseMessage, parseMessageType(e.payload.message.type)) + ), + // Return whether the protocol is supported + map((e) => { + const message = e.payload.message as DiscloseMessage + return message.protocols.map((p) => p.protocolId).includes(protocolUri) + }), + // TODO: make configurable + // If we don't have an answer in 7 seconds (no response, not supported, etc...) error + timeout(7000), + // We want to return false if an error occurred + catchError(() => of(false)) + ) + .subscribe(replaySubject) + + await this.queryFeatures(connectionId, { + query: protocolUri, + comment: 'Detect if protocol is supported', + }) + + const isProtocolSupported = await firstValueFrom(replaySubject) + return isProtocolSupported } public async queryFeatures(connectionId: string, options: { query: string; comment?: string }) { diff --git a/packages/core/src/modules/discover-features/messages/DiscloseMessage.ts b/packages/core/src/modules/discover-features/messages/DiscloseMessage.ts index 0fe10c74bb..82dbe9451e 100644 --- a/packages/core/src/modules/discover-features/messages/DiscloseMessage.ts +++ b/packages/core/src/modules/discover-features/messages/DiscloseMessage.ts @@ -1,7 +1,8 @@ import { Expose, Type } from 'class-transformer' -import { Equals, IsInstance, IsOptional, IsString } from 'class-validator' +import { IsInstance, IsOptional, IsString } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface DiscloseProtocolOptions { protocolId: string @@ -44,9 +45,9 @@ export class DiscloseMessage extends AgentMessage { } } - @Equals(DiscloseMessage.type) - public readonly type = DiscloseMessage.type - public static readonly type = 'https://didcomm.org/discover-features/1.0/disclose' + @IsValidMessageType(DiscloseMessage.type) + public readonly type = DiscloseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/discover-features/1.0/disclose') @IsInstance(DiscloseProtocol, { each: true }) @Type(() => DiscloseProtocol) diff --git a/packages/core/src/modules/discover-features/messages/QueryMessage.ts b/packages/core/src/modules/discover-features/messages/QueryMessage.ts index dd828656f4..35f635ccd5 100644 --- a/packages/core/src/modules/discover-features/messages/QueryMessage.ts +++ b/packages/core/src/modules/discover-features/messages/QueryMessage.ts @@ -1,6 +1,7 @@ -import { Equals, IsOptional, IsString } from 'class-validator' +import { IsOptional, IsString } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface DiscoverFeaturesQueryMessageOptions { id?: string @@ -19,9 +20,9 @@ export class QueryMessage extends AgentMessage { } } - @Equals(QueryMessage.type) - public readonly type = QueryMessage.type - public static readonly type = 'https://didcomm.org/discover-features/1.0/query' + @IsValidMessageType(QueryMessage.type) + public readonly type = QueryMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/discover-features/1.0/query') @IsString() public query!: string diff --git a/packages/core/src/modules/generic-records/GenericRecordsModule.ts b/packages/core/src/modules/generic-records/GenericRecordsModule.ts new file mode 100644 index 0000000000..68d4c309da --- /dev/null +++ b/packages/core/src/modules/generic-records/GenericRecordsModule.ts @@ -0,0 +1,77 @@ +import type { Logger } from '../../logger' +import type { GenericRecord, GenericRecordTags, SaveGenericRecordOption } from './repository/GenericRecord' + +import { Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../agent/AgentConfig' + +import { GenericRecordService } from './service/GenericRecordService' + +export type ContentType = { + content: string +} + +@scoped(Lifecycle.ContainerScoped) +export class GenericRecordsModule { + private genericRecordsService: GenericRecordService + private logger: Logger + public constructor(agentConfig: AgentConfig, genericRecordsService: GenericRecordService) { + this.genericRecordsService = genericRecordsService + this.logger = agentConfig.logger + } + + public async save({ content, tags }: SaveGenericRecordOption) { + try { + const record = await this.genericRecordsService.save({ + content: content, + tags: tags, + }) + return record + } catch (error) { + this.logger.error('Error while saving generic-record', { + error, + content, + errorMessage: error instanceof Error ? error.message : error, + }) + throw error + } + } + + public async delete(record: GenericRecord): Promise { + try { + await this.genericRecordsService.delete(record) + } catch (error) { + this.logger.error('Error while saving generic-record', { + error, + content: record.content, + errorMessage: error instanceof Error ? error.message : error, + }) + throw error + } + } + + public async update(record: GenericRecord): Promise { + try { + await this.genericRecordsService.update(record) + } catch (error) { + this.logger.error('Error while update generic-record', { + error, + content: record.content, + errorMessage: error instanceof Error ? error.message : error, + }) + throw error + } + } + + public async findById(id: string) { + return this.genericRecordsService.findById(id) + } + + public async findAllByQuery(query: Partial): Promise { + return this.genericRecordsService.findAllByQuery(query) + } + + public async getAll(): Promise { + return this.genericRecordsService.getAll() + } +} diff --git a/packages/core/src/modules/generic-records/repository/GenericRecord.ts b/packages/core/src/modules/generic-records/repository/GenericRecord.ts new file mode 100644 index 0000000000..b96cb5ebc8 --- /dev/null +++ b/packages/core/src/modules/generic-records/repository/GenericRecord.ts @@ -0,0 +1,45 @@ +import type { RecordTags, TagsBase } from '../../../storage/BaseRecord' + +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' + +export type GenericRecordTags = TagsBase + +export type BasicMessageTags = RecordTags + +export interface GenericRecordStorageProps { + id?: string + createdAt?: Date + tags?: GenericRecordTags + content: Record +} + +export interface SaveGenericRecordOption { + content: Record + id?: string + tags?: GenericRecordTags +} + +export class GenericRecord extends BaseRecord { + public content!: Record + + public static readonly type = 'GenericRecord' + public readonly type = GenericRecord.type + + public constructor(props: GenericRecordStorageProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.content = props.content + this._tags = props.tags ?? {} + } + } + + public getTags() { + return { + ...this._tags, + } + } +} diff --git a/packages/core/src/modules/generic-records/repository/GenericRecordsRepository.ts b/packages/core/src/modules/generic-records/repository/GenericRecordsRepository.ts new file mode 100644 index 0000000000..860c4249c4 --- /dev/null +++ b/packages/core/src/modules/generic-records/repository/GenericRecordsRepository.ts @@ -0,0 +1,14 @@ +import { inject, scoped, Lifecycle } from 'tsyringe' + +import { InjectionSymbols } from '../../../constants' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { GenericRecord } from './GenericRecord' + +@scoped(Lifecycle.ContainerScoped) +export class GenericRecordsRepository extends Repository { + public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { + super(GenericRecord, storageService) + } +} diff --git a/packages/core/src/modules/generic-records/service/GenericRecordService.ts b/packages/core/src/modules/generic-records/service/GenericRecordService.ts new file mode 100644 index 0000000000..2403ed7814 --- /dev/null +++ b/packages/core/src/modules/generic-records/service/GenericRecordService.ts @@ -0,0 +1,60 @@ +import type { GenericRecordTags, SaveGenericRecordOption } from '../repository/GenericRecord' + +import { Lifecycle, scoped } from 'tsyringe' + +import { AriesFrameworkError } from '../../../error' +import { GenericRecord } from '../repository/GenericRecord' +import { GenericRecordsRepository } from '../repository/GenericRecordsRepository' + +@scoped(Lifecycle.ContainerScoped) +export class GenericRecordService { + private genericRecordsRepository: GenericRecordsRepository + + public constructor(genericRecordsRepository: GenericRecordsRepository) { + this.genericRecordsRepository = genericRecordsRepository + } + + public async save({ content, tags }: SaveGenericRecordOption) { + const genericRecord = new GenericRecord({ + content: content, + tags: tags, + }) + + try { + await this.genericRecordsRepository.save(genericRecord) + return genericRecord + } catch (error) { + throw new AriesFrameworkError( + `Unable to store the genericRecord record with id ${genericRecord.id}. Message: ${error}` + ) + } + } + + public async delete(record: GenericRecord): Promise { + try { + await this.genericRecordsRepository.delete(record) + } catch (error) { + throw new AriesFrameworkError(`Unable to delete the genericRecord record with id ${record.id}. Message: ${error}`) + } + } + + public async update(record: GenericRecord): Promise { + try { + await this.genericRecordsRepository.update(record) + } catch (error) { + throw new AriesFrameworkError(`Unable to update the genericRecord record with id ${record.id}. Message: ${error}`) + } + } + + public async findAllByQuery(query: Partial) { + return this.genericRecordsRepository.findByQuery(query) + } + + public async findById(id: string): Promise { + return this.genericRecordsRepository.findById(id) + } + + public async getAll() { + return this.genericRecordsRepository.getAll() + } +} diff --git a/packages/core/src/modules/indy/services/IndyHolderService.ts b/packages/core/src/modules/indy/services/IndyHolderService.ts index 7dab13a57f..c8f0ca2f8b 100644 --- a/packages/core/src/modules/indy/services/IndyHolderService.ts +++ b/packages/core/src/modules/indy/services/IndyHolderService.ts @@ -1,41 +1,78 @@ +import type { Logger } from '../../../logger' +import type { RequestedCredentials } from '../../proofs' import type * as Indy from 'indy-sdk' import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../agent/AgentConfig' -import { IndySdkError } from '../../../error' +import { IndySdkError } from '../../../error/IndySdkError' import { isIndyError } from '../../../utils/indyError' import { IndyWallet } from '../../../wallet/IndyWallet' +import { IndyRevocationService } from './IndyRevocationService' + @scoped(Lifecycle.ContainerScoped) export class IndyHolderService { private indy: typeof Indy + private logger: Logger private wallet: IndyWallet + private indyRevocationService: IndyRevocationService - public constructor(agentConfig: AgentConfig, wallet: IndyWallet) { + public constructor(agentConfig: AgentConfig, indyRevocationService: IndyRevocationService, wallet: IndyWallet) { this.indy = agentConfig.agentDependencies.indy this.wallet = wallet + this.indyRevocationService = indyRevocationService + this.logger = agentConfig.logger } + /** + * Creates an Indy Proof in response to a proof request. Will create revocation state if the proof request requests proof of non-revocation + * + * @param proofRequest a Indy proof request + * @param requestedCredentials the requested credentials to use for the proof creation + * @param schemas schemas to use in proof creation + * @param credentialDefinitions credential definitions to use in proof creation + * @throws {Error} if there is an error during proof generation or revocation state generation + * @returns a promise of Indy Proof + * + * @todo support attribute non_revoked fields + */ public async createProof({ proofRequest, requestedCredentials, schemas, credentialDefinitions, - revocationStates = {}, - }: CreateProofOptions) { + }: CreateProofOptions): Promise { try { - return await this.indy.proverCreateProof( + this.logger.debug('Creating Indy Proof') + const revocationStates: Indy.RevStates = await this.indyRevocationService.createRevocationState( + proofRequest, + requestedCredentials + ) + + const indyProof: Indy.IndyProof = await this.indy.proverCreateProof( this.wallet.handle, proofRequest, - requestedCredentials, + requestedCredentials.toJSON(), this.wallet.masterSecretId, schemas, credentialDefinitions, revocationStates ) + + this.logger.trace('Created Indy Proof', { + indyProof, + }) + + return indyProof } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error creating Indy Proof`, { + error, + proofRequest, + requestedCredentials, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -49,7 +86,7 @@ export class IndyHolderService { credential, credentialDefinition, credentialId, - revocationRegistryDefinitions, + revocationRegistryDefinition, }: StoreCredentialOptions): Promise { try { return await this.indy.proverStoreCredential( @@ -58,10 +95,14 @@ export class IndyHolderService { credentialRequestMetadata, credential, credentialDefinition, - revocationRegistryDefinitions ?? null + revocationRegistryDefinition ?? null ) } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error storing Indy Credential '${credentialId}'`, { + error, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -78,7 +119,11 @@ export class IndyHolderService { try { return await this.indy.proverGetCredential(this.wallet.handle, credentialId) } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error getting Indy Credential '${credentialId}'`, { + error, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -101,7 +146,12 @@ export class IndyHolderService { this.wallet.masterSecretId ) } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error creating Indy Credential Request`, { + error, + credentialOffer, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -155,6 +205,24 @@ export class IndyHolderService { } } + /** + * Delete a credential stored in the wallet by id. + * + * @param credentialId the id (referent) of the credential + * + */ + public async deleteCredential(credentialId: Indy.CredentialId): Promise { + try { + return await this.indy.proverDeleteCredential(this.wallet.handle, credentialId) + } catch (error) { + this.logger.error(`Error deleting Indy Credential from Wallet`, { + error, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + private async fetchCredentialsForReferent(searchHandle: number, referent: string, limit?: number) { try { let credentials: Indy.IndyCredential[] = [] @@ -177,7 +245,11 @@ export class IndyHolderService { return credentials } catch (error) { - throw new IndySdkError(error) + this.logger.error(`Error Fetching Indy Credentials For Referent`, { + error, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error } } } @@ -201,13 +273,12 @@ export interface StoreCredentialOptions { credential: Indy.Cred credentialDefinition: Indy.CredDef credentialId?: Indy.CredentialId - revocationRegistryDefinitions?: Indy.RevRegsDefs + revocationRegistryDefinition?: Indy.RevocRegDef } export interface CreateProofOptions { proofRequest: Indy.IndyProofRequest - requestedCredentials: Indy.IndyRequestedCredentials + requestedCredentials: RequestedCredentials schemas: Indy.Schemas credentialDefinitions: Indy.CredentialDefs - revocationStates?: Indy.RevStates } diff --git a/packages/core/src/modules/indy/services/IndyIssuerService.ts b/packages/core/src/modules/indy/services/IndyIssuerService.ts index 011790247d..5658e76272 100644 --- a/packages/core/src/modules/indy/services/IndyIssuerService.ts +++ b/packages/core/src/modules/indy/services/IndyIssuerService.ts @@ -9,7 +9,6 @@ import type { CredReq, CredRevocId, CredValues, - BlobReaderHandle, } from 'indy-sdk' import { Lifecycle, scoped } from 'tsyringe' @@ -18,18 +17,21 @@ import { AgentConfig } from '../../../agent/AgentConfig' import { AriesFrameworkError } from '../../../error/AriesFrameworkError' import { IndySdkError } from '../../../error/IndySdkError' import { isIndyError } from '../../../utils/indyError' -import { getDirFromFilePath } from '../../../utils/path' import { IndyWallet } from '../../../wallet/IndyWallet' +import { IndyUtilitiesService } from './IndyUtilitiesService' + @scoped(Lifecycle.ContainerScoped) export class IndyIssuerService { private indy: typeof Indy private wallet: IndyWallet + private indyUtilitiesService: IndyUtilitiesService private fileSystem: FileSystem - public constructor(agentConfig: AgentConfig, wallet: IndyWallet) { + public constructor(agentConfig: AgentConfig, wallet: IndyWallet, indyUtilitiesService: IndyUtilitiesService) { this.indy = agentConfig.agentDependencies.indy this.wallet = wallet + this.indyUtilitiesService = indyUtilitiesService this.fileSystem = agentConfig.fileSystem } @@ -44,7 +46,7 @@ export class IndyIssuerService { return schema } catch (error) { - throw new IndySdkError(error) + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -74,7 +76,7 @@ export class IndyIssuerService { return credentialDefinition } catch (error) { - throw new IndySdkError(error) + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -88,7 +90,7 @@ export class IndyIssuerService { try { return await this.indy.issuerCreateCredentialOffer(this.wallet.handle, credentialDefinitionId) } catch (error) { - throw new IndySdkError(error) + throw isIndyError(error) ? new IndySdkError(error) : error } } @@ -106,7 +108,7 @@ export class IndyIssuerService { }: CreateCredentialOptions): Promise<[Cred, CredRevocId]> { try { // Indy SDK requires tailsReaderHandle. Use null if no tailsFilePath is present - const tailsReaderHandle = tailsFilePath ? await this.createTailsReader(tailsFilePath) : 0 + const tailsReaderHandle = tailsFilePath ? await this.indyUtilitiesService.createTailsReader(tailsFilePath) : 0 if (revocationRegistryId || tailsFilePath) { throw new AriesFrameworkError('Revocation not supported yet') @@ -123,42 +125,7 @@ export class IndyIssuerService { return [credential, credentialRevocationId] } catch (error) { - if (isIndyError(error)) { - throw new IndySdkError(error) - } - - throw error - } - } - - /** - * Get a handler for the blob storage tails file reader. - * - * @param tailsFilePath The path of the tails file - * @returns The blob storage reader handle - */ - private async createTailsReader(tailsFilePath: string): Promise { - try { - const tailsFileExists = await this.fileSystem.exists(tailsFilePath) - - // Extract directory from path (should also work with windows paths) - const dirname = getDirFromFilePath(tailsFilePath) - - if (!tailsFileExists) { - throw new AriesFrameworkError(`Tails file does not exist at path ${tailsFilePath}`) - } - - const tailsReaderConfig = { - base_dir: dirname, - } - - return await this.indy.openBlobStorageReader('default', tailsReaderConfig) - } catch (error) { - if (isIndyError(error)) { - throw new IndySdkError(error) - } - - throw error + throw isIndyError(error) ? new IndySdkError(error) : error } } } diff --git a/packages/core/src/modules/indy/services/IndyRevocationService.ts b/packages/core/src/modules/indy/services/IndyRevocationService.ts new file mode 100644 index 0000000000..4b88e88413 --- /dev/null +++ b/packages/core/src/modules/indy/services/IndyRevocationService.ts @@ -0,0 +1,199 @@ +import type { Logger } from '../../../logger' +import type { FileSystem } from '../../../storage/FileSystem' +import type { RevocationInterval } from '../../credentials' +import type { RequestedCredentials } from '../../proofs' +import type { default as Indy } from 'indy-sdk' + +import { scoped, Lifecycle } from 'tsyringe' + +import { AgentConfig } from '../../../agent/AgentConfig' +import { AriesFrameworkError } from '../../../error/AriesFrameworkError' +import { IndySdkError } from '../../../error/IndySdkError' +import { isIndyError } from '../../../utils/indyError' +import { IndyWallet } from '../../../wallet/IndyWallet' +import { IndyLedgerService } from '../../ledger' + +import { IndyUtilitiesService } from './IndyUtilitiesService' + +enum RequestReferentType { + Attribute = 'attribute', + Predicate = 'predicate', + SelfAttestedAttribute = 'self-attested-attribute', +} + +@scoped(Lifecycle.ContainerScoped) +export class IndyRevocationService { + private indy: typeof Indy + private indyUtilitiesService: IndyUtilitiesService + private fileSystem: FileSystem + private ledgerService: IndyLedgerService + private logger: Logger + private wallet: IndyWallet + + public constructor( + agentConfig: AgentConfig, + indyUtilitiesService: IndyUtilitiesService, + ledgerService: IndyLedgerService, + wallet: IndyWallet + ) { + this.fileSystem = agentConfig.fileSystem + this.indy = agentConfig.agentDependencies.indy + this.indyUtilitiesService = indyUtilitiesService + this.logger = agentConfig.logger + this.ledgerService = ledgerService + this.wallet = wallet + } + + public async createRevocationState( + proofRequest: Indy.IndyProofRequest, + requestedCredentials: RequestedCredentials + ): Promise { + try { + this.logger.debug(`Creating Revocation State(s) for proof request`, { + proofRequest, + requestedCredentials, + }) + const revocationStates: Indy.RevStates = {} + const referentCredentials = [] + + //Retrieve information for referents and push to single array + for (const [referent, requestedCredential] of Object.entries(requestedCredentials.requestedAttributes)) { + referentCredentials.push({ + referent, + credentialInfo: requestedCredential.credentialInfo, + type: RequestReferentType.Attribute, + }) + } + for (const [referent, requestedCredential] of Object.entries(requestedCredentials.requestedPredicates)) { + referentCredentials.push({ + referent, + credentialInfo: requestedCredential.credentialInfo, + type: RequestReferentType.Predicate, + }) + } + + for (const { referent, credentialInfo, type } of referentCredentials) { + if (!credentialInfo) { + throw new AriesFrameworkError( + `Credential for referent '${referent} does not have credential info for revocation state creation` + ) + } + + // Prefer referent-specific revocation interval over global revocation interval + const referentRevocationInterval = + type === RequestReferentType.Predicate + ? proofRequest.requested_predicates[referent].non_revoked + : proofRequest.requested_attributes[referent].non_revoked + const requestRevocationInterval = referentRevocationInterval ?? proofRequest.non_revoked + const credentialRevocationId = credentialInfo.credentialRevocationId + const revocationRegistryId = credentialInfo.revocationRegistryId + + // If revocation interval is present and the credential is revocable then create revocation state + if (requestRevocationInterval && credentialRevocationId && revocationRegistryId) { + this.logger.trace( + `Presentation is requesting proof of non revocation for ${type} referent '${referent}', creating revocation state for credential`, + { + requestRevocationInterval, + credentialRevocationId, + revocationRegistryId, + } + ) + + this.assertRevocationInterval(requestRevocationInterval) + + const { revocationRegistryDefinition } = await this.ledgerService.getRevocationRegistryDefinition( + revocationRegistryId + ) + + const { revocationRegistryDelta, deltaTimestamp } = await this.ledgerService.getRevocationRegistryDelta( + revocationRegistryId, + requestRevocationInterval?.to, + 0 + ) + + const { tailsLocation, tailsHash } = revocationRegistryDefinition.value + const tails = await this.indyUtilitiesService.downloadTails(tailsHash, tailsLocation) + + const revocationState = await this.indy.createRevocationState( + tails, + revocationRegistryDefinition, + revocationRegistryDelta, + deltaTimestamp, + credentialRevocationId + ) + const timestamp = revocationState.timestamp + + if (!revocationStates[revocationRegistryId]) { + revocationStates[revocationRegistryId] = {} + } + revocationStates[revocationRegistryId][timestamp] = revocationState + } + } + + this.logger.debug(`Created Revocation States for Proof Request`, { + revocationStates, + }) + + return revocationStates + } catch (error) { + this.logger.error(`Error creating Indy Revocation State for Proof Request`, { + error, + proofRequest, + requestedCredentials, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + + // Get revocation status for credential (given a from-to) + // Note from-to interval details: https://github.com/hyperledger/indy-hipe/blob/master/text/0011-cred-revocation/README.md#indy-node-revocation-registry-intervals + public async getRevocationStatus( + credentialRevocationId: string, + revocationRegistryDefinitionId: string, + requestRevocationInterval: RevocationInterval + ): Promise<{ revoked: boolean; deltaTimestamp: number }> { + this.logger.trace( + `Fetching Credential Revocation Status for Credential Revocation Id '${credentialRevocationId}' with revocation interval with to '${requestRevocationInterval.to}' & from '${requestRevocationInterval.from}'` + ) + + this.assertRevocationInterval(requestRevocationInterval) + + const { revocationRegistryDelta, deltaTimestamp } = await this.ledgerService.getRevocationRegistryDelta( + revocationRegistryDefinitionId, + requestRevocationInterval.to, + 0 + ) + + const revoked: boolean = revocationRegistryDelta.value.revoked?.includes(parseInt(credentialRevocationId)) || false + this.logger.trace( + `Credential with Credential Revocation Id '${credentialRevocationId}' is ${ + revoked ? '' : 'not ' + }revoked with revocation interval with to '${requestRevocationInterval.to}' & from '${ + requestRevocationInterval.from + }'` + ) + + return { + revoked, + deltaTimestamp, + } + } + + // TODO: Add Test + // Check revocation interval in accordance with https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0441-present-proof-best-practices/README.md#semantics-of-non-revocation-interval-endpoints + private assertRevocationInterval(requestRevocationInterval: RevocationInterval) { + if (!requestRevocationInterval.to) { + throw new AriesFrameworkError(`Presentation requests proof of non-revocation with no 'to' value specified`) + } + + if ( + (requestRevocationInterval.from || requestRevocationInterval.from === 0) && + requestRevocationInterval.to !== requestRevocationInterval.from + ) { + throw new AriesFrameworkError( + `Presentation requests proof of non-revocation with an interval from: '${requestRevocationInterval.from}' that does not match the interval to: '${requestRevocationInterval.to}', as specified in Aries RFC 0441` + ) + } + } +} diff --git a/packages/core/src/modules/indy/services/IndyUtilitiesService.ts b/packages/core/src/modules/indy/services/IndyUtilitiesService.ts new file mode 100644 index 0000000000..96b8ec4407 --- /dev/null +++ b/packages/core/src/modules/indy/services/IndyUtilitiesService.ts @@ -0,0 +1,84 @@ +import type { Logger } from '../../../logger' +import type { FileSystem } from '../../../storage/FileSystem' +import type { default as Indy, BlobReaderHandle } from 'indy-sdk' + +import { scoped, Lifecycle } from 'tsyringe' + +import { AgentConfig } from '../../../agent/AgentConfig' +import { AriesFrameworkError } from '../../../error' +import { IndySdkError } from '../../../error/IndySdkError' +import { isIndyError } from '../../../utils/indyError' +import { getDirFromFilePath } from '../../../utils/path' + +@scoped(Lifecycle.ContainerScoped) +export class IndyUtilitiesService { + private indy: typeof Indy + private logger: Logger + private fileSystem: FileSystem + + public constructor(agentConfig: AgentConfig) { + this.indy = agentConfig.agentDependencies.indy + this.logger = agentConfig.logger + this.fileSystem = agentConfig.fileSystem + } + + /** + * Get a handler for the blob storage tails file reader. + * + * @param tailsFilePath The path of the tails file + * @returns The blob storage reader handle + */ + public async createTailsReader(tailsFilePath: string): Promise { + try { + this.logger.debug(`Opening tails reader at path ${tailsFilePath}`) + const tailsFileExists = await this.fileSystem.exists(tailsFilePath) + + // Extract directory from path (should also work with windows paths) + const dirname = getDirFromFilePath(tailsFilePath) + + if (!tailsFileExists) { + throw new AriesFrameworkError(`Tails file does not exist at path ${tailsFilePath}`) + } + + const tailsReaderConfig = { + base_dir: dirname, + } + + const tailsReader = await this.indy.openBlobStorageReader('default', tailsReaderConfig) + this.logger.debug(`Opened tails reader at path ${tailsFilePath}`) + return tailsReader + } catch (error) { + if (isIndyError(error)) { + throw new IndySdkError(error) + } + + throw error + } + } + + public async downloadTails(hash: string, tailsLocation: string): Promise { + try { + this.logger.debug(`Checking to see if tails file for URL ${tailsLocation} has been stored in the FileSystem`) + const filePath = `${this.fileSystem.basePath}/afj/tails/${hash}` + + const tailsExists = await this.fileSystem.exists(filePath) + this.logger.debug(`Tails file for ${tailsLocation} ${tailsExists ? 'is stored' : 'is not stored'} at ${filePath}`) + if (!tailsExists) { + this.logger.debug(`Retrieving tails file from URL ${tailsLocation}`) + + await this.fileSystem.downloadToFile(tailsLocation, filePath) + this.logger.debug(`Saved tails file to FileSystem at path ${filePath}`) + + //TODO: Validate Tails File Hash + } + + this.logger.debug(`Tails file for URL ${tailsLocation} is stored in the FileSystem, opening tails reader`) + return this.createTailsReader(filePath) + } catch (error) { + this.logger.error(`Error while retrieving tails file from URL ${tailsLocation}`, { + error, + }) + throw isIndyError(error) ? new IndySdkError(error) : error + } + } +} diff --git a/packages/core/src/modules/indy/services/IndyVerifierService.ts b/packages/core/src/modules/indy/services/IndyVerifierService.ts index 6197767c58..8e0522357c 100644 --- a/packages/core/src/modules/indy/services/IndyVerifierService.ts +++ b/packages/core/src/modules/indy/services/IndyVerifierService.ts @@ -4,13 +4,17 @@ import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../agent/AgentConfig' import { IndySdkError } from '../../../error' +import { isIndyError } from '../../../utils/indyError' +import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' @scoped(Lifecycle.ContainerScoped) export class IndyVerifierService { private indy: typeof Indy + private ledgerService: IndyLedgerService - public constructor(agentConfig: AgentConfig) { + public constructor(agentConfig: AgentConfig, ledgerService: IndyLedgerService) { this.indy = agentConfig.agentDependencies.indy + this.ledgerService = ledgerService } public async verifyProof({ @@ -18,21 +22,48 @@ export class IndyVerifierService { proof, schemas, credentialDefinitions, - revocationRegistryDefinitions = {}, - revocationStates = {}, }: VerifyProofOptions): Promise { try { + const { revocationRegistryDefinitions, revocationRegistryStates } = await this.getRevocationRegistries(proof) + return await this.indy.verifierVerifyProof( proofRequest, proof, schemas, credentialDefinitions, revocationRegistryDefinitions, - revocationStates + revocationRegistryStates ) } catch (error) { - throw new IndySdkError(error) + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + + private async getRevocationRegistries(proof: Indy.IndyProof) { + const revocationRegistryDefinitions: Indy.RevocRegDefs = {} + const revocationRegistryStates: Indy.RevStates = Object.create(null) + for (const identifier of proof.identifiers) { + const revocationRegistryId = identifier.rev_reg_id + const timestamp = identifier.timestamp + + //Fetch Revocation Registry Definition if not already fetched + if (revocationRegistryId && !revocationRegistryDefinitions[revocationRegistryId]) { + const { revocationRegistryDefinition } = await this.ledgerService.getRevocationRegistryDefinition( + revocationRegistryId + ) + revocationRegistryDefinitions[revocationRegistryId] = revocationRegistryDefinition + } + + //Fetch Revocation Registry by Timestamp if not already fetched + if (revocationRegistryId && timestamp && !revocationRegistryStates[revocationRegistryId]?.[timestamp]) { + if (!revocationRegistryStates[revocationRegistryId]) { + revocationRegistryStates[revocationRegistryId] = Object.create(null) + } + const { revocationRegistry } = await this.ledgerService.getRevocationRegistry(revocationRegistryId, timestamp) + revocationRegistryStates[revocationRegistryId][timestamp] = revocationRegistry + } } + return { revocationRegistryDefinitions, revocationRegistryStates } } } @@ -41,6 +72,4 @@ export interface VerifyProofOptions { proof: Indy.IndyProof schemas: Indy.Schemas credentialDefinitions: Indy.CredentialDefs - revocationRegistryDefinitions?: Indy.RevRegsDefs - revocationStates?: Indy.RevStates } diff --git a/packages/core/src/modules/indy/services/__mocks__/IndyHolderService.ts b/packages/core/src/modules/indy/services/__mocks__/IndyHolderService.ts index bebdd2c926..1d6ed433b6 100644 --- a/packages/core/src/modules/indy/services/__mocks__/IndyHolderService.ts +++ b/packages/core/src/modules/indy/services/__mocks__/IndyHolderService.ts @@ -4,7 +4,7 @@ export const IndyHolderService = jest.fn(() => ({ storeCredential: jest.fn(({ credentialId }: StoreCredentialOptions) => Promise.resolve(credentialId ?? 'some-random-uuid') ), - + deleteCredential: jest.fn(() => Promise.resolve()), createCredentialRequest: jest.fn(({ holderDid, credentialDefinition }: CreateCredentialRequestOptions) => Promise.resolve([ { diff --git a/packages/core/src/modules/indy/services/index.ts b/packages/core/src/modules/indy/services/index.ts index 1fbeefc66f..fa01eaf419 100644 --- a/packages/core/src/modules/indy/services/index.ts +++ b/packages/core/src/modules/indy/services/index.ts @@ -1,3 +1,5 @@ export * from './IndyHolderService' export * from './IndyIssuerService' export * from './IndyVerifierService' +export * from './IndyUtilitiesService' +export * from './IndyRevocationService' diff --git a/packages/core/src/modules/ledger/IndyPool.ts b/packages/core/src/modules/ledger/IndyPool.ts index 1f3b65507f..9c48df7808 100644 --- a/packages/core/src/modules/ledger/IndyPool.ts +++ b/packages/core/src/modules/ledger/IndyPool.ts @@ -7,7 +7,7 @@ import { AriesFrameworkError, IndySdkError } from '../../error' import { isIndyError } from '../../utils/indyError' import { LedgerError } from './error/LedgerError' -import { isLedgerRejectResponse } from './ledgerUtil' +import { isLedgerRejectResponse, isLedgerReqnackResponse } from './ledgerUtil' export interface IndyPoolConfig { genesisPath?: string @@ -22,6 +22,7 @@ export class IndyPool { private fileSystem: FileSystem private poolConfig: IndyPoolConfig private _poolHandle?: number + private poolConnected?: Promise public authorAgreement?: AuthorAgreement | null public constructor(agentConfig: AgentConfig, poolConfig: IndyPoolConfig) { @@ -55,9 +56,6 @@ export class IndyPool { this._poolHandle = undefined - // FIXME: Add type to indy-sdk - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore await this.indy.closePoolLedger(poolHandle) } @@ -67,13 +65,25 @@ export class IndyPool { await this.close() } - // FIXME: Add type to indy-sdk - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - await this.indy.deletePoolLedgerConfig(this.agentConfig.poolName) + await this.indy.deletePoolLedgerConfig(this.poolConfig.id) } public async connect() { + if (!this.poolConnected) { + // Save the promise of connectToLedger to determine if we are done connecting + this.poolConnected = this.connectToLedger() + this.poolConnected.catch((error) => { + // Set poolConnected to undefined so we can retry connection upon failure + this.poolConnected = undefined + this.logger.error('Connection to pool: ' + this.poolConfig.genesisPath + ' failed.', { error }) + }) + return this.poolConnected + } else { + throw new AriesFrameworkError('Cannot attempt connection to ledger, already connecting.') + } + } + + private async connectToLedger() { const poolName = this.poolConfig.id const genesisPath = await this.getGenesisPath() @@ -112,7 +122,7 @@ export class IndyPool { public async submitReadRequest(request: Indy.LedgerRequest) { const response = await this.submitRequest(request) - if (isLedgerRejectResponse(response)) { + if (isLedgerRejectResponse(response) || isLedgerReqnackResponse(response)) { throw new LedgerError(`Ledger '${this.id}' rejected read transaction request: ${response.reason}`) } @@ -130,6 +140,15 @@ export class IndyPool { } private async getPoolHandle() { + if (this.poolConnected) { + // If we have tried to already connect to pool wait for it + try { + await this.poolConnected + } catch (error) { + this.logger.error('Connection to pool: ' + this.poolConfig.genesisPath + ' failed.', { error }) + } + } + if (!this._poolHandle) { return this.connect() } diff --git a/packages/core/src/modules/ledger/LedgerModule.ts b/packages/core/src/modules/ledger/LedgerModule.ts index 751468c050..d89d76b219 100644 --- a/packages/core/src/modules/ledger/LedgerModule.ts +++ b/packages/core/src/modules/ledger/LedgerModule.ts @@ -19,6 +19,13 @@ export class LedgerModule { this.wallet = wallet } + /** + * Connect to all the ledger pools + */ + public async connectToPools() { + await this.ledgerService.connectToPools() + } + public async registerPublicDid(did: string, verkey: string, alias: string, role?: NymRole) { const myPublicDid = this.wallet.publicDid?.did @@ -65,4 +72,16 @@ export class LedgerModule { public async getCredentialDefinition(id: string) { return this.ledgerService.getCredentialDefinition(id) } + + public async getRevocationRegistryDefinition(revocationRegistryDefinitionId: string) { + return this.ledgerService.getRevocationRegistryDefinition(revocationRegistryDefinitionId) + } + + public async getRevocationRegistryDelta( + revocationRegistryDefinitionId: string, + fromSeconds = 0, + toSeconds = new Date().getTime() + ) { + return this.ledgerService.getRevocationRegistryDelta(revocationRegistryDefinitionId, fromSeconds, toSeconds) + } } diff --git a/packages/core/src/modules/ledger/__tests__/IndyPoolService.test.ts b/packages/core/src/modules/ledger/__tests__/IndyPoolService.test.ts index 6d5a93962d..7bf8dcffd4 100644 --- a/packages/core/src/modules/ledger/__tests__/IndyPoolService.test.ts +++ b/packages/core/src/modules/ledger/__tests__/IndyPoolService.test.ts @@ -55,7 +55,7 @@ describe('IndyLedgerService', () => { beforeAll(async () => { wallet = new IndyWallet(config) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await wallet.initialize(config.walletConfig!) + await wallet.createAndOpen(config.walletConfig!) }) afterAll(async () => { @@ -132,7 +132,7 @@ describe('IndyLedgerService', () => { }) it('should return the first pool with a self certifying DID if at least one did is self certifying ', async () => { - const did = 'V6ty6ttM3EjuCtosH6sGtW' + const did = 'did:sov:q7ATwTYbQDgiigVijUAej' // Found on one production and one non production ledger const responses = getDidResponsesForDid(did, pools, { indicioMain: '~43X4NhAFqREffK7eWdKgFH', diff --git a/packages/core/src/modules/ledger/ledgerUtil.ts b/packages/core/src/modules/ledger/ledgerUtil.ts index a8063974b2..62e75f1e72 100644 --- a/packages/core/src/modules/ledger/ledgerUtil.ts +++ b/packages/core/src/modules/ledger/ledgerUtil.ts @@ -3,3 +3,7 @@ import type * as Indy from 'indy-sdk' export function isLedgerRejectResponse(response: Indy.LedgerResponse): response is Indy.LedgerRejectResponse { return response.op === 'REJECT' } + +export function isLedgerReqnackResponse(response: Indy.LedgerResponse): response is Indy.LedgerReqnackResponse { + return response.op === 'REQNACK' +} diff --git a/packages/core/src/modules/ledger/services/IndyLedgerService.ts b/packages/core/src/modules/ledger/services/IndyLedgerService.ts index 956c9fc2c1..7e6cf89fa9 100644 --- a/packages/core/src/modules/ledger/services/IndyLedgerService.ts +++ b/packages/core/src/modules/ledger/services/IndyLedgerService.ts @@ -14,10 +14,14 @@ import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../agent/AgentConfig' import { IndySdkError } from '../../../error/IndySdkError' -import { didFromCredentialDefinitionId, didFromSchemaId } from '../../../utils/did' +import { + didFromSchemaId, + didFromCredentialDefinitionId, + didFromRevocationRegistryDefinitionId, +} from '../../../utils/did' import { isIndyError } from '../../../utils/indyError' import { IndyWallet } from '../../../wallet/IndyWallet' -import { IndyIssuerService } from '../../indy' +import { IndyIssuerService } from '../../indy/services/IndyIssuerService' import { IndyPoolService } from './IndyPoolService' @@ -43,6 +47,10 @@ export class IndyLedgerService { this.indyPoolService = indyPoolService } + public async connectToPools() { + return this.indyPoolService.connectToPools() + } + public async registerPublicDid( submitterDid: string, targetDid: string, @@ -86,6 +94,35 @@ export class IndyLedgerService { return didResponse } + public async getEndpointsForDid(did: string) { + const { pool } = await this.indyPoolService.getPoolForDid(did) + + try { + this.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.id}'`) + + const request = await this.indy.buildGetAttribRequest(null, did, 'endpoint', null, null) + + this.logger.debug(`Submitting get endpoint ATTRIB request for did '${did}' to ledger '${pool.id}'`) + const response = await this.submitReadRequest(pool, request) + + if (!response.result.data) return {} + + const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib + this.logger.debug(`Got endpoints '${JSON.stringify(endpoints)}' for did '${did}' from ledger '${pool.id}'`, { + response, + endpoints, + }) + + return endpoints ?? {} + } catch (error) { + this.logger.error(`Error retrieving endpoints for did '${did}' from ledger '${pool.id}'`, { + error, + }) + + throw isIndyError(error) ? new IndySdkError(error) : error + } + } + public async registerSchema(did: string, schemaTemplate: SchemaTemplate): Promise { const pool = this.indyPoolService.ledgerWritePool @@ -121,16 +158,19 @@ export class IndyLedgerService { const { pool } = await this.indyPoolService.getPoolForDid(did) try { - this.logger.debug(`Get schema '${schemaId}' from ledger '${pool.id}'`) + this.logger.debug(`Getting schema '${schemaId}' from ledger '${pool.id}'`) const request = await this.indy.buildGetSchemaRequest(null, schemaId) - this.logger.debug(`Submitting get schema request for schema '${schemaId}' to ledger '${pool.id}'`) + this.logger.trace(`Submitting get schema request for schema '${schemaId}' to ledger '${pool.id}'`) const response = await this.submitReadRequest(pool, request) + this.logger.trace(`Got un-parsed schema '${schemaId}' from ledger '${pool.id}'`, { + response, + }) + const [, schema] = await this.indy.parseGetSchemaResponse(response) this.logger.debug(`Got schema '${schemaId}' from ledger '${pool.id}'`, { - response, schema, }) @@ -153,7 +193,7 @@ export class IndyLedgerService { try { this.logger.debug( - `Register credential definition on ledger '${pool.id}' with did '${did}'`, + `Registering credential definition on ledger '${pool.id}' with did '${did}'`, credentialDefinitionTemplate ) const { schema, tag, signatureType, supportRevocation } = credentialDefinitionTemplate @@ -197,19 +237,19 @@ export class IndyLedgerService { this.logger.debug(`Using ledger '${pool.id}' to retrieve credential definition '${credentialDefinitionId}'`) try { - this.logger.debug(`Get credential definition '${credentialDefinitionId}' from ledger '${pool.id}'`) - const request = await this.indy.buildGetCredDefRequest(null, credentialDefinitionId) - this.logger.debug( + this.logger.trace( `Submitting get credential definition request for credential definition '${credentialDefinitionId}' to ledger '${pool.id}'` ) const response = await this.submitReadRequest(pool, request) + this.logger.trace(`Got un-parsed credential definition '${credentialDefinitionId}' from ledger '${pool.id}'`, { + response, + }) const [, credentialDefinition] = await this.indy.parseGetCredDefResponse(response) this.logger.debug(`Got credential definition '${credentialDefinitionId}' from ledger '${pool.id}'`, { - response, credentialDefinition, }) @@ -225,6 +265,157 @@ export class IndyLedgerService { } } + public async getRevocationRegistryDefinition( + revocationRegistryDefinitionId: string + ): Promise { + const did = didFromRevocationRegistryDefinitionId(revocationRegistryDefinitionId) + const { pool } = await this.indyPoolService.getPoolForDid(did) + + this.logger.debug( + `Using ledger '${pool.id}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` + ) + try { + //TODO - implement a cache + this.logger.trace( + `Revocation Registry Definition '${revocationRegistryDefinitionId}' not cached, retrieving from ledger` + ) + + const request = await this.indy.buildGetRevocRegDefRequest(null, revocationRegistryDefinitionId) + + this.logger.trace( + `Submitting get revocation registry definition request for revocation registry definition '${revocationRegistryDefinitionId}' to ledger` + ) + const response = await this.submitReadRequest(pool, request) + this.logger.trace( + `Got un-parsed revocation registry definition '${revocationRegistryDefinitionId}' from ledger '${pool.id}'`, + { + response, + } + ) + + const [, revocationRegistryDefinition] = await this.indy.parseGetRevocRegDefResponse(response) + + this.logger.debug(`Got revocation registry definition '${revocationRegistryDefinitionId}' from ledger`, { + revocationRegistryDefinition, + }) + + return { revocationRegistryDefinition, revocationRegistryDefinitionTxnTime: response.result.txnTime } + } catch (error) { + this.logger.error( + `Error retrieving revocation registry definition '${revocationRegistryDefinitionId}' from ledger`, + { + error, + revocationRegistryDefinitionId: revocationRegistryDefinitionId, + pool: pool.id, + } + ) + throw error + } + } + + //Retrieves the accumulated state of a revocation registry by id given a revocation interval from & to (used primarily for proof creation) + public async getRevocationRegistryDelta( + revocationRegistryDefinitionId: string, + to: number = new Date().getTime(), + from = 0 + ): Promise { + //TODO - implement a cache + const did = didFromRevocationRegistryDefinitionId(revocationRegistryDefinitionId) + const { pool } = await this.indyPoolService.getPoolForDid(did) + + this.logger.debug( + `Using ledger '${pool.id}' to retrieve revocation registry delta with revocation registry definition id: '${revocationRegistryDefinitionId}'`, + { + to, + from, + } + ) + + try { + const request = await this.indy.buildGetRevocRegDeltaRequest(null, revocationRegistryDefinitionId, from, to) + + this.logger.trace( + `Submitting get revocation registry delta request for revocation registry '${revocationRegistryDefinitionId}' to ledger` + ) + + const response = await this.submitReadRequest(pool, request) + this.logger.trace( + `Got revocation registry delta unparsed-response '${revocationRegistryDefinitionId}' from ledger`, + { + response, + } + ) + + const [, revocationRegistryDelta, deltaTimestamp] = await this.indy.parseGetRevocRegDeltaResponse(response) + + this.logger.debug(`Got revocation registry delta '${revocationRegistryDefinitionId}' from ledger`, { + revocationRegistryDelta, + deltaTimestamp, + to, + from, + }) + + return { revocationRegistryDelta, deltaTimestamp } + } catch (error) { + this.logger.error( + `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation?"`, + { + error, + revocationRegistryId: revocationRegistryDefinitionId, + pool: pool.id, + } + ) + throw error + } + } + + //Retrieves the accumulated state of a revocation registry by id given a timestamp (used primarily for verification) + public async getRevocationRegistry( + revocationRegistryDefinitionId: string, + timestamp: number + ): Promise { + //TODO - implement a cache + const did = didFromRevocationRegistryDefinitionId(revocationRegistryDefinitionId) + const { pool } = await this.indyPoolService.getPoolForDid(did) + + this.logger.debug( + `Using ledger '${pool.id}' to retrieve revocation registry accumulated state with revocation registry definition id: '${revocationRegistryDefinitionId}'`, + { + timestamp, + } + ) + + try { + const request = await this.indy.buildGetRevocRegRequest(null, revocationRegistryDefinitionId, timestamp) + + this.logger.trace( + `Submitting get revocation registry request for revocation registry '${revocationRegistryDefinitionId}' to ledger` + ) + const response = await this.submitReadRequest(pool, request) + this.logger.trace( + `Got un-parsed revocation registry '${revocationRegistryDefinitionId}' from ledger '${pool.id}'`, + { + response, + } + ) + + const [, revocationRegistry, ledgerTimestamp] = await this.indy.parseGetRevocRegResponse(response) + this.logger.debug(`Got revocation registry '${revocationRegistryDefinitionId}' from ledger`, { + ledgerTimestamp, + revocationRegistry, + }) + + return { revocationRegistry, ledgerTimestamp } + } catch (error) { + this.logger.error(`Error retrieving revocation registry '${revocationRegistryDefinitionId}' from ledger`, { + error, + revocationRegistryId: revocationRegistryDefinitionId, + pool: pool.id, + }) + throw error + } + } + private async submitWriteRequest( pool: IndyPool, request: LedgerRequest, @@ -335,3 +526,25 @@ export interface CredentialDefinitionTemplate { signatureType: 'CL' supportRevocation: boolean } + +export interface ParseRevocationRegistryDefinitionTemplate { + revocationRegistryDefinition: Indy.RevocRegDef + revocationRegistryDefinitionTxnTime: number +} + +export interface ParseRevocationRegistryDeltaTemplate { + revocationRegistryDelta: Indy.RevocRegDelta + deltaTimestamp: number +} + +export interface ParseRevocationRegistryTemplate { + revocationRegistry: Indy.RevocReg + ledgerTimestamp: number +} + +export interface IndyEndpointAttrib { + endpoint?: string + types?: Array<'endpoint' | 'did-communication' | 'DIDComm'> + routingKeys?: string[] + [key: string]: unknown +} diff --git a/packages/core/src/modules/ledger/services/IndyPoolService.ts b/packages/core/src/modules/ledger/services/IndyPoolService.ts index 4a6eb5fce3..ba12b147aa 100644 --- a/packages/core/src/modules/ledger/services/IndyPoolService.ts +++ b/packages/core/src/modules/ledger/services/IndyPoolService.ts @@ -36,6 +36,16 @@ export class IndyPoolService { this.didCache = new PersistedLruCache(DID_POOL_CACHE_ID, DID_POOL_CACHE_LIMIT, cacheRepository) } + /** + * Create connections to all ledger pools + */ + public async connectToPools() { + const poolsPromises = this.pools.map((pool) => { + return pool.connect() + }) + return Promise.all(poolsPromises) + } + /** * Get the pool used for writing to the ledger. For now we always use the first pool * as the pool that writes to the ledger diff --git a/packages/core/src/modules/oob/OutOfBandModule.ts b/packages/core/src/modules/oob/OutOfBandModule.ts new file mode 100644 index 0000000000..e8d9d05751 --- /dev/null +++ b/packages/core/src/modules/oob/OutOfBandModule.ts @@ -0,0 +1,678 @@ +import type { AgentMessage } from '../../agent/AgentMessage' +import type { AgentMessageReceivedEvent } from '../../agent/Events' +import type { Key } from '../../crypto' +import type { Logger } from '../../logger' +import type { ConnectionRecord, Routing } from '../../modules/connections' +import type { PlaintextMessage } from '../../types' +import type { HandshakeReusedEvent, OutOfBandStateChangedEvent } from './domain/OutOfBandEvents' + +import { parseUrl } from 'query-string' +import { catchError, EmptyError, first, firstValueFrom, map, of, timeout } from 'rxjs' +import { Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../agent/AgentConfig' +import { Dispatcher } from '../../agent/Dispatcher' +import { EventEmitter } from '../../agent/EventEmitter' +import { AgentEventTypes } from '../../agent/Events' +import { MessageSender } from '../../agent/MessageSender' +import { createOutboundMessage } from '../../agent/helpers' +import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' +import { AriesFrameworkError } from '../../error' +import { + DidExchangeState, + HandshakeProtocol, + ConnectionInvitationMessage, + ConnectionsModule, +} from '../../modules/connections' +import { JsonTransformer } from '../../utils' +import { parseMessageType, supportsIncomingMessageType } from '../../utils/messageType' +import { DidsModule } from '../dids' +import { didKeyToVerkey, verkeyToDidKey } from '../dids/helpers' +import { outOfBandServiceToNumAlgo2Did } from '../dids/methods/peer/peerDidNumAlgo2' +import { MediationRecipientService } from '../routing' + +import { OutOfBandService } from './OutOfBandService' +import { OutOfBandDidCommService } from './domain/OutOfBandDidCommService' +import { OutOfBandEventTypes } from './domain/OutOfBandEvents' +import { OutOfBandRole } from './domain/OutOfBandRole' +import { OutOfBandState } from './domain/OutOfBandState' +import { HandshakeReuseHandler } from './handlers' +import { HandshakeReuseAcceptedHandler } from './handlers/HandshakeReuseAcceptedHandler' +import { convertToNewInvitation, convertToOldInvitation } from './helpers' +import { OutOfBandInvitation } from './messages' +import { OutOfBandRecord } from './repository/OutOfBandRecord' + +const didCommProfiles = ['didcomm/aip1', 'didcomm/aip2;env=rfc19'] + +export interface CreateOutOfBandInvitationConfig { + label?: string + alias?: string + imageUrl?: string + goalCode?: string + goal?: string + handshake?: boolean + handshakeProtocols?: HandshakeProtocol[] + messages?: AgentMessage[] + multiUseInvitation?: boolean + autoAcceptConnection?: boolean + routing?: Routing +} + +export interface ReceiveOutOfBandInvitationConfig { + label?: string + alias?: string + imageUrl?: string + autoAcceptInvitation?: boolean + autoAcceptConnection?: boolean + reuseConnection?: boolean + routing?: Routing +} + +@scoped(Lifecycle.ContainerScoped) +export class OutOfBandModule { + private outOfBandService: OutOfBandService + private mediationRecipientService: MediationRecipientService + private connectionsModule: ConnectionsModule + private dids: DidsModule + private dispatcher: Dispatcher + private messageSender: MessageSender + private eventEmitter: EventEmitter + private agentConfig: AgentConfig + private logger: Logger + + public constructor( + dispatcher: Dispatcher, + agentConfig: AgentConfig, + outOfBandService: OutOfBandService, + mediationRecipientService: MediationRecipientService, + connectionsModule: ConnectionsModule, + dids: DidsModule, + messageSender: MessageSender, + eventEmitter: EventEmitter + ) { + this.dispatcher = dispatcher + this.agentConfig = agentConfig + this.logger = agentConfig.logger + this.outOfBandService = outOfBandService + this.mediationRecipientService = mediationRecipientService + this.connectionsModule = connectionsModule + this.dids = dids + this.messageSender = messageSender + this.eventEmitter = eventEmitter + this.registerHandlers(dispatcher) + } + + /** + * Creates an outbound out-of-band record containing out-of-band invitation message defined in + * Aries RFC 0434: Out-of-Band Protocol 1.1. + * + * It automatically adds all supported handshake protocols by agent to `hanshake_protocols`. You + * can modify this by setting `handshakeProtocols` in `config` parameter. If you want to create + * invitation without handhsake, you can set `handshake` to `false`. + * + * If `config` parameter contains `messages` it adds them to `requests~attach` attribute. + * + * Agent role: sender (inviter) + * + * @param config configuration of how out-of-band invitation should be created + * @returns out-of-band record + */ + public async createInvitation(config: CreateOutOfBandInvitationConfig = {}): Promise { + const multiUseInvitation = config.multiUseInvitation ?? false + const handshake = config.handshake ?? true + const customHandshakeProtocols = config.handshakeProtocols + const autoAcceptConnection = config.autoAcceptConnection ?? this.agentConfig.autoAcceptConnections + // We don't want to treat an empty array as messages being provided + const messages = config.messages && config.messages.length > 0 ? config.messages : undefined + const label = config.label ?? this.agentConfig.label + const imageUrl = config.imageUrl ?? this.agentConfig.connectionImageUrl + + if (!handshake && !messages) { + throw new AriesFrameworkError( + 'One or both of handshake_protocols and requests~attach MUST be included in the message.' + ) + } + + if (!handshake && customHandshakeProtocols) { + throw new AriesFrameworkError(`Attribute 'handshake' can not be 'false' when 'handshakeProtocols' is defined.`) + } + + // For now we disallow creating multi-use invitation with attachments. This would mean we need multi-use + // credential and presentation exchanges. + if (messages && multiUseInvitation) { + throw new AriesFrameworkError("Attribute 'multiUseInvitation' can not be 'true' when 'messages' is defined.") + } + + let handshakeProtocols + if (handshake) { + // Find supported handshake protocol preserving the order of handshake protocols defined + // by agent + if (customHandshakeProtocols) { + this.assertHandshakeProtocols(customHandshakeProtocols) + handshakeProtocols = customHandshakeProtocols + } else { + handshakeProtocols = this.getSupportedHandshakeProtocols() + } + } + + const routing = config.routing ?? (await this.mediationRecipientService.getRouting({})) + + const services = routing.endpoints.map((endpoint, index) => { + return new OutOfBandDidCommService({ + id: `#inline-${index}`, + serviceEndpoint: endpoint, + recipientKeys: [routing.verkey].map(verkeyToDidKey), + routingKeys: routing.routingKeys.map(verkeyToDidKey), + }) + }) + + const options = { + label, + goal: config.goal, + goalCode: config.goalCode, + imageUrl, + accept: didCommProfiles, + services, + handshakeProtocols, + } + const outOfBandInvitation = new OutOfBandInvitation(options) + + if (messages) { + messages.forEach((message) => { + if (message.service) { + // We can remove `~service` attribute from message. Newer OOB messages have `services` attribute instead. + message.service = undefined + } + outOfBandInvitation.addRequest(message) + }) + } + + const outOfBandRecord = new OutOfBandRecord({ + did: routing.did, + mediatorId: routing.mediatorId, + role: OutOfBandRole.Sender, + state: OutOfBandState.AwaitResponse, + outOfBandInvitation: outOfBandInvitation, + reusable: multiUseInvitation, + autoAcceptConnection, + }) + + await this.outOfBandService.save(outOfBandRecord) + this.eventEmitter.emit({ + type: OutOfBandEventTypes.OutOfBandStateChanged, + payload: { + outOfBandRecord, + previousState: null, + }, + }) + + return outOfBandRecord + } + + /** + * Creates an outbound out-of-band record in the same way how `createInvitation` method does it, + * but it also converts out-of-band invitation message to an "legacy" invitation message defined + * in RFC 0160: Connection Protocol and returns it together with out-of-band record. + * + * Agent role: sender (inviter) + * + * @param config configuration of how out-of-band invitation should be created + * @returns out-of-band record and connection invitation + */ + public async createLegacyInvitation(config: CreateOutOfBandInvitationConfig = {}) { + if (config.handshake === false) { + throw new AriesFrameworkError( + `Invalid value of handshake in config. Value is ${config.handshake}, but this method supports only 'true' or 'undefined'.` + ) + } + if ( + !config.handshakeProtocols || + (config.handshakeProtocols?.length === 1 && config.handshakeProtocols.includes(HandshakeProtocol.Connections)) + ) { + const outOfBandRecord = await this.createInvitation({ + ...config, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + return { outOfBandRecord, invitation: convertToOldInvitation(outOfBandRecord.outOfBandInvitation) } + } + throw new AriesFrameworkError( + `Invalid value of handshakeProtocols in config. Value is ${config.handshakeProtocols}, but this method supports only ${HandshakeProtocol.Connections}.` + ) + } + + /** + * Parses URL, decodes invitation and calls `receiveMessage` with parsed invitation message. + * + * Agent role: receiver (invitee) + * + * @param invitationUrl url containing a base64 encoded invitation to receive + * @param config configuration of how out-of-band invitation should be processed + * @returns out-of-band record and connection record if one has been created + */ + public async receiveInvitationFromUrl(invitationUrl: string, config: ReceiveOutOfBandInvitationConfig = {}) { + const message = await this.parseInvitation(invitationUrl) + return this.receiveInvitation(message, config) + } + + /** + * Parses URL containing encoded invitation and returns invitation message. + * + * @param invitationUrl URL containing encoded invitation + * + * @returns OutOfBandInvitation + */ + public async parseInvitation(invitationUrl: string) { + const parsedUrl = parseUrl(invitationUrl).query + if (parsedUrl['oob']) { + const outOfBandInvitation = await OutOfBandInvitation.fromUrl(invitationUrl) + return outOfBandInvitation + } else if (parsedUrl['c_i'] || parsedUrl['d_m']) { + const invitation = await ConnectionInvitationMessage.fromUrl(invitationUrl) + return convertToNewInvitation(invitation) + } + throw new AriesFrameworkError( + 'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters: `oob`, `c_i` or `d_m`.' + ) + } + + /** + * Creates inbound out-of-band record and assigns out-of-band invitation message to it if the + * message is valid. It automatically passes out-of-band invitation for further processing to + * `acceptInvitation` method. If you don't want to do that you can set `autoAcceptInvitation` + * attribute in `config` parameter to `false` and accept the message later by calling + * `acceptInvitation`. + * + * It supports both OOB (Aries RFC 0434: Out-of-Band Protocol 1.1) and Connection Invitation + * (0160: Connection Protocol). + * + * Agent role: receiver (invitee) + * + * @param outOfBandInvitation + * @param config config for handling of invitation + * + * @returns out-of-band record and connection record if one has been created. + */ + public async receiveInvitation( + outOfBandInvitation: OutOfBandInvitation, + config: ReceiveOutOfBandInvitationConfig = {} + ): Promise<{ outOfBandRecord: OutOfBandRecord; connectionRecord?: ConnectionRecord }> { + const { handshakeProtocols } = outOfBandInvitation + const { routing } = config + + const autoAcceptInvitation = config.autoAcceptInvitation ?? true + const autoAcceptConnection = config.autoAcceptConnection ?? true + const reuseConnection = config.reuseConnection ?? false + const label = config.label ?? this.agentConfig.label + const alias = config.alias + const imageUrl = config.imageUrl ?? this.agentConfig.connectionImageUrl + + const messages = outOfBandInvitation.getRequests() + + if ((!handshakeProtocols || handshakeProtocols.length === 0) && (!messages || messages?.length === 0)) { + throw new AriesFrameworkError( + 'One or both of handshake_protocols and requests~attach MUST be included in the message.' + ) + } + + // Make sure we haven't processed this invitation before. + let outOfBandRecord = await this.findByInvitationId(outOfBandInvitation.id) + if (outOfBandRecord) { + throw new AriesFrameworkError( + `An out of band record with invitation ${outOfBandInvitation.id} already exists. Invitations should have a unique id.` + ) + } + + outOfBandRecord = new OutOfBandRecord({ + role: OutOfBandRole.Receiver, + state: OutOfBandState.Initial, + outOfBandInvitation: outOfBandInvitation, + autoAcceptConnection, + }) + await this.outOfBandService.save(outOfBandRecord) + this.eventEmitter.emit({ + type: OutOfBandEventTypes.OutOfBandStateChanged, + payload: { + outOfBandRecord, + previousState: null, + }, + }) + + if (autoAcceptInvitation) { + return await this.acceptInvitation(outOfBandRecord.id, { + label, + alias, + imageUrl, + autoAcceptConnection, + reuseConnection, + routing, + }) + } + + return { outOfBandRecord } + } + + /** + * Creates a connection if the out-of-band invitation message contains `handshake_protocols` + * attribute, except for the case when connection already exists and `reuseConnection` is enabled. + * + * It passes first supported message from `requests~attach` attribute to the agent, except for the + * case reuse of connection is applied when it just sends `handshake-reuse` message to existing + * connection. + * + * Agent role: receiver (invitee) + * + * @param outOfBandId + * @param config + * @returns out-of-band record and connection record if one has been created. + */ + public async acceptInvitation( + outOfBandId: string, + config: { + autoAcceptConnection?: boolean + reuseConnection?: boolean + label?: string + alias?: string + imageUrl?: string + mediatorId?: string + routing?: Routing + } + ) { + const outOfBandRecord = await this.outOfBandService.getById(outOfBandId) + + const { outOfBandInvitation } = outOfBandRecord + const { label, alias, imageUrl, autoAcceptConnection, reuseConnection, routing } = config + const { handshakeProtocols, services } = outOfBandInvitation + const messages = outOfBandInvitation.getRequests() + + const existingConnection = await this.findExistingConnection(services) + + await this.outOfBandService.updateState(outOfBandRecord, OutOfBandState.PrepareResponse) + + if (handshakeProtocols) { + this.logger.debug('Out of band message contains handshake protocols.') + + let connectionRecord + if (existingConnection && reuseConnection) { + this.logger.debug( + `Connection already exists and reuse is enabled. Reusing an existing connection with ID ${existingConnection.id}.` + ) + + if (!messages) { + this.logger.debug('Out of band message does not contain any request messages.') + const isHandshakeReuseSuccessful = await this.handleHandshakeReuse(outOfBandRecord, existingConnection) + + // Handshake reuse was successful + if (isHandshakeReuseSuccessful) { + this.logger.debug(`Handshake reuse successful. Reusing existing connection ${existingConnection.id}.`) + connectionRecord = existingConnection + } else { + // Handshake reuse failed. Not setting connection record + this.logger.debug(`Handshake reuse failed. Not using existing connection ${existingConnection.id}.`) + } + } else { + // Handshake reuse because we found a connection and we can respond directly to the message + this.logger.debug(`Reusing existing connection ${existingConnection.id}.`) + connectionRecord = existingConnection + } + } + + // If no existing connection was found, reuseConnection is false, or we didn't receive a + // handshake-reuse-accepted message we create a new connection + if (!connectionRecord) { + this.logger.debug('Connection does not exist or reuse is disabled. Creating a new connection.') + // Find first supported handshake protocol preserving the order of handshake protocols + // defined by `handshake_protocols` attribute in the invitation message + const handshakeProtocol = this.getFirstSupportedProtocol(handshakeProtocols) + connectionRecord = await this.connectionsModule.acceptOutOfBandInvitation(outOfBandRecord, { + label, + alias, + imageUrl, + autoAcceptConnection, + protocol: handshakeProtocol, + routing, + }) + } + + if (messages) { + this.logger.debug('Out of band message contains request messages.') + if (connectionRecord.isReady) { + await this.emitWithConnection(connectionRecord, messages) + } else { + // Wait until the connection is ready and then pass the messages to the agent for further processing + this.connectionsModule + .returnWhenIsConnected(connectionRecord.id) + .then((connectionRecord) => this.emitWithConnection(connectionRecord, messages)) + .catch((error) => { + if (error instanceof EmptyError) { + this.logger.warn( + `Agent unsubscribed before connection got into ${DidExchangeState.Completed} state`, + error + ) + } else { + this.logger.error('Promise waiting for the connection to be complete failed.', error) + } + }) + } + } + return { outOfBandRecord, connectionRecord } + } else if (messages) { + this.logger.debug('Out of band message contains only request messages.') + if (existingConnection) { + this.logger.debug('Connection already exists.', { connectionId: existingConnection.id }) + await this.emitWithConnection(existingConnection, messages) + } else { + await this.emitWithServices(services, messages) + } + } + return { outOfBandRecord } + } + + public async findByRecipientKey(recipientKey: Key) { + return this.outOfBandService.findByRecipientKey(recipientKey) + } + + public async findByInvitationId(invitationId: string) { + return this.outOfBandService.findByInvitationId(invitationId) + } + + /** + * Retrieve all out of bands records + * + * @returns List containing all out of band records + */ + public getAll() { + return this.outOfBandService.getAll() + } + + /** + * Retrieve a out of band record by id + * + * @param outOfBandId The out of band record id + * @throws {RecordNotFoundError} If no record is found + * @return The out of band record + * + */ + public getById(outOfBandId: string): Promise { + return this.outOfBandService.getById(outOfBandId) + } + + /** + * Find an out of band record by id + * + * @param outOfBandId the out of band record id + * @returns The out of band record or null if not found + */ + public findById(outOfBandId: string): Promise { + return this.outOfBandService.findById(outOfBandId) + } + + /** + * Delete an out of band record by id + * + * @param outOfBandId the out of band record id + */ + public async deleteById(outOfBandId: string) { + return this.outOfBandService.deleteById(outOfBandId) + } + + private assertHandshakeProtocols(handshakeProtocols: HandshakeProtocol[]) { + if (!this.areHandshakeProtocolsSupported(handshakeProtocols)) { + const supportedProtocols = this.getSupportedHandshakeProtocols() + throw new AriesFrameworkError( + `Handshake protocols [${handshakeProtocols}] are not supported. Supported protocols are [${supportedProtocols}]` + ) + } + } + + private areHandshakeProtocolsSupported(handshakeProtocols: HandshakeProtocol[]) { + const supportedProtocols = this.getSupportedHandshakeProtocols() + return handshakeProtocols.every((p) => supportedProtocols.includes(p)) + } + + private getSupportedHandshakeProtocols(): HandshakeProtocol[] { + const handshakeMessageFamilies = ['https://didcomm.org/didexchange', 'https://didcomm.org/connections'] + const handshakeProtocols = this.dispatcher.filterSupportedProtocolsByMessageFamilies(handshakeMessageFamilies) + + if (handshakeProtocols.length === 0) { + throw new AriesFrameworkError('There is no handshake protocol supported. Agent can not create a connection.') + } + + // Order protocols according to `handshakeMessageFamilies` array + const orderedProtocols = handshakeMessageFamilies + .map((messageFamily) => handshakeProtocols.find((p) => p.startsWith(messageFamily))) + .filter((item): item is string => !!item) + + return orderedProtocols as HandshakeProtocol[] + } + + private getFirstSupportedProtocol(handshakeProtocols: HandshakeProtocol[]) { + const supportedProtocols = this.getSupportedHandshakeProtocols() + const handshakeProtocol = handshakeProtocols.find((p) => supportedProtocols.includes(p)) + if (!handshakeProtocol) { + throw new AriesFrameworkError( + `Handshake protocols [${handshakeProtocols}] are not supported. Supported protocols are [${supportedProtocols}]` + ) + } + return handshakeProtocol + } + + private async findExistingConnection(services: Array) { + this.logger.debug('Searching for an existing connection for out-of-band invitation services.', { services }) + + // TODO: for each did we should look for a connection with the invitation did OR a connection with theirDid that matches the service did + for (const didOrService of services) { + // We need to check if the service is an instance of string because of limitations from class-validator + if (typeof didOrService === 'string' || didOrService instanceof String) { + // TODO await this.connectionsModule.findByTheirDid() + throw new AriesFrameworkError('Dids are not currently supported in out-of-band invitation services attribute.') + } + + const did = outOfBandServiceToNumAlgo2Did(didOrService) + const connections = await this.connectionsModule.findByInvitationDid(did) + this.logger.debug(`Retrieved ${connections.length} connections for invitation did ${did}`) + + if (connections.length === 1) { + const [firstConnection] = connections + return firstConnection + } else if (connections.length > 1) { + this.logger.warn(`There is more than one connection created from invitationDid ${did}. Taking the first one.`) + const [firstConnection] = connections + return firstConnection + } + return null + } + } + + private async emitWithConnection(connectionRecord: ConnectionRecord, messages: PlaintextMessage[]) { + const supportedMessageTypes = this.dispatcher.supportedMessageTypes + const plaintextMessage = messages.find((message) => + supportedMessageTypes.find((type) => supportsIncomingMessageType(parseMessageType(message['@type']), type)) + ) + + if (!plaintextMessage) { + throw new AriesFrameworkError('There is no message in requests~attach supported by agent.') + } + + this.logger.debug(`Message with type ${plaintextMessage['@type']} can be processed.`) + + this.eventEmitter.emit({ + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: plaintextMessage, + connection: connectionRecord, + }, + }) + } + + private async emitWithServices(services: Array, messages: PlaintextMessage[]) { + if (!services || services.length === 0) { + throw new AriesFrameworkError(`There are no services. We can not emit messages`) + } + + const supportedMessageTypes = this.dispatcher.supportedMessageTypes + const plaintextMessage = messages.find((message) => + supportedMessageTypes.find((type) => supportsIncomingMessageType(parseMessageType(message['@type']), type)) + ) + + if (!plaintextMessage) { + throw new AriesFrameworkError('There is no message in requests~attach supported by agent.') + } + + this.logger.debug(`Message with type ${plaintextMessage['@type']} can be processed.`) + + // The framework currently supports only older OOB messages with `~service` decorator. + // TODO: support receiving messages with other services so we don't have to transform the service + // to ~service decorator + const [service] = services + + if (typeof service === 'string') { + throw new AriesFrameworkError('Dids are not currently supported in out-of-band invitation services attribute.') + } + + const serviceDecorator = new ServiceDecorator({ + recipientKeys: service.recipientKeys.map(didKeyToVerkey), + routingKeys: service.routingKeys?.map(didKeyToVerkey) || [], + serviceEndpoint: service.serviceEndpoint, + }) + + plaintextMessage['~service'] = JsonTransformer.toJSON(serviceDecorator) + this.eventEmitter.emit({ + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: plaintextMessage, + }, + }) + } + + private async handleHandshakeReuse(outOfBandRecord: OutOfBandRecord, connectionRecord: ConnectionRecord) { + const reuseMessage = await this.outOfBandService.createHandShakeReuse(outOfBandRecord, connectionRecord) + + const reuseAcceptedEventPromise = firstValueFrom( + this.eventEmitter.observable(OutOfBandEventTypes.HandshakeReused).pipe( + // Find the first reuse event where the handshake reuse accepted matches the reuse message thread + // TODO: Should we store the reuse state? Maybe we can keep it in memory for now + first( + (event) => + event.payload.reuseThreadId === reuseMessage.threadId && + event.payload.outOfBandRecord.id === outOfBandRecord.id && + event.payload.connectionRecord.id === connectionRecord.id + ), + // If the event is found, we return the value true + map(() => true), + timeout(15000), + // If timeout is reached, we return false + catchError(() => of(false)) + ) + ) + + const outbound = createOutboundMessage(connectionRecord, reuseMessage) + await this.messageSender.sendMessage(outbound) + + return reuseAcceptedEventPromise + } + + private registerHandlers(dispatcher: Dispatcher) { + dispatcher.registerHandler(new HandshakeReuseHandler(this.outOfBandService)) + dispatcher.registerHandler(new HandshakeReuseAcceptedHandler(this.outOfBandService)) + } +} diff --git a/packages/core/src/modules/oob/OutOfBandService.ts b/packages/core/src/modules/oob/OutOfBandService.ts new file mode 100644 index 0000000000..a0c51b61ed --- /dev/null +++ b/packages/core/src/modules/oob/OutOfBandService.ts @@ -0,0 +1,168 @@ +import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' +import type { Key } from '../../crypto' +import type { ConnectionRecord } from '../connections' +import type { HandshakeReusedEvent, OutOfBandStateChangedEvent } from './domain/OutOfBandEvents' +import type { OutOfBandRecord } from './repository' + +import { scoped, Lifecycle } from 'tsyringe' + +import { EventEmitter } from '../../agent/EventEmitter' +import { AriesFrameworkError } from '../../error' + +import { OutOfBandEventTypes } from './domain/OutOfBandEvents' +import { OutOfBandRole } from './domain/OutOfBandRole' +import { OutOfBandState } from './domain/OutOfBandState' +import { HandshakeReuseMessage } from './messages' +import { HandshakeReuseAcceptedMessage } from './messages/HandshakeReuseAcceptedMessage' +import { OutOfBandRepository } from './repository' + +@scoped(Lifecycle.ContainerScoped) +export class OutOfBandService { + private outOfBandRepository: OutOfBandRepository + private eventEmitter: EventEmitter + + public constructor(outOfBandRepository: OutOfBandRepository, eventEmitter: EventEmitter) { + this.outOfBandRepository = outOfBandRepository + this.eventEmitter = eventEmitter + } + + public async processHandshakeReuse(messageContext: InboundMessageContext) { + const reuseMessage = messageContext.message + const parentThreadId = reuseMessage.thread?.parentThreadId + + if (!parentThreadId) { + throw new AriesFrameworkError('handshake-reuse message must have a parent thread id') + } + + const outOfBandRecord = await this.findByInvitationId(parentThreadId) + if (!outOfBandRecord) { + throw new AriesFrameworkError('No out of band record found for handshake-reuse message') + } + + // Assert + outOfBandRecord.assertRole(OutOfBandRole.Sender) + outOfBandRecord.assertState(OutOfBandState.AwaitResponse) + + const requestLength = outOfBandRecord.outOfBandInvitation.getRequests()?.length ?? 0 + if (requestLength > 0) { + throw new AriesFrameworkError('Handshake reuse should only be used when no requests are present') + } + + const reusedConnection = messageContext.assertReadyConnection() + this.eventEmitter.emit({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + reuseThreadId: reuseMessage.threadId, + connectionRecord: reusedConnection, + outOfBandRecord, + }, + }) + + // If the out of band record is not reusable we can set the state to done + if (!outOfBandRecord.reusable) { + await this.updateState(outOfBandRecord, OutOfBandState.Done) + } + + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + threadId: reuseMessage.threadId, + parentThreadId, + }) + + return reuseAcceptedMessage + } + + public async processHandshakeReuseAccepted(messageContext: InboundMessageContext) { + const reuseAcceptedMessage = messageContext.message + const parentThreadId = reuseAcceptedMessage.thread?.parentThreadId + + if (!parentThreadId) { + throw new AriesFrameworkError('handshake-reuse-accepted message must have a parent thread id') + } + + const outOfBandRecord = await this.findByInvitationId(parentThreadId) + if (!outOfBandRecord) { + throw new AriesFrameworkError('No out of band record found for handshake-reuse-accepted message') + } + + // Assert + outOfBandRecord.assertRole(OutOfBandRole.Receiver) + outOfBandRecord.assertState(OutOfBandState.PrepareResponse) + + const reusedConnection = messageContext.assertReadyConnection() + + // Checks whether the connection associated with reuse accepted message matches with the connection + // associated with the reuse message. + // FIXME: not really a fan of the reuseConnectionId, but it's the only way I can think of now to get the connection + // associated with the reuse message. Maybe we can at least move it to the metadata and remove it directly afterwards? + // But this is an issue in general that has also come up for ACA-Py. How do I find the connection associated with an oob record? + // Because it doesn't work really well with connection reuse. + if (outOfBandRecord.reuseConnectionId !== reusedConnection.id) { + throw new AriesFrameworkError('handshake-reuse-accepted is not in response to a handshake-reuse message.') + } + + this.eventEmitter.emit({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + reuseThreadId: reuseAcceptedMessage.threadId, + connectionRecord: reusedConnection, + outOfBandRecord, + }, + }) + + // receiver role is never reusable, so we can set the state to done + await this.updateState(outOfBandRecord, OutOfBandState.Done) + } + + public async createHandShakeReuse(outOfBandRecord: OutOfBandRecord, connectionRecord: ConnectionRecord) { + const reuseMessage = new HandshakeReuseMessage({ parentThreadId: outOfBandRecord.outOfBandInvitation.id }) + + // Store the reuse connection id + outOfBandRecord.reuseConnectionId = connectionRecord.id + await this.outOfBandRepository.update(outOfBandRecord) + + return reuseMessage + } + + public async save(outOfBandRecord: OutOfBandRecord) { + return this.outOfBandRepository.save(outOfBandRecord) + } + + public async updateState(outOfBandRecord: OutOfBandRecord, newState: OutOfBandState) { + const previousState = outOfBandRecord.state + outOfBandRecord.state = newState + await this.outOfBandRepository.update(outOfBandRecord) + + this.eventEmitter.emit({ + type: OutOfBandEventTypes.OutOfBandStateChanged, + payload: { + outOfBandRecord, + previousState, + }, + }) + } + + public async findById(outOfBandRecordId: string) { + return this.outOfBandRepository.findById(outOfBandRecordId) + } + + public async getById(outOfBandRecordId: string) { + return this.outOfBandRepository.getById(outOfBandRecordId) + } + + public async findByInvitationId(invitationId: string) { + return this.outOfBandRepository.findSingleByQuery({ invitationId }) + } + + public async findByRecipientKey(recipientKey: Key) { + return this.outOfBandRepository.findSingleByQuery({ recipientKeyFingerprints: [recipientKey.fingerprint] }) + } + + public async getAll() { + return this.outOfBandRepository.getAll() + } + + public async deleteById(outOfBandId: string) { + const outOfBandRecord = await this.getById(outOfBandId) + return this.outOfBandRepository.delete(outOfBandRecord) + } +} diff --git a/packages/core/src/modules/oob/__tests__/OutOfBandMessage.test.ts b/packages/core/src/modules/oob/__tests__/OutOfBandMessage.test.ts new file mode 100644 index 0000000000..71ae342e84 --- /dev/null +++ b/packages/core/src/modules/oob/__tests__/OutOfBandMessage.test.ts @@ -0,0 +1,150 @@ +import type { ValidationError } from 'class-validator' + +import { JsonEncoder } from '../../../utils/JsonEncoder' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { OutOfBandInvitation } from '../messages/OutOfBandInvitation' + +describe('OutOfBandInvitation', () => { + describe('toUrl', () => { + test('encode the message into the URL containg the base64 encoded invitation as the oob query parameter', async () => { + const domain = 'https://example.com/ssi' + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + } + const invitation = JsonTransformer.fromJSON(json, OutOfBandInvitation) + const invitationUrl = invitation.toUrl({ + domain, + }) + + expect(invitationUrl).toBe(`${domain}?oob=${JsonEncoder.toBase64URL(json)}`) + }) + }) + + describe('fromUrl', () => { + test('decode the URL containing the base64 encoded invitation as the oob parameter into an `OutOfBandInvitation`', async () => { + const invitationUrl = + 'http://example.com/ssi?oob=eyJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvb3V0LW9mLWJhbmQvMS4xL2ludml0YXRpb24iLCJAaWQiOiI2OTIxMmEzYS1kMDY4LTRmOWQtYTJkZC00NzQxYmNhODlhZjMiLCJsYWJlbCI6IkZhYmVyIENvbGxlZ2UiLCJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJUbyBpc3N1ZSBhIEZhYmVyIENvbGxlZ2UgR3JhZHVhdGUgY3JlZGVudGlhbCIsImhhbmRzaGFrZV9wcm90b2NvbHMiOlsiaHR0cHM6Ly9kaWRjb21tLm9yZy9kaWRleGNoYW5nZS8xLjAiLCJodHRwczovL2RpZGNvbW0ub3JnL2Nvbm5lY3Rpb25zLzEuMCJdLCJzZXJ2aWNlcyI6WyJkaWQ6c292OkxqZ3BTVDJyanNveFllZ1FEUm03RUwiXX0K' + + const invitation = await OutOfBandInvitation.fromUrl(invitationUrl) + const json = JsonTransformer.toJSON(invitation) + expect(json).toEqual({ + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + }) + }) + }) + + describe('fromJson', () => { + test('create an instance of `OutOfBandInvitation` from JSON object', async () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + } + + const invitation = await OutOfBandInvitation.fromJson(json) + + expect(invitation).toBeDefined() + expect(invitation).toBeInstanceOf(OutOfBandInvitation) + }) + + test('create an instance of `OutOfBandInvitation` from JSON object with inline service', async () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [ + { + id: '#inline', + recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + routingKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + serviceEndpoint: 'https://example.com/ssi', + }, + ], + } + + const invitation = await OutOfBandInvitation.fromJson(json) + expect(invitation).toBeDefined() + expect(invitation).toBeInstanceOf(OutOfBandInvitation) + }) + + test('throw validation error when services attribute is empty', async () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [], + } + + expect.assertions(1) + try { + await OutOfBandInvitation.fromJson(json) + } catch (error) { + const [firstError] = error as [ValidationError] + expect(firstError.constraints).toEqual({ arrayNotEmpty: 'services should not be empty' }) + } + }) + + test('throw validation error when incorrect service object present in services attribute', async () => { + const json = { + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + '@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3', + label: 'Faber College', + goal_code: 'issue-vc', + goal: 'To issue a Faber College Graduate credential', + handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], + services: [ + { + id: '#inline', + routingKeys: ['did:sov:LjgpST2rjsoxYegQDRm7EL'], + serviceEndpoint: 'https://example.com/ssi', + }, + ], + } + + expect.assertions(1) + try { + await OutOfBandInvitation.fromJson(json) + } catch (error) { + const [firstError] = error as [ValidationError] + + expect(firstError).toMatchObject({ + children: [ + { + children: [ + { + constraints: { + arrayNotEmpty: 'recipientKeys should not be empty', + isDidKeyString: 'each value in recipientKeys must be a did:key string', + }, + }, + { constraints: { isDidKeyString: 'each value in routingKeys must be a did:key string' } }, + ], + }, + ], + }) + } + }) + }) +}) diff --git a/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts b/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts new file mode 100644 index 0000000000..dd1c98098b --- /dev/null +++ b/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts @@ -0,0 +1,498 @@ +import type { Wallet } from '../../../wallet/Wallet' + +import { getAgentConfig, getMockConnection, getMockOutOfBand, mockFunction } from '../../../../tests/helpers' +import { EventEmitter } from '../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { KeyType, Key } from '../../../crypto' +import { AriesFrameworkError } from '../../../error' +import { IndyWallet } from '../../../wallet/IndyWallet' +import { DidExchangeState } from '../../connections/models' +import { OutOfBandService } from '../OutOfBandService' +import { OutOfBandEventTypes } from '../domain/OutOfBandEvents' +import { OutOfBandRole } from '../domain/OutOfBandRole' +import { OutOfBandState } from '../domain/OutOfBandState' +import { HandshakeReuseMessage } from '../messages' +import { HandshakeReuseAcceptedMessage } from '../messages/HandshakeReuseAcceptedMessage' +import { OutOfBandRepository } from '../repository' + +jest.mock('../repository/OutOfBandRepository') +const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock + +const key = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + +describe('OutOfBandService', () => { + const agentConfig = getAgentConfig('OutOfBandServiceTest') + let wallet: Wallet + let outOfBandRepository: OutOfBandRepository + let outOfBandService: OutOfBandService + let eventEmitter: EventEmitter + + beforeAll(async () => { + wallet = new IndyWallet(agentConfig) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(agentConfig.walletConfig!) + }) + + afterAll(async () => { + await wallet.delete() + }) + + beforeEach(async () => { + eventEmitter = new EventEmitter(agentConfig) + outOfBandRepository = new OutOfBandRepositoryMock() + outOfBandService = new OutOfBandService(outOfBandRepository, eventEmitter) + }) + + describe('processHandshakeReuse', () => { + test('throw error when no parentThreadId is present', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + reuseMessage.setThread({ + parentThreadId: undefined, + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + senderKey: key, + recipientKey: key, + }) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new AriesFrameworkError('handshake-reuse message must have a parent thread id') + ) + }) + + test('throw error when no out of band record is found for parentThreadId', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + senderKey: key, + recipientKey: key, + }) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new AriesFrameworkError('No out of band record found for handshake-reuse message') + ) + }) + + test('throw error when role or state is incorrect ', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + senderKey: key, + recipientKey: key, + }) + + // Correct state, incorrect role + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Receiver, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new AriesFrameworkError('Invalid out-of-band record role receiver, expected is sender.') + ) + + mockOob.state = OutOfBandState.PrepareResponse + mockOob.role = OutOfBandRole.Sender + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new AriesFrameworkError('Invalid out-of-band record state prepare-response, valid states are: await-response.') + ) + }) + + test('throw error when the out of band record has request messages ', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + senderKey: key, + recipientKey: key, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + }) + mockOob.outOfBandInvitation.addRequest(reuseMessage) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new AriesFrameworkError('Handshake reuse should only be used when no requests are present') + ) + }) + + test("throw error when the message context doesn't have a ready connection", async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + senderKey: key, + recipientKey: key, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuse(messageContext)).rejects.toThrowError( + new AriesFrameworkError(`No connection associated with incoming message ${reuseMessage.type}`) + ) + }) + + test('emits handshake reused event ', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const reuseListener = jest.fn() + + const connection = getMockConnection({ state: DidExchangeState.Completed }) + const messageContext = new InboundMessageContext(reuseMessage, { + senderKey: key, + recipientKey: key, + connection, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + eventEmitter.on(OutOfBandEventTypes.HandshakeReused, reuseListener) + await outOfBandService.processHandshakeReuse(messageContext) + eventEmitter.off(OutOfBandEventTypes.HandshakeReused, reuseListener) + + expect(reuseListener).toHaveBeenCalledTimes(1) + const [[reuseEvent]] = reuseListener.mock.calls + + expect(reuseEvent).toMatchObject({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + connectionRecord: connection, + outOfBandRecord: mockOob, + reuseThreadId: reuseMessage.threadId, + }, + }) + }) + + it('updates state to done if out of band record is not reusable', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + senderKey: key, + recipientKey: key, + connection: getMockConnection({ state: DidExchangeState.Completed }), + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + reusable: true, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + const updateStateSpy = jest.spyOn(outOfBandService, 'updateState') + + // Reusable shouldn't update state + await outOfBandService.processHandshakeReuse(messageContext) + expect(updateStateSpy).not.toHaveBeenCalled() + + // Non-reusable should update state + mockOob.reusable = false + await outOfBandService.processHandshakeReuse(messageContext) + expect(updateStateSpy).toHaveBeenCalledWith(mockOob, OutOfBandState.Done) + }) + + it('returns a handshake-reuse-accepted message', async () => { + const reuseMessage = new HandshakeReuseMessage({ + parentThreadId: 'parentThreadId', + }) + + const messageContext = new InboundMessageContext(reuseMessage, { + senderKey: key, + recipientKey: key, + connection: getMockConnection({ state: DidExchangeState.Completed }), + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.AwaitResponse, + role: OutOfBandRole.Sender, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + const reuseAcceptedMessage = await outOfBandService.processHandshakeReuse(messageContext) + + expect(reuseAcceptedMessage).toBeInstanceOf(HandshakeReuseAcceptedMessage) + expect(reuseAcceptedMessage.thread).toMatchObject({ + threadId: reuseMessage.id, + parentThreadId: reuseMessage.thread?.parentThreadId, + }) + }) + }) + + describe('processHandshakeReuseAccepted', () => { + test('throw error when no parentThreadId is present', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + threadId: 'threadId', + parentThreadId: 'parentThreadId', + }) + + reuseAcceptedMessage.setThread({ + parentThreadId: undefined, + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + senderKey: key, + recipientKey: key, + }) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new AriesFrameworkError('handshake-reuse-accepted message must have a parent thread id') + ) + }) + + test('throw error when no out of band record is found for parentThreadId', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + senderKey: key, + recipientKey: key, + }) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new AriesFrameworkError('No out of band record found for handshake-reuse-accepted message') + ) + }) + + test('throw error when role or state is incorrect ', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + senderKey: key, + recipientKey: key, + }) + + // Correct state, incorrect role + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Sender, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new AriesFrameworkError('Invalid out-of-band record role sender, expected is receiver.') + ) + + mockOob.state = OutOfBandState.AwaitResponse + mockOob.role = OutOfBandRole.Receiver + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new AriesFrameworkError('Invalid out-of-band record state await-response, valid states are: prepare-response.') + ) + }) + + test("throw error when the message context doesn't have a ready connection", async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + senderKey: key, + recipientKey: key, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Receiver, + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new AriesFrameworkError(`No connection associated with incoming message ${reuseAcceptedMessage.type}`) + ) + }) + + test("throw error when the reuseConnectionId on the oob record doesn't match with the inbound message connection id", async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + senderKey: key, + recipientKey: key, + connection: getMockConnection({ state: DidExchangeState.Completed, id: 'connectionId' }), + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Receiver, + reuseConnectionId: 'anotherConnectionId', + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + await expect(outOfBandService.processHandshakeReuseAccepted(messageContext)).rejects.toThrowError( + new AriesFrameworkError(`handshake-reuse-accepted is not in response to a handshake-reuse message.`) + ) + }) + + test('emits handshake reused event ', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const reuseListener = jest.fn() + + const connection = getMockConnection({ state: DidExchangeState.Completed, id: 'connectionId' }) + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + senderKey: key, + recipientKey: key, + connection, + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Receiver, + reuseConnectionId: 'connectionId', + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + eventEmitter.on(OutOfBandEventTypes.HandshakeReused, reuseListener) + await outOfBandService.processHandshakeReuseAccepted(messageContext) + eventEmitter.off(OutOfBandEventTypes.HandshakeReused, reuseListener) + + expect(reuseListener).toHaveBeenCalledTimes(1) + const [[reuseEvent]] = reuseListener.mock.calls + + expect(reuseEvent).toMatchObject({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + connectionRecord: connection, + outOfBandRecord: mockOob, + reuseThreadId: reuseAcceptedMessage.threadId, + }, + }) + }) + + it('updates state to done', async () => { + const reuseAcceptedMessage = new HandshakeReuseAcceptedMessage({ + parentThreadId: 'parentThreadId', + threadId: 'threadId', + }) + + const messageContext = new InboundMessageContext(reuseAcceptedMessage, { + senderKey: key, + recipientKey: key, + connection: getMockConnection({ state: DidExchangeState.Completed, id: 'connectionId' }), + }) + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.PrepareResponse, + role: OutOfBandRole.Receiver, + reusable: true, + reuseConnectionId: 'connectionId', + }) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(mockOob) + + const updateStateSpy = jest.spyOn(outOfBandService, 'updateState') + + await outOfBandService.processHandshakeReuseAccepted(messageContext) + expect(updateStateSpy).toHaveBeenCalledWith(mockOob, OutOfBandState.Done) + }) + }) + + describe('updateState', () => { + test('updates the state on the out of band record', async () => { + const mockOob = getMockOutOfBand({ + state: OutOfBandState.Initial, + }) + + await outOfBandService.updateState(mockOob, OutOfBandState.Done) + + expect(mockOob.state).toEqual(OutOfBandState.Done) + }) + + test('updates the record in the out of band repository', async () => { + const mockOob = getMockOutOfBand({ + state: OutOfBandState.Initial, + }) + + await outOfBandService.updateState(mockOob, OutOfBandState.Done) + + expect(outOfBandRepository.update).toHaveBeenCalledWith(mockOob) + }) + + test('emits an OutOfBandStateChangedEvent', async () => { + const stateChangedListener = jest.fn() + + const mockOob = getMockOutOfBand({ + state: OutOfBandState.Initial, + }) + + eventEmitter.on(OutOfBandEventTypes.OutOfBandStateChanged, stateChangedListener) + await outOfBandService.updateState(mockOob, OutOfBandState.Done) + eventEmitter.off(OutOfBandEventTypes.OutOfBandStateChanged, stateChangedListener) + + expect(stateChangedListener).toHaveBeenCalledTimes(1) + const [[stateChangedEvent]] = stateChangedListener.mock.calls + + expect(stateChangedEvent).toMatchObject({ + type: OutOfBandEventTypes.OutOfBandStateChanged, + payload: { + outOfBandRecord: mockOob, + previousState: OutOfBandState.Initial, + }, + }) + }) + }) + + describe('repository methods', () => { + it('getById should return value from outOfBandRepository.getById', async () => { + const expected = getMockOutOfBand() + mockFunction(outOfBandRepository.getById).mockReturnValue(Promise.resolve(expected)) + const result = await outOfBandService.getById(expected.id) + expect(outOfBandRepository.getById).toBeCalledWith(expected.id) + + expect(result).toBe(expected) + }) + + it('findById should return value from outOfBandRepository.findById', async () => { + const expected = getMockOutOfBand() + mockFunction(outOfBandRepository.findById).mockReturnValue(Promise.resolve(expected)) + const result = await outOfBandService.findById(expected.id) + expect(outOfBandRepository.findById).toBeCalledWith(expected.id) + + expect(result).toBe(expected) + }) + + it('getAll should return value from outOfBandRepository.getAll', async () => { + const expected = [getMockOutOfBand(), getMockOutOfBand()] + + mockFunction(outOfBandRepository.getAll).mockReturnValue(Promise.resolve(expected)) + const result = await outOfBandService.getAll() + expect(outOfBandRepository.getAll).toBeCalledWith() + + expect(result).toEqual(expect.arrayContaining(expected)) + }) + }) +}) diff --git a/packages/core/src/modules/oob/__tests__/helpers.test.ts b/packages/core/src/modules/oob/__tests__/helpers.test.ts new file mode 100644 index 0000000000..e81093276a --- /dev/null +++ b/packages/core/src/modules/oob/__tests__/helpers.test.ts @@ -0,0 +1,136 @@ +import { JsonTransformer } from '../../../utils' +import { ConnectionInvitationMessage } from '../../connections' +import { DidCommV1Service } from '../../dids' +import { convertToNewInvitation, convertToOldInvitation } from '../helpers' +import { OutOfBandInvitation } from '../messages' + +describe('convertToNewInvitation', () => { + it('should convert a connection invitation with service to an out of band invitation', () => { + const connectionInvitation = new ConnectionInvitationMessage({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + recipientKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + serviceEndpoint: 'https://my-agent.com', + routingKeys: ['6fioC1zcDPyPEL19pXRS2E4iJ46zH7xP6uSgAaPdwDrx'], + }) + + const oobInvitation = convertToNewInvitation(connectionInvitation) + + expect(oobInvitation).toMatchObject({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + services: [ + { + id: '#inline', + recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + routingKeys: ['did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL'], + serviceEndpoint: 'https://my-agent.com', + }, + ], + }) + }) + + it('should convert a connection invitation with public did to an out of band invitation', () => { + const connectionInvitation = new ConnectionInvitationMessage({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + did: 'did:sov:a-did', + }) + + const oobInvitation = convertToNewInvitation(connectionInvitation) + + expect(oobInvitation).toMatchObject({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + services: ['did:sov:a-did'], + }) + }) + + it('throws an error when no did and serviceEndpoint/routingKeys are present in the connection invitation', () => { + const connectionInvitation = JsonTransformer.fromJSON( + { + '@id': 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + '@type': 'https://didcomm.org/connections/1.0/invitation', + label: 'a-label', + imageUrl: 'https://my-image.com', + }, + ConnectionInvitationMessage + ) + + expect(() => convertToNewInvitation(connectionInvitation)).toThrowError( + 'Missing required serviceEndpoint, routingKeys and/or did fields in connection invitation' + ) + }) +}) + +describe('convertToOldInvitation', () => { + it('should convert an out of band invitation with inline service to a connection invitation', () => { + const oobInvitation = new OutOfBandInvitation({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + services: [ + new DidCommV1Service({ + id: '#inline', + recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + routingKeys: ['did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL'], + serviceEndpoint: 'https://my-agent.com', + }), + ], + }) + + const connectionInvitation = convertToOldInvitation(oobInvitation) + + expect(connectionInvitation).toMatchObject({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + recipientKeys: ['8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K'], + routingKeys: ['6fioC1zcDPyPEL19pXRS2E4iJ46zH7xP6uSgAaPdwDrx'], + serviceEndpoint: 'https://my-agent.com', + }) + }) + + it('should convert an out of band invitation with did service to a connection invitation', () => { + const oobInvitation = new OutOfBandInvitation({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + services: ['did:sov:a-did'], + }) + + const connectionInvitation = convertToOldInvitation(oobInvitation) + + expect(connectionInvitation).toMatchObject({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + did: 'did:sov:a-did', + }) + }) + + it('throws an error when more than service is present in the out of band invitation', () => { + const oobInvitation = new OutOfBandInvitation({ + id: 'd88ff8fd-6c43-4683-969e-11a87a572cf2', + imageUrl: 'https://my-image.com', + label: 'a-label', + services: [ + new DidCommV1Service({ + id: '#inline', + recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], + routingKeys: ['did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL'], + serviceEndpoint: 'https://my-agent.com', + }), + 'did:sov:a-did', + ], + }) + + expect(() => convertToOldInvitation(oobInvitation)).toThrowError( + `Attribute 'services' MUST have exactly one entry. It contains 2 entries.` + ) + }) +}) diff --git a/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts b/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts new file mode 100644 index 0000000000..f351520fba --- /dev/null +++ b/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts @@ -0,0 +1,56 @@ +import type { ValidationOptions } from 'class-validator' + +import { ArrayNotEmpty, buildMessage, IsOptional, isString, IsString, ValidateBy } from 'class-validator' + +import { DidDocumentService } from '../../dids' + +export class OutOfBandDidCommService extends DidDocumentService { + public constructor(options: { + id: string + serviceEndpoint: string + recipientKeys: string[] + routingKeys?: string[] + accept?: string[] + }) { + super({ ...options, type: OutOfBandDidCommService.type }) + + if (options) { + this.recipientKeys = options.recipientKeys + this.routingKeys = options.routingKeys + this.accept = options.accept + } + } + + public static type = 'did-communication' + + @ArrayNotEmpty() + @IsDidKeyString({ each: true }) + public recipientKeys!: string[] + + @IsDidKeyString({ each: true }) + @IsOptional() + public routingKeys?: string[] + + @IsString({ each: true }) + @IsOptional() + public accept?: string[] +} + +/** + * Checks if a given value is a did:key did string + */ +function IsDidKeyString(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isDidKeyString', + validator: { + validate: (value): boolean => isString(value) && value.startsWith('did:key:'), + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be a did:key string', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/oob/domain/OutOfBandEvents.ts b/packages/core/src/modules/oob/domain/OutOfBandEvents.ts new file mode 100644 index 0000000000..a3936cc784 --- /dev/null +++ b/packages/core/src/modules/oob/domain/OutOfBandEvents.ts @@ -0,0 +1,27 @@ +import type { BaseEvent } from '../../../agent/Events' +import type { ConnectionRecord } from '../../connections' +import type { OutOfBandRecord } from '../repository' +import type { OutOfBandState } from './OutOfBandState' + +export enum OutOfBandEventTypes { + OutOfBandStateChanged = 'OutOfBandStateChanged', + HandshakeReused = 'HandshakeReused', +} + +export interface OutOfBandStateChangedEvent extends BaseEvent { + type: typeof OutOfBandEventTypes.OutOfBandStateChanged + payload: { + outOfBandRecord: OutOfBandRecord + previousState: OutOfBandState | null + } +} + +export interface HandshakeReusedEvent extends BaseEvent { + type: typeof OutOfBandEventTypes.HandshakeReused + payload: { + // We need the thread id (can be multiple reuse happening at the same time) + reuseThreadId: string + outOfBandRecord: OutOfBandRecord + connectionRecord: ConnectionRecord + } +} diff --git a/packages/core/src/modules/oob/domain/OutOfBandRole.ts b/packages/core/src/modules/oob/domain/OutOfBandRole.ts new file mode 100644 index 0000000000..fb047d46ba --- /dev/null +++ b/packages/core/src/modules/oob/domain/OutOfBandRole.ts @@ -0,0 +1,4 @@ +export const enum OutOfBandRole { + Sender = 'sender', + Receiver = 'receiver', +} diff --git a/packages/core/src/modules/oob/domain/OutOfBandState.ts b/packages/core/src/modules/oob/domain/OutOfBandState.ts new file mode 100644 index 0000000000..b127a1db24 --- /dev/null +++ b/packages/core/src/modules/oob/domain/OutOfBandState.ts @@ -0,0 +1,6 @@ +export const enum OutOfBandState { + Initial = 'initial', + AwaitResponse = 'await-response', + PrepareResponse = 'prepare-response', + Done = 'done', +} diff --git a/packages/core/src/modules/oob/handlers/HandshakeReuseAcceptedHandler.ts b/packages/core/src/modules/oob/handlers/HandshakeReuseAcceptedHandler.ts new file mode 100644 index 0000000000..41b616b443 --- /dev/null +++ b/packages/core/src/modules/oob/handlers/HandshakeReuseAcceptedHandler.ts @@ -0,0 +1,20 @@ +import type { Handler } from '../../../agent/Handler' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { OutOfBandService } from '../OutOfBandService' + +import { HandshakeReuseAcceptedMessage } from '../messages/HandshakeReuseAcceptedMessage' + +export class HandshakeReuseAcceptedHandler implements Handler { + public supportedMessages = [HandshakeReuseAcceptedMessage] + private outOfBandService: OutOfBandService + + public constructor(outOfBandService: OutOfBandService) { + this.outOfBandService = outOfBandService + } + + public async handle(messageContext: InboundMessageContext) { + messageContext.assertReadyConnection() + + await this.outOfBandService.processHandshakeReuseAccepted(messageContext) + } +} diff --git a/packages/core/src/modules/oob/handlers/HandshakeReuseHandler.ts b/packages/core/src/modules/oob/handlers/HandshakeReuseHandler.ts new file mode 100644 index 0000000000..632eddd96a --- /dev/null +++ b/packages/core/src/modules/oob/handlers/HandshakeReuseHandler.ts @@ -0,0 +1,22 @@ +import type { Handler } from '../../../agent/Handler' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { OutOfBandService } from '../OutOfBandService' + +import { createOutboundMessage } from '../../../agent/helpers' +import { HandshakeReuseMessage } from '../messages/HandshakeReuseMessage' + +export class HandshakeReuseHandler implements Handler { + public supportedMessages = [HandshakeReuseMessage] + private outOfBandService: OutOfBandService + + public constructor(outOfBandService: OutOfBandService) { + this.outOfBandService = outOfBandService + } + + public async handle(messageContext: InboundMessageContext) { + const connectionRecord = messageContext.assertReadyConnection() + const handshakeReuseAcceptedMessage = await this.outOfBandService.processHandshakeReuse(messageContext) + + return createOutboundMessage(connectionRecord, handshakeReuseAcceptedMessage) + } +} diff --git a/packages/core/src/modules/oob/handlers/index.ts b/packages/core/src/modules/oob/handlers/index.ts new file mode 100644 index 0000000000..c9edcca3d6 --- /dev/null +++ b/packages/core/src/modules/oob/handlers/index.ts @@ -0,0 +1 @@ +export * from './HandshakeReuseHandler' diff --git a/packages/core/src/modules/oob/helpers.ts b/packages/core/src/modules/oob/helpers.ts new file mode 100644 index 0000000000..b0c1a913a7 --- /dev/null +++ b/packages/core/src/modules/oob/helpers.ts @@ -0,0 +1,68 @@ +import type { OutOfBandInvitationOptions } from './messages' + +import { AriesFrameworkError } from '../../error' +import { ConnectionInvitationMessage, HandshakeProtocol } from '../connections' +import { didKeyToVerkey, verkeyToDidKey } from '../dids/helpers' + +import { OutOfBandDidCommService } from './domain/OutOfBandDidCommService' +import { OutOfBandInvitation } from './messages' + +export function convertToNewInvitation(oldInvitation: ConnectionInvitationMessage) { + let service + + if (oldInvitation.did) { + service = oldInvitation.did + } else if (oldInvitation.serviceEndpoint && oldInvitation.recipientKeys && oldInvitation.recipientKeys.length > 0) { + service = new OutOfBandDidCommService({ + id: '#inline', + recipientKeys: oldInvitation.recipientKeys?.map(verkeyToDidKey), + routingKeys: oldInvitation.routingKeys?.map(verkeyToDidKey), + serviceEndpoint: oldInvitation.serviceEndpoint, + }) + } else { + throw new Error('Missing required serviceEndpoint, routingKeys and/or did fields in connection invitation') + } + + const options: OutOfBandInvitationOptions = { + id: oldInvitation.id, + label: oldInvitation.label, + imageUrl: oldInvitation.imageUrl, + accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], + services: [service], + handshakeProtocols: [HandshakeProtocol.Connections], + } + + return new OutOfBandInvitation(options) +} + +export function convertToOldInvitation(newInvitation: OutOfBandInvitation) { + if (newInvitation.services.length > 1) { + throw new AriesFrameworkError( + `Attribute 'services' MUST have exactly one entry. It contains ${newInvitation.services.length} entries.` + ) + } + + const [service] = newInvitation.services + + let options + if (typeof service === 'string') { + options = { + id: newInvitation.id, + label: newInvitation.label, + did: service, + imageUrl: newInvitation.imageUrl, + } + } else { + options = { + id: newInvitation.id, + label: newInvitation.label, + recipientKeys: service.recipientKeys.map(didKeyToVerkey), + routingKeys: service.routingKeys?.map(didKeyToVerkey), + serviceEndpoint: service.serviceEndpoint, + imageUrl: newInvitation.imageUrl, + } + } + + const connectionInvitationMessage = new ConnectionInvitationMessage(options) + return connectionInvitationMessage +} diff --git a/packages/core/src/modules/oob/messages/HandshakeReuseAcceptedMessage.ts b/packages/core/src/modules/oob/messages/HandshakeReuseAcceptedMessage.ts new file mode 100644 index 0000000000..bfffcdab5b --- /dev/null +++ b/packages/core/src/modules/oob/messages/HandshakeReuseAcceptedMessage.ts @@ -0,0 +1,26 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface HandshakeReuseAcceptedMessageOptions { + id?: string + threadId: string + parentThreadId: string +} + +export class HandshakeReuseAcceptedMessage extends AgentMessage { + public constructor(options: HandshakeReuseAcceptedMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.setThread({ + threadId: options.threadId, + parentThreadId: options.parentThreadId, + }) + } + } + + @IsValidMessageType(HandshakeReuseAcceptedMessage.type) + public readonly type = HandshakeReuseAcceptedMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/out-of-band/1.1/handshake-reuse-accepted') +} diff --git a/packages/core/src/modules/oob/messages/HandshakeReuseMessage.ts b/packages/core/src/modules/oob/messages/HandshakeReuseMessage.ts new file mode 100644 index 0000000000..363e4bf0fe --- /dev/null +++ b/packages/core/src/modules/oob/messages/HandshakeReuseMessage.ts @@ -0,0 +1,25 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface HandshakeReuseMessageOptions { + id?: string + parentThreadId: string +} + +export class HandshakeReuseMessage extends AgentMessage { + public constructor(options: HandshakeReuseMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.setThread({ + threadId: this.id, + parentThreadId: options.parentThreadId, + }) + } + } + + @IsValidMessageType(HandshakeReuseMessage.type) + public readonly type = HandshakeReuseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/out-of-band/1.1/handshake-reuse') +} diff --git a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts new file mode 100644 index 0000000000..95849a08d5 --- /dev/null +++ b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts @@ -0,0 +1,170 @@ +import type { PlaintextMessage } from '../../../types' +import type { HandshakeProtocol } from '../../connections' + +import { Expose, Transform, TransformationType, Type } from 'class-transformer' +import { ArrayNotEmpty, IsArray, IsInstance, IsOptional, IsUrl, ValidateNested } from 'class-validator' +import { parseUrl } from 'query-string' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../error' +import { JsonEncoder } from '../../../utils/JsonEncoder' +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { MessageValidator } from '../../../utils/MessageValidator' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { IsStringOrInstance } from '../../../utils/validators' +import { outOfBandServiceToNumAlgo2Did } from '../../dids/methods/peer/peerDidNumAlgo2' +import { OutOfBandDidCommService } from '../domain/OutOfBandDidCommService' + +export interface OutOfBandInvitationOptions { + id?: string + label: string + goalCode?: string + goal?: string + accept?: string[] + handshakeProtocols?: HandshakeProtocol[] + services: Array + imageUrl?: string +} + +export class OutOfBandInvitation extends AgentMessage { + public constructor(options: OutOfBandInvitationOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.label = options.label + this.goalCode = options.goalCode + this.goal = options.goal + this.accept = options.accept + this.handshakeProtocols = options.handshakeProtocols + this.services = options.services + this.imageUrl = options.imageUrl + } + } + + public addRequest(message: AgentMessage) { + if (!this.requests) this.requests = [] + const requestAttachment = new Attachment({ + id: this.generateId(), + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(message.toJSON()), + }), + }) + this.requests.push(requestAttachment) + } + + public getRequests(): PlaintextMessage[] | undefined { + return this.requests?.map((request) => request.getDataAsJson()) + } + + public toUrl({ domain }: { domain: string }) { + const invitationJson = this.toJSON() + const encodedInvitation = JsonEncoder.toBase64URL(invitationJson) + const invitationUrl = `${domain}?oob=${encodedInvitation}` + return invitationUrl + } + + public static async fromUrl(invitationUrl: string) { + const parsedUrl = parseUrl(invitationUrl).query + const encodedInvitation = parsedUrl['oob'] + + if (typeof encodedInvitation === 'string') { + const invitationJson = JsonEncoder.fromBase64(encodedInvitation) + const invitation = this.fromJson(invitationJson) + + return invitation + } else { + throw new AriesFrameworkError( + 'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters; `oob`' + ) + } + } + + public static async fromJson(json: Record) { + const invitation = JsonTransformer.fromJSON(json, OutOfBandInvitation) + await MessageValidator.validate(invitation) + return invitation + } + + public get invitationDids() { + const dids = this.services.map((didOrService) => { + if (typeof didOrService === 'string') { + return didOrService + } + return outOfBandServiceToNumAlgo2Did(didOrService) + }) + return dids + } + + @IsValidMessageType(OutOfBandInvitation.type) + public readonly type = OutOfBandInvitation.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/out-of-band/1.1/invitation') + + public readonly label!: string + + @Expose({ name: 'goal_code' }) + public readonly goalCode?: string + + public readonly goal?: string + + public readonly accept?: string[] + + @Expose({ name: 'handshake_protocols' }) + public handshakeProtocols?: HandshakeProtocol[] + + @Expose({ name: 'requests~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ + each: true, + }) + @IsInstance(Attachment, { each: true }) + @IsOptional() + private requests?: Attachment[] + + @IsArray() + @ArrayNotEmpty() + @OutOfBandServiceTransformer() + @IsStringOrInstance(OutOfBandDidCommService, { each: true }) + @ValidateNested({ each: true }) + public services!: Array + + /** + * Custom property. It is not part of the RFC. + */ + @IsOptional() + @IsUrl() + public readonly imageUrl?: string +} + +/** + * Decorator that transforms authentication json to corresponding class instances + * + * @example + * class Example { + * VerificationMethodTransformer() + * private authentication: VerificationMethod + * } + */ +function OutOfBandServiceTransformer() { + return Transform(({ value, type }: { value: Array; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + return value.map((service) => { + // did + if (typeof service === 'string') return new String(service) + + // inline didcomm service + return JsonTransformer.fromJSON(service, OutOfBandDidCommService) + }) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + return value.map((service) => + typeof service === 'string' || service instanceof String ? service.toString() : JsonTransformer.toJSON(service) + ) + } + + // PLAIN_TO_PLAIN + return value + }) +} diff --git a/packages/core/src/modules/oob/messages/index.ts b/packages/core/src/modules/oob/messages/index.ts new file mode 100644 index 0000000000..1849ee4f54 --- /dev/null +++ b/packages/core/src/modules/oob/messages/index.ts @@ -0,0 +1,2 @@ +export * from './OutOfBandInvitation' +export * from './HandshakeReuseMessage' diff --git a/packages/core/src/modules/oob/repository/OutOfBandRecord.ts b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts new file mode 100644 index 0000000000..e48fde05b7 --- /dev/null +++ b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts @@ -0,0 +1,105 @@ +import type { Key } from '../../../crypto' +import type { TagsBase } from '../../../storage/BaseRecord' +import type { OutOfBandDidCommService } from '../domain/OutOfBandDidCommService' +import type { OutOfBandRole } from '../domain/OutOfBandRole' +import type { OutOfBandState } from '../domain/OutOfBandState' + +import { Type } from 'class-transformer' + +import { AriesFrameworkError } from '../../../error' +import { BaseRecord } from '../../../storage/BaseRecord' +import { uuid } from '../../../utils/uuid' +import { DidKey } from '../../dids' +import { OutOfBandInvitation } from '../messages' + +export interface OutOfBandRecordProps { + id?: string + createdAt?: Date + updatedAt?: Date + tags?: TagsBase + outOfBandInvitation: OutOfBandInvitation + role: OutOfBandRole + state: OutOfBandState + autoAcceptConnection?: boolean + reusable?: boolean + did?: string + mediatorId?: string + reuseConnectionId?: string +} + +type DefaultOutOfBandRecordTags = { + role: OutOfBandRole + state: OutOfBandState + invitationId: string + recipientKeyFingerprints: string[] +} + +export class OutOfBandRecord extends BaseRecord { + @Type(() => OutOfBandInvitation) + public outOfBandInvitation!: OutOfBandInvitation + public role!: OutOfBandRole + public state!: OutOfBandState + public reusable!: boolean + public autoAcceptConnection?: boolean + public did?: string + public mediatorId?: string + public reuseConnectionId?: string + + public static readonly type = 'OutOfBandRecord' + public readonly type = OutOfBandRecord.type + + public constructor(props: OutOfBandRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.outOfBandInvitation = props.outOfBandInvitation + this.role = props.role + this.state = props.state + this.autoAcceptConnection = props.autoAcceptConnection + this.reusable = props.reusable ?? false + this.did = props.did + this.mediatorId = props.mediatorId + this.reuseConnectionId = props.reuseConnectionId + this._tags = props.tags ?? {} + } + } + + public getTags() { + return { + ...this._tags, + role: this.role, + state: this.state, + invitationId: this.outOfBandInvitation.id, + recipientKeyFingerprints: this.getRecipientKeys().map((key) => key.fingerprint), + } + } + + // TODO: this only takes into account inline didcomm services, won't work for public dids + public getRecipientKeys(): Key[] { + return this.outOfBandInvitation.services + .filter((s): s is OutOfBandDidCommService => typeof s !== 'string') + .map((s) => s.recipientKeys) + .reduce((acc, curr) => [...acc, ...curr], []) + .map((didKey) => DidKey.fromDid(didKey).key) + } + + public assertRole(expectedRole: OutOfBandRole) { + if (this.role !== expectedRole) { + throw new AriesFrameworkError(`Invalid out-of-band record role ${this.role}, expected is ${expectedRole}.`) + } + } + + public assertState(expectedStates: OutOfBandState | OutOfBandState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new AriesFrameworkError( + `Invalid out-of-band record state ${this.state}, valid states are: ${expectedStates.join(', ')}.` + ) + } + } +} diff --git a/packages/core/src/modules/oob/repository/OutOfBandRepository.ts b/packages/core/src/modules/oob/repository/OutOfBandRepository.ts new file mode 100644 index 0000000000..2d26da222d --- /dev/null +++ b/packages/core/src/modules/oob/repository/OutOfBandRepository.ts @@ -0,0 +1,14 @@ +import { inject, scoped, Lifecycle } from 'tsyringe' + +import { InjectionSymbols } from '../../../constants' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { OutOfBandRecord } from './OutOfBandRecord' + +@scoped(Lifecycle.ContainerScoped) +export class OutOfBandRepository extends Repository { + public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { + super(OutOfBandRecord, storageService) + } +} diff --git a/packages/core/src/modules/oob/repository/index.ts b/packages/core/src/modules/oob/repository/index.ts new file mode 100644 index 0000000000..8bfa55b8dd --- /dev/null +++ b/packages/core/src/modules/oob/repository/index.ts @@ -0,0 +1,2 @@ +export * from './OutOfBandRecord' +export * from './OutOfBandRepository' diff --git a/packages/core/src/modules/problem-reports/errors/ProblemReportError.ts b/packages/core/src/modules/problem-reports/errors/ProblemReportError.ts new file mode 100644 index 0000000000..708e694d59 --- /dev/null +++ b/packages/core/src/modules/problem-reports/errors/ProblemReportError.ts @@ -0,0 +1,20 @@ +import { AriesFrameworkError } from '../../../error/AriesFrameworkError' +import { ProblemReportMessage } from '../messages/ProblemReportMessage' + +export interface ProblemReportErrorOptions { + problemCode: string +} + +export class ProblemReportError extends AriesFrameworkError { + public problemReport: ProblemReportMessage + + public constructor(message: string, { problemCode }: ProblemReportErrorOptions) { + super(message) + this.problemReport = new ProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/problem-reports/errors/index.ts b/packages/core/src/modules/problem-reports/errors/index.ts new file mode 100644 index 0000000000..1eb23b7c6b --- /dev/null +++ b/packages/core/src/modules/problem-reports/errors/index.ts @@ -0,0 +1 @@ +export * from './ProblemReportError' diff --git a/packages/core/src/modules/problem-reports/index.ts b/packages/core/src/modules/problem-reports/index.ts new file mode 100644 index 0000000000..479c831166 --- /dev/null +++ b/packages/core/src/modules/problem-reports/index.ts @@ -0,0 +1,3 @@ +export * from './errors' +export * from './messages' +export * from './models' diff --git a/packages/core/src/modules/problem-reports/messages/ProblemReportMessage.ts b/packages/core/src/modules/problem-reports/messages/ProblemReportMessage.ts new file mode 100644 index 0000000000..2c4a8d16fc --- /dev/null +++ b/packages/core/src/modules/problem-reports/messages/ProblemReportMessage.ts @@ -0,0 +1,122 @@ +// Create a base ProblemReportMessage message class and add it to the messages directory +import { Expose } from 'class-transformer' +import { IsEnum, IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export enum WhoRetriesStatus { + You = 'YOU', + Me = 'ME', + Both = 'BOTH', + None = 'NONE', +} + +export enum ImpactStatus { + Message = 'MESSAGE', + Thread = 'THREAD', + Connection = 'CONNECTION', +} + +export enum WhereStatus { + Cloud = 'CLOUD', + Edge = 'EDGE', + Wire = 'WIRE', + Agency = 'AGENCY', +} + +export enum OtherStatus { + You = 'YOU', + Me = 'ME', + Other = 'OTHER', +} + +export interface DescriptionOptions { + en: string + code: string +} + +export interface FixHintOptions { + en: string +} + +export interface ProblemReportMessageOptions { + id?: string + description: DescriptionOptions + problemItems?: string[] + whoRetries?: WhoRetriesStatus + fixHint?: FixHintOptions + impact?: ImpactStatus + where?: WhereStatus + noticedTime?: string + trackingUri?: string + escalationUri?: string +} + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class ProblemReportMessage extends AgentMessage { + /** + * Create new ReportProblem instance. + * @param options + */ + public constructor(options: ProblemReportMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.description = options.description + this.problemItems = options.problemItems + this.whoRetries = options.whoRetries + this.fixHint = options.fixHint + this.impact = options.impact + this.where = options.where + this.noticedTime = options.noticedTime + this.trackingUri = options.trackingUri + this.escalationUri = options.escalationUri + } + } + + @IsValidMessageType(ProblemReportMessage.type) + public readonly type: string = ProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/notification/1.0/problem-report') + + public description!: DescriptionOptions + + @IsOptional() + @Expose({ name: 'problem_items' }) + public problemItems?: string[] + + @IsOptional() + @IsEnum(WhoRetriesStatus) + @Expose({ name: 'who_retries' }) + public whoRetries?: WhoRetriesStatus + + @IsOptional() + @Expose({ name: 'fix_hint' }) + public fixHint?: FixHintOptions + + @IsOptional() + @IsEnum(WhereStatus) + public where?: WhereStatus + + @IsOptional() + @IsEnum(ImpactStatus) + public impact?: ImpactStatus + + @IsOptional() + @IsString() + @Expose({ name: 'noticed_time' }) + public noticedTime?: string + + @IsOptional() + @IsString() + @Expose({ name: 'tracking_uri' }) + public trackingUri?: string + + @IsOptional() + @IsString() + @Expose({ name: 'escalation_uri' }) + public escalationUri?: string +} diff --git a/packages/core/src/modules/problem-reports/messages/index.ts b/packages/core/src/modules/problem-reports/messages/index.ts new file mode 100644 index 0000000000..57670e5421 --- /dev/null +++ b/packages/core/src/modules/problem-reports/messages/index.ts @@ -0,0 +1 @@ +export * from './ProblemReportMessage' diff --git a/packages/core/src/modules/problem-reports/models/ProblemReportReason.ts b/packages/core/src/modules/problem-reports/models/ProblemReportReason.ts new file mode 100644 index 0000000000..6f85917c1a --- /dev/null +++ b/packages/core/src/modules/problem-reports/models/ProblemReportReason.ts @@ -0,0 +1,3 @@ +export enum ProblemReportReason { + MessageParseFailure = 'message-parse-failure', +} diff --git a/packages/core/src/modules/problem-reports/models/index.ts b/packages/core/src/modules/problem-reports/models/index.ts new file mode 100644 index 0000000000..1cbfb94d73 --- /dev/null +++ b/packages/core/src/modules/problem-reports/models/index.ts @@ -0,0 +1 @@ +export * from './ProblemReportReason' diff --git a/packages/core/src/modules/proofs/ProofsModule.ts b/packages/core/src/modules/proofs/ProofsModule.ts index 92e59a4944..d0dbcd1a96 100644 --- a/packages/core/src/modules/proofs/ProofsModule.ts +++ b/packages/core/src/modules/proofs/ProofsModule.ts @@ -16,12 +16,15 @@ import { ConnectionService } from '../connections/services/ConnectionService' import { MediationRecipientService } from '../routing/services/MediationRecipientService' import { ProofResponseCoordinator } from './ProofResponseCoordinator' +import { PresentationProblemReportReason } from './errors' import { ProposePresentationHandler, RequestPresentationHandler, PresentationAckHandler, PresentationHandler, + PresentationProblemReportHandler, } from './handlers' +import { PresentationProblemReportMessage } from './messages/PresentationProblemReportMessage' import { ProofRequest } from './models/ProofRequest' import { ProofService } from './services' @@ -255,8 +258,8 @@ export class ProofsModule { await this.messageSender.sendMessageToService({ message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], returnRoute: true, }) @@ -302,12 +305,12 @@ export class ProofsModule { // Use ~service decorator otherwise else if (proofRecord.requestMessage?.service && proofRecord.presentationMessage?.service) { const recipientService = proofRecord.presentationMessage?.service - const ourService = proofRecord.requestMessage?.service + const ourService = proofRecord.requestMessage.service await this.messageSender.sendMessageToService({ message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], returnRoute: true, }) } @@ -351,7 +354,10 @@ export class ProofsModule { ) } - return this.proofService.getRequestedCredentialsForProofRequest(indyProofRequest, presentationPreview) + return this.proofService.getRequestedCredentialsForProofRequest(indyProofRequest, { + presentationProposal: presentationPreview, + filterByNonRevocationRequirements: config?.filterByNonRevocationRequirements ?? true, + }) } /** @@ -368,6 +374,33 @@ export class ProofsModule { return this.proofService.autoSelectCredentialsForProofRequest(retrievedCredentials) } + /** + * Send problem report message for a proof record + * @param proofRecordId The id of the proof record for which to send problem report + * @param message message to send + * @returns proof record associated with the proof problem report message + */ + public async sendProblemReport(proofRecordId: string, message: string) { + const record = await this.proofService.getById(proofRecordId) + if (!record.connectionId) { + throw new AriesFrameworkError(`No connectionId found for proof record '${record.id}'.`) + } + const connection = await this.connectionService.getById(record.connectionId) + const presentationProblemReportMessage = new PresentationProblemReportMessage({ + description: { + en: message, + code: PresentationProblemReportReason.Abandoned, + }, + }) + presentationProblemReportMessage.setThread({ + threadId: record.threadId, + }) + const outboundMessage = createOutboundMessage(connection, presentationProblemReportMessage) + await this.messageSender.sendMessage(outboundMessage) + + return record + } + /** * Retrieve all proof records * @@ -426,6 +459,7 @@ export class ProofsModule { new PresentationHandler(this.proofService, this.agentConfig, this.proofResponseCoordinator) ) dispatcher.registerHandler(new PresentationAckHandler(this.proofService)) + dispatcher.registerHandler(new PresentationProblemReportHandler(this.proofService)) } } @@ -443,6 +477,17 @@ export interface GetRequestedCredentialsConfig { * Whether to filter the retrieved credentials using the presentation preview. * This configuration will only have effect if a presentation proposal message is available * containing a presentation preview. + * + * @default false */ filterByPresentationPreview?: boolean + + /** + * Whether to filter the retrieved credentials using the non-revocation request in the proof request. + * This configuration will only have effect if the proof request requires proof on non-revocation of any kind. + * Default to true + * + * @default true + */ + filterByNonRevocationRequirements?: boolean } diff --git a/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts b/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts index cee8eda160..0d0b74cde8 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts +++ b/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts @@ -14,7 +14,7 @@ describe('ProofRequest', () => { name: 'Timo', restrictions: [ { - schema_id: 'string', + schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', }, ], }, @@ -26,7 +26,7 @@ describe('ProofRequest', () => { p_value: 10, restrictions: [ { - schema_id: 'string', + schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', }, ], }, @@ -49,7 +49,7 @@ describe('ProofRequest', () => { names: [], restrictions: [ { - schema_id: 'string', + schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', }, ], }, @@ -61,7 +61,7 @@ describe('ProofRequest', () => { p_value: 10, restrictions: [ { - schema_id: 'string', + schema_id: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', }, ], }, diff --git a/packages/core/src/modules/proofs/__tests__/ProofService.test.ts b/packages/core/src/modules/proofs/__tests__/ProofService.test.ts new file mode 100644 index 0000000000..d654dd924a --- /dev/null +++ b/packages/core/src/modules/proofs/__tests__/ProofService.test.ts @@ -0,0 +1,265 @@ +import type { Wallet } from '../../../wallet/Wallet' +import type { CredentialRepository } from '../../credentials/repository' +import type { ProofStateChangedEvent } from '../ProofEvents' +import type { CustomProofTags } from './../repository/ProofRecord' + +import { getAgentConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' +import { EventEmitter } from '../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' +import { ConnectionService, DidExchangeState } from '../../connections' +import { IndyHolderService } from '../../indy/services/IndyHolderService' +import { IndyRevocationService } from '../../indy/services/IndyRevocationService' +import { IndyLedgerService } from '../../ledger/services' +import { ProofEventTypes } from '../ProofEvents' +import { ProofState } from '../ProofState' +import { PresentationProblemReportReason } from '../errors/PresentationProblemReportReason' +import { INDY_PROOF_REQUEST_ATTACHMENT_ID } from '../messages' +import { ProofRecord } from '../repository/ProofRecord' +import { ProofRepository } from '../repository/ProofRepository' +import { ProofService } from '../services' + +import { IndyVerifierService } from './../../indy/services/IndyVerifierService' +import { PresentationProblemReportMessage } from './../messages/PresentationProblemReportMessage' +import { RequestPresentationMessage } from './../messages/RequestPresentationMessage' +import { credDef } from './fixtures' + +// Mock classes +jest.mock('../repository/ProofRepository') +jest.mock('../../../modules/ledger/services/IndyLedgerService') +jest.mock('../../indy/services/IndyHolderService') +jest.mock('../../indy/services/IndyIssuerService') +jest.mock('../../indy/services/IndyVerifierService') +jest.mock('../../indy/services/IndyRevocationService') +jest.mock('../../connections/services/ConnectionService') + +// Mock typed object +const ProofRepositoryMock = ProofRepository as jest.Mock +const IndyLedgerServiceMock = IndyLedgerService as jest.Mock +const IndyHolderServiceMock = IndyHolderService as jest.Mock +const IndyVerifierServiceMock = IndyVerifierService as jest.Mock +const IndyRevocationServiceMock = IndyRevocationService as jest.Mock +const connectionServiceMock = ConnectionService as jest.Mock + +const connection = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const requestAttachment = new Attachment({ + id: INDY_PROOF_REQUEST_ATTACHMENT_ID, + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJuYW1lIjogIlByb29mIHJlcXVlc3QiLCAibm9uX3Jldm9rZWQiOiB7ImZyb20iOiAxNjQwOTk1MTk5LCAidG8iOiAxNjQwOTk1MTk5fSwgIm5vbmNlIjogIjEiLCAicmVxdWVzdGVkX2F0dHJpYnV0ZXMiOiB7ImFkZGl0aW9uYWxQcm9wMSI6IHsibmFtZSI6ICJmYXZvdXJpdGVEcmluayIsICJub25fcmV2b2tlZCI6IHsiZnJvbSI6IDE2NDA5OTUxOTksICJ0byI6IDE2NDA5OTUxOTl9LCAicmVzdHJpY3Rpb25zIjogW3siY3JlZF9kZWZfaWQiOiAiV2dXeHF6dHJOb29HOTJSWHZ4U1RXdjozOkNMOjIwOnRhZyJ9XX19LCAicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOiB7fSwgInZlcnNpb24iOiAiMS4wIn0=', + }), +}) + +// A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` +// object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. +const mockProofRecord = ({ + state, + requestMessage, + threadId, + connectionId, + tags, + id, +}: { + state?: ProofState + requestMessage?: RequestPresentationMessage + tags?: CustomProofTags + threadId?: string + connectionId?: string + id?: string +} = {}) => { + const requestPresentationMessage = new RequestPresentationMessage({ + comment: 'some comment', + requestPresentationAttachments: [requestAttachment], + }) + + const proofRecord = new ProofRecord({ + requestMessage, + id, + state: state || ProofState.RequestSent, + threadId: threadId ?? requestPresentationMessage.id, + connectionId: connectionId ?? '123', + tags, + }) + + return proofRecord +} + +describe('ProofService', () => { + let proofRepository: ProofRepository + let proofService: ProofService + let ledgerService: IndyLedgerService + let wallet: Wallet + let indyVerifierService: IndyVerifierService + let indyHolderService: IndyHolderService + let indyRevocationService: IndyRevocationService + let eventEmitter: EventEmitter + let credentialRepository: CredentialRepository + let connectionService: ConnectionService + + beforeEach(() => { + const agentConfig = getAgentConfig('ProofServiceTest') + proofRepository = new ProofRepositoryMock() + indyVerifierService = new IndyVerifierServiceMock() + indyHolderService = new IndyHolderServiceMock() + indyRevocationService = new IndyRevocationServiceMock() + ledgerService = new IndyLedgerServiceMock() + eventEmitter = new EventEmitter(agentConfig) + connectionService = new connectionServiceMock() + + proofService = new ProofService( + proofRepository, + ledgerService, + wallet, + agentConfig, + indyHolderService, + indyVerifierService, + indyRevocationService, + connectionService, + eventEmitter, + credentialRepository + ) + + mockFunction(ledgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) + }) + + describe('processProofRequest', () => { + let presentationRequest: RequestPresentationMessage + let messageContext: InboundMessageContext + + beforeEach(() => { + presentationRequest = new RequestPresentationMessage({ + comment: 'abcd', + requestPresentationAttachments: [requestAttachment], + }) + messageContext = new InboundMessageContext(presentationRequest, { + connection, + }) + }) + + test(`creates and return proof record in ${ProofState.PresentationReceived} state with offer, without thread ID`, async () => { + const repositorySaveSpy = jest.spyOn(proofRepository, 'save') + + // when + const returnedProofRecord = await proofService.processRequest(messageContext) + + // then + const expectedProofRecord = { + type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + state: ProofState.RequestReceived, + threadId: presentationRequest.id, + connectionId: connection.id, + } + expect(repositorySaveSpy).toHaveBeenCalledTimes(1) + const [[createdProofRecord]] = repositorySaveSpy.mock.calls + expect(createdProofRecord).toMatchObject(expectedProofRecord) + expect(returnedProofRecord).toMatchObject(expectedProofRecord) + }) + + test(`emits stateChange event with ${ProofState.RequestReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(ProofEventTypes.ProofStateChanged, eventListenerMock) + + // when + await proofService.processRequest(messageContext) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'ProofStateChanged', + payload: { + previousState: null, + proofRecord: expect.objectContaining({ + state: ProofState.RequestReceived, + }), + }, + }) + }) + }) + + describe('createProblemReport', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let proof: ProofRecord + + beforeEach(() => { + proof = mockProofRecord({ + state: ProofState.RequestReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + }) + + test('returns problem report message base once get error', async () => { + // given + mockFunction(proofRepository.getById).mockReturnValue(Promise.resolve(proof)) + + // when + const presentationProblemReportMessage = await new PresentationProblemReportMessage({ + description: { + en: 'Indy error', + code: PresentationProblemReportReason.Abandoned, + }, + }) + + presentationProblemReportMessage.setThread({ threadId }) + // then + expect(presentationProblemReportMessage.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/present-proof/1.0/problem-report', + '~thread': { + thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + }, + }) + }) + }) + + describe('processProblemReport', () => { + let proof: ProofRecord + let messageContext: InboundMessageContext + + beforeEach(() => { + proof = mockProofRecord({ + state: ProofState.RequestReceived, + }) + + const presentationProblemReportMessage = new PresentationProblemReportMessage({ + description: { + en: 'Indy error', + code: PresentationProblemReportReason.Abandoned, + }, + }) + presentationProblemReportMessage.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(presentationProblemReportMessage, { + connection, + }) + }) + + test(`updates problem report error message and returns proof record`, async () => { + const repositoryUpdateSpy = jest.spyOn(proofRepository, 'update') + + // given + mockFunction(proofRepository.getSingleByQuery).mockReturnValue(Promise.resolve(proof)) + + // when + const returnedCredentialRecord = await proofService.processProblemReport(messageContext) + + // then + const expectedCredentialRecord = { + errorMessage: 'abandoned: Indy error', + } + expect(proofRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { + threadId: 'somethreadid', + connectionId: connection.id, + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + }) +}) diff --git a/packages/core/src/modules/proofs/__tests__/fixtures.ts b/packages/core/src/modules/proofs/__tests__/fixtures.ts new file mode 100644 index 0000000000..10606073b8 --- /dev/null +++ b/packages/core/src/modules/proofs/__tests__/fixtures.ts @@ -0,0 +1,17 @@ +export const credDef = { + ver: '1.0', + id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:16:TAG', + schemaId: '16', + type: 'CL', + tag: 'TAG', + value: { + primary: { + n: '92498022445845202032348897620554299694896009176315493627722439892023558526259875239808280186111059586069456394012963552956574651629517633396592827947162983189649269173220440607665417484696688946624963596710652063849006738050417440697782608643095591808084344059908523401576738321329706597491345875134180790935098782801918369980296355919072827164363500681884641551147645504164254206270541724042784184712124576190438261715948768681331862924634233043594086219221089373455065715714369325926959533971768008691000560918594972006312159600845441063618991760512232714992293187779673708252226326233136573974603552763615191259713', + s: '10526250116244590830801226936689232818708299684432892622156345407187391699799320507237066062806731083222465421809988887959680863378202697458984451550048737847231343182195679453915452156726746705017249911605739136361885518044604626564286545453132948801604882107628140153824106426249153436206037648809856342458324897885659120708767794055147846459394129610878181859361616754832462886951623882371283575513182530118220334228417923423365966593298195040550255217053655606887026300020680355874881473255854564974899509540795154002250551880061649183753819902391970912501350100175974791776321455551753882483918632271326727061054', + r: [Object], + rctxt: + '46370806529776888197599056685386177334629311939451963919411093310852010284763705864375085256873240323432329015015526097014834809926159013231804170844321552080493355339505872140068998254185756917091385820365193200970156007391350745837300010513687490459142965515562285631984769068796922482977754955668569724352923519618227464510753980134744424528043503232724934196990461197793822566137436901258663918660818511283047475389958180983391173176526879694302021471636017119966755980327241734084462963412467297412455580500138233383229217300797768907396564522366006433982511590491966618857814545264741708965590546773466047139517', + z: '84153935869396527029518633753040092509512111365149323230260584738724940130382637900926220255597132853379358675015222072417404334537543844616589463419189203852221375511010886284448841979468767444910003114007224993233448170299654815710399828255375084265247114471334540928216537567325499206413940771681156686116516158907421215752364889506967984343660576422672840921988126699885304325384925457260272972771547695861942114712679509318179363715259460727275178310181122162544785290813713205047589943947592273130618286905125194410421355167030389500160371886870735704712739886223342214864760968555566496288314800410716250791012', + }, + }, +} diff --git a/packages/core/src/modules/proofs/errors/PresentationProblemReportError.ts b/packages/core/src/modules/proofs/errors/PresentationProblemReportError.ts new file mode 100644 index 0000000000..2869a026d5 --- /dev/null +++ b/packages/core/src/modules/proofs/errors/PresentationProblemReportError.ts @@ -0,0 +1,24 @@ +import type { ProblemReportErrorOptions } from '../../problem-reports' +import type { PresentationProblemReportReason } from './PresentationProblemReportReason' + +import { PresentationProblemReportMessage } from '../messages' + +import { ProblemReportError } from './../../problem-reports/errors/ProblemReportError' + +interface PresentationProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: PresentationProblemReportReason +} + +export class PresentationProblemReportError extends ProblemReportError { + public problemReport: PresentationProblemReportMessage + + public constructor(public message: string, { problemCode }: PresentationProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new PresentationProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/proofs/errors/PresentationProblemReportReason.ts b/packages/core/src/modules/proofs/errors/PresentationProblemReportReason.ts new file mode 100644 index 0000000000..0fc1676dcc --- /dev/null +++ b/packages/core/src/modules/proofs/errors/PresentationProblemReportReason.ts @@ -0,0 +1,8 @@ +/** + * Presentation error code in RFC 0037. + * + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0037-present-proof/README.md + */ +export enum PresentationProblemReportReason { + Abandoned = 'abandoned', +} diff --git a/packages/core/src/modules/proofs/errors/index.ts b/packages/core/src/modules/proofs/errors/index.ts new file mode 100644 index 0000000000..5e0ca1453b --- /dev/null +++ b/packages/core/src/modules/proofs/errors/index.ts @@ -0,0 +1,2 @@ +export * from './PresentationProblemReportError' +export * from './PresentationProblemReportReason' diff --git a/packages/core/src/modules/proofs/handlers/PresentationHandler.ts b/packages/core/src/modules/proofs/handlers/PresentationHandler.ts index 660254080e..c00fa139c7 100644 --- a/packages/core/src/modules/proofs/handlers/PresentationHandler.ts +++ b/packages/core/src/modules/proofs/handlers/PresentationHandler.ts @@ -46,8 +46,8 @@ export class PresentationHandler implements Handler { return createOutboundServiceMessage({ payload: message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], }) } diff --git a/packages/core/src/modules/proofs/handlers/PresentationProblemReportHandler.ts b/packages/core/src/modules/proofs/handlers/PresentationProblemReportHandler.ts new file mode 100644 index 0000000000..925941e3a4 --- /dev/null +++ b/packages/core/src/modules/proofs/handlers/PresentationProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' +import type { ProofService } from '../services' + +import { PresentationProblemReportMessage } from '../messages' + +export class PresentationProblemReportHandler implements Handler { + private proofService: ProofService + public supportedMessages = [PresentationProblemReportMessage] + + public constructor(proofService: ProofService) { + this.proofService = proofService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.proofService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts b/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts index fdeb944f36..b2df52c6d4 100644 --- a/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts +++ b/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts @@ -41,19 +41,20 @@ export class RequestPresentationHandler implements Handler { messageContext: HandlerInboundMessage ) { const indyProofRequest = record.requestMessage?.indyProofRequest + const presentationProposal = record.proposalMessage?.presentationProposal this.agentConfig.logger.info( `Automatically sending presentation with autoAccept on ${this.agentConfig.autoAcceptProofs}` ) if (!indyProofRequest) { + this.agentConfig.logger.error('Proof request is undefined.') return } - const retrievedCredentials = await this.proofService.getRequestedCredentialsForProofRequest( - indyProofRequest, - record.proposalMessage?.presentationProposal - ) + const retrievedCredentials = await this.proofService.getRequestedCredentialsForProofRequest(indyProofRequest, { + presentationProposal, + }) const requestedCredentials = this.proofService.autoSelectCredentialsForProofRequest(retrievedCredentials) @@ -79,8 +80,8 @@ export class RequestPresentationHandler implements Handler { return createOutboundServiceMessage({ payload: message, - service: recipientService.toDidCommService(), - senderKey: ourService.recipientKeys[0], + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], }) } diff --git a/packages/core/src/modules/proofs/handlers/index.ts b/packages/core/src/modules/proofs/handlers/index.ts index 75adea32eb..ba30911942 100644 --- a/packages/core/src/modules/proofs/handlers/index.ts +++ b/packages/core/src/modules/proofs/handlers/index.ts @@ -2,3 +2,4 @@ export * from './PresentationAckHandler' export * from './PresentationHandler' export * from './ProposePresentationHandler' export * from './RequestPresentationHandler' +export * from './PresentationProblemReportHandler' diff --git a/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts b/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts index ca75fbef18..12d405f6dc 100644 --- a/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts +++ b/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts @@ -1,7 +1,6 @@ import type { AckMessageOptions } from '../../common' -import { Equals } from 'class-validator' - +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' import { AckMessage } from '../../common' export type PresentationAckMessageOptions = AckMessageOptions @@ -14,7 +13,7 @@ export class PresentationAckMessage extends AckMessage { super(options) } - @Equals(PresentationAckMessage.type) - public readonly type = PresentationAckMessage.type - public static readonly type = 'https://didcomm.org/present-proof/1.0/ack' + @IsValidMessageType(PresentationAckMessage.type) + public readonly type = PresentationAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/ack') } diff --git a/packages/core/src/modules/proofs/messages/PresentationMessage.ts b/packages/core/src/modules/proofs/messages/PresentationMessage.ts index 65bd1d3aa2..72d68cbdcc 100644 --- a/packages/core/src/modules/proofs/messages/PresentationMessage.ts +++ b/packages/core/src/modules/proofs/messages/PresentationMessage.ts @@ -1,11 +1,11 @@ import type { IndyProof } from 'indy-sdk' import { Expose, Type } from 'class-transformer' -import { Equals, IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class-validator' +import { IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' import { Attachment } from '../../../decorators/attachment/Attachment' -import { JsonEncoder } from '../../../utils/JsonEncoder' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export const INDY_PROOF_ATTACHMENT_ID = 'libindy-presentation-0' @@ -30,13 +30,13 @@ export class PresentationMessage extends AgentMessage { this.id = options.id ?? this.generateId() this.comment = options.comment this.presentationAttachments = options.presentationAttachments - this.attachments = options.attachments + this.appendedAttachments = options.attachments } } - @Equals(PresentationMessage.type) - public readonly type = PresentationMessage.type - public static readonly type = 'https://didcomm.org/present-proof/1.0/presentation' + @IsValidMessageType(PresentationMessage.type) + public readonly type = PresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/presentation') /** * Provides some human readable information about this request for a presentation. @@ -60,12 +60,7 @@ export class PresentationMessage extends AgentMessage { public get indyProof(): IndyProof | null { const attachment = this.presentationAttachments.find((attachment) => attachment.id === INDY_PROOF_ATTACHMENT_ID) - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null - } - - const proofJson = JsonEncoder.fromBase64(attachment.data.base64) + const proofJson = attachment?.getDataAsJson() ?? null return proofJson } diff --git a/packages/core/src/modules/proofs/messages/PresentationPreview.ts b/packages/core/src/modules/proofs/messages/PresentationPreview.ts index 655656704f..c309aaee85 100644 --- a/packages/core/src/modules/proofs/messages/PresentationPreview.ts +++ b/packages/core/src/modules/proofs/messages/PresentationPreview.ts @@ -1,18 +1,19 @@ import { Expose, Transform, Type } from 'class-transformer' import { - Equals, IsEnum, IsInstance, IsInt, IsMimeType, IsOptional, IsString, + Matches, ValidateIf, ValidateNested, } from 'class-validator' +import { credDefIdRegex } from '../../../utils' import { JsonTransformer } from '../../../utils/JsonTransformer' -import { replaceLegacyDidSovPrefix } from '../../../utils/messageType' +import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../utils/messageType' import { PredicateType } from '../models/PredicateType' export interface PresentationPreviewAttributeOptions { @@ -39,6 +40,7 @@ export class PresentationPreviewAttribute { @Expose({ name: 'cred_def_id' }) @IsString() @ValidateIf((o: PresentationPreviewAttribute) => o.referent !== undefined) + @Matches(credDefIdRegex) public credentialDefinitionId?: string @Expose({ name: 'mime-type' }) @@ -81,6 +83,7 @@ export class PresentationPreviewPredicate { @Expose({ name: 'cred_def_id' }) @IsString() + @Matches(credDefIdRegex) public credentialDefinitionId!: string @IsEnum(PredicateType) @@ -115,12 +118,12 @@ export class PresentationPreview { } @Expose({ name: '@type' }) - @Equals(PresentationPreview.type) + @IsValidMessageType(PresentationPreview.type) @Transform(({ value }) => replaceLegacyDidSovPrefix(value), { toClassOnly: true, }) - public readonly type = PresentationPreview.type - public static readonly type = 'https://didcomm.org/present-proof/1.0/presentation-preview' + public readonly type = PresentationPreview.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/presentation-preview') @Type(() => PresentationPreviewAttribute) @ValidateNested({ each: true }) diff --git a/packages/core/src/modules/proofs/messages/PresentationProblemReportMessage.ts b/packages/core/src/modules/proofs/messages/PresentationProblemReportMessage.ts new file mode 100644 index 0000000000..2d62a6e2b9 --- /dev/null +++ b/packages/core/src/modules/proofs/messages/PresentationProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage' + +export type PresentationProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class PresentationProblemReportMessage extends ProblemReportMessage { + /** + * Create new PresentationProblemReportMessage instance. + * @param options + */ + public constructor(options: PresentationProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(PresentationProblemReportMessage.type) + public readonly type = PresentationProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/problem-report') +} diff --git a/packages/core/src/modules/proofs/messages/ProposePresentationMessage.ts b/packages/core/src/modules/proofs/messages/ProposePresentationMessage.ts index 5a2ea31028..409b1cfe23 100644 --- a/packages/core/src/modules/proofs/messages/ProposePresentationMessage.ts +++ b/packages/core/src/modules/proofs/messages/ProposePresentationMessage.ts @@ -1,7 +1,8 @@ import { Expose, Type } from 'class-transformer' -import { Equals, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' import { PresentationPreview } from './PresentationPreview' @@ -27,9 +28,9 @@ export class ProposePresentationMessage extends AgentMessage { } } - @Equals(ProposePresentationMessage.type) - public readonly type = ProposePresentationMessage.type - public static readonly type = 'https://didcomm.org/present-proof/1.0/propose-presentation' + @IsValidMessageType(ProposePresentationMessage.type) + public readonly type = ProposePresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/propose-presentation') /** * Provides some human readable information about the proposed presentation. diff --git a/packages/core/src/modules/proofs/messages/RequestPresentationMessage.ts b/packages/core/src/modules/proofs/messages/RequestPresentationMessage.ts index fad50b97cc..e03b4f2fa4 100644 --- a/packages/core/src/modules/proofs/messages/RequestPresentationMessage.ts +++ b/packages/core/src/modules/proofs/messages/RequestPresentationMessage.ts @@ -1,10 +1,10 @@ import { Expose, Type } from 'class-transformer' -import { Equals, IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class-validator' +import { IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' import { Attachment } from '../../../decorators/attachment/Attachment' -import { JsonEncoder } from '../../../utils/JsonEncoder' import { JsonTransformer } from '../../../utils/JsonTransformer' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' import { ProofRequest } from '../models' export interface RequestPresentationOptions { @@ -31,9 +31,9 @@ export class RequestPresentationMessage extends AgentMessage { } } - @Equals(RequestPresentationMessage.type) - public readonly type = RequestPresentationMessage.type - public static readonly type = 'https://didcomm.org/present-proof/1.0/request-presentation' + @IsValidMessageType(RequestPresentationMessage.type) + public readonly type = RequestPresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/request-presentation') /** * Provides some human readable information about this request for a presentation. @@ -58,14 +58,8 @@ export class RequestPresentationMessage extends AgentMessage { const attachment = this.requestPresentationAttachments.find( (attachment) => attachment.id === INDY_PROOF_REQUEST_ATTACHMENT_ID ) - - // Return null if attachment is not found - if (!attachment?.data?.base64) { - return null - } - // Extract proof request from attachment - const proofRequestJson = JsonEncoder.fromBase64(attachment.data.base64) + const proofRequestJson = attachment?.getDataAsJson() ?? null const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) return proofRequest diff --git a/packages/core/src/modules/proofs/messages/index.ts b/packages/core/src/modules/proofs/messages/index.ts index c6228f03e5..f2ad906c75 100644 --- a/packages/core/src/modules/proofs/messages/index.ts +++ b/packages/core/src/modules/proofs/messages/index.ts @@ -3,3 +3,4 @@ export * from './RequestPresentationMessage' export * from './PresentationMessage' export * from './PresentationPreview' export * from './PresentationAckMessage' +export * from './PresentationProblemReportMessage' diff --git a/packages/core/src/modules/proofs/models/AttributeFilter.ts b/packages/core/src/modules/proofs/models/AttributeFilter.ts index 4dbaab8bca..90b628799e 100644 --- a/packages/core/src/modules/proofs/models/AttributeFilter.ts +++ b/packages/core/src/modules/proofs/models/AttributeFilter.ts @@ -1,5 +1,7 @@ import { Expose, Transform, TransformationType, Type } from 'class-transformer' -import { IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsInstance, IsOptional, IsString, Matches, ValidateNested } from 'class-validator' + +import { credDefIdRegex, indyDidRegex, schemaIdRegex, schemaVersionRegex } from '../../../utils' export class AttributeValue { public constructor(options: AttributeValue) { @@ -32,11 +34,13 @@ export class AttributeFilter { @Expose({ name: 'schema_id' }) @IsOptional() @IsString() + @Matches(schemaIdRegex) public schemaId?: string @Expose({ name: 'schema_issuer_did' }) @IsOptional() @IsString() + @Matches(indyDidRegex) public schemaIssuerDid?: string @Expose({ name: 'schema_name' }) @@ -47,16 +51,21 @@ export class AttributeFilter { @Expose({ name: 'schema_version' }) @IsOptional() @IsString() + @Matches(schemaVersionRegex, { + message: 'Version must be X.X or X.X.X', + }) public schemaVersion?: string @Expose({ name: 'issuer_did' }) @IsOptional() @IsString() + @Matches(indyDidRegex) public issuerDid?: string @Expose({ name: 'cred_def_id' }) @IsOptional() @IsString() + @Matches(credDefIdRegex) public credentialDefinitionId?: string @IsOptional() diff --git a/packages/core/src/modules/proofs/models/ProofIdentifier.ts b/packages/core/src/modules/proofs/models/ProofIdentifier.ts index d12a896359..66f337e8b2 100644 --- a/packages/core/src/modules/proofs/models/ProofIdentifier.ts +++ b/packages/core/src/modules/proofs/models/ProofIdentifier.ts @@ -1,5 +1,7 @@ import { Expose } from 'class-transformer' -import { IsNumber, IsOptional, IsString } from 'class-validator' +import { IsNumber, IsOptional, IsString, Matches } from 'class-validator' + +import { credDefIdRegex } from '../../../utils' export class ProofIdentifier { public constructor(options: ProofIdentifier) { @@ -17,6 +19,7 @@ export class ProofIdentifier { @Expose({ name: 'cred_def_id' }) @IsString() + @Matches(credDefIdRegex) public credentialDefinitionId!: string @Expose({ name: 'rev_reg_id' }) diff --git a/packages/core/src/modules/proofs/models/RequestedAttribute.ts b/packages/core/src/modules/proofs/models/RequestedAttribute.ts index 929759ef10..4998a8b097 100644 --- a/packages/core/src/modules/proofs/models/RequestedAttribute.ts +++ b/packages/core/src/modules/proofs/models/RequestedAttribute.ts @@ -13,6 +13,7 @@ export class RequestedAttribute { this.timestamp = options.timestamp this.revealed = options.revealed this.credentialInfo = options.credentialInfo + this.revoked = options.revoked } } @@ -29,5 +30,8 @@ export class RequestedAttribute { public revealed!: boolean @Exclude({ toPlainOnly: true }) - public credentialInfo!: IndyCredentialInfo + public credentialInfo?: IndyCredentialInfo + + @Exclude({ toPlainOnly: true }) + public revoked?: boolean } diff --git a/packages/core/src/modules/proofs/models/RequestedPredicate.ts b/packages/core/src/modules/proofs/models/RequestedPredicate.ts index f4d08657b1..5e7d4dc5f9 100644 --- a/packages/core/src/modules/proofs/models/RequestedPredicate.ts +++ b/packages/core/src/modules/proofs/models/RequestedPredicate.ts @@ -12,6 +12,7 @@ export class RequestedPredicate { this.credentialId = options.credentialId this.timestamp = options.timestamp this.credentialInfo = options.credentialInfo + this.revoked = options.revoked } } @@ -25,5 +26,8 @@ export class RequestedPredicate { public timestamp?: number @Exclude({ toPlainOnly: true }) - public credentialInfo!: IndyCredentialInfo + public credentialInfo?: IndyCredentialInfo + + @Exclude({ toPlainOnly: true }) + public revoked?: boolean } diff --git a/packages/core/src/modules/proofs/repository/ProofRecord.ts b/packages/core/src/modules/proofs/repository/ProofRecord.ts index 2a9cc2b2f8..bf4faa5435 100644 --- a/packages/core/src/modules/proofs/repository/ProofRecord.ts +++ b/packages/core/src/modules/proofs/repository/ProofRecord.ts @@ -20,6 +20,7 @@ export interface ProofRecordProps { presentationId?: string tags?: CustomProofTags autoAcceptProof?: AutoAcceptProof + errorMessage?: string // message data proposalMessage?: ProposePresentationMessage @@ -41,6 +42,7 @@ export class ProofRecord extends BaseRecord { public presentationId?: string public state!: ProofState public autoAcceptProof?: AutoAcceptProof + public errorMessage?: string // message data @Type(() => ProposePresentationMessage) @@ -69,6 +71,7 @@ export class ProofRecord extends BaseRecord { this.presentationId = props.presentationId this.autoAcceptProof = props.autoAcceptProof this._tags = props.tags ?? {} + this.errorMessage = props.errorMessage } } diff --git a/packages/core/src/modules/proofs/services/ProofService.ts b/packages/core/src/modules/proofs/services/ProofService.ts index 8a3e1184d9..24ce0ab284 100644 --- a/packages/core/src/modules/proofs/services/ProofService.ts +++ b/packages/core/src/modules/proofs/services/ProofService.ts @@ -5,6 +5,7 @@ import type { ConnectionRecord } from '../../connections' import type { AutoAcceptProof } from '../ProofAutoAcceptType' import type { ProofStateChangedEvent } from '../ProofEvents' import type { PresentationPreview, PresentationPreviewAttribute } from '../messages' +import type { PresentationProblemReportMessage } from './../messages/PresentationProblemReportMessage' import type { CredDef, IndyProof, Schema } from 'indy-sdk' import { validateOrReject } from 'class-validator' @@ -17,15 +18,17 @@ import { Attachment, AttachmentData } from '../../../decorators/attachment/Attac import { AriesFrameworkError } from '../../../error' import { JsonEncoder } from '../../../utils/JsonEncoder' import { JsonTransformer } from '../../../utils/JsonTransformer' +import { checkProofRequestForDuplicates } from '../../../utils/indyProofRequest' import { uuid } from '../../../utils/uuid' import { Wallet } from '../../../wallet/Wallet' import { AckStatus } from '../../common' import { ConnectionService } from '../../connections' -import { CredentialUtils, Credential, CredentialRepository } from '../../credentials' -import { IndyHolderService, IndyVerifierService } from '../../indy' +import { CredentialUtils, Credential, CredentialRepository, IndyCredentialInfo } from '../../credentials' +import { IndyHolderService, IndyVerifierService, IndyRevocationService } from '../../indy' import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' import { ProofEventTypes } from '../ProofEvents' import { ProofState } from '../ProofState' +import { PresentationProblemReportError, PresentationProblemReportReason } from '../errors' import { INDY_PROOF_ATTACHMENT_ID, INDY_PROOF_REQUEST_ATTACHMENT_ID, @@ -62,6 +65,7 @@ export class ProofService { private logger: Logger private indyHolderService: IndyHolderService private indyVerifierService: IndyVerifierService + private indyRevocationService: IndyRevocationService private connectionService: ConnectionService private eventEmitter: EventEmitter @@ -72,6 +76,7 @@ export class ProofService { agentConfig: AgentConfig, indyHolderService: IndyHolderService, indyVerifierService: IndyVerifierService, + indyRevocationService: IndyRevocationService, connectionService: ConnectionService, eventEmitter: EventEmitter, credentialRepository: CredentialRepository @@ -83,6 +88,7 @@ export class ProofService { this.logger = agentConfig.logger this.indyHolderService = indyHolderService this.indyVerifierService = indyVerifierService + this.indyRevocationService = indyRevocationService this.connectionService = connectionService this.eventEmitter = eventEmitter } @@ -160,7 +166,7 @@ export class ProofService { // Update record proofRecord.proposalMessage = proposalMessage - this.updateState(proofRecord, ProofState.ProposalSent) + await this.updateState(proofRecord, ProofState.ProposalSent) return { message: proposalMessage, proofRecord } } @@ -252,6 +258,9 @@ export class ProofService { comment?: string } ): Promise> { + // Assert attribute and predicate (group) names do not match + checkProofRequestForDuplicates(proofRequest) + // Assert proofRecord.assertState(ProofState.ProposalReceived) @@ -297,6 +306,9 @@ export class ProofService { ): Promise> { this.logger.debug(`Creating proof request`) + // Assert attribute and predicate (group) names do not match + checkProofRequestForDuplicates(proofRequest) + // Assert connectionRecord?.assertReady() @@ -351,12 +363,16 @@ export class ProofService { // Assert attachment if (!proofRequest) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for presentation request with thread id ${proofRequestMessage.threadId}` + throw new PresentationProblemReportError( + `Missing required base64 or json encoded attachment data for presentation request with thread id ${proofRequestMessage.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } ) } await validateOrReject(proofRequest) + // Assert attribute and predicate (group) names do not match + checkProofRequestForDuplicates(proofRequest) + this.logger.debug('received proof request', proofRequest) try { @@ -419,8 +435,9 @@ export class ProofService { const indyProofRequest = proofRecord.requestMessage?.indyProofRequest if (!indyProofRequest) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for presentation with thread id ${proofRecord.threadId}` + throw new PresentationProblemReportError( + `Missing required base64 or json encoded attachment data for presentation with thread id ${proofRecord.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } ) } @@ -485,14 +502,16 @@ export class ProofService { const indyProofRequest = proofRecord.requestMessage?.indyProofRequest if (!indyProofJson) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for presentation with thread id ${presentationMessage.threadId}` + throw new PresentationProblemReportError( + `Missing required base64 or json encoded attachment data for presentation with thread id ${presentationMessage.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } ) } if (!indyProofRequest) { - throw new AriesFrameworkError( - `Missing required base64 encoded attachment data for presentation request with thread id ${presentationMessage.threadId}` + throw new PresentationProblemReportError( + `Missing required base64 or json encoded attachment data for presentation request with thread id ${presentationMessage.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } ) } @@ -558,6 +577,29 @@ export class ProofService { return proofRecord } + /** + * Process a received {@link PresentationProblemReportMessage}. + * + * @param messageContext The message context containing a presentation problem report message + * @returns proof record associated with the presentation acknowledgement message + * + */ + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message: presentationProblemReportMessage } = messageContext + + const connection = messageContext.assertReadyConnection() + + this.logger.debug(`Processing problem report with id ${presentationProblemReportMessage.id}`) + + const proofRecord = await this.getByThreadAndConnectionId(presentationProblemReportMessage.threadId, connection?.id) + + proofRecord.errorMessage = `${presentationProblemReportMessage.description.code}: ${presentationProblemReportMessage.description.en}` + await this.update(proofRecord) + return proofRecord + } + public async generateProofRequestNonce() { return this.wallet.generateNonce() } @@ -670,6 +712,12 @@ export class ProofService { // List the requested attributes requestedAttributesNames.push(...(requestedAttributes.names ?? [requestedAttributes.name])) + //Get credentialInfo + if (!requestedAttribute.credentialInfo) { + const indyCredentialInfo = await this.indyHolderService.getCredential(requestedAttribute.credentialId) + requestedAttribute.credentialInfo = JsonTransformer.fromJSON(indyCredentialInfo, IndyCredentialInfo) + } + // Find the attributes that have a hashlink as a value for (const attribute of Object.values(requestedAttribute.credentialInfo.attributes)) { if (attribute.toLowerCase().startsWith('hl:')) { @@ -682,7 +730,7 @@ export class ProofService { for (const credentialId of credentialIds) { // Get the credentialRecord that matches the ID - const credentialRecord = await this.credentialRepository.getSingleByQuery({ credentialId }) + const credentialRecord = await this.credentialRepository.getSingleByQuery({ credentialIds: [credentialId] }) if (credentialRecord.linkedAttachments) { // Get the credentials that have a hashlink as value and are requested @@ -717,7 +765,10 @@ export class ProofService { */ public async getRequestedCredentialsForProofRequest( proofRequest: ProofRequest, - presentationProposal?: PresentationPreview + config: { + presentationProposal?: PresentationPreview + filterByNonRevocationRequirements?: boolean + } = {} ): Promise { const retrievedCredentials = new RetrievedCredentials({}) @@ -727,7 +778,7 @@ export class ProofService { // If we have exactly one credential, or no proposal to pick preferences // on the credentials to use, we will use the first one - if (credentials.length === 1 || !presentationProposal) { + if (credentials.length === 1 || !config.presentationProposal) { credentialMatch = credentials } // If we have a proposal we will use that to determine the credentials to use @@ -740,7 +791,7 @@ export class ProofService { // Check if credentials matches all parameters from proposal return names.every((name) => - presentationProposal.attributes.find( + config.presentationProposal?.attributes.find( (a) => a.name === name && a.credentialDefinitionId === credentialDefinitionId && @@ -750,24 +801,60 @@ export class ProofService { }) } - retrievedCredentials.requestedAttributes[referent] = credentialMatch.map((credential: Credential) => { - return new RequestedAttribute({ - credentialId: credential.credentialInfo.referent, - revealed: true, - credentialInfo: credential.credentialInfo, + retrievedCredentials.requestedAttributes[referent] = await Promise.all( + credentialMatch.map(async (credential: Credential) => { + const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem({ + proofRequest, + requestedItem: requestedAttribute, + credential, + }) + + return new RequestedAttribute({ + credentialId: credential.credentialInfo.referent, + revealed: true, + credentialInfo: credential.credentialInfo, + timestamp: deltaTimestamp, + revoked, + }) }) - }) + ) + + // We only attach revoked state if non-revocation is requested. So if revoked is true it means + // the credential is not applicable to the proof request + if (config.filterByNonRevocationRequirements) { + retrievedCredentials.requestedAttributes[referent] = retrievedCredentials.requestedAttributes[referent].filter( + (r) => !r.revoked + ) + } } - for (const [referent] of proofRequest.requestedPredicates.entries()) { + for (const [referent, requestedPredicate] of proofRequest.requestedPredicates.entries()) { const credentials = await this.getCredentialsForProofRequest(proofRequest, referent) - retrievedCredentials.requestedPredicates[referent] = credentials.map((credential) => { - return new RequestedPredicate({ - credentialId: credential.credentialInfo.referent, - credentialInfo: credential.credentialInfo, + retrievedCredentials.requestedPredicates[referent] = await Promise.all( + credentials.map(async (credential) => { + const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem({ + proofRequest, + requestedItem: requestedPredicate, + credential, + }) + + return new RequestedPredicate({ + credentialId: credential.credentialInfo.referent, + credentialInfo: credential.credentialInfo, + timestamp: deltaTimestamp, + revoked, + }) }) - }) + ) + + // We only attach revoked state if non-revocation is requested. So if revoked is true it means + // the credential is not applicable to the proof request + if (config.filterByNonRevocationRequirements) { + retrievedCredentials.requestedPredicates[referent] = retrievedCredentials.requestedPredicates[referent].filter( + (r) => !r.revoked + ) + } } return retrievedCredentials @@ -822,10 +909,11 @@ export class ProofService { for (const [referent, attribute] of proof.requestedProof.revealedAttributes.entries()) { if (!CredentialUtils.checkValidEncoding(attribute.raw, attribute.encoded)) { - throw new AriesFrameworkError( + throw new PresentationProblemReportError( `The encoded value for '${referent}' is invalid. ` + `Expected '${CredentialUtils.encode(attribute.raw)}'. ` + - `Actual '${attribute.encoded}'` + `Actual '${attribute.encoded}'`, + { problemCode: PresentationProblemReportReason.Abandoned } ) } } @@ -917,24 +1005,30 @@ export class ProofService { proofRequest: ProofRequest, requestedCredentials: RequestedCredentials ): Promise { - const credentialObjects = [ - ...Object.values(requestedCredentials.requestedAttributes), - ...Object.values(requestedCredentials.requestedPredicates), - ].map((c) => c.credentialInfo) + const credentialObjects = await Promise.all( + [ + ...Object.values(requestedCredentials.requestedAttributes), + ...Object.values(requestedCredentials.requestedPredicates), + ].map(async (c) => { + if (c.credentialInfo) { + return c.credentialInfo + } + const credentialInfo = await this.indyHolderService.getCredential(c.credentialId) + return JsonTransformer.fromJSON(credentialInfo, IndyCredentialInfo) + }) + ) const schemas = await this.getSchemas(new Set(credentialObjects.map((c) => c.schemaId))) const credentialDefinitions = await this.getCredentialDefinitions( new Set(credentialObjects.map((c) => c.credentialDefinitionId)) ) - const proof = await this.indyHolderService.createProof({ + return this.indyHolderService.createProof({ proofRequest: proofRequest.toJSON(), - requestedCredentials: requestedCredentials.toJSON(), + requestedCredentials: requestedCredentials, schemas, credentialDefinitions, }) - - return proof } private async getCredentialsForProofRequest( @@ -949,6 +1043,43 @@ export class ProofService { return JsonTransformer.fromJSON(credentialsJson, Credential) as unknown as Credential[] } + private async getRevocationStatusForRequestedItem({ + proofRequest, + requestedItem, + credential, + }: { + proofRequest: ProofRequest + requestedItem: ProofAttributeInfo | ProofPredicateInfo + credential: Credential + }) { + const requestNonRevoked = requestedItem.nonRevoked ?? proofRequest.nonRevoked + const credentialRevocationId = credential.credentialInfo.credentialRevocationId + const revocationRegistryId = credential.credentialInfo.revocationRegistryId + + // If revocation interval is present and the credential is revocable then fetch the revocation status of credentials for display + if (requestNonRevoked && credentialRevocationId && revocationRegistryId) { + this.logger.trace( + `Presentation is requesting proof of non revocation, getting revocation status for credential`, + { + requestNonRevoked, + credentialRevocationId, + revocationRegistryId, + } + ) + + // Note presentation from-to's vs ledger from-to's: https://github.com/hyperledger/indy-hipe/blob/master/text/0011-cred-revocation/README.md#indy-node-revocation-registry-intervals + const status = await this.indyRevocationService.getRevocationStatus( + credentialRevocationId, + revocationRegistryId, + requestNonRevoked + ) + + return status + } + + return { revoked: undefined, deltaTimestamp: undefined } + } + /** * Update the record to a new state and emit an state changed event. Also updates the record * in storage. diff --git a/packages/core/src/modules/routing/MediatorModule.ts b/packages/core/src/modules/routing/MediatorModule.ts index 6f8a8bdb45..891c31ce03 100644 --- a/packages/core/src/modules/routing/MediatorModule.ts +++ b/packages/core/src/modules/routing/MediatorModule.ts @@ -1,4 +1,4 @@ -import type { WireMessage } from '../../types' +import type { EncryptedMessage } from '../../types' import type { MediationRecord } from './repository' import { Lifecycle, scoped } from 'tsyringe' @@ -6,6 +6,7 @@ import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../agent/AgentConfig' import { Dispatcher } from '../../agent/Dispatcher' import { EventEmitter } from '../../agent/EventEmitter' +import { MessageReceiver } from '../../agent/MessageReceiver' import { MessageSender } from '../../agent/MessageSender' import { createOutboundMessage } from '../../agent/helpers' import { ConnectionService } from '../connections/services' @@ -29,6 +30,7 @@ export class MediatorModule { mediationService: MediatorService, messagePickupService: MessagePickupService, messageSender: MessageSender, + messageReceiver: MessageReceiver, eventEmitter: EventEmitter, agentConfig: AgentConfig, connectionService: ConnectionService @@ -54,7 +56,7 @@ export class MediatorModule { return mediationRecord } - public queueMessage(connectionId: string, message: WireMessage) { + public queueMessage(connectionId: string, message: EncryptedMessage) { return this.messagePickupService.queueMessage(connectionId, message) } diff --git a/packages/core/src/modules/routing/MediatorPickupStrategy.ts b/packages/core/src/modules/routing/MediatorPickupStrategy.ts index 59841c6b8d..d4889b6ac9 100644 --- a/packages/core/src/modules/routing/MediatorPickupStrategy.ts +++ b/packages/core/src/modules/routing/MediatorPickupStrategy.ts @@ -1,6 +1,9 @@ export enum MediatorPickupStrategy { // Explicit pickup strategy means picking up messages using the pickup protocol - Explicit = 'Explicit', + PickUpV1 = 'PickUpV1', + + // Supports pickup v2 + PickUpV2 = 'PickUpV2', // Implicit pickup strategy means picking up messages only using return route // decorator. This is what ACA-Py currently uses diff --git a/packages/core/src/modules/routing/RecipientModule.ts b/packages/core/src/modules/routing/RecipientModule.ts index b8068bf7c2..6c60994e7c 100644 --- a/packages/core/src/modules/routing/RecipientModule.ts +++ b/packages/core/src/modules/routing/RecipientModule.ts @@ -4,28 +4,34 @@ import type { OutboundMessage } from '../../types' import type { ConnectionRecord } from '../connections' import type { MediationStateChangedEvent } from './RoutingEvents' import type { MediationRecord } from './index' +import type { GetRoutingOptions } from './services/MediationRecipientService' -import { firstValueFrom, interval, ReplaySubject } from 'rxjs' -import { filter, first, takeUntil, throttleTime, timeout, delay, tap } from 'rxjs/operators' +import { firstValueFrom, interval, ReplaySubject, timer } from 'rxjs' +import { filter, first, takeUntil, throttleTime, timeout, tap, delayWhen } from 'rxjs/operators' import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../agent/AgentConfig' import { Dispatcher } from '../../agent/Dispatcher' import { EventEmitter } from '../../agent/EventEmitter' +import { MessageReceiver } from '../../agent/MessageReceiver' import { MessageSender } from '../../agent/MessageSender' import { createOutboundMessage } from '../../agent/helpers' import { AriesFrameworkError } from '../../error' import { TransportEventTypes } from '../../transport' -import { ConnectionInvitationMessage } from '../connections' import { ConnectionService } from '../connections/services' +import { DidsModule } from '../dids' +import { DiscoverFeaturesModule } from '../discover-features' import { MediatorPickupStrategy } from './MediatorPickupStrategy' import { RoutingEventTypes } from './RoutingEvents' +import { MessageDeliveryHandler, StatusHandler } from './handlers' import { KeylistUpdateResponseHandler } from './handlers/KeylistUpdateResponseHandler' import { MediationDenyHandler } from './handlers/MediationDenyHandler' import { MediationGrantHandler } from './handlers/MediationGrantHandler' +import { StatusRequestMessage } from './messages' import { BatchPickupMessage } from './messages/BatchPickupMessage' import { MediationState } from './models/MediationState' +import { MediationRepository } from './repository' import { MediationRecipientService } from './services/MediationRecipientService' @scoped(Lifecycle.ContainerScoped) @@ -33,24 +39,36 @@ export class RecipientModule { private agentConfig: AgentConfig private mediationRecipientService: MediationRecipientService private connectionService: ConnectionService + private dids: DidsModule private messageSender: MessageSender + private messageReceiver: MessageReceiver private eventEmitter: EventEmitter private logger: Logger + private discoverFeaturesModule: DiscoverFeaturesModule + private mediationRepository: MediationRepository public constructor( dispatcher: Dispatcher, agentConfig: AgentConfig, mediationRecipientService: MediationRecipientService, connectionService: ConnectionService, + dids: DidsModule, messageSender: MessageSender, - eventEmitter: EventEmitter + messageReceiver: MessageReceiver, + eventEmitter: EventEmitter, + discoverFeaturesModule: DiscoverFeaturesModule, + mediationRepository: MediationRepository ) { this.agentConfig = agentConfig this.connectionService = connectionService + this.dids = dids this.mediationRecipientService = mediationRecipientService this.messageSender = messageSender + this.messageReceiver = messageReceiver this.eventEmitter = eventEmitter this.logger = agentConfig.logger + this.discoverFeaturesModule = discoverFeaturesModule + this.mediationRepository = mediationRepository this.registerHandlers(dispatcher) } @@ -92,37 +110,34 @@ export class RecipientModule { } private async openMediationWebSocket(mediator: MediationRecord) { - const { message, connectionRecord } = await this.connectionService.createTrustPing(mediator.connectionId, { + const connection = await this.connectionService.getById(mediator.connectionId) + const { message, connectionRecord } = await this.connectionService.createTrustPing(connection, { responseRequested: false, }) const websocketSchemes = ['ws', 'wss'] - const hasWebSocketTransport = connectionRecord.theirDidDoc?.didCommServices?.some((s) => - websocketSchemes.includes(s.protocolScheme) - ) + const didDocument = connectionRecord.theirDid && (await this.dids.resolveDidDocument(connectionRecord.theirDid)) + const services = didDocument && didDocument?.didCommServices + const hasWebSocketTransport = services && services.some((s) => websocketSchemes.includes(s.protocolScheme)) if (!hasWebSocketTransport) { throw new AriesFrameworkError('Cannot open websocket to connection without websocket service endpoint') } - try { - await this.messageSender.sendMessage(createOutboundMessage(connectionRecord, message), { - transportPriority: { - schemes: websocketSchemes, - restrictive: true, - // TODO: add keepAlive: true to enforce through the public api - // we need to keep the socket alive. It already works this way, but would - // be good to make more explicit from the public facing API. - // This would also make it easier to change the internal API later on. - // keepAlive: true, - }, - }) - } catch (error) { - this.logger.warn('Unable to open websocket connection to mediator', { error }) - } + await this.messageSender.sendMessage(createOutboundMessage(connectionRecord, message), { + transportPriority: { + schemes: websocketSchemes, + restrictive: true, + // TODO: add keepAlive: true to enforce through the public api + // we need to keep the socket alive. It already works this way, but would + // be good to make more explicit from the public facing API. + // This would also make it easier to change the internal API later on. + // keepAlive: true, + }, + }) } - private async initiateImplicitPickup(mediator: MediationRecord) { + private async openWebSocketAndPickUp(mediator: MediationRecord) { let interval = 50 // Listens to Outbound websocket closed events and will reopen the websocket connection @@ -140,45 +155,104 @@ export class RecipientModule { // Increase the interval (recursive back-off) tap(() => (interval *= 2)), // Wait for interval time before reconnecting - delay(interval) + delayWhen(() => timer(interval)) ) .subscribe(async () => { this.logger.warn( `Websocket connection to mediator with connectionId '${mediator.connectionId}' is closed, attempting to reconnect...` ) - this.openMediationWebSocket(mediator) + try { + await this.openMediationWebSocket(mediator) + if (mediator.pickupStrategy === MediatorPickupStrategy.PickUpV2) { + // Start Pickup v2 protocol to receive messages received while websocket offline + await this.sendStatusRequest({ mediatorId: mediator.id }) + } + } catch (error) { + this.logger.warn('Unable to re-open websocket connection to mediator', { error }) + } }) - - await this.openMediationWebSocket(mediator) + try { + await this.openMediationWebSocket(mediator) + } catch (error) { + this.logger.warn('Unable to open websocket connection to mediator', { error }) + } } public async initiateMessagePickup(mediator: MediationRecord) { - const { mediatorPickupStrategy, mediatorPollingInterval } = this.agentConfig - + const { mediatorPollingInterval } = this.agentConfig + const mediatorPickupStrategy = await this.getPickupStrategyForMediator(mediator) const mediatorConnection = await this.connectionService.getById(mediator.connectionId) - // Explicit means polling every X seconds with batch message - if (mediatorPickupStrategy === MediatorPickupStrategy.Explicit) { - this.agentConfig.logger.info(`Starting explicit (batch) pickup of messages from mediator '${mediator.id}'`) - const subscription = interval(mediatorPollingInterval) - .pipe(takeUntil(this.agentConfig.stop$)) - .subscribe(async () => { - await this.pickupMessages(mediatorConnection) - }) - - return subscription + switch (mediatorPickupStrategy) { + case MediatorPickupStrategy.PickUpV2: + this.agentConfig.logger.info(`Starting pickup of messages from mediator '${mediator.id}'`) + await this.openWebSocketAndPickUp(mediator) + await this.sendStatusRequest({ mediatorId: mediator.id }) + break + case MediatorPickupStrategy.PickUpV1: { + // Explicit means polling every X seconds with batch message + this.agentConfig.logger.info(`Starting explicit (batch) pickup of messages from mediator '${mediator.id}'`) + const subscription = interval(mediatorPollingInterval) + .pipe(takeUntil(this.agentConfig.stop$)) + .subscribe(async () => { + await this.pickupMessages(mediatorConnection) + }) + return subscription + } + case MediatorPickupStrategy.Implicit: + // Implicit means sending ping once and keeping connection open. This requires a long-lived transport + // such as WebSockets to work + this.agentConfig.logger.info(`Starting implicit pickup of messages from mediator '${mediator.id}'`) + await this.openWebSocketAndPickUp(mediator) + break + default: + this.agentConfig.logger.info( + `Skipping pickup of messages from mediator '${mediator.id}' due to pickup strategy none` + ) } + } - // Implicit means sending ping once and keeping connection open. This requires a long-lived transport - // such as WebSockets to work - else if (mediatorPickupStrategy === MediatorPickupStrategy.Implicit) { - this.agentConfig.logger.info(`Starting implicit pickup of messages from mediator '${mediator.id}'`) - await this.initiateImplicitPickup(mediator) - } else { - this.agentConfig.logger.info( - `Skipping pickup of messages from mediator '${mediator.id}' due to pickup strategy none` + private async sendStatusRequest(config: { mediatorId: string; recipientKey?: string }) { + const mediationRecord = await this.mediationRecipientService.getById(config.mediatorId) + + const statusRequestMessage = await this.mediationRecipientService.createStatusRequest(mediationRecord, { + recipientKey: config.recipientKey, + }) + + const mediatorConnection = await this.connectionService.getById(mediationRecord.connectionId) + return this.messageSender.sendMessage(createOutboundMessage(mediatorConnection, statusRequestMessage)) + } + + private async getPickupStrategyForMediator(mediator: MediationRecord) { + let mediatorPickupStrategy = mediator.pickupStrategy ?? this.agentConfig.mediatorPickupStrategy + + // If mediator pickup strategy is not configured we try to query if batch pickup + // is supported through the discover features protocol + if (!mediatorPickupStrategy) { + const isPickUpV2Supported = await this.discoverFeaturesModule.isProtocolSupported( + mediator.connectionId, + StatusRequestMessage ) + if (isPickUpV2Supported) { + mediatorPickupStrategy = MediatorPickupStrategy.PickUpV2 + } else { + const isBatchPickupSupported = await this.discoverFeaturesModule.isProtocolSupported( + mediator.connectionId, + BatchPickupMessage + ) + + // Use explicit pickup strategy + mediatorPickupStrategy = isBatchPickupSupported + ? MediatorPickupStrategy.PickUpV1 + : MediatorPickupStrategy.Implicit + } + + // Store the result so it can be reused next time + mediator.pickupStrategy = mediatorPickupStrategy + await this.mediationRepository.update(mediator) } + + return mediatorPickupStrategy } public async discoverMediation() { @@ -263,63 +337,33 @@ export class RecipientModule { return event.payload.mediationRecord } - public async provision(mediatorConnInvite: string) { - this.logger.debug('Provision Mediation with invitation', { invite: mediatorConnInvite }) - // Connect to mediator through provided invitation - // Also requests mediation and sets as default mediator - // Assumption: processInvitation is a URL-encoded invitation - const invitation = await ConnectionInvitationMessage.fromUrl(mediatorConnInvite) - - // Check if invitation has been used already - if (!invitation || !invitation.recipientKeys || !invitation.recipientKeys[0]) { - throw new AriesFrameworkError(`Invalid mediation invitation. Invitation must have at least one recipient key.`) - } - - let mediationRecord: MediationRecord | null = null - - const connection = await this.connectionService.findByInvitationKey(invitation.recipientKeys[0]) - if (!connection) { - this.logger.debug('Mediation Connection does not exist, creating connection') - const routing = await this.mediationRecipientService.getRouting() - - const invitationConnectionRecord = await this.connectionService.processInvitation(invitation, { - autoAcceptConnection: true, - routing, - }) - this.logger.debug('Processed mediation invitation', { - connectionId: invitationConnectionRecord, - }) - const { message, connectionRecord } = await this.connectionService.createRequest(invitationConnectionRecord.id) - const outbound = createOutboundMessage(connectionRecord, message) - await this.messageSender.sendMessage(outbound) - - const completedConnectionRecord = await this.connectionService.returnWhenIsConnected(connectionRecord.id) - this.logger.debug('Connection completed, requesting mediation') - mediationRecord = await this.requestAndAwaitGrant(completedConnectionRecord, 60000) // TODO: put timeout as a config parameter - this.logger.debug('Mediation Granted, setting as default mediator') - await this.setDefaultMediator(mediationRecord) + /** + * Requests mediation for a given connection and sets that as default mediator. + * + * @param connection connection record which will be used for mediation + * @returns mediation record + */ + public async provision(connection: ConnectionRecord) { + this.logger.debug('Connection completed, requesting mediation') + + let mediation = await this.findByConnectionId(connection.id) + if (!mediation) { + this.agentConfig.logger.info(`Requesting mediation for connection ${connection.id}`) + mediation = await this.requestAndAwaitGrant(connection, 60000) // TODO: put timeout as a config parameter + this.logger.debug('Mediation granted, setting as default mediator') + await this.setDefaultMediator(mediation) this.logger.debug('Default mediator set') - } else if (connection && !connection.isReady) { - const connectionRecord = await this.connectionService.returnWhenIsConnected(connection.id) - mediationRecord = await this.requestAndAwaitGrant(connectionRecord, 60000) // TODO: put timeout as a config parameter - await this.setDefaultMediator(mediationRecord) } else { - this.agentConfig.logger.warn('Mediator Invitation in configuration has already been used to create a connection.') - const mediator = await this.findByConnectionId(connection.id) - if (!mediator) { - this.agentConfig.logger.warn('requesting mediation over connection.') - mediationRecord = await this.requestAndAwaitGrant(connection, 60000) // TODO: put timeout as a config parameter - await this.setDefaultMediator(mediationRecord) - } else { - this.agentConfig.logger.warn( - `Mediator Invitation in configuration has already been ${ - mediator.isReady ? 'granted' : 'requested' - } mediation` - ) - } + this.agentConfig.logger.warn( + `Mediator invitation has already been ${mediation.isReady ? 'granted' : 'requested'}` + ) } - return mediationRecord + return mediation + } + + public async getRouting(options: GetRoutingOptions) { + return this.mediationRecipientService.getRouting(options) } // Register handlers for the several messages for the mediator. @@ -327,6 +371,8 @@ export class RecipientModule { dispatcher.registerHandler(new KeylistUpdateResponseHandler(this.mediationRecipientService)) dispatcher.registerHandler(new MediationGrantHandler(this.mediationRecipientService)) dispatcher.registerHandler(new MediationDenyHandler(this.mediationRecipientService)) + dispatcher.registerHandler(new StatusHandler(this.mediationRecipientService)) + dispatcher.registerHandler(new MessageDeliveryHandler(this.mediationRecipientService)) //dispatcher.registerHandler(new KeylistListHandler(this.mediationRecipientService)) // TODO: write this } } diff --git a/packages/core/src/modules/routing/__tests__/mediation.test.ts b/packages/core/src/modules/routing/__tests__/mediation.test.ts index e3b4352671..42265474fa 100644 --- a/packages/core/src/modules/routing/__tests__/mediation.test.ts +++ b/packages/core/src/modules/routing/__tests__/mediation.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SubjectMessage } from '../../../../../../tests/transport/SubjectInboundTransport' import { Subject } from 'rxjs' @@ -6,17 +7,23 @@ import { SubjectInboundTransport } from '../../../../../../tests/transport/Subje import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' import { getBaseConfig, waitForBasicMessage } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' -import { ConnectionRecord } from '../../connections' +import { sleep } from '../../../utils/sleep' +import { ConnectionRecord, HandshakeProtocol } from '../../connections' +import { MediatorPickupStrategy } from '../MediatorPickupStrategy' import { MediationState } from '../models/MediationState' -const recipientConfig = getBaseConfig('Mediation: Recipient') +const recipientConfig = getBaseConfig('Mediation: Recipient', { + indyLedgers: [], +}) const mediatorConfig = getBaseConfig('Mediation: Mediator', { autoAcceptMediationRequests: true, endpoints: ['rxjs:mediator'], + indyLedgers: [], }) const senderConfig = getBaseConfig('Mediation: Sender', { endpoints: ['rxjs:sender'], + indyLedgers: [], }) describe('mediator establishment', () => { @@ -25,15 +32,18 @@ describe('mediator establishment', () => { let senderAgent: Agent afterEach(async () => { - await recipientAgent.shutdown({ - deleteWallet: true, - }) - await mediatorAgent.shutdown({ - deleteWallet: true, - }) - await senderAgent.shutdown({ - deleteWallet: true, - }) + // We want to stop the mediator polling before the agent is shutdown. + // FIXME: add a way to stop mediator polling from the public api, and make sure this is + // being handled in the agent shutdown so we don't get any errors with wallets being closed. + recipientAgent.config.stop$.next(true) + await sleep(1000) + + await recipientAgent?.shutdown() + await recipientAgent?.wallet.delete() + await mediatorAgent?.shutdown() + await mediatorAgent?.wallet.delete() + await senderAgent?.shutdown() + await senderAgent?.wallet.delete() }) test(`Mediation end-to-end flow @@ -55,85 +65,76 @@ describe('mediator establishment', () => { // Initialize mediatorReceived message mediatorAgent = new Agent(mediatorConfig.config, recipientConfig.agentDependencies) - mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(mediatorMessages, subjectMap)) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) await mediatorAgent.initialize() // Create connection to use for recipient - const { - invitation: mediatorInvitation, - connectionRecord: { id: mediatorRecipientConnectionId }, - } = await mediatorAgent.connections.createConnection({ - autoAcceptConnection: true, + const mediatorOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.DidExchange], }) // Initialize recipient with mediation connections invitation recipientAgent = new Agent( { ...recipientConfig.config, - mediatorConnectionsInvite: mediatorInvitation.toUrl({ + mediatorConnectionsInvite: mediatorOutOfBandRecord.outOfBandInvitation.toUrl({ domain: 'https://example.com/ssi', }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }, recipientConfig.agentDependencies ) - recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(recipientMessages, subjectMap)) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) await recipientAgent.initialize() const recipientMediator = await recipientAgent.mediationRecipient.findDefaultMediator() // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain, @typescript-eslint/no-non-null-assertion - const recipientMediatorConnection = await recipientAgent.connections.getById(recipientMediator?.connectionId!) + const recipientMediatorConnection = await recipientAgent.connections.getById(recipientMediator!.connectionId) expect(recipientMediatorConnection).toBeInstanceOf(ConnectionRecord) expect(recipientMediatorConnection?.isReady).toBe(true) - const mediatorRecipientConnection = await mediatorAgent.connections.getById(mediatorRecipientConnectionId) - expect(mediatorRecipientConnection.isReady).toBe(true) + const [mediatorRecipientConnection] = await mediatorAgent.connections.findAllByOutOfBandId( + mediatorOutOfBandRecord.id + ) + expect(mediatorRecipientConnection!.isReady).toBe(true) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(mediatorRecipientConnection).toBeConnectedWith(recipientMediatorConnection!) - expect(recipientMediatorConnection).toBeConnectedWith(mediatorRecipientConnection) + expect(mediatorRecipientConnection).toBeConnectedWith(recipientMediatorConnection) + expect(recipientMediatorConnection).toBeConnectedWith(mediatorRecipientConnection!) expect(recipientMediator?.state).toBe(MediationState.Granted) // Initialize sender agent senderAgent = new Agent(senderConfig.config, senderConfig.agentDependencies) - senderAgent.registerOutboundTransport(new SubjectOutboundTransport(senderMessages, subjectMap)) + senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages)) await senderAgent.initialize() - const { - invitation: recipientInvitation, - connectionRecord: { id: recipientSenderConnectionId }, - } = await recipientAgent.connections.createConnection({ - autoAcceptConnection: true, + const recipientOutOfBandRecord = await recipientAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.Connections], }) + const recipientInvitation = recipientOutOfBandRecord.outOfBandInvitation - const endpoints = mediatorConfig.config.endpoints ?? [] - expect(recipientInvitation.serviceEndpoint).toBe(endpoints[0]) - - let senderRecipientConnection = await senderAgent.connections.receiveInvitationFromUrl( - recipientInvitation.toUrl({ - domain: 'https://example.com/ssi', - }), - { - autoAcceptConnection: true, - } - ) - - const recipientSenderConnection = await recipientAgent.connections.returnWhenIsConnected( - recipientSenderConnectionId + let { connectionRecord: senderRecipientConnection } = await senderAgent.oob.receiveInvitationFromUrl( + recipientInvitation.toUrl({ domain: 'https://example.com/ssi' }) ) - senderRecipientConnection = await senderAgent.connections.getById(senderRecipientConnection.id) + senderRecipientConnection = await senderAgent.connections.returnWhenIsConnected(senderRecipientConnection!.id) + let [recipientSenderConnection] = await recipientAgent.connections.findAllByOutOfBandId(recipientOutOfBandRecord.id) expect(recipientSenderConnection).toBeConnectedWith(senderRecipientConnection) - expect(senderRecipientConnection).toBeConnectedWith(recipientSenderConnection) - - expect(recipientSenderConnection.isReady).toBe(true) + expect(senderRecipientConnection).toBeConnectedWith(recipientSenderConnection!) + expect(recipientSenderConnection!.isReady).toBe(true) expect(senderRecipientConnection.isReady).toBe(true) + recipientSenderConnection = await recipientAgent.connections.returnWhenIsConnected(recipientSenderConnection!.id) + const message = 'hello, world' await senderAgent.basicMessages.sendMessage(senderRecipientConnection.id, message) @@ -156,43 +157,44 @@ describe('mediator establishment', () => { // Initialize mediator mediatorAgent = new Agent(mediatorConfig.config, recipientConfig.agentDependencies) - mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(mediatorMessages, subjectMap)) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) await mediatorAgent.initialize() // Create connection to use for recipient - const { - invitation: mediatorInvitation, - connectionRecord: { id: mediatorRecipientConnectionId }, - } = await mediatorAgent.connections.createConnection({ - autoAcceptConnection: true, + const mediatorOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.Connections], }) // Initialize recipient with mediation connections invitation recipientAgent = new Agent( { ...recipientConfig.config, - mediatorConnectionsInvite: mediatorInvitation.toUrl({ domain: 'https://example.com/ssi' }), + mediatorConnectionsInvite: mediatorOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com/ssi', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }, recipientConfig.agentDependencies ) - recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(recipientMessages, subjectMap)) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) await recipientAgent.initialize() const recipientMediator = await recipientAgent.mediationRecipient.findDefaultMediator() - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain, @typescript-eslint/no-non-null-assertion - const recipientMediatorConnection = await recipientAgent.connections.getById(recipientMediator?.connectionId!) - - expect(recipientMediatorConnection).toBeInstanceOf(ConnectionRecord) + const recipientMediatorConnection = await recipientAgent.connections.getById(recipientMediator!.connectionId) expect(recipientMediatorConnection?.isReady).toBe(true) - const mediatorRecipientConnection = await mediatorAgent.connections.getById(mediatorRecipientConnectionId) - expect(mediatorRecipientConnection.isReady).toBe(true) + const [mediatorRecipientConnection] = await mediatorAgent.connections.findAllByOutOfBandId( + mediatorOutOfBandRecord.id + ) + expect(mediatorRecipientConnection!.isReady).toBe(true) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(mediatorRecipientConnection).toBeConnectedWith(recipientMediatorConnection!) - expect(recipientMediatorConnection).toBeConnectedWith(mediatorRecipientConnection) + expect(recipientMediatorConnection).toBeConnectedWith(mediatorRecipientConnection!) expect(recipientMediator?.state).toBe(MediationState.Granted) @@ -201,51 +203,42 @@ describe('mediator establishment', () => { recipientAgent = new Agent( { ...recipientConfig.config, - mediatorConnectionsInvite: mediatorInvitation.toUrl({ + mediatorConnectionsInvite: mediatorOutOfBandRecord.outOfBandInvitation.toUrl({ domain: 'https://example.com/ssi', }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }, recipientConfig.agentDependencies ) - recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(recipientMessages, subjectMap)) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) await recipientAgent.initialize() // Initialize sender agent senderAgent = new Agent(senderConfig.config, senderConfig.agentDependencies) - senderAgent.registerOutboundTransport(new SubjectOutboundTransport(senderMessages, subjectMap)) + senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages)) await senderAgent.initialize() - const { - invitation: recipientInvitation, - connectionRecord: { id: recipientSenderConnectionId }, - } = await recipientAgent.connections.createConnection({ - autoAcceptConnection: true, + const recipientOutOfBandRecord = await recipientAgent.oob.createInvitation({ + label: 'mediator invitation', + handshake: true, + handshakeProtocols: [HandshakeProtocol.Connections], }) + const recipientInvitation = recipientOutOfBandRecord.outOfBandInvitation - const endpoints = mediatorConfig.config.endpoints ?? [] - expect(recipientInvitation.serviceEndpoint).toBe(endpoints[0]) - - let senderRecipientConnection = await senderAgent.connections.receiveInvitationFromUrl( - recipientInvitation.toUrl({ - domain: 'https://example.com/ssi', - }), - { - autoAcceptConnection: true, - } + let { connectionRecord: senderRecipientConnection } = await senderAgent.oob.receiveInvitationFromUrl( + recipientInvitation.toUrl({ domain: 'https://example.com/ssi' }) ) - const recipientSenderConnection = await recipientAgent.connections.returnWhenIsConnected( - recipientSenderConnectionId + senderRecipientConnection = await senderAgent.connections.returnWhenIsConnected(senderRecipientConnection!.id) + const [recipientSenderConnection] = await recipientAgent.connections.findAllByOutOfBandId( + recipientOutOfBandRecord.id ) - - senderRecipientConnection = await senderAgent.connections.getById(senderRecipientConnection.id) - expect(recipientSenderConnection).toBeConnectedWith(senderRecipientConnection) - expect(senderRecipientConnection).toBeConnectedWith(recipientSenderConnection) + expect(senderRecipientConnection).toBeConnectedWith(recipientSenderConnection!) - expect(recipientSenderConnection.isReady).toBe(true) + expect(recipientSenderConnection!.isReady).toBe(true) expect(senderRecipientConnection.isReady).toBe(true) const message = 'hello, world' diff --git a/packages/core/src/modules/routing/__tests__/mediationRecipient.test.ts b/packages/core/src/modules/routing/__tests__/mediationRecipient.test.ts new file mode 100644 index 0000000000..e46829ade0 --- /dev/null +++ b/packages/core/src/modules/routing/__tests__/mediationRecipient.test.ts @@ -0,0 +1,264 @@ +import type { Wallet } from '../../../wallet/Wallet' + +import { getAgentConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' +import { EventEmitter } from '../../../agent/EventEmitter' +import { AgentEventTypes } from '../../../agent/Events' +import { MessageSender } from '../../../agent/MessageSender' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { Attachment } from '../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../error' +import { IndyWallet } from '../../../wallet/IndyWallet' +import { ConnectionRepository, DidExchangeState } from '../../connections' +import { ConnectionService } from '../../connections/services/ConnectionService' +import { DidRepository } from '../../dids/repository' +import { DeliveryRequestMessage, MessageDeliveryMessage, MessagesReceivedMessage, StatusMessage } from '../messages' +import { MediationRole, MediationState } from '../models' +import { MediationRecord, MediationRepository } from '../repository' +import { MediationRecipientService } from '../services' + +jest.mock('../repository/MediationRepository') +const MediationRepositoryMock = MediationRepository as jest.Mock + +jest.mock('../../connections/repository/ConnectionRepository') +const ConnectionRepositoryMock = ConnectionRepository as jest.Mock + +jest.mock('../../dids/repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock + +jest.mock('../../../agent/EventEmitter') +const EventEmitterMock = EventEmitter as jest.Mock + +jest.mock('../../../agent/MessageSender') +const MessageSenderMock = MessageSender as jest.Mock + +const connectionImageUrl = 'https://example.com/image.png' + +const mockConnection = getMockConnection({ + state: DidExchangeState.Completed, +}) + +describe('MediationRecipientService', () => { + const config = getAgentConfig('MediationRecipientServiceTest', { + endpoints: ['http://agent.com:8080'], + connectionImageUrl, + }) + + let wallet: Wallet + let mediationRepository: MediationRepository + let didRepository: DidRepository + let eventEmitter: EventEmitter + let connectionService: ConnectionService + let connectionRepository: ConnectionRepository + let messageSender: MessageSender + let mediationRecipientService: MediationRecipientService + let mediationRecord: MediationRecord + + beforeAll(async () => { + wallet = new IndyWallet(config) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(config.walletConfig!) + }) + + afterAll(async () => { + await wallet.delete() + }) + + beforeEach(async () => { + eventEmitter = new EventEmitterMock() + connectionRepository = new ConnectionRepositoryMock() + didRepository = new DidRepositoryMock() + connectionService = new ConnectionService(wallet, config, connectionRepository, didRepository, eventEmitter) + mediationRepository = new MediationRepositoryMock() + messageSender = new MessageSenderMock() + + // Mock default return value + mediationRecord = new MediationRecord({ + connectionId: 'connectionId', + role: MediationRole.Recipient, + state: MediationState.Granted, + threadId: 'threadId', + }) + mockFunction(mediationRepository.getByConnectionId).mockResolvedValue(mediationRecord) + + mediationRecipientService = new MediationRecipientService( + wallet, + connectionService, + messageSender, + config, + mediationRepository, + eventEmitter + ) + }) + + describe('createStatusRequest', () => { + it('creates a status request message', async () => { + const statusRequestMessage = await mediationRecipientService.createStatusRequest(mediationRecord, { + recipientKey: 'a-key', + }) + + expect(statusRequestMessage).toMatchObject({ + id: expect.any(String), + recipientKey: 'a-key', + }) + }) + + it('it throws an error when the mediation record has incorrect role or state', async () => { + mediationRecord.role = MediationRole.Mediator + await expect(mediationRecipientService.createStatusRequest(mediationRecord)).rejects.toThrowError( + 'Mediation record has invalid role MEDIATOR. Expected role RECIPIENT.' + ) + + mediationRecord.role = MediationRole.Recipient + mediationRecord.state = MediationState.Requested + + await expect(mediationRecipientService.createStatusRequest(mediationRecord)).rejects.toThrowError( + 'Mediation record is not ready to be used. Expected granted, found invalid state requested' + ) + }) + }) + + describe('processStatus', () => { + it('if status request has a message count of zero returns nothing', async () => { + const status = new StatusMessage({ + messageCount: 0, + }) + + const messageContext = new InboundMessageContext(status, { connection: mockConnection }) + const deliveryRequestMessage = await mediationRecipientService.processStatus(messageContext) + expect(deliveryRequestMessage).toBeNull() + }) + + it('if it has a message count greater than zero return a valid delivery request', async () => { + const status = new StatusMessage({ + messageCount: 1, + }) + const messageContext = new InboundMessageContext(status, { connection: mockConnection }) + + const deliveryRequestMessage = await mediationRecipientService.processStatus(messageContext) + expect(deliveryRequestMessage) + expect(deliveryRequestMessage).toEqual(new DeliveryRequestMessage({ id: deliveryRequestMessage?.id, limit: 1 })) + }) + + it('it throws an error when the mediation record has incorrect role or state', async () => { + const status = new StatusMessage({ + messageCount: 1, + }) + const messageContext = new InboundMessageContext(status, { connection: mockConnection }) + + mediationRecord.role = MediationRole.Mediator + await expect(mediationRecipientService.processStatus(messageContext)).rejects.toThrowError( + 'Mediation record has invalid role MEDIATOR. Expected role RECIPIENT.' + ) + + mediationRecord.role = MediationRole.Recipient + mediationRecord.state = MediationState.Requested + + await expect(mediationRecipientService.processStatus(messageContext)).rejects.toThrowError( + 'Mediation record is not ready to be used. Expected granted, found invalid state requested' + ) + }) + }) + + describe('processDelivery', () => { + it('if the delivery has no attachments expect an error', async () => { + const messageContext = new InboundMessageContext({} as MessageDeliveryMessage, { connection: mockConnection }) + + await expect(mediationRecipientService.processDelivery(messageContext)).rejects.toThrowError( + new AriesFrameworkError('Error processing attachments') + ) + }) + + it('should return a message received with an message id list in it', async () => { + const messageDeliveryMessage = new MessageDeliveryMessage({ + attachments: [ + new Attachment({ + id: '1', + data: { + json: { + a: 'value', + }, + }, + }), + ], + }) + const messageContext = new InboundMessageContext(messageDeliveryMessage, { connection: mockConnection }) + + const messagesReceivedMessage = await mediationRecipientService.processDelivery(messageContext) + + expect(messagesReceivedMessage).toEqual( + new MessagesReceivedMessage({ + id: messagesReceivedMessage.id, + messageIdList: ['1'], + }) + ) + }) + + it('calls the event emitter for each message', async () => { + const messageDeliveryMessage = new MessageDeliveryMessage({ + attachments: [ + new Attachment({ + id: '1', + data: { + json: { + first: 'value', + }, + }, + }), + new Attachment({ + id: '2', + data: { + json: { + second: 'value', + }, + }, + }), + ], + }) + const messageContext = new InboundMessageContext(messageDeliveryMessage, { connection: mockConnection }) + + await mediationRecipientService.processDelivery(messageContext) + + expect(eventEmitter.emit).toHaveBeenCalledTimes(2) + expect(eventEmitter.emit).toHaveBeenNthCalledWith(1, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: { first: 'value' }, + }, + }) + expect(eventEmitter.emit).toHaveBeenNthCalledWith(2, { + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: { second: 'value' }, + }, + }) + }) + + it('it throws an error when the mediation record has incorrect role or state', async () => { + const messageDeliveryMessage = new MessageDeliveryMessage({ + attachments: [ + new Attachment({ + id: '1', + data: { + json: { + a: 'value', + }, + }, + }), + ], + }) + const messageContext = new InboundMessageContext(messageDeliveryMessage, { connection: mockConnection }) + + mediationRecord.role = MediationRole.Mediator + await expect(mediationRecipientService.processDelivery(messageContext)).rejects.toThrowError( + 'Mediation record has invalid role MEDIATOR. Expected role RECIPIENT.' + ) + + mediationRecord.role = MediationRole.Recipient + mediationRecord.state = MediationState.Requested + + await expect(mediationRecipientService.processDelivery(messageContext)).rejects.toThrowError( + 'Mediation record is not ready to be used. Expected granted, found invalid state requested' + ) + }) + }) +}) diff --git a/packages/core/src/modules/routing/error/RoutingProblemReportReason.ts b/packages/core/src/modules/routing/error/RoutingProblemReportReason.ts new file mode 100644 index 0000000000..be5b373257 --- /dev/null +++ b/packages/core/src/modules/routing/error/RoutingProblemReportReason.ts @@ -0,0 +1,3 @@ +export enum RoutingProblemReportReason { + ErrorProcessingAttachments = 'error-processing-attachments', +} diff --git a/packages/core/src/modules/routing/error/index.ts b/packages/core/src/modules/routing/error/index.ts new file mode 100644 index 0000000000..d117e1d699 --- /dev/null +++ b/packages/core/src/modules/routing/error/index.ts @@ -0,0 +1 @@ +export * from './RoutingProblemReportReason' diff --git a/packages/core/src/modules/routing/handlers/ForwardHandler.ts b/packages/core/src/modules/routing/handlers/ForwardHandler.ts index 217bfae8db..8755f8c1f1 100644 --- a/packages/core/src/modules/routing/handlers/ForwardHandler.ts +++ b/packages/core/src/modules/routing/handlers/ForwardHandler.ts @@ -23,12 +23,12 @@ export class ForwardHandler implements Handler { } public async handle(messageContext: HandlerInboundMessage) { - const { packedMessage, mediationRecord } = await this.mediatorService.processForwardMessage(messageContext) + const { encryptedMessage, mediationRecord } = await this.mediatorService.processForwardMessage(messageContext) const connectionRecord = await this.connectionService.getById(mediationRecord.connectionId) // The message inside the forward message is packed so we just send the packed // message to the connection associated with it - await this.messageSender.sendPackage({ connection: connectionRecord, packedMessage }) + await this.messageSender.sendPackage({ connection: connectionRecord, encryptedMessage }) } } diff --git a/packages/core/src/modules/routing/handlers/KeylistUpdateResponseHandler.ts b/packages/core/src/modules/routing/handlers/KeylistUpdateResponseHandler.ts index 193ad60ff7..23a0c4a96f 100644 --- a/packages/core/src/modules/routing/handlers/KeylistUpdateResponseHandler.ts +++ b/packages/core/src/modules/routing/handlers/KeylistUpdateResponseHandler.ts @@ -13,7 +13,7 @@ export class KeylistUpdateResponseHandler implements Handler { public async handle(messageContext: HandlerInboundMessage) { if (!messageContext.connection) { - throw new Error(`Connection for verkey ${messageContext.recipientVerkey} not found!`) + throw new Error(`Connection for verkey ${messageContext.recipientKey} not found!`) } return await this.mediationRecipientService.processKeylistUpdateResults(messageContext) } diff --git a/packages/core/src/modules/routing/handlers/MediationDenyHandler.ts b/packages/core/src/modules/routing/handlers/MediationDenyHandler.ts index ec1413640a..fa32169a7b 100644 --- a/packages/core/src/modules/routing/handlers/MediationDenyHandler.ts +++ b/packages/core/src/modules/routing/handlers/MediationDenyHandler.ts @@ -13,7 +13,7 @@ export class MediationDenyHandler implements Handler { public async handle(messageContext: HandlerInboundMessage) { if (!messageContext.connection) { - throw new Error(`Connection for verkey ${messageContext.recipientVerkey} not found!`) + throw new Error(`Connection for verkey ${messageContext.recipientKey} not found!`) } await this.mediationRecipientService.processMediationDeny(messageContext) } diff --git a/packages/core/src/modules/routing/handlers/MediationGrantHandler.ts b/packages/core/src/modules/routing/handlers/MediationGrantHandler.ts index a5bed233ed..5706216fbb 100644 --- a/packages/core/src/modules/routing/handlers/MediationGrantHandler.ts +++ b/packages/core/src/modules/routing/handlers/MediationGrantHandler.ts @@ -13,7 +13,7 @@ export class MediationGrantHandler implements Handler { public async handle(messageContext: HandlerInboundMessage) { if (!messageContext.connection) { - throw new Error(`Connection for verkey ${messageContext.recipientVerkey} not found!`) + throw new Error(`Connection for key ${messageContext.recipientKey} not found!`) } await this.mediationRecipientService.processMediationGrant(messageContext) } diff --git a/packages/core/src/modules/routing/handlers/MediationRequestHandler.ts b/packages/core/src/modules/routing/handlers/MediationRequestHandler.ts index 67a0b4b864..9a4b90ca7c 100644 --- a/packages/core/src/modules/routing/handlers/MediationRequestHandler.ts +++ b/packages/core/src/modules/routing/handlers/MediationRequestHandler.ts @@ -18,7 +18,7 @@ export class MediationRequestHandler implements Handler { public async handle(messageContext: HandlerInboundMessage) { if (!messageContext.connection) { - throw new AriesFrameworkError(`Connection for verkey ${messageContext.recipientVerkey} not found!`) + throw new AriesFrameworkError(`Connection for verkey ${messageContext.recipientKey} not found!`) } const mediationRecord = await this.mediatorService.processMediationRequest(messageContext) diff --git a/packages/core/src/modules/routing/handlers/MessageDeliveryHandler.ts b/packages/core/src/modules/routing/handlers/MessageDeliveryHandler.ts new file mode 100644 index 0000000000..1eb11ed0ed --- /dev/null +++ b/packages/core/src/modules/routing/handlers/MessageDeliveryHandler.ts @@ -0,0 +1,24 @@ +import type { Handler } from '../../../agent/Handler' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { MediationRecipientService } from '../services' + +import { createOutboundMessage } from '../../../agent/helpers' +import { MessageDeliveryMessage } from '../messages' + +export class MessageDeliveryHandler implements Handler { + public supportedMessages = [MessageDeliveryMessage] + private mediationRecipientService: MediationRecipientService + + public constructor(mediationRecipientService: MediationRecipientService) { + this.mediationRecipientService = mediationRecipientService + } + + public async handle(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const deliveryReceivedMessage = await this.mediationRecipientService.processDelivery(messageContext) + + if (deliveryReceivedMessage) { + return createOutboundMessage(connection, deliveryReceivedMessage) + } + } +} diff --git a/packages/core/src/modules/routing/handlers/StatusHandler.ts b/packages/core/src/modules/routing/handlers/StatusHandler.ts new file mode 100644 index 0000000000..b3ea61fe3d --- /dev/null +++ b/packages/core/src/modules/routing/handlers/StatusHandler.ts @@ -0,0 +1,24 @@ +import type { Handler } from '../../../agent/Handler' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { MediationRecipientService } from '../services' + +import { createOutboundMessage } from '../../../agent/helpers' +import { StatusMessage } from '../messages/StatusMessage' + +export class StatusHandler implements Handler { + public supportedMessages = [StatusMessage] + private mediatorRecipientService: MediationRecipientService + + public constructor(mediatorRecipientService: MediationRecipientService) { + this.mediatorRecipientService = mediatorRecipientService + } + + public async handle(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const deliveryRequestMessage = await this.mediatorRecipientService.processStatus(messageContext) + + if (deliveryRequestMessage) { + return createOutboundMessage(connection, deliveryRequestMessage) + } + } +} diff --git a/packages/core/src/modules/routing/handlers/index.ts b/packages/core/src/modules/routing/handlers/index.ts index 52096e760b..0c8f48dc41 100644 --- a/packages/core/src/modules/routing/handlers/index.ts +++ b/packages/core/src/modules/routing/handlers/index.ts @@ -3,3 +3,5 @@ export * from './KeylistUpdateHandler' export * from './BatchHandler' export * from './BatchPickupHandler' export * from './KeylistUpdateResponseHandler' +export * from './StatusHandler' +export * from './MessageDeliveryHandler' diff --git a/packages/core/src/modules/routing/messages/BatchMessage.ts b/packages/core/src/modules/routing/messages/BatchMessage.ts index 3163475007..44f021edc0 100644 --- a/packages/core/src/modules/routing/messages/BatchMessage.ts +++ b/packages/core/src/modules/routing/messages/BatchMessage.ts @@ -1,13 +1,14 @@ import { Type, Expose } from 'class-transformer' -import { Equals, Matches, IsArray, ValidateNested, IsObject, IsInstance } from 'class-validator' +import { Matches, IsArray, ValidateNested, IsObject, IsInstance } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' import { MessageIdRegExp } from '../../../agent/BaseMessage' -import { WireMessage } from '../../../types' +import { EncryptedMessage } from '../../../types' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' import { uuid } from '../../../utils/uuid' export class BatchMessageMessage { - public constructor(options: { id?: string; message: WireMessage }) { + public constructor(options: { id?: string; message: EncryptedMessage }) { if (options) { this.id = options.id || uuid() this.message = options.message @@ -18,7 +19,7 @@ export class BatchMessageMessage { public id!: string @IsObject() - public message!: WireMessage + public message!: EncryptedMessage } export interface BatchMessageOptions { @@ -41,9 +42,9 @@ export class BatchMessage extends AgentMessage { } } - @Equals(BatchMessage.type) - public readonly type = BatchMessage.type - public static readonly type = 'https://didcomm.org/messagepickup/1.0/batch' + @IsValidMessageType(BatchMessage.type) + public readonly type = BatchMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/1.0/batch') @Type(() => BatchMessageMessage) @IsArray() diff --git a/packages/core/src/modules/routing/messages/BatchPickupMessage.ts b/packages/core/src/modules/routing/messages/BatchPickupMessage.ts index 27f98cc970..83952cedeb 100644 --- a/packages/core/src/modules/routing/messages/BatchPickupMessage.ts +++ b/packages/core/src/modules/routing/messages/BatchPickupMessage.ts @@ -1,7 +1,8 @@ import { Expose } from 'class-transformer' -import { Equals, IsInt } from 'class-validator' +import { IsInt } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface BatchPickupMessageOptions { id?: string @@ -28,9 +29,9 @@ export class BatchPickupMessage extends AgentMessage { } } - @Equals(BatchPickupMessage.type) - public readonly type = BatchPickupMessage.type - public static readonly type = 'https://didcomm.org/messagepickup/1.0/batch-pickup' + @IsValidMessageType(BatchPickupMessage.type) + public readonly type = BatchPickupMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/1.0/batch-pickup') @IsInt() @Expose({ name: 'batch_size' }) diff --git a/packages/core/src/modules/routing/messages/DeliveryRequestMessage.ts b/packages/core/src/modules/routing/messages/DeliveryRequestMessage.ts new file mode 100644 index 0000000000..fc190a8603 --- /dev/null +++ b/packages/core/src/modules/routing/messages/DeliveryRequestMessage.ts @@ -0,0 +1,37 @@ +import { Expose } from 'class-transformer' +import { IsInt, IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DeliveryRequestMessageOptions { + id?: string + recipientKey?: string + limit: number +} + +export class DeliveryRequestMessage extends AgentMessage { + public constructor(options: DeliveryRequestMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.recipientKey = options.recipientKey + this.limit = options.limit + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(DeliveryRequestMessage.type) + public readonly type = DeliveryRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/delivery-request') + + @IsString() + @IsOptional() + @Expose({ name: 'recipient_key' }) + public recipientKey?: string + + @IsInt() + public limit!: number +} diff --git a/packages/core/src/modules/routing/messages/ForwardMessage.ts b/packages/core/src/modules/routing/messages/ForwardMessage.ts index adf5df99e8..9b4d6d658c 100644 --- a/packages/core/src/modules/routing/messages/ForwardMessage.ts +++ b/packages/core/src/modules/routing/messages/ForwardMessage.ts @@ -1,13 +1,14 @@ import { Expose } from 'class-transformer' -import { Equals, IsObject, IsString } from 'class-validator' +import { IsObject, IsString } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' -import { WireMessage } from '../../../types' +import { EncryptedMessage } from '../../../types' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface ForwardMessageOptions { id?: string to: string - message: WireMessage + message: EncryptedMessage } /** @@ -29,14 +30,14 @@ export class ForwardMessage extends AgentMessage { } } - @Equals(ForwardMessage.type) - public readonly type = ForwardMessage.type - public static readonly type = 'https://didcomm.org/routing/1.0/forward' + @IsValidMessageType(ForwardMessage.type) + public readonly type = ForwardMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/routing/1.0/forward') @IsString() public to!: string @Expose({ name: 'msg' }) @IsObject() - public message!: WireMessage + public message!: EncryptedMessage } diff --git a/packages/core/src/modules/routing/messages/KeylistMessage.ts b/packages/core/src/modules/routing/messages/KeylistMessage.ts index fe7c23ea4a..7e58af9ac4 100644 --- a/packages/core/src/modules/routing/messages/KeylistMessage.ts +++ b/packages/core/src/modules/routing/messages/KeylistMessage.ts @@ -1,7 +1,8 @@ import { Type } from 'class-transformer' -import { Equals, IsArray, ValidateNested } from 'class-validator' +import { IsArray, ValidateNested } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface KeylistMessageOptions { id?: string @@ -21,9 +22,9 @@ export class KeylistMessage extends AgentMessage { } } - @Equals(KeylistMessage.type) - public readonly type = KeylistMessage.type - public static readonly type = 'https://didcomm.org/coordinate-mediation/1.0/keylist' + @IsValidMessageType(KeylistMessage.type) + public readonly type = KeylistMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/keylist') @Type(() => Keylist) @IsArray() diff --git a/packages/core/src/modules/routing/messages/KeylistUpdateMessage.ts b/packages/core/src/modules/routing/messages/KeylistUpdateMessage.ts index 7e88a22c81..e17a9edf79 100644 --- a/packages/core/src/modules/routing/messages/KeylistUpdateMessage.ts +++ b/packages/core/src/modules/routing/messages/KeylistUpdateMessage.ts @@ -1,8 +1,9 @@ import { Expose, Type } from 'class-transformer' -import { Equals, IsArray, ValidateNested, IsString, IsEnum, IsInstance } from 'class-validator' +import { IsArray, ValidateNested, IsString, IsEnum, IsInstance } from 'class-validator' import { Verkey } from 'indy-sdk' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export enum KeylistUpdateAction { add = 'add', @@ -45,9 +46,9 @@ export class KeylistUpdateMessage extends AgentMessage { } } - @Equals(KeylistUpdateMessage.type) - public readonly type = KeylistUpdateMessage.type - public static readonly type = 'https://didcomm.org/coordinate-mediation/1.0/keylist-update' + @IsValidMessageType(KeylistUpdateMessage.type) + public readonly type = KeylistUpdateMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/keylist-update') @Type(() => KeylistUpdate) @IsArray() diff --git a/packages/core/src/modules/routing/messages/KeylistUpdateResponseMessage.ts b/packages/core/src/modules/routing/messages/KeylistUpdateResponseMessage.ts index 7cab8a1376..7367184e7a 100644 --- a/packages/core/src/modules/routing/messages/KeylistUpdateResponseMessage.ts +++ b/packages/core/src/modules/routing/messages/KeylistUpdateResponseMessage.ts @@ -1,8 +1,9 @@ import { Expose, Type } from 'class-transformer' -import { Equals, IsArray, IsEnum, IsInstance, IsString, ValidateNested } from 'class-validator' +import { IsArray, IsEnum, IsInstance, IsString, ValidateNested } from 'class-validator' import { Verkey } from 'indy-sdk' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' import { KeylistUpdateAction } from './KeylistUpdateMessage' @@ -57,9 +58,9 @@ export class KeylistUpdateResponseMessage extends AgentMessage { } } - @Equals(KeylistUpdateResponseMessage.type) - public readonly type = KeylistUpdateResponseMessage.type - public static readonly type = 'https://didcomm.org/coordinate-mediation/1.0/keylist-update-response' + @IsValidMessageType(KeylistUpdateResponseMessage.type) + public readonly type = KeylistUpdateResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/keylist-update-response') @Type(() => KeylistUpdated) @IsArray() diff --git a/packages/core/src/modules/routing/messages/MediationDenyMessage.ts b/packages/core/src/modules/routing/messages/MediationDenyMessage.ts index a9da9538ab..30f0868ff7 100644 --- a/packages/core/src/modules/routing/messages/MediationDenyMessage.ts +++ b/packages/core/src/modules/routing/messages/MediationDenyMessage.ts @@ -1,6 +1,5 @@ -import { Equals } from 'class-validator' - import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface MediationDenyMessageOptions { id: string @@ -20,7 +19,7 @@ export class MediationDenyMessage extends AgentMessage { } } - @Equals(MediationDenyMessage.type) - public readonly type = MediationDenyMessage.type - public static readonly type = 'https://didcomm.org/coordinate-mediation/1.0/mediate-deny' + @IsValidMessageType(MediationDenyMessage.type) + public readonly type = MediationDenyMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/mediate-deny') } diff --git a/packages/core/src/modules/routing/messages/MediationGrantMessage.ts b/packages/core/src/modules/routing/messages/MediationGrantMessage.ts index 3131ebc13e..baebf09913 100644 --- a/packages/core/src/modules/routing/messages/MediationGrantMessage.ts +++ b/packages/core/src/modules/routing/messages/MediationGrantMessage.ts @@ -1,7 +1,8 @@ import { Expose } from 'class-transformer' -import { Equals, IsArray, IsNotEmpty, IsString } from 'class-validator' +import { IsArray, IsNotEmpty, IsString } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface MediationGrantMessageOptions { id?: string @@ -30,9 +31,9 @@ export class MediationGrantMessage extends AgentMessage { } } - @Equals(MediationGrantMessage.type) - public readonly type = MediationGrantMessage.type - public static readonly type = 'https://didcomm.org/coordinate-mediation/1.0/mediate-grant' + @IsValidMessageType(MediationGrantMessage.type) + public readonly type = MediationGrantMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/mediate-grant') @IsNotEmpty() @IsArray() diff --git a/packages/core/src/modules/routing/messages/MediationRequestMessage.ts b/packages/core/src/modules/routing/messages/MediationRequestMessage.ts index ac529a801a..788ebd3044 100644 --- a/packages/core/src/modules/routing/messages/MediationRequestMessage.ts +++ b/packages/core/src/modules/routing/messages/MediationRequestMessage.ts @@ -1,7 +1,5 @@ -import { Expose, Type } from 'class-transformer' -import { Equals, IsDate } from 'class-validator' - import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' export interface MediationRequestMessageOptions { sentTime?: Date @@ -26,17 +24,11 @@ export class MediationRequestMessage extends AgentMessage { if (options) { this.id = options.id || this.generateId() - this.sentTime = options.sentTime || new Date() this.addLocale(options.locale || 'en') } } - @Equals(MediationRequestMessage.type) - public readonly type = MediationRequestMessage.type - public static readonly type = 'https://didcomm.org/coordinate-mediation/1.0/mediate-request' - - @Expose({ name: 'sent_time' }) - @Type(() => Date) - @IsDate() - public sentTime!: Date + @IsValidMessageType(MediationRequestMessage.type) + public readonly type = MediationRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/coordinate-mediation/1.0/mediate-request') } diff --git a/packages/core/src/modules/routing/messages/MessageDeliveryMessage.ts b/packages/core/src/modules/routing/messages/MessageDeliveryMessage.ts new file mode 100644 index 0000000000..c3ebf34a16 --- /dev/null +++ b/packages/core/src/modules/routing/messages/MessageDeliveryMessage.ts @@ -0,0 +1,36 @@ +import type { Attachment } from '../../../decorators/attachment/Attachment' + +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface MessageDeliveryMessageOptions { + id?: string + recipientKey?: string + attachments: Attachment[] +} + +export class MessageDeliveryMessage extends AgentMessage { + public constructor(options: MessageDeliveryMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.recipientKey = options.recipientKey + this.appendedAttachments = options.attachments + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(MessageDeliveryMessage.type) + public readonly type = MessageDeliveryMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/delivery') + + @IsString() + @IsOptional() + @Expose({ name: 'recipient_key' }) + public recipientKey?: string +} diff --git a/packages/core/src/modules/routing/messages/MessagesReceivedMessage.ts b/packages/core/src/modules/routing/messages/MessagesReceivedMessage.ts new file mode 100644 index 0000000000..84edf6f920 --- /dev/null +++ b/packages/core/src/modules/routing/messages/MessagesReceivedMessage.ts @@ -0,0 +1,32 @@ +import { Expose } from 'class-transformer' +import { IsArray, IsOptional } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface MessagesReceivedMessageOptions { + id?: string + messageIdList: string[] +} + +export class MessagesReceivedMessage extends AgentMessage { + public constructor(options: MessagesReceivedMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.messageIdList = options.messageIdList + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(MessagesReceivedMessage.type) + public readonly type = MessagesReceivedMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/messages-received') + + @IsArray() + @IsOptional() + @Expose({ name: 'message_id_list' }) + public messageIdList?: string[] +} diff --git a/packages/core/src/modules/routing/messages/StatusMessage.ts b/packages/core/src/modules/routing/messages/StatusMessage.ts new file mode 100644 index 0000000000..467806767d --- /dev/null +++ b/packages/core/src/modules/routing/messages/StatusMessage.ts @@ -0,0 +1,75 @@ +import { Expose, Transform } from 'class-transformer' +import { IsBoolean, IsDate, IsInt, IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { ReturnRouteTypes } from '../../../decorators/transport/TransportDecorator' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { DateParser } from '../../../utils/transformers' + +export interface StatusMessageOptions { + id?: string + recipientKey?: string + messageCount: number + longestWaitedSeconds?: number + newestReceivedTime?: Date + oldestReceivedTime?: Date + totalBytes?: number + liveDelivery?: boolean +} + +export class StatusMessage extends AgentMessage { + public constructor(options: StatusMessageOptions) { + super() + if (options) { + this.id = options.id || this.generateId() + this.recipientKey = options.recipientKey + this.messageCount = options.messageCount + this.longestWaitedSeconds = options.longestWaitedSeconds + this.newestReceivedTime = options.newestReceivedTime + this.oldestReceivedTime = options.oldestReceivedTime + this.totalBytes = options.totalBytes + this.liveDelivery = options.liveDelivery + } + this.setReturnRouting(ReturnRouteTypes.all) + } + + @IsValidMessageType(StatusMessage.type) + public readonly type = StatusMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/status') + + @IsString() + @IsOptional() + @Expose({ name: 'recipient_key' }) + public recipientKey?: string + + @IsInt() + @Expose({ name: 'message_count' }) + public messageCount!: number + + @IsInt() + @IsOptional() + @Expose({ name: 'longest_waited_seconds' }) + public longestWaitedSeconds?: number + + @Expose({ name: 'newest_received_time' }) + @Transform(({ value }) => DateParser(value)) + @IsDate() + @IsOptional() + public newestReceivedTime?: Date + + @IsOptional() + @Transform(({ value }) => DateParser(value)) + @IsDate() + @Expose({ name: 'oldest_received_time' }) + public oldestReceivedTime?: Date + + @IsOptional() + @IsInt() + @Expose({ name: 'total_bytes' }) + public totalBytes?: number + + @IsOptional() + @IsBoolean() + @Expose({ name: 'live_delivery' }) + public liveDelivery?: boolean +} diff --git a/packages/core/src/modules/routing/messages/StatusRequestMessage.ts b/packages/core/src/modules/routing/messages/StatusRequestMessage.ts new file mode 100644 index 0000000000..1ab1ffbe89 --- /dev/null +++ b/packages/core/src/modules/routing/messages/StatusRequestMessage.ts @@ -0,0 +1,30 @@ +import { Expose } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface StatusRequestMessageOptions { + id?: string + recipientKey?: string +} + +export class StatusRequestMessage extends AgentMessage { + public constructor(options: StatusRequestMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.recipientKey = options.recipientKey + } + } + + @IsValidMessageType(StatusRequestMessage.type) + public readonly type = StatusRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/messagepickup/2.0/status-request') + + @IsString() + @IsOptional() + @Expose({ name: 'recipient_key' }) + public recipientKey?: string +} diff --git a/packages/core/src/modules/routing/messages/index.ts b/packages/core/src/modules/routing/messages/index.ts index b267aeacf1..5859a18cd5 100644 --- a/packages/core/src/modules/routing/messages/index.ts +++ b/packages/core/src/modules/routing/messages/index.ts @@ -6,3 +6,8 @@ export * from './KeylistUpdateResponseMessage' export * from './MediationGrantMessage' export * from './MediationDenyMessage' export * from './MediationRequestMessage' +export * from './DeliveryRequestMessage' +export * from './StatusMessage' +export * from './StatusRequestMessage' +export * from './MessageDeliveryMessage' +export * from './MessagesReceivedMessage' diff --git a/packages/core/src/modules/routing/repository/MediationRecord.ts b/packages/core/src/modules/routing/repository/MediationRecord.ts index 47b43e8175..24115007b8 100644 --- a/packages/core/src/modules/routing/repository/MediationRecord.ts +++ b/packages/core/src/modules/routing/repository/MediationRecord.ts @@ -1,8 +1,11 @@ import type { MediationRole } from '../models/MediationRole' +import { Transform } from 'class-transformer' + import { AriesFrameworkError } from '../../../error' import { BaseRecord } from '../../../storage/BaseRecord' import { uuid } from '../../../utils/uuid' +import { MediatorPickupStrategy } from '../MediatorPickupStrategy' import { MediationState } from '../models/MediationState' export interface MediationRecordProps { @@ -15,6 +18,7 @@ export interface MediationRecordProps { endpoint?: string recipientKeys?: string[] routingKeys?: string[] + pickupStrategy?: MediatorPickupStrategy tags?: CustomMediationTags } @@ -41,6 +45,15 @@ export class MediationRecord public recipientKeys!: string[] public routingKeys!: string[] + @Transform(({ value }) => { + if (value === 'Explicit') { + return MediatorPickupStrategy.PickUpV1 + } else { + return value + } + }) + public pickupStrategy?: MediatorPickupStrategy + public static readonly type = 'MediationRecord' public readonly type = MediationRecord.type @@ -57,6 +70,8 @@ export class MediationRecord this.state = props.state this.role = props.role this.endpoint = props.endpoint ?? undefined + this.pickupStrategy = props.pickupStrategy + this._tags = props.tags ?? {} } } @@ -71,6 +86,20 @@ export class MediationRecord } } + public addRecipientKey(recipientKey: string) { + this.recipientKeys.push(recipientKey) + } + + public removeRecipientKey(recipientKey: string): boolean { + const index = this.recipientKeys.indexOf(recipientKey, 0) + if (index > -1) { + this.recipientKeys.splice(index, 1) + return true + } + + return false + } + public get isReady() { return this.state === MediationState.Granted } diff --git a/packages/core/src/modules/routing/services/MediationRecipientService.ts b/packages/core/src/modules/routing/services/MediationRecipientService.ts index 9b04398035..331ad38ac5 100644 --- a/packages/core/src/modules/routing/services/MediationRecipientService.ts +++ b/packages/core/src/modules/routing/services/MediationRecipientService.ts @@ -1,9 +1,17 @@ import type { AgentMessage } from '../../../agent/AgentMessage' +import type { AgentMessageReceivedEvent } from '../../../agent/Events' import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { EncryptedMessage } from '../../../types' import type { ConnectionRecord } from '../../connections' import type { Routing } from '../../connections/services/ConnectionService' import type { MediationStateChangedEvent, KeylistUpdatedEvent } from '../RoutingEvents' -import type { MediationGrantMessage, MediationDenyMessage, KeylistUpdateResponseMessage } from '../messages' +import type { + KeylistUpdateResponseMessage, + MediationDenyMessage, + MediationGrantMessage, + MessageDeliveryMessage, +} from '../messages' +import type { StatusMessage } from '../messages/StatusMessage' import { firstValueFrom, ReplaySubject } from 'rxjs' import { filter, first, timeout } from 'rxjs/operators' @@ -11,14 +19,23 @@ import { inject, Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../agent/AgentConfig' import { EventEmitter } from '../../../agent/EventEmitter' +import { AgentEventTypes } from '../../../agent/Events' import { MessageSender } from '../../../agent/MessageSender' import { createOutboundMessage } from '../../../agent/helpers' import { InjectionSymbols } from '../../../constants' import { AriesFrameworkError } from '../../../error' import { Wallet } from '../../../wallet/Wallet' import { ConnectionService } from '../../connections/services/ConnectionService' +import { ProblemReportError } from '../../problem-reports' import { RoutingEventTypes } from '../RoutingEvents' -import { KeylistUpdateAction, MediationRequestMessage } from '../messages' +import { RoutingProblemReportReason } from '../error' +import { + StatusRequestMessage, + DeliveryRequestMessage, + MessagesReceivedMessage, + KeylistUpdateAction, + MediationRequestMessage, +} from '../messages' import { KeylistUpdate, KeylistUpdateMessage } from '../messages/KeylistUpdateMessage' import { MediationRole, MediationState } from '../models' import { MediationRecord } from '../repository/MediationRecord' @@ -27,7 +44,7 @@ import { MediationRepository } from '../repository/MediationRepository' @scoped(Lifecycle.ContainerScoped) export class MediationRecipientService { private wallet: Wallet - private mediatorRepository: MediationRepository + private mediationRepository: MediationRepository private eventEmitter: EventEmitter private connectionService: ConnectionService private messageSender: MessageSender @@ -43,12 +60,29 @@ export class MediationRecipientService { ) { this.config = config this.wallet = wallet - this.mediatorRepository = mediatorRepository + this.mediationRepository = mediatorRepository this.eventEmitter = eventEmitter this.connectionService = connectionService this.messageSender = messageSender } + public async createStatusRequest( + mediationRecord: MediationRecord, + config: { + recipientKey?: string + } = {} + ) { + mediationRecord.assertRole(MediationRole.Recipient) + mediationRecord.assertReady() + + const { recipientKey } = config + const statusRequest = new StatusRequestMessage({ + recipientKey, + }) + + return statusRequest + } + public async createRequest( connection: ConnectionRecord ): Promise> { @@ -57,10 +91,10 @@ export class MediationRecipientService { const mediationRecord = new MediationRecord({ threadId: message.threadId, state: MediationState.Requested, - role: MediationRole.Mediator, + role: MediationRole.Recipient, connectionId: connection.id, }) - await this.mediatorRepository.save(mediationRecord) + await this.mediationRepository.save(mediationRecord) this.eventEmitter.emit({ type: RoutingEventTypes.MediationStateChanged, payload: { @@ -77,10 +111,11 @@ export class MediationRecipientService { const connection = messageContext.assertReadyConnection() // Mediation record must already exists to be updated to granted status - const mediationRecord = await this.mediatorRepository.getByConnectionId(connection.id) + const mediationRecord = await this.mediationRepository.getByConnectionId(connection.id) // Assert mediationRecord.assertState(MediationState.Requested) + mediationRecord.assertRole(MediationRole.Recipient) // Update record mediationRecord.endpoint = messageContext.message.endpoint @@ -92,17 +127,24 @@ export class MediationRecipientService { // Assert ready connection const connection = messageContext.assertReadyConnection() - const mediationRecord = await this.mediatorRepository.getByConnectionId(connection.id) + const mediationRecord = await this.mediationRepository.getByConnectionId(connection.id) + + // Assert + mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Recipient) + const keylist = messageContext.message.updated // update keylist in mediationRecord for (const update of keylist) { if (update.action === KeylistUpdateAction.add) { - await this.saveRoute(update.recipientKey, mediationRecord) + mediationRecord.addRecipientKey(update.recipientKey) } else if (update.action === KeylistUpdateAction.remove) { - await this.removeRoute(update.recipientKey, mediationRecord) + mediationRecord.removeRecipientKey(update.recipientKey) } } + + await this.mediationRepository.update(mediationRecord) this.eventEmitter.emit({ type: RoutingEventTypes.RecipientKeylistUpdated, payload: { @@ -120,6 +162,9 @@ export class MediationRecipientService { const message = this.createKeylistUpdateMessage(verKey) const connection = await this.connectionService.getById(mediationRecord.connectionId) + mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Recipient) + // Create observable for event const observable = this.eventEmitter.observable(RoutingEventTypes.RecipientKeylistUpdated) const subject = new ReplaySubject(1) @@ -155,7 +200,17 @@ export class MediationRecipientService { return keylistUpdateMessage } - public async getRouting(mediationRecord?: MediationRecord): Promise { + public async getRouting({ mediatorId, useDefaultMediator = true }: GetRoutingOptions = {}): Promise { + let mediationRecord: MediationRecord | null = null + + if (mediatorId) { + mediationRecord = await this.getById(mediatorId) + } else if (useDefaultMediator) { + // If no mediatorId is provided, and useDefaultMediator is true (default) + // We use the default mediator if available + mediationRecord = await this.findDefaultMediator() + } + let endpoints = this.config.endpoints let routingKeys: string[] = [] @@ -172,19 +227,6 @@ export class MediationRecipientService { return { endpoints, routingKeys, did, verkey, mediatorId: mediationRecord?.id } } - public async saveRoute(recipientKey: string, mediationRecord: MediationRecord) { - mediationRecord.recipientKeys.push(recipientKey) - this.mediatorRepository.update(mediationRecord) - } - - public async removeRoute(recipientKey: string, mediationRecord: MediationRecord) { - const index = mediationRecord.recipientKeys.indexOf(recipientKey, 0) - if (index > -1) { - mediationRecord.recipientKeys.splice(index, 1) - } - this.mediatorRepository.update(mediationRecord) - } - public async processMediationDeny(messageContext: InboundMessageContext) { const connection = messageContext.assertReadyConnection() @@ -196,6 +238,7 @@ export class MediationRecipientService { } // Assert + mediationRecord.assertRole(MediationRole.Recipient) mediationRecord.assertState(MediationState.Requested) // Update record @@ -204,6 +247,62 @@ export class MediationRecipientService { return mediationRecord } + public async processStatus(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const { message: statusMessage } = messageContext + const { messageCount, recipientKey } = statusMessage + + const mediationRecord = await this.mediationRepository.getByConnectionId(connection.id) + + mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Recipient) + + //No messages to be sent + if (messageCount === 0) return null + + const { maximumMessagePickup } = this.config + const limit = messageCount < maximumMessagePickup ? messageCount : maximumMessagePickup + + const deliveryRequestMessage = new DeliveryRequestMessage({ + limit, + recipientKey, + }) + + return deliveryRequestMessage + } + + public async processDelivery(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + + const { appendedAttachments } = messageContext.message + + const mediationRecord = await this.mediationRepository.getByConnectionId(connection.id) + + mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Recipient) + + if (!appendedAttachments) + throw new ProblemReportError('Error processing attachments', { + problemCode: RoutingProblemReportReason.ErrorProcessingAttachments, + }) + + const ids: string[] = [] + for (const attachment of appendedAttachments) { + ids.push(attachment.id) + + this.eventEmitter.emit({ + type: AgentEventTypes.AgentMessageReceived, + payload: { + message: attachment.getDataAsJson(), + }, + }) + } + + return new MessagesReceivedMessage({ + messageIdList: ids, + }) + } + /** * Update the record to a new state and emit an state changed event. Also updates the record * in storage. @@ -215,7 +314,7 @@ export class MediationRecipientService { private async updateState(mediationRecord: MediationRecord, newState: MediationState) { const previousState = mediationRecord.state mediationRecord.state = newState - await this.mediatorRepository.update(mediationRecord) + await this.mediationRepository.update(mediationRecord) this.eventEmitter.emit({ type: RoutingEventTypes.MediationStateChanged, @@ -228,25 +327,25 @@ export class MediationRecipientService { } public async getById(id: string): Promise { - return this.mediatorRepository.getById(id) + return this.mediationRepository.getById(id) } public async findByConnectionId(connectionId: string): Promise { - return this.mediatorRepository.findSingleByQuery({ connectionId }) + return this.mediationRepository.findSingleByQuery({ connectionId }) } public async getMediators(): Promise { - return this.mediatorRepository.getAll() + return this.mediationRepository.getAll() } public async findDefaultMediator(): Promise { - return this.mediatorRepository.findSingleByQuery({ default: true }) + return this.mediationRepository.findSingleByQuery({ default: true }) } public async discoverMediation(mediatorId?: string): Promise { // If mediatorId is passed, always use it (and error if it is not found) if (mediatorId) { - return this.mediatorRepository.getById(mediatorId) + return this.mediationRepository.getById(mediatorId) } const defaultMediator = await this.findDefaultMediator() @@ -262,16 +361,16 @@ export class MediationRecipientService { } public async setDefaultMediator(mediator: MediationRecord) { - const mediationRecords = await this.mediatorRepository.findByQuery({ default: true }) + const mediationRecords = await this.mediationRepository.findByQuery({ default: true }) for (const record of mediationRecords) { record.setTag('default', false) - await this.mediatorRepository.update(record) + await this.mediationRepository.update(record) } // Set record coming in tag to true and then update. mediator.setTag('default', true) - await this.mediatorRepository.update(mediator) + await this.mediationRepository.update(mediator) } public async clearDefaultMediator() { @@ -279,7 +378,7 @@ export class MediationRecipientService { if (mediationRecord) { mediationRecord.setTag('default', false) - await this.mediatorRepository.update(mediationRecord) + await this.mediationRepository.update(mediationRecord) } } } @@ -288,3 +387,16 @@ export interface MediationProtocolMsgReturnType - ): Promise<{ mediationRecord: MediationRecord; packedMessage: WireMessage }> { + ): Promise<{ mediationRecord: MediationRecord; encryptedMessage: EncryptedMessage }> { const { message } = messageContext // TODO: update to class-validator validation @@ -92,9 +92,10 @@ export class MediatorService { // Assert mediation record is ready to be used mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Mediator) return { - packedMessage: message.message, + encryptedMessage: message.message, mediationRecord, } } @@ -108,6 +109,9 @@ export class MediatorService { const mediationRecord = await this.mediationRepository.getByConnectionId(connection.id) + mediationRecord.assertReady() + mediationRecord.assertRole(MediationRole.Mediator) + for (const update of message.updates) { const updated = new KeylistUpdated({ action: update.action, @@ -115,47 +119,20 @@ export class MediatorService { result: KeylistUpdateResult.NoChange, }) if (update.action === KeylistUpdateAction.add) { - updated.result = await this.saveRoute(update.recipientKey, mediationRecord) + mediationRecord.addRecipientKey(update.recipientKey) + updated.result = KeylistUpdateResult.Success + keylist.push(updated) } else if (update.action === KeylistUpdateAction.remove) { - updated.result = await this.removeRoute(update.recipientKey, mediationRecord) + const success = mediationRecord.removeRecipientKey(update.recipientKey) + updated.result = success ? KeylistUpdateResult.Success : KeylistUpdateResult.NoChange keylist.push(updated) } } - return new KeylistUpdateResponseMessage({ keylist, threadId: message.threadId }) - } - - public async saveRoute(recipientKey: string, mediationRecord: MediationRecord) { - try { - mediationRecord.recipientKeys.push(recipientKey) - this.mediationRepository.update(mediationRecord) - return KeylistUpdateResult.Success - } catch (error) { - this.agentConfig.logger.error( - `Error processing keylist update action for verkey '${recipientKey}' and mediation record '${mediationRecord.id}'` - ) - return KeylistUpdateResult.ServerError - } - } - - public async removeRoute(recipientKey: string, mediationRecord: MediationRecord) { - try { - const index = mediationRecord.recipientKeys.indexOf(recipientKey, 0) - if (index > -1) { - mediationRecord.recipientKeys.splice(index, 1) - - await this.mediationRepository.update(mediationRecord) - return KeylistUpdateResult.Success - } + await this.mediationRepository.update(mediationRecord) - return KeylistUpdateResult.ServerError - } catch (error) { - this.agentConfig.logger.error( - `Error processing keylist remove action for verkey '${recipientKey}' and mediation record '${mediationRecord.id}'` - ) - return KeylistUpdateResult.ServerError - } + return new KeylistUpdateResponseMessage({ keylist, threadId: message.threadId }) } public async createGrantMediationMessage(mediationRecord: MediationRecord) { @@ -163,8 +140,7 @@ export class MediatorService { mediationRecord.assertState(MediationState.Requested) mediationRecord.assertRole(MediationRole.Mediator) - mediationRecord.state = MediationState.Granted - await this.mediationRepository.update(mediationRecord) + await this.updateState(mediationRecord, MediationState.Granted) const message = new MediationGrantMessage({ endpoint: this.agentConfig.endpoints[0], diff --git a/packages/core/src/modules/routing/services/MessagePickupService.ts b/packages/core/src/modules/routing/services/MessagePickupService.ts index fb6a781845..27a3692bcd 100644 --- a/packages/core/src/modules/routing/services/MessagePickupService.ts +++ b/packages/core/src/modules/routing/services/MessagePickupService.ts @@ -1,5 +1,5 @@ import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import type { WireMessage } from '../../../types' +import type { EncryptedMessage } from '../../../types' import type { BatchPickupMessage } from '../messages' import { inject, scoped, Lifecycle } from 'tsyringe' @@ -40,7 +40,7 @@ export class MessagePickupService { return createOutboundMessage(connection, batchMessage) } - public queueMessage(connectionId: string, message: WireMessage) { + public queueMessage(connectionId: string, message: EncryptedMessage) { this.messageRepository.add(connectionId, message) } } diff --git a/packages/core/src/modules/vc/SignatureSuiteRegistry.ts b/packages/core/src/modules/vc/SignatureSuiteRegistry.ts new file mode 100644 index 0000000000..469d0a4aaf --- /dev/null +++ b/packages/core/src/modules/vc/SignatureSuiteRegistry.ts @@ -0,0 +1,55 @@ +import { suites } from '../../../types/jsonld-signatures' +import { KeyType } from '../../crypto' +import { Ed25519Signature2018 } from '../../crypto/signature-suites' +import { BbsBlsSignature2020, BbsBlsSignatureProof2020 } from '../../crypto/signature-suites/bbs' +import { AriesFrameworkError } from '../../error' + +const LinkedDataSignature = suites.LinkedDataSignature + +export interface SuiteInfo { + suiteClass: typeof LinkedDataSignature + proofType: string + requiredKeyType: string + keyType: string +} + +export class SignatureSuiteRegistry { + private suiteMapping: SuiteInfo[] = [ + { + suiteClass: Ed25519Signature2018, + proofType: 'Ed25519Signature2018', + requiredKeyType: 'Ed25519VerificationKey2018', + keyType: KeyType.Ed25519, + }, + { + suiteClass: BbsBlsSignature2020, + proofType: 'BbsBlsSignature2020', + requiredKeyType: 'BbsBlsSignatureProof2020', + keyType: KeyType.Bls12381g2, + }, + { + suiteClass: BbsBlsSignatureProof2020, + proofType: 'BbsBlsSignatureProof2020', + requiredKeyType: 'BbsBlsSignatureProof2020', + keyType: KeyType.Bls12381g2, + }, + ] + + public get supportedProofTypes(): string[] { + return this.suiteMapping.map((x) => x.proofType) + } + + public getByKeyType(keyType: KeyType) { + return this.suiteMapping.find((x) => x.keyType === keyType) + } + + public getByProofType(proofType: string) { + const suiteInfo = this.suiteMapping.find((x) => x.proofType === proofType) + + if (!suiteInfo) { + throw new AriesFrameworkError(`No signature suite for proof type: ${proofType}`) + } + + return suiteInfo + } +} diff --git a/packages/core/src/modules/vc/W3cCredentialService.ts b/packages/core/src/modules/vc/W3cCredentialService.ts new file mode 100644 index 0000000000..5efaeda92f --- /dev/null +++ b/packages/core/src/modules/vc/W3cCredentialService.ts @@ -0,0 +1,391 @@ +import type { Key } from '../../crypto/Key' +import type { DocumentLoaderResult } from '../../utils' +import type { W3cVerifyCredentialResult } from './models' +import type { + CreatePresentationOptions, + DeriveProofOptions, + SignCredentialOptions, + SignPresentationOptions, + StoreCredentialOptions, + VerifyCredentialOptions, + VerifyPresentationOptions, +} from './models/W3cCredentialServiceOptions' +import type { VerifyPresentationResult } from './models/presentation/VerifyPresentationResult' + +import { inject, Lifecycle, scoped } from 'tsyringe' + +import jsonld, { documentLoaderNode, documentLoaderXhr } from '../../../types/jsonld' +import vc from '../../../types/vc' +import { AgentConfig } from '../../agent/AgentConfig' +import { createWalletKeyPairClass } from '../../crypto/WalletKeyPair' +import { deriveProof } from '../../crypto/signature-suites/bbs' +import { AriesFrameworkError } from '../../error' +import { Logger } from '../../logger' +import { JsonTransformer, orArrayToArray, w3cDate } from '../../utils' +import { isNodeJS, isReactNative } from '../../utils/environment' +import { Wallet } from '../../wallet' +import { DidResolverService, VerificationMethod } from '../dids' +import { getKeyDidMappingByVerificationMethod } from '../dids/domain/key-type' + +import { SignatureSuiteRegistry } from './SignatureSuiteRegistry' +import { W3cVerifiableCredential } from './models' +import { W3cCredentialRecord } from './models/credential/W3cCredentialRecord' +import { W3cCredentialRepository } from './models/credential/W3cCredentialRepository' +import { W3cPresentation } from './models/presentation/W3Presentation' +import { W3cVerifiablePresentation } from './models/presentation/W3cVerifiablePresentation' + +@scoped(Lifecycle.ContainerScoped) +export class W3cCredentialService { + private wallet: Wallet + private w3cCredentialRepository: W3cCredentialRepository + private didResolver: DidResolverService + private agentConfig: AgentConfig + private logger: Logger + private suiteRegistry: SignatureSuiteRegistry + + public constructor( + @inject('Wallet') wallet: Wallet, + w3cCredentialRepository: W3cCredentialRepository, + didResolver: DidResolverService, + agentConfig: AgentConfig, + logger: Logger + ) { + this.wallet = wallet + this.w3cCredentialRepository = w3cCredentialRepository + this.didResolver = didResolver + this.agentConfig = agentConfig + this.logger = logger + this.suiteRegistry = new SignatureSuiteRegistry() + } + + /** + * Signs a credential + * + * @param credential the credential to be signed + * @returns the signed credential + */ + public async signCredential(options: SignCredentialOptions): Promise { + const WalletKeyPair = createWalletKeyPairClass(this.wallet) + + const signingKey = await this.getPublicKeyFromVerificationMethod(options.verificationMethod) + const suiteInfo = this.suiteRegistry.getByProofType(options.proofType) + + if (signingKey.keyType !== suiteInfo.keyType) { + throw new AriesFrameworkError('The key type of the verification method does not match the suite') + } + + const keyPair = new WalletKeyPair({ + controller: options.credential.issuerId, // should we check this against the verificationMethod.controller? + id: options.verificationMethod, + key: signingKey, + wallet: this.wallet, + }) + + const SuiteClass = suiteInfo.suiteClass + + const suite = new SuiteClass({ + key: keyPair, + LDKeyClass: WalletKeyPair, + proof: { + verificationMethod: options.verificationMethod, + }, + useNativeCanonize: false, + date: options.created ?? w3cDate(), + }) + + const result = await vc.issue({ + credential: JsonTransformer.toJSON(options.credential), + suite: suite, + purpose: options.proofPurpose, + documentLoader: this.documentLoader, + }) + + return JsonTransformer.fromJSON(result, W3cVerifiableCredential) + } + + /** + * Verifies the signature(s) of a credential + * + * @param credential the credential to be verified + * @returns the verification result + */ + public async verifyCredential(options: VerifyCredentialOptions): Promise { + const suites = this.getSignatureSuitesForCredential(options.credential) + + const verifyOptions: Record = { + credential: JsonTransformer.toJSON(options.credential), + suite: suites, + documentLoader: this.documentLoader, + } + + // this is a hack because vcjs throws if purpose is passed as undefined or null + if (options.proofPurpose) { + verifyOptions['purpose'] = options.proofPurpose + } + + const result = await vc.verifyCredential(verifyOptions) + + return result as unknown as W3cVerifyCredentialResult + } + + /** + * Utility method that creates a {@link W3cPresentation} from one or more {@link W3cVerifiableCredential}s. + * + * **NOTE: the presentation that is returned is unsigned.** + * + * @param credentials One or more instances of {@link W3cVerifiableCredential} + * @param [id] an optional unique identifier for the presentation + * @param [holderUrl] an optional identifier identifying the entity that is generating the presentation + * @returns An instance of {@link W3cPresentation} + */ + public async createPresentation(options: CreatePresentationOptions): Promise { + if (!Array.isArray(options.credentials)) { + options.credentials = [options.credentials] + } + + const presentationJson = vc.createPresentation({ + verifiableCredential: options.credentials.map((credential) => JsonTransformer.toJSON(credential)), + id: options.id, + holder: options.holderUrl, + }) + + return JsonTransformer.fromJSON(presentationJson, W3cPresentation) + } + + /** + * Signs a presentation including the credentials it includes + * + * @param presentation the presentation to be signed + * @returns the signed presentation + */ + public async signPresentation(options: SignPresentationOptions): Promise { + // create keyPair + const WalletKeyPair = createWalletKeyPairClass(this.wallet) + + const suiteInfo = this.suiteRegistry.getByProofType(options.signatureType) + + if (!suiteInfo) { + throw new AriesFrameworkError(`The requested proofType ${options.signatureType} is not supported`) + } + + const signingKey = await this.getPublicKeyFromVerificationMethod(options.verificationMethod) + + if (signingKey.keyType !== suiteInfo.keyType) { + throw new AriesFrameworkError('The key type of the verification method does not match the suite') + } + + const verificationMethodObject = (await this.documentLoader(options.verificationMethod)).document as Record< + string, + unknown + > + + const keyPair = new WalletKeyPair({ + controller: verificationMethodObject['controller'] as string, + id: options.verificationMethod, + key: signingKey, + wallet: this.wallet, + }) + + const suite = new suiteInfo.suiteClass({ + LDKeyClass: WalletKeyPair, + proof: { + verificationMethod: options.verificationMethod, + }, + date: new Date().toISOString(), + key: keyPair, + useNativeCanonize: false, + }) + + const result = await vc.signPresentation({ + presentation: JsonTransformer.toJSON(options.presentation), + suite: suite, + challenge: options.challenge, + documentLoader: this.documentLoader, + }) + + return JsonTransformer.fromJSON(result, W3cVerifiablePresentation) + } + + /** + * Verifies a presentation including the credentials it includes + * + * @param presentation the presentation to be verified + * @returns the verification result + */ + public async verifyPresentation(options: VerifyPresentationOptions): Promise { + // create keyPair + const WalletKeyPair = createWalletKeyPairClass(this.wallet) + + let proofs = options.presentation.proof + + if (!Array.isArray(proofs)) { + proofs = [proofs] + } + if (options.purpose) { + proofs = proofs.filter((proof) => proof.proofPurpose === options.purpose.term) + } + + const presentationSuites = proofs.map((proof) => { + const SuiteClass = this.suiteRegistry.getByProofType(proof.type).suiteClass + return new SuiteClass({ + LDKeyClass: WalletKeyPair, + proof: { + verificationMethod: proof.verificationMethod, + }, + date: proof.created, + useNativeCanonize: false, + }) + }) + + const credentials = Array.isArray(options.presentation.verifiableCredential) + ? options.presentation.verifiableCredential + : [options.presentation.verifiableCredential] + + const credentialSuites = credentials.map((credential) => this.getSignatureSuitesForCredential(credential)) + const allSuites = presentationSuites.concat(...credentialSuites) + + const verifyOptions: Record = { + presentation: JsonTransformer.toJSON(options.presentation), + suite: allSuites, + challenge: options.challenge, + documentLoader: this.documentLoader, + } + + // this is a hack because vcjs throws if purpose is passed as undefined or null + if (options.purpose) { + verifyOptions['presentationPurpose'] = options.purpose + } + + const result = await vc.verify(verifyOptions) + + return result as unknown as VerifyPresentationResult + } + + public async deriveProof(options: DeriveProofOptions): Promise { + const suiteInfo = this.suiteRegistry.getByProofType('BbsBlsSignatureProof2020') + const SuiteClass = suiteInfo.suiteClass + + const suite = new SuiteClass() + + const proof = await deriveProof(JsonTransformer.toJSON(options.credential), options.revealDocument, { + suite: suite, + documentLoader: this.documentLoader, + }) + + return proof + } + + public documentLoader = async (url: string): Promise => { + if (url.startsWith('did:')) { + const result = await this.didResolver.resolve(url) + + if (result.didResolutionMetadata.error || !result.didDocument) { + throw new AriesFrameworkError(`Unable to resolve DID: ${url}`) + } + + const framed = await jsonld.frame(result.didDocument.toJSON(), { + '@context': result.didDocument.context, + '@embed': '@never', + id: url, + }) + + return { + contextUrl: null, + documentUrl: url, + document: framed, + } + } + + let loader + + if (isNodeJS()) { + loader = documentLoaderNode.apply(jsonld, []) + } else if (isReactNative()) { + loader = documentLoaderXhr.apply(jsonld, []) + } else { + throw new AriesFrameworkError('Unsupported environment') + } + + return await loader(url) + } + + private async getPublicKeyFromVerificationMethod(verificationMethod: string): Promise { + const verificationMethodObject = await this.documentLoader(verificationMethod) + const verificationMethodClass = JsonTransformer.fromJSON(verificationMethodObject.document, VerificationMethod) + + const key = getKeyDidMappingByVerificationMethod(verificationMethodClass) + + return key.getKeyFromVerificationMethod(verificationMethodClass) + } + + /** + * Writes a credential to storage + * + * @param record the credential to be stored + * @returns the credential record that was written to storage + */ + public async storeCredential(options: StoreCredentialOptions): Promise { + // Get the expanded types + const expandedTypes = ( + await jsonld.expand(JsonTransformer.toJSON(options.record), { documentLoader: this.documentLoader }) + )[0]['@type'] + + // Create an instance of the w3cCredentialRecord + const w3cCredentialRecord = new W3cCredentialRecord({ + tags: { expandedTypes: orArrayToArray(expandedTypes) }, + credential: options.record, + }) + + // Store the w3c credential record + await this.w3cCredentialRepository.save(w3cCredentialRecord) + + return w3cCredentialRecord + } + + public async getAllCredentials(): Promise { + const allRecords = await this.w3cCredentialRepository.getAll() + return allRecords.map((record) => record.credential) + } + + public async getCredentialById(id: string): Promise { + return (await this.w3cCredentialRepository.getById(id)).credential + } + + public async findCredentialsByQuery( + query: Parameters[0] + ): Promise { + const result = await this.w3cCredentialRepository.findByQuery(query) + return result.map((record) => record.credential) + } + + public async findSingleCredentialByQuery( + query: Parameters[0] + ): Promise { + const result = await this.w3cCredentialRepository.findSingleByQuery(query) + return result?.credential + } + + private getSignatureSuitesForCredential(credential: W3cVerifiableCredential) { + const WalletKeyPair = createWalletKeyPairClass(this.wallet) + + let proofs = credential.proof + + if (!Array.isArray(proofs)) { + proofs = [proofs] + } + + return proofs.map((proof) => { + const SuiteClass = this.suiteRegistry.getByProofType(proof.type)?.suiteClass + if (SuiteClass) { + return new SuiteClass({ + LDKeyClass: WalletKeyPair, + proof: { + verificationMethod: proof.verificationMethod, + }, + date: proof.created, + useNativeCanonize: false, + }) + } + }) + } +} diff --git a/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts new file mode 100644 index 0000000000..304cf14231 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/W3cCredentialService.test.ts @@ -0,0 +1,430 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' + +import { getAgentConfig } from '../../../../tests/helpers' +import { TestLogger } from '../../../../tests/logger' +import { purposes } from '../../../../types/jsonld-signatures' +import { KeyType } from '../../../crypto' +import { Key } from '../../../crypto/Key' +import { LogLevel } from '../../../logger' +import { JsonTransformer, orArrayToArray } from '../../../utils' +import { IndyWallet } from '../../../wallet/IndyWallet' +import { WalletError } from '../../../wallet/error' +import { DidKey, DidResolverService } from '../../dids' +import { DidRepository } from '../../dids/repository' +import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' +import { W3cCredentialService } from '../W3cCredentialService' +import { W3cCredential, W3cVerifiableCredential } from '../models' +import { LinkedDataProof } from '../models/LinkedDataProof' +import { W3cCredentialRepository } from '../models/credential/W3cCredentialRepository' +import { W3cPresentation } from '../models/presentation/W3Presentation' +import { W3cVerifiablePresentation } from '../models/presentation/W3cVerifiablePresentation' +import { CredentialIssuancePurpose } from '../proof-purposes/CredentialIssuancePurpose' + +import { customDocumentLoader } from './documentLoader' +import { BbsBlsSignature2020Fixtures, Ed25519Signature2018Fixtures } from './fixtures' + +jest.mock('../../ledger/services/IndyLedgerService') + +const IndyLedgerServiceMock = IndyLedgerService as jest.Mock +const DidRepositoryMock = DidRepository as unknown as jest.Mock + +jest.mock('../models/credential/W3cCredentialRepository') +const W3cCredentialRepositoryMock = W3cCredentialRepository as jest.Mock + +describe('W3cCredentialService', () => { + let wallet: IndyWallet + let agentConfig: AgentConfig + let didResolverService: DidResolverService + let logger: TestLogger + let w3cCredentialService: W3cCredentialService + let w3cCredentialRepository: W3cCredentialRepository + const seed = 'testseed000000000000000000000001' + + beforeAll(async () => { + agentConfig = getAgentConfig('W3cCredentialServiceTest') + wallet = new IndyWallet(agentConfig) + logger = new TestLogger(LogLevel.error) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(agentConfig.walletConfig!) + didResolverService = new DidResolverService(agentConfig, new IndyLedgerServiceMock(), new DidRepositoryMock()) + w3cCredentialRepository = new W3cCredentialRepositoryMock() + w3cCredentialService = new W3cCredentialService( + wallet, + w3cCredentialRepository, + didResolverService, + agentConfig, + logger + ) + w3cCredentialService.documentLoader = customDocumentLoader + }) + + afterAll(async () => { + await wallet.delete() + }) + + describe('Ed25519Signature2018', () => { + let issuerDidKey: DidKey + let verificationMethod: string + beforeAll(async () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const issuerDidInfo = await wallet.createDid({ seed }) + const issuerKey = Key.fromPublicKeyBase58(issuerDidInfo.verkey, KeyType.Ed25519) + issuerDidKey = new DidKey(issuerKey) + verificationMethod = `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}` + }) + describe('signCredential', () => { + it('should return a successfully signed credential', async () => { + const credentialJson = Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT + + const credential = JsonTransformer.fromJSON(credentialJson, W3cCredential) + + const vc = await w3cCredentialService.signCredential({ + credential, + proofType: 'Ed25519Signature2018', + verificationMethod: verificationMethod, + }) + + expect(vc).toBeInstanceOf(W3cVerifiableCredential) + expect(vc.issuer).toEqual(issuerDidKey.did) + expect(Array.isArray(vc.proof)).toBe(false) + expect(vc.proof).toBeInstanceOf(LinkedDataProof) + + // @ts-ignore + expect(vc.proof.verificationMethod).toEqual(verificationMethod) + }) + + it('should throw because of verificationMethod does not belong to this wallet', async () => { + const credentialJson = Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT + credentialJson.issuer = issuerDidKey.did + + const credential = JsonTransformer.fromJSON(credentialJson, W3cCredential) + + expect(async () => { + await w3cCredentialService.signCredential({ + credential, + proofType: 'Ed25519Signature2018', + verificationMethod: + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + }) + }).rejects.toThrowError(WalletError) + }) + }) + describe('verifyCredential', () => { + it('should credential verify successfully', async () => { + const vc = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cVerifiableCredential + ) + const result = await w3cCredentialService.verifyCredential({ credential: vc }) + + expect(result.verified).toBe(true) + expect(result.error).toBeUndefined() + + expect(result.results.length).toBe(1) + + expect(result.results[0].verified).toBe(true) + expect(result.results[0].error).toBeUndefined() + }) + it('should fail because of invalid signature', async () => { + const vc = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_BAD_SIGNED, + W3cVerifiableCredential + ) + const result = await w3cCredentialService.verifyCredential({ credential: vc }) + + expect(result.verified).toBe(false) + expect(result.error).toBeDefined() + + // @ts-ignore + expect(result.error.errors[0]).toBeInstanceOf(Error) + // @ts-ignore + expect(result.error.errors[0].message).toBe('Invalid signature.') + }) + it('should fail because of an unsigned statement', async () => { + const vcJson = { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + credentialSubject: { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED.credentialSubject, + alumniOf: 'oops', + }, + } + + const vc = JsonTransformer.fromJSON(vcJson, W3cVerifiableCredential) + const result = await w3cCredentialService.verifyCredential({ credential: vc }) + + expect(result.verified).toBe(false) + + // @ts-ignore + expect(result.error.errors[0]).toBeInstanceOf(Error) + // @ts-ignore + expect(result.error.errors[0].message).toBe('Invalid signature.') + }) + it('should fail because of a changed statement', async () => { + const vcJson = { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + credentialSubject: { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED.credentialSubject, + degree: { + ...Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED.credentialSubject.degree, + name: 'oops', + }, + }, + } + + const vc = JsonTransformer.fromJSON(vcJson, W3cVerifiableCredential) + const result = await w3cCredentialService.verifyCredential({ credential: vc }) + + expect(result.verified).toBe(false) + + // @ts-ignore + expect(result.error.errors[0]).toBeInstanceOf(Error) + // @ts-ignore + expect(result.error.errors[0].message).toBe('Invalid signature.') + }) + }) + describe('createPresentation', () => { + it('should successfully create a presentation from single verifiable credential', async () => { + const vc = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cVerifiableCredential + ) + const result = await w3cCredentialService.createPresentation({ credentials: vc }) + + expect(result).toBeInstanceOf(W3cPresentation) + + expect(result.type).toEqual(expect.arrayContaining(['VerifiablePresentation'])) + + expect(result.verifiableCredential).toHaveLength(1) + expect(result.verifiableCredential).toEqual(expect.arrayContaining([vc])) + }) + it('should successfully create a presentation from two verifiable credential', async () => { + const vc1 = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cVerifiableCredential + ) + const vc2 = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cVerifiableCredential + ) + + const vcs = [vc1, vc2] + const result = await w3cCredentialService.createPresentation({ credentials: vcs }) + + expect(result).toBeInstanceOf(W3cPresentation) + + expect(result.type).toEqual(expect.arrayContaining(['VerifiablePresentation'])) + + expect(result.verifiableCredential).toHaveLength(2) + expect(result.verifiableCredential).toEqual(expect.arrayContaining([vc1, vc2])) + }) + }) + describe('signPresentation', () => { + it('should successfully create a presentation from single verifiable credential', async () => { + const presentation = JsonTransformer.fromJSON(Ed25519Signature2018Fixtures.TEST_VP_DOCUMENT, W3cPresentation) + + const purpose = new CredentialIssuancePurpose({ + controller: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }, + date: new Date().toISOString(), + }) + + const verifiablePresentation = await w3cCredentialService.signPresentation({ + presentation: presentation, + purpose: purpose, + signatureType: 'Ed25519Signature2018', + challenge: '7bf32d0b-39d4-41f3-96b6-45de52988e4c', + verificationMethod: verificationMethod, + }) + + expect(verifiablePresentation).toBeInstanceOf(W3cVerifiablePresentation) + }) + }) + describe('verifyPresentation', () => { + it('should successfully verify a presentation containing a single verifiable credential', async () => { + const vp = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_VP_DOCUMENT_SIGNED, + W3cVerifiablePresentation + ) + + const result = await w3cCredentialService.verifyPresentation({ + presentation: vp, + proofType: 'Ed25519Signature2018', + challenge: '7bf32d0b-39d4-41f3-96b6-45de52988e4c', + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }) + + expect(result.verified).toBe(true) + }) + }) + describe('storeCredential', () => { + it('should store a credential', async () => { + const credential = JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cVerifiableCredential + ) + + const w3cCredentialRecord = await w3cCredentialService.storeCredential({ record: credential }) + + expect(w3cCredentialRecord).toMatchObject({ + type: 'W3cCredentialRecord', + id: expect.any(String), + createdAt: expect.any(Date), + credential: expect.any(W3cVerifiableCredential), + }) + + expect(w3cCredentialRecord.getTags()).toMatchObject({ + expandedTypes: [ + 'https://www.w3.org/2018/credentials#VerifiableCredential', + 'https://example.org/examples#UniversityDegreeCredential', + ], + }) + }) + }) + }) + + describe('BbsBlsSignature2020', () => { + let issuerDidKey: DidKey + let verificationMethod: string + beforeAll(async () => { + const key = await wallet.createKey({ keyType: KeyType.Bls12381g2, seed }) + issuerDidKey = new DidKey(key) + verificationMethod = `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}` + }) + describe('signCredential', () => { + it('should return a successfully signed credential bbs', async () => { + const credentialJson = BbsBlsSignature2020Fixtures.TEST_LD_DOCUMENT + credentialJson.issuer = issuerDidKey.did + + const credential = JsonTransformer.fromJSON(credentialJson, W3cCredential) + + const vc = await w3cCredentialService.signCredential({ + credential, + proofType: 'BbsBlsSignature2020', + verificationMethod: verificationMethod, + }) + + expect(vc).toBeInstanceOf(W3cVerifiableCredential) + expect(vc.issuer).toEqual(issuerDidKey.did) + expect(Array.isArray(vc.proof)).toBe(false) + expect(vc.proof).toBeInstanceOf(LinkedDataProof) + + vc.proof = vc.proof as LinkedDataProof + expect(vc.proof.verificationMethod).toEqual(verificationMethod) + }) + }) + describe('verifyCredential', () => { + it('should verify the credential successfully', async () => { + const result = await w3cCredentialService.verifyCredential({ + credential: JsonTransformer.fromJSON( + BbsBlsSignature2020Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cVerifiableCredential + ), + proofPurpose: new purposes.AssertionProofPurpose(), + }) + + expect(result.verified).toEqual(true) + }) + }) + describe('deriveProof', () => { + it('should derive proof successfully', async () => { + const credentialJson = BbsBlsSignature2020Fixtures.TEST_LD_DOCUMENT_SIGNED + + const vc = JsonTransformer.fromJSON(credentialJson, W3cVerifiableCredential) + + const revealDocument = { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://w3id.org/citizenship/v1', + 'https://w3id.org/security/bbs/v1', + ], + type: ['VerifiableCredential', 'PermanentResidentCard'], + credentialSubject: { + '@explicit': true, + type: ['PermanentResident', 'Person'], + givenName: {}, + familyName: {}, + gender: {}, + }, + } + + const result = await w3cCredentialService.deriveProof({ + credential: vc, + revealDocument: revealDocument, + verificationMethod: verificationMethod, + }) + + // result.proof = result.proof as LinkedDataProof + expect(orArrayToArray(result.proof)[0].verificationMethod).toBe( + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN' + ) + }) + }) + describe('verifyDerived', () => { + it('should verify the derived proof successfully', async () => { + const result = await w3cCredentialService.verifyCredential({ + credential: JsonTransformer.fromJSON(BbsBlsSignature2020Fixtures.TEST_VALID_DERIVED, W3cVerifiableCredential), + proofPurpose: new purposes.AssertionProofPurpose(), + }) + expect(result.verified).toEqual(true) + }) + }) + describe('createPresentation', () => { + it('should create a presentation successfully', async () => { + const vc = JsonTransformer.fromJSON(BbsBlsSignature2020Fixtures.TEST_VALID_DERIVED, W3cVerifiableCredential) + const result = await w3cCredentialService.createPresentation({ credentials: vc }) + + expect(result).toBeInstanceOf(W3cPresentation) + + expect(result.type).toEqual(expect.arrayContaining(['VerifiablePresentation'])) + + expect(result.verifiableCredential).toHaveLength(1) + expect(result.verifiableCredential).toEqual(expect.arrayContaining([vc])) + }) + }) + describe('signPresentation', () => { + it('should sign the presentation successfully', async () => { + const signingKey = Key.fromPublicKeyBase58((await wallet.createDid({ seed })).verkey, KeyType.Ed25519) + const signingDidKey = new DidKey(signingKey) + const verificationMethod = `${signingDidKey.did}#${signingDidKey.key.fingerprint}` + const presentation = JsonTransformer.fromJSON(BbsBlsSignature2020Fixtures.TEST_VP_DOCUMENT, W3cPresentation) + + const purpose = new CredentialIssuancePurpose({ + controller: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }, + date: new Date().toISOString(), + }) + + const verifiablePresentation = await w3cCredentialService.signPresentation({ + presentation: presentation, + purpose: purpose, + signatureType: 'Ed25519Signature2018', + challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + verificationMethod: verificationMethod, + }) + + expect(verifiablePresentation).toBeInstanceOf(W3cVerifiablePresentation) + }) + }) + describe('verifyPresentation', () => { + it('should successfully verify a presentation containing a single verifiable credential bbs', async () => { + const vp = JsonTransformer.fromJSON( + BbsBlsSignature2020Fixtures.TEST_VP_DOCUMENT_SIGNED, + W3cVerifiablePresentation + ) + + const result = await w3cCredentialService.verifyPresentation({ + presentation: vp, + proofType: 'Ed25519Signature2018', + challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }) + + expect(result.verified).toBe(true) + }) + }) + }) +}) diff --git a/packages/core/src/modules/vc/__tests__/contexts/X25519_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/X25519_v1.ts new file mode 100644 index 0000000000..3a5b8bf768 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/X25519_v1.ts @@ -0,0 +1,26 @@ +export const X25519_V1 = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + X25519KeyAgreementKey2019: { + '@id': 'https://w3id.org/security#X25519KeyAgreementKey2019', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + publicKeyBase58: { + '@id': 'https://w3id.org/security#publicKeyBase58', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/bbs_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/bbs_v1.ts new file mode 100644 index 0000000000..d7fa000420 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/bbs_v1.ts @@ -0,0 +1,92 @@ +export const BBS_V1 = { + '@context': { + '@version': 1.1, + id: '@id', + type: '@type', + BbsBlsSignature2020: { + '@id': 'https://w3id.org/security#BbsBlsSignature2020', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + proofValue: 'https://w3id.org/security#proofValue', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + BbsBlsSignatureProof2020: { + '@id': 'https://w3id.org/security#BbsBlsSignatureProof2020', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + Bls12381G1Key2020: 'https://w3id.org/security#Bls12381G1Key2020', + Bls12381G2Key2020: 'https://w3id.org/security#Bls12381G2Key2020', + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/citizenship_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/citizenship_v1.ts new file mode 100644 index 0000000000..13eb72776c --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/citizenship_v1.ts @@ -0,0 +1,45 @@ +export const CITIZENSHIP_V1 = { + '@context': { + '@version': 1.1, + '@protected': true, + name: 'http://schema.org/name', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + image: { '@id': 'http://schema.org/image', '@type': '@id' }, + PermanentResidentCard: { + '@id': 'https://w3id.org/citizenship#PermanentResidentCard', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + name: 'http://schema.org/name', + identifier: 'http://schema.org/identifier', + image: { '@id': 'http://schema.org/image', '@type': '@id' }, + }, + }, + PermanentResident: { + '@id': 'https://w3id.org/citizenship#PermanentResident', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + ctzn: 'https://w3id.org/citizenship#', + schema: 'http://schema.org/', + xsd: 'http://www.w3.org/2001/XMLSchema#', + birthCountry: 'ctzn:birthCountry', + birthDate: { '@id': 'schema:birthDate', '@type': 'xsd:dateTime' }, + commuterClassification: 'ctzn:commuterClassification', + familyName: 'schema:familyName', + gender: 'schema:gender', + givenName: 'schema:givenName', + lprCategory: 'ctzn:lprCategory', + lprNumber: 'ctzn:lprNumber', + residentSince: { '@id': 'ctzn:residentSince', '@type': 'xsd:dateTime' }, + }, + }, + Person: 'http://schema.org/Person', + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/credentials_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/credentials_v1.ts new file mode 100644 index 0000000000..f401fb33f5 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/credentials_v1.ts @@ -0,0 +1,250 @@ +export const CREDENTIALS_V1 = { + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + VerifiableCredential: { + '@id': 'https://www.w3.org/2018/credentials#VerifiableCredential', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + cred: 'https://www.w3.org/2018/credentials#', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + credentialSchema: { + '@id': 'cred:credentialSchema', + '@type': '@id', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + cred: 'https://www.w3.org/2018/credentials#', + JsonSchemaValidator2018: 'cred:JsonSchemaValidator2018', + }, + }, + credentialStatus: { '@id': 'cred:credentialStatus', '@type': '@id' }, + credentialSubject: { '@id': 'cred:credentialSubject', '@type': '@id' }, + evidence: { '@id': 'cred:evidence', '@type': '@id' }, + expirationDate: { + '@id': 'cred:expirationDate', + '@type': 'xsd:dateTime', + }, + holder: { '@id': 'cred:holder', '@type': '@id' }, + issued: { '@id': 'cred:issued', '@type': 'xsd:dateTime' }, + issuer: { '@id': 'cred:issuer', '@type': '@id' }, + issuanceDate: { '@id': 'cred:issuanceDate', '@type': 'xsd:dateTime' }, + proof: { '@id': 'sec:proof', '@type': '@id', '@container': '@graph' }, + refreshService: { + '@id': 'cred:refreshService', + '@type': '@id', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + cred: 'https://www.w3.org/2018/credentials#', + ManualRefreshService2018: 'cred:ManualRefreshService2018', + }, + }, + termsOfUse: { '@id': 'cred:termsOfUse', '@type': '@id' }, + validFrom: { '@id': 'cred:validFrom', '@type': 'xsd:dateTime' }, + validUntil: { '@id': 'cred:validUntil', '@type': 'xsd:dateTime' }, + }, + }, + VerifiablePresentation: { + '@id': 'https://www.w3.org/2018/credentials#VerifiablePresentation', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + cred: 'https://www.w3.org/2018/credentials#', + sec: 'https://w3id.org/security#', + holder: { '@id': 'cred:holder', '@type': '@id' }, + proof: { '@id': 'sec:proof', '@type': '@id', '@container': '@graph' }, + verifiableCredential: { + '@id': 'cred:verifiableCredential', + '@type': '@id', + '@container': '@graph', + }, + }, + }, + EcdsaSecp256k1Signature2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256k1Signature2019', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + challenge: 'sec:challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'xsd:dateTime', + }, + domain: 'sec:domain', + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + jws: 'sec:jws', + nonce: 'sec:nonce', + proofPurpose: { + '@id': 'sec:proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'sec:proofValue', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + }, + }, + EcdsaSecp256r1Signature2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256r1Signature2019', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + challenge: 'sec:challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'xsd:dateTime', + }, + domain: 'sec:domain', + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + jws: 'sec:jws', + nonce: 'sec:nonce', + proofPurpose: { + '@id': 'sec:proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'sec:proofValue', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + }, + }, + Ed25519Signature2018: { + '@id': 'https://w3id.org/security#Ed25519Signature2018', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + challenge: 'sec:challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'xsd:dateTime', + }, + domain: 'sec:domain', + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + jws: 'sec:jws', + nonce: 'sec:nonce', + proofPurpose: { + '@id': 'sec:proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'sec:proofValue', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + }, + }, + RsaSignature2018: { + '@id': 'https://w3id.org/security#RsaSignature2018', + '@context': { + '@version': 1.1, + '@protected': true, + challenge: 'sec:challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'xsd:dateTime', + }, + domain: 'sec:domain', + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + jws: 'sec:jws', + nonce: 'sec:nonce', + proofPurpose: { + '@id': 'sec:proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + sec: 'https://w3id.org/security#', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'sec:proofValue', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + }, + }, + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/did_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/did_v1.ts new file mode 100644 index 0000000000..2a431389b3 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/did_v1.ts @@ -0,0 +1,56 @@ +export const DID_V1 = { + '@context': { + '@protected': true, + id: '@id', + type: '@type', + alsoKnownAs: { + '@id': 'https://www.w3.org/ns/activitystreams#alsoKnownAs', + '@type': '@id', + }, + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + service: { + '@id': 'https://www.w3.org/ns/did#service', + '@type': '@id', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + serviceEndpoint: { + '@id': 'https://www.w3.org/ns/did#serviceEndpoint', + '@type': '@id', + }, + }, + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/ed25519_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/ed25519_v1.ts new file mode 100644 index 0000000000..29e09035d4 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/ed25519_v1.ts @@ -0,0 +1,91 @@ +export const ED25519_V1 = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + Ed25519VerificationKey2018: { + '@id': 'https://w3id.org/security#Ed25519VerificationKey2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + publicKeyBase58: { + '@id': 'https://w3id.org/security#publicKeyBase58', + }, + }, + }, + Ed25519Signature2018: { + '@id': 'https://w3id.org/security#Ed25519Signature2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + jws: { + '@id': 'https://w3id.org/security#jws', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/examples_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/examples_v1.ts new file mode 100644 index 0000000000..c0894b4553 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/examples_v1.ts @@ -0,0 +1,46 @@ +export const EXAMPLES_V1 = { + '@context': [ + { '@version': 1.1 }, + 'https://www.w3.org/ns/odrl.jsonld', + { + ex: 'https://example.org/examples#', + schema: 'http://schema.org/', + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + '3rdPartyCorrelation': 'ex:3rdPartyCorrelation', + AllVerifiers: 'ex:AllVerifiers', + Archival: 'ex:Archival', + BachelorDegree: 'ex:BachelorDegree', + Child: 'ex:Child', + CLCredentialDefinition2019: 'ex:CLCredentialDefinition2019', + CLSignature2019: 'ex:CLSignature2019', + IssuerPolicy: 'ex:IssuerPolicy', + HolderPolicy: 'ex:HolderPolicy', + Mother: 'ex:Mother', + RelationshipCredential: 'ex:RelationshipCredential', + UniversityDegreeCredential: 'ex:UniversityDegreeCredential', + ZkpExampleSchema2018: 'ex:ZkpExampleSchema2018', + issuerData: 'ex:issuerData', + attributes: 'ex:attributes', + signature: 'ex:signature', + signatureCorrectnessProof: 'ex:signatureCorrectnessProof', + primaryProof: 'ex:primaryProof', + nonRevocationProof: 'ex:nonRevocationProof', + alumniOf: { '@id': 'schema:alumniOf', '@type': 'rdf:HTML' }, + child: { '@id': 'ex:child', '@type': '@id' }, + degree: 'ex:degree', + degreeType: 'ex:degreeType', + degreeSchool: 'ex:degreeSchool', + college: 'ex:college', + name: { '@id': 'schema:name', '@type': 'rdf:HTML' }, + givenName: 'schema:givenName', + familyName: 'schema:familyName', + parent: { '@id': 'ex:parent', '@type': '@id' }, + referenceId: 'ex:referenceId', + documentPresence: 'ex:documentPresence', + evidenceDocument: 'ex:evidenceDocument', + spouse: 'schema:spouse', + subjectPresence: 'ex:subjectPresence', + verifier: { '@id': 'ex:verifier', '@type': '@id' }, + }, + ], +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/index.ts b/packages/core/src/modules/vc/__tests__/contexts/index.ts new file mode 100644 index 0000000000..c66801c24a --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/index.ts @@ -0,0 +1,11 @@ +export * from './bbs_v1' +export * from './citizenship_v1' +export * from './credentials_v1' +export * from './did_v1' +export * from './examples_v1' +export * from './odrl' +export * from './schema_org' +export * from './security_v1' +export * from './security_v2' +export * from './security_v3_unstable' +export * from './vaccination_v1' diff --git a/packages/core/src/modules/vc/__tests__/contexts/odrl.ts b/packages/core/src/modules/vc/__tests__/contexts/odrl.ts new file mode 100644 index 0000000000..6efea2320e --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/odrl.ts @@ -0,0 +1,181 @@ +export const ODRL = { + '@context': { + odrl: 'http://www.w3.org/ns/odrl/2/', + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + rdfs: 'http://www.w3.org/2000/01/rdf-schema#', + owl: 'http://www.w3.org/2002/07/owl#', + skos: 'http://www.w3.org/2004/02/skos/core#', + dct: 'http://purl.org/dc/terms/', + xsd: 'http://www.w3.org/2001/XMLSchema#', + vcard: 'http://www.w3.org/2006/vcard/ns#', + foaf: 'http://xmlns.com/foaf/0.1/', + schema: 'http://schema.org/', + cc: 'http://creativecommons.org/ns#', + uid: '@id', + type: '@type', + Policy: 'odrl:Policy', + Rule: 'odrl:Rule', + profile: { '@type': '@id', '@id': 'odrl:profile' }, + inheritFrom: { '@type': '@id', '@id': 'odrl:inheritFrom' }, + ConflictTerm: 'odrl:ConflictTerm', + conflict: { '@type': '@vocab', '@id': 'odrl:conflict' }, + perm: 'odrl:perm', + prohibit: 'odrl:prohibit', + invalid: 'odrl:invalid', + Agreement: 'odrl:Agreement', + Assertion: 'odrl:Assertion', + Offer: 'odrl:Offer', + Privacy: 'odrl:Privacy', + Request: 'odrl:Request', + Set: 'odrl:Set', + Ticket: 'odrl:Ticket', + Asset: 'odrl:Asset', + AssetCollection: 'odrl:AssetCollection', + relation: { '@type': '@id', '@id': 'odrl:relation' }, + hasPolicy: { '@type': '@id', '@id': 'odrl:hasPolicy' }, + target: { '@type': '@id', '@id': 'odrl:target' }, + output: { '@type': '@id', '@id': 'odrl:output' }, + partOf: { '@type': '@id', '@id': 'odrl:partOf' }, + source: { '@type': '@id', '@id': 'odrl:source' }, + Party: 'odrl:Party', + PartyCollection: 'odrl:PartyCollection', + function: { '@type': '@vocab', '@id': 'odrl:function' }, + PartyScope: 'odrl:PartyScope', + assignee: { '@type': '@id', '@id': 'odrl:assignee' }, + assigner: { '@type': '@id', '@id': 'odrl:assigner' }, + assigneeOf: { '@type': '@id', '@id': 'odrl:assigneeOf' }, + assignerOf: { '@type': '@id', '@id': 'odrl:assignerOf' }, + attributedParty: { '@type': '@id', '@id': 'odrl:attributedParty' }, + attributingParty: { '@type': '@id', '@id': 'odrl:attributingParty' }, + compensatedParty: { '@type': '@id', '@id': 'odrl:compensatedParty' }, + compensatingParty: { '@type': '@id', '@id': 'odrl:compensatingParty' }, + consentingParty: { '@type': '@id', '@id': 'odrl:consentingParty' }, + consentedParty: { '@type': '@id', '@id': 'odrl:consentedParty' }, + informedParty: { '@type': '@id', '@id': 'odrl:informedParty' }, + informingParty: { '@type': '@id', '@id': 'odrl:informingParty' }, + trackingParty: { '@type': '@id', '@id': 'odrl:trackingParty' }, + trackedParty: { '@type': '@id', '@id': 'odrl:trackedParty' }, + contractingParty: { '@type': '@id', '@id': 'odrl:contractingParty' }, + contractedParty: { '@type': '@id', '@id': 'odrl:contractedParty' }, + Action: 'odrl:Action', + action: { '@type': '@vocab', '@id': 'odrl:action' }, + includedIn: { '@type': '@id', '@id': 'odrl:includedIn' }, + implies: { '@type': '@id', '@id': 'odrl:implies' }, + Permission: 'odrl:Permission', + permission: { '@type': '@id', '@id': 'odrl:permission' }, + Prohibition: 'odrl:Prohibition', + prohibition: { '@type': '@id', '@id': 'odrl:prohibition' }, + obligation: { '@type': '@id', '@id': 'odrl:obligation' }, + use: 'odrl:use', + grantUse: 'odrl:grantUse', + aggregate: 'odrl:aggregate', + annotate: 'odrl:annotate', + anonymize: 'odrl:anonymize', + archive: 'odrl:archive', + concurrentUse: 'odrl:concurrentUse', + derive: 'odrl:derive', + digitize: 'odrl:digitize', + display: 'odrl:display', + distribute: 'odrl:distribute', + execute: 'odrl:execute', + extract: 'odrl:extract', + give: 'odrl:give', + index: 'odrl:index', + install: 'odrl:install', + modify: 'odrl:modify', + move: 'odrl:move', + play: 'odrl:play', + present: 'odrl:present', + print: 'odrl:print', + read: 'odrl:read', + reproduce: 'odrl:reproduce', + sell: 'odrl:sell', + stream: 'odrl:stream', + textToSpeech: 'odrl:textToSpeech', + transfer: 'odrl:transfer', + transform: 'odrl:transform', + translate: 'odrl:translate', + Duty: 'odrl:Duty', + duty: { '@type': '@id', '@id': 'odrl:duty' }, + consequence: { '@type': '@id', '@id': 'odrl:consequence' }, + remedy: { '@type': '@id', '@id': 'odrl:remedy' }, + acceptTracking: 'odrl:acceptTracking', + attribute: 'odrl:attribute', + compensate: 'odrl:compensate', + delete: 'odrl:delete', + ensureExclusivity: 'odrl:ensureExclusivity', + include: 'odrl:include', + inform: 'odrl:inform', + nextPolicy: 'odrl:nextPolicy', + obtainConsent: 'odrl:obtainConsent', + reviewPolicy: 'odrl:reviewPolicy', + uninstall: 'odrl:uninstall', + watermark: 'odrl:watermark', + Constraint: 'odrl:Constraint', + LogicalConstraint: 'odrl:LogicalConstraint', + constraint: { '@type': '@id', '@id': 'odrl:constraint' }, + refinement: { '@type': '@id', '@id': 'odrl:refinement' }, + Operator: 'odrl:Operator', + operator: { '@type': '@vocab', '@id': 'odrl:operator' }, + RightOperand: 'odrl:RightOperand', + rightOperand: 'odrl:rightOperand', + rightOperandReference: { + '@type': 'xsd:anyURI', + '@id': 'odrl:rightOperandReference', + }, + LeftOperand: 'odrl:LeftOperand', + leftOperand: { '@type': '@vocab', '@id': 'odrl:leftOperand' }, + unit: 'odrl:unit', + dataType: { '@type': 'xsd:anyType', '@id': 'odrl:datatype' }, + status: 'odrl:status', + absolutePosition: 'odrl:absolutePosition', + absoluteSpatialPosition: 'odrl:absoluteSpatialPosition', + absoluteTemporalPosition: 'odrl:absoluteTemporalPosition', + absoluteSize: 'odrl:absoluteSize', + count: 'odrl:count', + dateTime: 'odrl:dateTime', + delayPeriod: 'odrl:delayPeriod', + deliveryChannel: 'odrl:deliveryChannel', + elapsedTime: 'odrl:elapsedTime', + event: 'odrl:event', + fileFormat: 'odrl:fileFormat', + industry: 'odrl:industry:', + language: 'odrl:language', + media: 'odrl:media', + meteredTime: 'odrl:meteredTime', + payAmount: 'odrl:payAmount', + percentage: 'odrl:percentage', + product: 'odrl:product', + purpose: 'odrl:purpose', + recipient: 'odrl:recipient', + relativePosition: 'odrl:relativePosition', + relativeSpatialPosition: 'odrl:relativeSpatialPosition', + relativeTemporalPosition: 'odrl:relativeTemporalPosition', + relativeSize: 'odrl:relativeSize', + resolution: 'odrl:resolution', + spatial: 'odrl:spatial', + spatialCoordinates: 'odrl:spatialCoordinates', + systemDevice: 'odrl:systemDevice', + timeInterval: 'odrl:timeInterval', + unitOfCount: 'odrl:unitOfCount', + version: 'odrl:version', + virtualLocation: 'odrl:virtualLocation', + eq: 'odrl:eq', + gt: 'odrl:gt', + gteq: 'odrl:gteq', + lt: 'odrl:lt', + lteq: 'odrl:lteq', + neq: 'odrl:neg', + isA: 'odrl:isA', + hasPart: 'odrl:hasPart', + isPartOf: 'odrl:isPartOf', + isAllOf: 'odrl:isAllOf', + isAnyOf: 'odrl:isAnyOf', + isNoneOf: 'odrl:isNoneOf', + or: 'odrl:or', + xone: 'odrl:xone', + and: 'odrl:and', + andSequence: 'odrl:andSequence', + policyUsage: 'odrl:policyUsage', + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/schema_org.ts b/packages/core/src/modules/vc/__tests__/contexts/schema_org.ts new file mode 100644 index 0000000000..818951da70 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/schema_org.ts @@ -0,0 +1,2838 @@ +export const SCHEMA_ORG = { + '@context': { + type: '@type', + id: '@id', + HTML: { '@id': 'rdf:HTML' }, + '@vocab': 'http://schema.org/', + rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + rdfs: 'http://www.w3.org/2000/01/rdf-schema#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + schema: 'http://schema.org/', + owl: 'http://www.w3.org/2002/07/owl#', + dc: 'http://purl.org/dc/elements/1.1/', + dct: 'http://purl.org/dc/terms/', + dctype: 'http://purl.org/dc/dcmitype/', + void: 'http://rdfs.org/ns/void#', + dcat: 'http://www.w3.org/ns/dcat#', + '3DModel': { '@id': 'schema:3DModel' }, + AMRadioChannel: { '@id': 'schema:AMRadioChannel' }, + APIReference: { '@id': 'schema:APIReference' }, + Abdomen: { '@id': 'schema:Abdomen' }, + AboutPage: { '@id': 'schema:AboutPage' }, + AcceptAction: { '@id': 'schema:AcceptAction' }, + Accommodation: { '@id': 'schema:Accommodation' }, + AccountingService: { '@id': 'schema:AccountingService' }, + AchieveAction: { '@id': 'schema:AchieveAction' }, + Action: { '@id': 'schema:Action' }, + ActionAccessSpecification: { '@id': 'schema:ActionAccessSpecification' }, + ActionStatusType: { '@id': 'schema:ActionStatusType' }, + ActivateAction: { '@id': 'schema:ActivateAction' }, + ActivationFee: { '@id': 'schema:ActivationFee' }, + ActiveActionStatus: { '@id': 'schema:ActiveActionStatus' }, + ActiveNotRecruiting: { '@id': 'schema:ActiveNotRecruiting' }, + AddAction: { '@id': 'schema:AddAction' }, + AdministrativeArea: { '@id': 'schema:AdministrativeArea' }, + AdultEntertainment: { '@id': 'schema:AdultEntertainment' }, + AdvertiserContentArticle: { '@id': 'schema:AdvertiserContentArticle' }, + AerobicActivity: { '@id': 'schema:AerobicActivity' }, + AggregateOffer: { '@id': 'schema:AggregateOffer' }, + AggregateRating: { '@id': 'schema:AggregateRating' }, + AgreeAction: { '@id': 'schema:AgreeAction' }, + Airline: { '@id': 'schema:Airline' }, + Airport: { '@id': 'schema:Airport' }, + AlbumRelease: { '@id': 'schema:AlbumRelease' }, + AlignmentObject: { '@id': 'schema:AlignmentObject' }, + AllWheelDriveConfiguration: { '@id': 'schema:AllWheelDriveConfiguration' }, + AllergiesHealthAspect: { '@id': 'schema:AllergiesHealthAspect' }, + AllocateAction: { '@id': 'schema:AllocateAction' }, + AmpStory: { '@id': 'schema:AmpStory' }, + AmusementPark: { '@id': 'schema:AmusementPark' }, + AnaerobicActivity: { '@id': 'schema:AnaerobicActivity' }, + AnalysisNewsArticle: { '@id': 'schema:AnalysisNewsArticle' }, + AnatomicalStructure: { '@id': 'schema:AnatomicalStructure' }, + AnatomicalSystem: { '@id': 'schema:AnatomicalSystem' }, + Anesthesia: { '@id': 'schema:Anesthesia' }, + AnimalShelter: { '@id': 'schema:AnimalShelter' }, + Answer: { '@id': 'schema:Answer' }, + Apartment: { '@id': 'schema:Apartment' }, + ApartmentComplex: { '@id': 'schema:ApartmentComplex' }, + Appearance: { '@id': 'schema:Appearance' }, + AppendAction: { '@id': 'schema:AppendAction' }, + ApplyAction: { '@id': 'schema:ApplyAction' }, + ApprovedIndication: { '@id': 'schema:ApprovedIndication' }, + Aquarium: { '@id': 'schema:Aquarium' }, + ArchiveComponent: { '@id': 'schema:ArchiveComponent' }, + ArchiveOrganization: { '@id': 'schema:ArchiveOrganization' }, + ArriveAction: { '@id': 'schema:ArriveAction' }, + ArtGallery: { '@id': 'schema:ArtGallery' }, + Artery: { '@id': 'schema:Artery' }, + Article: { '@id': 'schema:Article' }, + AskAction: { '@id': 'schema:AskAction' }, + AskPublicNewsArticle: { '@id': 'schema:AskPublicNewsArticle' }, + AssessAction: { '@id': 'schema:AssessAction' }, + AssignAction: { '@id': 'schema:AssignAction' }, + Atlas: { '@id': 'schema:Atlas' }, + Attorney: { '@id': 'schema:Attorney' }, + Audience: { '@id': 'schema:Audience' }, + AudioObject: { '@id': 'schema:AudioObject' }, + Audiobook: { '@id': 'schema:Audiobook' }, + AudiobookFormat: { '@id': 'schema:AudiobookFormat' }, + AuthoritativeLegalValue: { '@id': 'schema:AuthoritativeLegalValue' }, + AuthorizeAction: { '@id': 'schema:AuthorizeAction' }, + AutoBodyShop: { '@id': 'schema:AutoBodyShop' }, + AutoDealer: { '@id': 'schema:AutoDealer' }, + AutoPartsStore: { '@id': 'schema:AutoPartsStore' }, + AutoRental: { '@id': 'schema:AutoRental' }, + AutoRepair: { '@id': 'schema:AutoRepair' }, + AutoWash: { '@id': 'schema:AutoWash' }, + AutomatedTeller: { '@id': 'schema:AutomatedTeller' }, + AutomotiveBusiness: { '@id': 'schema:AutomotiveBusiness' }, + Ayurvedic: { '@id': 'schema:Ayurvedic' }, + BackOrder: { '@id': 'schema:BackOrder' }, + BackgroundNewsArticle: { '@id': 'schema:BackgroundNewsArticle' }, + Bacteria: { '@id': 'schema:Bacteria' }, + Bakery: { '@id': 'schema:Bakery' }, + Balance: { '@id': 'schema:Balance' }, + BankAccount: { '@id': 'schema:BankAccount' }, + BankOrCreditUnion: { '@id': 'schema:BankOrCreditUnion' }, + BarOrPub: { '@id': 'schema:BarOrPub' }, + Barcode: { '@id': 'schema:Barcode' }, + BasicIncome: { '@id': 'schema:BasicIncome' }, + Beach: { '@id': 'schema:Beach' }, + BeautySalon: { '@id': 'schema:BeautySalon' }, + BedAndBreakfast: { '@id': 'schema:BedAndBreakfast' }, + BedDetails: { '@id': 'schema:BedDetails' }, + BedType: { '@id': 'schema:BedType' }, + BefriendAction: { '@id': 'schema:BefriendAction' }, + BenefitsHealthAspect: { '@id': 'schema:BenefitsHealthAspect' }, + BikeStore: { '@id': 'schema:BikeStore' }, + Blog: { '@id': 'schema:Blog' }, + BlogPosting: { '@id': 'schema:BlogPosting' }, + BloodTest: { '@id': 'schema:BloodTest' }, + BoardingPolicyType: { '@id': 'schema:BoardingPolicyType' }, + BoatReservation: { '@id': 'schema:BoatReservation' }, + BoatTerminal: { '@id': 'schema:BoatTerminal' }, + BoatTrip: { '@id': 'schema:BoatTrip' }, + BodyMeasurementArm: { '@id': 'schema:BodyMeasurementArm' }, + BodyMeasurementBust: { '@id': 'schema:BodyMeasurementBust' }, + BodyMeasurementChest: { '@id': 'schema:BodyMeasurementChest' }, + BodyMeasurementFoot: { '@id': 'schema:BodyMeasurementFoot' }, + BodyMeasurementHand: { '@id': 'schema:BodyMeasurementHand' }, + BodyMeasurementHead: { '@id': 'schema:BodyMeasurementHead' }, + BodyMeasurementHeight: { '@id': 'schema:BodyMeasurementHeight' }, + BodyMeasurementHips: { '@id': 'schema:BodyMeasurementHips' }, + BodyMeasurementInsideLeg: { '@id': 'schema:BodyMeasurementInsideLeg' }, + BodyMeasurementNeck: { '@id': 'schema:BodyMeasurementNeck' }, + BodyMeasurementTypeEnumeration: { + '@id': 'schema:BodyMeasurementTypeEnumeration', + }, + BodyMeasurementUnderbust: { '@id': 'schema:BodyMeasurementUnderbust' }, + BodyMeasurementWaist: { '@id': 'schema:BodyMeasurementWaist' }, + BodyMeasurementWeight: { '@id': 'schema:BodyMeasurementWeight' }, + BodyOfWater: { '@id': 'schema:BodyOfWater' }, + Bone: { '@id': 'schema:Bone' }, + Book: { '@id': 'schema:Book' }, + BookFormatType: { '@id': 'schema:BookFormatType' }, + BookSeries: { '@id': 'schema:BookSeries' }, + BookStore: { '@id': 'schema:BookStore' }, + BookmarkAction: { '@id': 'schema:BookmarkAction' }, + Boolean: { '@id': 'schema:Boolean' }, + BorrowAction: { '@id': 'schema:BorrowAction' }, + BowlingAlley: { '@id': 'schema:BowlingAlley' }, + BrainStructure: { '@id': 'schema:BrainStructure' }, + Brand: { '@id': 'schema:Brand' }, + BreadcrumbList: { '@id': 'schema:BreadcrumbList' }, + Brewery: { '@id': 'schema:Brewery' }, + Bridge: { '@id': 'schema:Bridge' }, + BroadcastChannel: { '@id': 'schema:BroadcastChannel' }, + BroadcastEvent: { '@id': 'schema:BroadcastEvent' }, + BroadcastFrequencySpecification: { + '@id': 'schema:BroadcastFrequencySpecification', + }, + BroadcastRelease: { '@id': 'schema:BroadcastRelease' }, + BroadcastService: { '@id': 'schema:BroadcastService' }, + BrokerageAccount: { '@id': 'schema:BrokerageAccount' }, + BuddhistTemple: { '@id': 'schema:BuddhistTemple' }, + BusOrCoach: { '@id': 'schema:BusOrCoach' }, + BusReservation: { '@id': 'schema:BusReservation' }, + BusStation: { '@id': 'schema:BusStation' }, + BusStop: { '@id': 'schema:BusStop' }, + BusTrip: { '@id': 'schema:BusTrip' }, + BusinessAudience: { '@id': 'schema:BusinessAudience' }, + BusinessEntityType: { '@id': 'schema:BusinessEntityType' }, + BusinessEvent: { '@id': 'schema:BusinessEvent' }, + BusinessFunction: { '@id': 'schema:BusinessFunction' }, + BusinessSupport: { '@id': 'schema:BusinessSupport' }, + BuyAction: { '@id': 'schema:BuyAction' }, + CDCPMDRecord: { '@id': 'schema:CDCPMDRecord' }, + CDFormat: { '@id': 'schema:CDFormat' }, + CT: { '@id': 'schema:CT' }, + CableOrSatelliteService: { '@id': 'schema:CableOrSatelliteService' }, + CafeOrCoffeeShop: { '@id': 'schema:CafeOrCoffeeShop' }, + Campground: { '@id': 'schema:Campground' }, + CampingPitch: { '@id': 'schema:CampingPitch' }, + Canal: { '@id': 'schema:Canal' }, + CancelAction: { '@id': 'schema:CancelAction' }, + Car: { '@id': 'schema:Car' }, + CarUsageType: { '@id': 'schema:CarUsageType' }, + Cardiovascular: { '@id': 'schema:Cardiovascular' }, + CardiovascularExam: { '@id': 'schema:CardiovascularExam' }, + CaseSeries: { '@id': 'schema:CaseSeries' }, + Casino: { '@id': 'schema:Casino' }, + CassetteFormat: { '@id': 'schema:CassetteFormat' }, + CategoryCode: { '@id': 'schema:CategoryCode' }, + CategoryCodeSet: { '@id': 'schema:CategoryCodeSet' }, + CatholicChurch: { '@id': 'schema:CatholicChurch' }, + CausesHealthAspect: { '@id': 'schema:CausesHealthAspect' }, + Cemetery: { '@id': 'schema:Cemetery' }, + Chapter: { '@id': 'schema:Chapter' }, + CharitableIncorporatedOrganization: { + '@id': 'schema:CharitableIncorporatedOrganization', + }, + CheckAction: { '@id': 'schema:CheckAction' }, + CheckInAction: { '@id': 'schema:CheckInAction' }, + CheckOutAction: { '@id': 'schema:CheckOutAction' }, + CheckoutPage: { '@id': 'schema:CheckoutPage' }, + ChildCare: { '@id': 'schema:ChildCare' }, + ChildrensEvent: { '@id': 'schema:ChildrensEvent' }, + Chiropractic: { '@id': 'schema:Chiropractic' }, + ChooseAction: { '@id': 'schema:ChooseAction' }, + Church: { '@id': 'schema:Church' }, + City: { '@id': 'schema:City' }, + CityHall: { '@id': 'schema:CityHall' }, + CivicStructure: { '@id': 'schema:CivicStructure' }, + Claim: { '@id': 'schema:Claim' }, + ClaimReview: { '@id': 'schema:ClaimReview' }, + Class: { '@id': 'schema:Class' }, + CleaningFee: { '@id': 'schema:CleaningFee' }, + Clinician: { '@id': 'schema:Clinician' }, + Clip: { '@id': 'schema:Clip' }, + ClothingStore: { '@id': 'schema:ClothingStore' }, + CoOp: { '@id': 'schema:CoOp' }, + Code: { '@id': 'schema:Code' }, + CohortStudy: { '@id': 'schema:CohortStudy' }, + Collection: { '@id': 'schema:Collection' }, + CollectionPage: { '@id': 'schema:CollectionPage' }, + CollegeOrUniversity: { '@id': 'schema:CollegeOrUniversity' }, + ComedyClub: { '@id': 'schema:ComedyClub' }, + ComedyEvent: { '@id': 'schema:ComedyEvent' }, + ComicCoverArt: { '@id': 'schema:ComicCoverArt' }, + ComicIssue: { '@id': 'schema:ComicIssue' }, + ComicSeries: { '@id': 'schema:ComicSeries' }, + ComicStory: { '@id': 'schema:ComicStory' }, + Comment: { '@id': 'schema:Comment' }, + CommentAction: { '@id': 'schema:CommentAction' }, + CommentPermission: { '@id': 'schema:CommentPermission' }, + CommunicateAction: { '@id': 'schema:CommunicateAction' }, + CommunityHealth: { '@id': 'schema:CommunityHealth' }, + CompilationAlbum: { '@id': 'schema:CompilationAlbum' }, + CompleteDataFeed: { '@id': 'schema:CompleteDataFeed' }, + Completed: { '@id': 'schema:Completed' }, + CompletedActionStatus: { '@id': 'schema:CompletedActionStatus' }, + CompoundPriceSpecification: { '@id': 'schema:CompoundPriceSpecification' }, + ComputerLanguage: { '@id': 'schema:ComputerLanguage' }, + ComputerStore: { '@id': 'schema:ComputerStore' }, + ConfirmAction: { '@id': 'schema:ConfirmAction' }, + Consortium: { '@id': 'schema:Consortium' }, + ConsumeAction: { '@id': 'schema:ConsumeAction' }, + ContactPage: { '@id': 'schema:ContactPage' }, + ContactPoint: { '@id': 'schema:ContactPoint' }, + ContactPointOption: { '@id': 'schema:ContactPointOption' }, + ContagiousnessHealthAspect: { '@id': 'schema:ContagiousnessHealthAspect' }, + Continent: { '@id': 'schema:Continent' }, + ControlAction: { '@id': 'schema:ControlAction' }, + ConvenienceStore: { '@id': 'schema:ConvenienceStore' }, + Conversation: { '@id': 'schema:Conversation' }, + CookAction: { '@id': 'schema:CookAction' }, + Corporation: { '@id': 'schema:Corporation' }, + CorrectionComment: { '@id': 'schema:CorrectionComment' }, + Country: { '@id': 'schema:Country' }, + Course: { '@id': 'schema:Course' }, + CourseInstance: { '@id': 'schema:CourseInstance' }, + Courthouse: { '@id': 'schema:Courthouse' }, + CoverArt: { '@id': 'schema:CoverArt' }, + CovidTestingFacility: { '@id': 'schema:CovidTestingFacility' }, + CreateAction: { '@id': 'schema:CreateAction' }, + CreativeWork: { '@id': 'schema:CreativeWork' }, + CreativeWorkSeason: { '@id': 'schema:CreativeWorkSeason' }, + CreativeWorkSeries: { '@id': 'schema:CreativeWorkSeries' }, + CreditCard: { '@id': 'schema:CreditCard' }, + Crematorium: { '@id': 'schema:Crematorium' }, + CriticReview: { '@id': 'schema:CriticReview' }, + CrossSectional: { '@id': 'schema:CrossSectional' }, + CssSelectorType: { '@id': 'schema:CssSelectorType' }, + CurrencyConversionService: { '@id': 'schema:CurrencyConversionService' }, + DDxElement: { '@id': 'schema:DDxElement' }, + DJMixAlbum: { '@id': 'schema:DJMixAlbum' }, + DVDFormat: { '@id': 'schema:DVDFormat' }, + DamagedCondition: { '@id': 'schema:DamagedCondition' }, + DanceEvent: { '@id': 'schema:DanceEvent' }, + DanceGroup: { '@id': 'schema:DanceGroup' }, + DataCatalog: { '@id': 'schema:DataCatalog' }, + DataDownload: { '@id': 'schema:DataDownload' }, + DataFeed: { '@id': 'schema:DataFeed' }, + DataFeedItem: { '@id': 'schema:DataFeedItem' }, + DataType: { '@id': 'schema:DataType' }, + Dataset: { '@id': 'schema:Dataset' }, + Date: { '@id': 'schema:Date' }, + DateTime: { '@id': 'schema:DateTime' }, + DatedMoneySpecification: { '@id': 'schema:DatedMoneySpecification' }, + DayOfWeek: { '@id': 'schema:DayOfWeek' }, + DaySpa: { '@id': 'schema:DaySpa' }, + DeactivateAction: { '@id': 'schema:DeactivateAction' }, + DecontextualizedContent: { '@id': 'schema:DecontextualizedContent' }, + DefenceEstablishment: { '@id': 'schema:DefenceEstablishment' }, + DefinedRegion: { '@id': 'schema:DefinedRegion' }, + DefinedTerm: { '@id': 'schema:DefinedTerm' }, + DefinedTermSet: { '@id': 'schema:DefinedTermSet' }, + DefinitiveLegalValue: { '@id': 'schema:DefinitiveLegalValue' }, + DeleteAction: { '@id': 'schema:DeleteAction' }, + DeliveryChargeSpecification: { '@id': 'schema:DeliveryChargeSpecification' }, + DeliveryEvent: { '@id': 'schema:DeliveryEvent' }, + DeliveryMethod: { '@id': 'schema:DeliveryMethod' }, + DeliveryTimeSettings: { '@id': 'schema:DeliveryTimeSettings' }, + Demand: { '@id': 'schema:Demand' }, + DemoAlbum: { '@id': 'schema:DemoAlbum' }, + Dentist: { '@id': 'schema:Dentist' }, + Dentistry: { '@id': 'schema:Dentistry' }, + DepartAction: { '@id': 'schema:DepartAction' }, + DepartmentStore: { '@id': 'schema:DepartmentStore' }, + DepositAccount: { '@id': 'schema:DepositAccount' }, + Dermatologic: { '@id': 'schema:Dermatologic' }, + Dermatology: { '@id': 'schema:Dermatology' }, + DiabeticDiet: { '@id': 'schema:DiabeticDiet' }, + Diagnostic: { '@id': 'schema:Diagnostic' }, + DiagnosticLab: { '@id': 'schema:DiagnosticLab' }, + DiagnosticProcedure: { '@id': 'schema:DiagnosticProcedure' }, + Diet: { '@id': 'schema:Diet' }, + DietNutrition: { '@id': 'schema:DietNutrition' }, + DietarySupplement: { '@id': 'schema:DietarySupplement' }, + DigitalAudioTapeFormat: { '@id': 'schema:DigitalAudioTapeFormat' }, + DigitalDocument: { '@id': 'schema:DigitalDocument' }, + DigitalDocumentPermission: { '@id': 'schema:DigitalDocumentPermission' }, + DigitalDocumentPermissionType: { + '@id': 'schema:DigitalDocumentPermissionType', + }, + DigitalFormat: { '@id': 'schema:DigitalFormat' }, + DisabilitySupport: { '@id': 'schema:DisabilitySupport' }, + DisagreeAction: { '@id': 'schema:DisagreeAction' }, + Discontinued: { '@id': 'schema:Discontinued' }, + DiscoverAction: { '@id': 'schema:DiscoverAction' }, + DiscussionForumPosting: { '@id': 'schema:DiscussionForumPosting' }, + DislikeAction: { '@id': 'schema:DislikeAction' }, + Distance: { '@id': 'schema:Distance' }, + DistanceFee: { '@id': 'schema:DistanceFee' }, + Distillery: { '@id': 'schema:Distillery' }, + DonateAction: { '@id': 'schema:DonateAction' }, + DoseSchedule: { '@id': 'schema:DoseSchedule' }, + DoubleBlindedTrial: { '@id': 'schema:DoubleBlindedTrial' }, + DownloadAction: { '@id': 'schema:DownloadAction' }, + Downpayment: { '@id': 'schema:Downpayment' }, + DrawAction: { '@id': 'schema:DrawAction' }, + Drawing: { '@id': 'schema:Drawing' }, + DrinkAction: { '@id': 'schema:DrinkAction' }, + DriveWheelConfigurationValue: { '@id': 'schema:DriveWheelConfigurationValue' }, + DrivingSchoolVehicleUsage: { '@id': 'schema:DrivingSchoolVehicleUsage' }, + Drug: { '@id': 'schema:Drug' }, + DrugClass: { '@id': 'schema:DrugClass' }, + DrugCost: { '@id': 'schema:DrugCost' }, + DrugCostCategory: { '@id': 'schema:DrugCostCategory' }, + DrugLegalStatus: { '@id': 'schema:DrugLegalStatus' }, + DrugPregnancyCategory: { '@id': 'schema:DrugPregnancyCategory' }, + DrugPrescriptionStatus: { '@id': 'schema:DrugPrescriptionStatus' }, + DrugStrength: { '@id': 'schema:DrugStrength' }, + DryCleaningOrLaundry: { '@id': 'schema:DryCleaningOrLaundry' }, + Duration: { '@id': 'schema:Duration' }, + EBook: { '@id': 'schema:EBook' }, + EPRelease: { '@id': 'schema:EPRelease' }, + EUEnergyEfficiencyCategoryA: { '@id': 'schema:EUEnergyEfficiencyCategoryA' }, + EUEnergyEfficiencyCategoryA1Plus: { + '@id': 'schema:EUEnergyEfficiencyCategoryA1Plus', + }, + EUEnergyEfficiencyCategoryA2Plus: { + '@id': 'schema:EUEnergyEfficiencyCategoryA2Plus', + }, + EUEnergyEfficiencyCategoryA3Plus: { + '@id': 'schema:EUEnergyEfficiencyCategoryA3Plus', + }, + EUEnergyEfficiencyCategoryB: { '@id': 'schema:EUEnergyEfficiencyCategoryB' }, + EUEnergyEfficiencyCategoryC: { '@id': 'schema:EUEnergyEfficiencyCategoryC' }, + EUEnergyEfficiencyCategoryD: { '@id': 'schema:EUEnergyEfficiencyCategoryD' }, + EUEnergyEfficiencyCategoryE: { '@id': 'schema:EUEnergyEfficiencyCategoryE' }, + EUEnergyEfficiencyCategoryF: { '@id': 'schema:EUEnergyEfficiencyCategoryF' }, + EUEnergyEfficiencyCategoryG: { '@id': 'schema:EUEnergyEfficiencyCategoryG' }, + EUEnergyEfficiencyEnumeration: { + '@id': 'schema:EUEnergyEfficiencyEnumeration', + }, + Ear: { '@id': 'schema:Ear' }, + EatAction: { '@id': 'schema:EatAction' }, + EditedOrCroppedContent: { '@id': 'schema:EditedOrCroppedContent' }, + EducationEvent: { '@id': 'schema:EducationEvent' }, + EducationalAudience: { '@id': 'schema:EducationalAudience' }, + EducationalOccupationalCredential: { + '@id': 'schema:EducationalOccupationalCredential', + }, + EducationalOccupationalProgram: { + '@id': 'schema:EducationalOccupationalProgram', + }, + EducationalOrganization: { '@id': 'schema:EducationalOrganization' }, + EffectivenessHealthAspect: { '@id': 'schema:EffectivenessHealthAspect' }, + Electrician: { '@id': 'schema:Electrician' }, + ElectronicsStore: { '@id': 'schema:ElectronicsStore' }, + ElementarySchool: { '@id': 'schema:ElementarySchool' }, + EmailMessage: { '@id': 'schema:EmailMessage' }, + Embassy: { '@id': 'schema:Embassy' }, + Emergency: { '@id': 'schema:Emergency' }, + EmergencyService: { '@id': 'schema:EmergencyService' }, + EmployeeRole: { '@id': 'schema:EmployeeRole' }, + EmployerAggregateRating: { '@id': 'schema:EmployerAggregateRating' }, + EmployerReview: { '@id': 'schema:EmployerReview' }, + EmploymentAgency: { '@id': 'schema:EmploymentAgency' }, + Endocrine: { '@id': 'schema:Endocrine' }, + EndorseAction: { '@id': 'schema:EndorseAction' }, + EndorsementRating: { '@id': 'schema:EndorsementRating' }, + Energy: { '@id': 'schema:Energy' }, + EnergyConsumptionDetails: { '@id': 'schema:EnergyConsumptionDetails' }, + EnergyEfficiencyEnumeration: { '@id': 'schema:EnergyEfficiencyEnumeration' }, + EnergyStarCertified: { '@id': 'schema:EnergyStarCertified' }, + EnergyStarEnergyEfficiencyEnumeration: { + '@id': 'schema:EnergyStarEnergyEfficiencyEnumeration', + }, + EngineSpecification: { '@id': 'schema:EngineSpecification' }, + EnrollingByInvitation: { '@id': 'schema:EnrollingByInvitation' }, + EntertainmentBusiness: { '@id': 'schema:EntertainmentBusiness' }, + EntryPoint: { '@id': 'schema:EntryPoint' }, + Enumeration: { '@id': 'schema:Enumeration' }, + Episode: { '@id': 'schema:Episode' }, + Event: { '@id': 'schema:Event' }, + EventAttendanceModeEnumeration: { + '@id': 'schema:EventAttendanceModeEnumeration', + }, + EventCancelled: { '@id': 'schema:EventCancelled' }, + EventMovedOnline: { '@id': 'schema:EventMovedOnline' }, + EventPostponed: { '@id': 'schema:EventPostponed' }, + EventRescheduled: { '@id': 'schema:EventRescheduled' }, + EventReservation: { '@id': 'schema:EventReservation' }, + EventScheduled: { '@id': 'schema:EventScheduled' }, + EventSeries: { '@id': 'schema:EventSeries' }, + EventStatusType: { '@id': 'schema:EventStatusType' }, + EventVenue: { '@id': 'schema:EventVenue' }, + EvidenceLevelA: { '@id': 'schema:EvidenceLevelA' }, + EvidenceLevelB: { '@id': 'schema:EvidenceLevelB' }, + EvidenceLevelC: { '@id': 'schema:EvidenceLevelC' }, + ExchangeRateSpecification: { '@id': 'schema:ExchangeRateSpecification' }, + ExchangeRefund: { '@id': 'schema:ExchangeRefund' }, + ExerciseAction: { '@id': 'schema:ExerciseAction' }, + ExerciseGym: { '@id': 'schema:ExerciseGym' }, + ExercisePlan: { '@id': 'schema:ExercisePlan' }, + ExhibitionEvent: { '@id': 'schema:ExhibitionEvent' }, + Eye: { '@id': 'schema:Eye' }, + FAQPage: { '@id': 'schema:FAQPage' }, + FDAcategoryA: { '@id': 'schema:FDAcategoryA' }, + FDAcategoryB: { '@id': 'schema:FDAcategoryB' }, + FDAcategoryC: { '@id': 'schema:FDAcategoryC' }, + FDAcategoryD: { '@id': 'schema:FDAcategoryD' }, + FDAcategoryX: { '@id': 'schema:FDAcategoryX' }, + FDAnotEvaluated: { '@id': 'schema:FDAnotEvaluated' }, + FMRadioChannel: { '@id': 'schema:FMRadioChannel' }, + FailedActionStatus: { '@id': 'schema:FailedActionStatus' }, + False: { '@id': 'schema:False' }, + FastFoodRestaurant: { '@id': 'schema:FastFoodRestaurant' }, + Female: { '@id': 'schema:Female' }, + Festival: { '@id': 'schema:Festival' }, + FilmAction: { '@id': 'schema:FilmAction' }, + FinancialProduct: { '@id': 'schema:FinancialProduct' }, + FinancialService: { '@id': 'schema:FinancialService' }, + FindAction: { '@id': 'schema:FindAction' }, + FireStation: { '@id': 'schema:FireStation' }, + Flexibility: { '@id': 'schema:Flexibility' }, + Flight: { '@id': 'schema:Flight' }, + FlightReservation: { '@id': 'schema:FlightReservation' }, + Float: { '@id': 'schema:Float' }, + FloorPlan: { '@id': 'schema:FloorPlan' }, + Florist: { '@id': 'schema:Florist' }, + FollowAction: { '@id': 'schema:FollowAction' }, + FoodEstablishment: { '@id': 'schema:FoodEstablishment' }, + FoodEstablishmentReservation: { '@id': 'schema:FoodEstablishmentReservation' }, + FoodEvent: { '@id': 'schema:FoodEvent' }, + FoodService: { '@id': 'schema:FoodService' }, + FourWheelDriveConfiguration: { '@id': 'schema:FourWheelDriveConfiguration' }, + Friday: { '@id': 'schema:Friday' }, + FrontWheelDriveConfiguration: { '@id': 'schema:FrontWheelDriveConfiguration' }, + FullRefund: { '@id': 'schema:FullRefund' }, + FundingAgency: { '@id': 'schema:FundingAgency' }, + FundingScheme: { '@id': 'schema:FundingScheme' }, + Fungus: { '@id': 'schema:Fungus' }, + FurnitureStore: { '@id': 'schema:FurnitureStore' }, + Game: { '@id': 'schema:Game' }, + GamePlayMode: { '@id': 'schema:GamePlayMode' }, + GameServer: { '@id': 'schema:GameServer' }, + GameServerStatus: { '@id': 'schema:GameServerStatus' }, + GardenStore: { '@id': 'schema:GardenStore' }, + GasStation: { '@id': 'schema:GasStation' }, + Gastroenterologic: { '@id': 'schema:Gastroenterologic' }, + GatedResidenceCommunity: { '@id': 'schema:GatedResidenceCommunity' }, + GenderType: { '@id': 'schema:GenderType' }, + GeneralContractor: { '@id': 'schema:GeneralContractor' }, + Genetic: { '@id': 'schema:Genetic' }, + Genitourinary: { '@id': 'schema:Genitourinary' }, + GeoCircle: { '@id': 'schema:GeoCircle' }, + GeoCoordinates: { '@id': 'schema:GeoCoordinates' }, + GeoShape: { '@id': 'schema:GeoShape' }, + GeospatialGeometry: { '@id': 'schema:GeospatialGeometry' }, + Geriatric: { '@id': 'schema:Geriatric' }, + GettingAccessHealthAspect: { '@id': 'schema:GettingAccessHealthAspect' }, + GiveAction: { '@id': 'schema:GiveAction' }, + GlutenFreeDiet: { '@id': 'schema:GlutenFreeDiet' }, + GolfCourse: { '@id': 'schema:GolfCourse' }, + GovernmentBenefitsType: { '@id': 'schema:GovernmentBenefitsType' }, + GovernmentBuilding: { '@id': 'schema:GovernmentBuilding' }, + GovernmentOffice: { '@id': 'schema:GovernmentOffice' }, + GovernmentOrganization: { '@id': 'schema:GovernmentOrganization' }, + GovernmentPermit: { '@id': 'schema:GovernmentPermit' }, + GovernmentService: { '@id': 'schema:GovernmentService' }, + Grant: { '@id': 'schema:Grant' }, + GraphicNovel: { '@id': 'schema:GraphicNovel' }, + GroceryStore: { '@id': 'schema:GroceryStore' }, + GroupBoardingPolicy: { '@id': 'schema:GroupBoardingPolicy' }, + Guide: { '@id': 'schema:Guide' }, + Gynecologic: { '@id': 'schema:Gynecologic' }, + HVACBusiness: { '@id': 'schema:HVACBusiness' }, + Hackathon: { '@id': 'schema:Hackathon' }, + HairSalon: { '@id': 'schema:HairSalon' }, + HalalDiet: { '@id': 'schema:HalalDiet' }, + Hardcover: { '@id': 'schema:Hardcover' }, + HardwareStore: { '@id': 'schema:HardwareStore' }, + Head: { '@id': 'schema:Head' }, + HealthAndBeautyBusiness: { '@id': 'schema:HealthAndBeautyBusiness' }, + HealthAspectEnumeration: { '@id': 'schema:HealthAspectEnumeration' }, + HealthCare: { '@id': 'schema:HealthCare' }, + HealthClub: { '@id': 'schema:HealthClub' }, + HealthInsurancePlan: { '@id': 'schema:HealthInsurancePlan' }, + HealthPlanCostSharingSpecification: { + '@id': 'schema:HealthPlanCostSharingSpecification', + }, + HealthPlanFormulary: { '@id': 'schema:HealthPlanFormulary' }, + HealthPlanNetwork: { '@id': 'schema:HealthPlanNetwork' }, + HealthTopicContent: { '@id': 'schema:HealthTopicContent' }, + HearingImpairedSupported: { '@id': 'schema:HearingImpairedSupported' }, + Hematologic: { '@id': 'schema:Hematologic' }, + HighSchool: { '@id': 'schema:HighSchool' }, + HinduDiet: { '@id': 'schema:HinduDiet' }, + HinduTemple: { '@id': 'schema:HinduTemple' }, + HobbyShop: { '@id': 'schema:HobbyShop' }, + HomeAndConstructionBusiness: { '@id': 'schema:HomeAndConstructionBusiness' }, + HomeGoodsStore: { '@id': 'schema:HomeGoodsStore' }, + Homeopathic: { '@id': 'schema:Homeopathic' }, + Hospital: { '@id': 'schema:Hospital' }, + Hostel: { '@id': 'schema:Hostel' }, + Hotel: { '@id': 'schema:Hotel' }, + HotelRoom: { '@id': 'schema:HotelRoom' }, + House: { '@id': 'schema:House' }, + HousePainter: { '@id': 'schema:HousePainter' }, + HowItWorksHealthAspect: { '@id': 'schema:HowItWorksHealthAspect' }, + HowOrWhereHealthAspect: { '@id': 'schema:HowOrWhereHealthAspect' }, + HowTo: { '@id': 'schema:HowTo' }, + HowToDirection: { '@id': 'schema:HowToDirection' }, + HowToItem: { '@id': 'schema:HowToItem' }, + HowToSection: { '@id': 'schema:HowToSection' }, + HowToStep: { '@id': 'schema:HowToStep' }, + HowToSupply: { '@id': 'schema:HowToSupply' }, + HowToTip: { '@id': 'schema:HowToTip' }, + HowToTool: { '@id': 'schema:HowToTool' }, + HyperToc: { '@id': 'schema:HyperToc' }, + HyperTocEntry: { '@id': 'schema:HyperTocEntry' }, + IceCreamShop: { '@id': 'schema:IceCreamShop' }, + IgnoreAction: { '@id': 'schema:IgnoreAction' }, + ImageGallery: { '@id': 'schema:ImageGallery' }, + ImageObject: { '@id': 'schema:ImageObject' }, + ImagingTest: { '@id': 'schema:ImagingTest' }, + InForce: { '@id': 'schema:InForce' }, + InStock: { '@id': 'schema:InStock' }, + InStoreOnly: { '@id': 'schema:InStoreOnly' }, + IndividualProduct: { '@id': 'schema:IndividualProduct' }, + Infectious: { '@id': 'schema:Infectious' }, + InfectiousAgentClass: { '@id': 'schema:InfectiousAgentClass' }, + InfectiousDisease: { '@id': 'schema:InfectiousDisease' }, + InformAction: { '@id': 'schema:InformAction' }, + IngredientsHealthAspect: { '@id': 'schema:IngredientsHealthAspect' }, + InsertAction: { '@id': 'schema:InsertAction' }, + InstallAction: { '@id': 'schema:InstallAction' }, + Installment: { '@id': 'schema:Installment' }, + InsuranceAgency: { '@id': 'schema:InsuranceAgency' }, + Intangible: { '@id': 'schema:Intangible' }, + Integer: { '@id': 'schema:Integer' }, + InteractAction: { '@id': 'schema:InteractAction' }, + InteractionCounter: { '@id': 'schema:InteractionCounter' }, + InternationalTrial: { '@id': 'schema:InternationalTrial' }, + InternetCafe: { '@id': 'schema:InternetCafe' }, + InvestmentFund: { '@id': 'schema:InvestmentFund' }, + InvestmentOrDeposit: { '@id': 'schema:InvestmentOrDeposit' }, + InviteAction: { '@id': 'schema:InviteAction' }, + Invoice: { '@id': 'schema:Invoice' }, + InvoicePrice: { '@id': 'schema:InvoicePrice' }, + ItemAvailability: { '@id': 'schema:ItemAvailability' }, + ItemList: { '@id': 'schema:ItemList' }, + ItemListOrderAscending: { '@id': 'schema:ItemListOrderAscending' }, + ItemListOrderDescending: { '@id': 'schema:ItemListOrderDescending' }, + ItemListOrderType: { '@id': 'schema:ItemListOrderType' }, + ItemListUnordered: { '@id': 'schema:ItemListUnordered' }, + ItemPage: { '@id': 'schema:ItemPage' }, + JewelryStore: { '@id': 'schema:JewelryStore' }, + JobPosting: { '@id': 'schema:JobPosting' }, + JoinAction: { '@id': 'schema:JoinAction' }, + Joint: { '@id': 'schema:Joint' }, + KosherDiet: { '@id': 'schema:KosherDiet' }, + LaboratoryScience: { '@id': 'schema:LaboratoryScience' }, + LakeBodyOfWater: { '@id': 'schema:LakeBodyOfWater' }, + Landform: { '@id': 'schema:Landform' }, + LandmarksOrHistoricalBuildings: { + '@id': 'schema:LandmarksOrHistoricalBuildings', + }, + Language: { '@id': 'schema:Language' }, + LaserDiscFormat: { '@id': 'schema:LaserDiscFormat' }, + LearningResource: { '@id': 'schema:LearningResource' }, + LeaveAction: { '@id': 'schema:LeaveAction' }, + LeftHandDriving: { '@id': 'schema:LeftHandDriving' }, + LegalForceStatus: { '@id': 'schema:LegalForceStatus' }, + LegalService: { '@id': 'schema:LegalService' }, + LegalValueLevel: { '@id': 'schema:LegalValueLevel' }, + Legislation: { '@id': 'schema:Legislation' }, + LegislationObject: { '@id': 'schema:LegislationObject' }, + LegislativeBuilding: { '@id': 'schema:LegislativeBuilding' }, + LeisureTimeActivity: { '@id': 'schema:LeisureTimeActivity' }, + LendAction: { '@id': 'schema:LendAction' }, + Library: { '@id': 'schema:Library' }, + LibrarySystem: { '@id': 'schema:LibrarySystem' }, + LifestyleModification: { '@id': 'schema:LifestyleModification' }, + Ligament: { '@id': 'schema:Ligament' }, + LikeAction: { '@id': 'schema:LikeAction' }, + LimitedAvailability: { '@id': 'schema:LimitedAvailability' }, + LimitedByGuaranteeCharity: { '@id': 'schema:LimitedByGuaranteeCharity' }, + LinkRole: { '@id': 'schema:LinkRole' }, + LiquorStore: { '@id': 'schema:LiquorStore' }, + ListItem: { '@id': 'schema:ListItem' }, + ListPrice: { '@id': 'schema:ListPrice' }, + ListenAction: { '@id': 'schema:ListenAction' }, + LiteraryEvent: { '@id': 'schema:LiteraryEvent' }, + LiveAlbum: { '@id': 'schema:LiveAlbum' }, + LiveBlogPosting: { '@id': 'schema:LiveBlogPosting' }, + LivingWithHealthAspect: { '@id': 'schema:LivingWithHealthAspect' }, + LoanOrCredit: { '@id': 'schema:LoanOrCredit' }, + LocalBusiness: { '@id': 'schema:LocalBusiness' }, + LocationFeatureSpecification: { '@id': 'schema:LocationFeatureSpecification' }, + LockerDelivery: { '@id': 'schema:LockerDelivery' }, + Locksmith: { '@id': 'schema:Locksmith' }, + LodgingBusiness: { '@id': 'schema:LodgingBusiness' }, + LodgingReservation: { '@id': 'schema:LodgingReservation' }, + Longitudinal: { '@id': 'schema:Longitudinal' }, + LoseAction: { '@id': 'schema:LoseAction' }, + LowCalorieDiet: { '@id': 'schema:LowCalorieDiet' }, + LowFatDiet: { '@id': 'schema:LowFatDiet' }, + LowLactoseDiet: { '@id': 'schema:LowLactoseDiet' }, + LowSaltDiet: { '@id': 'schema:LowSaltDiet' }, + Lung: { '@id': 'schema:Lung' }, + LymphaticVessel: { '@id': 'schema:LymphaticVessel' }, + MRI: { '@id': 'schema:MRI' }, + MSRP: { '@id': 'schema:MSRP' }, + Male: { '@id': 'schema:Male' }, + Manuscript: { '@id': 'schema:Manuscript' }, + Map: { '@id': 'schema:Map' }, + MapCategoryType: { '@id': 'schema:MapCategoryType' }, + MarryAction: { '@id': 'schema:MarryAction' }, + Mass: { '@id': 'schema:Mass' }, + MathSolver: { '@id': 'schema:MathSolver' }, + MaximumDoseSchedule: { '@id': 'schema:MaximumDoseSchedule' }, + MayTreatHealthAspect: { '@id': 'schema:MayTreatHealthAspect' }, + MeasurementTypeEnumeration: { '@id': 'schema:MeasurementTypeEnumeration' }, + MediaGallery: { '@id': 'schema:MediaGallery' }, + MediaManipulationRatingEnumeration: { + '@id': 'schema:MediaManipulationRatingEnumeration', + }, + MediaObject: { '@id': 'schema:MediaObject' }, + MediaReview: { '@id': 'schema:MediaReview' }, + MediaSubscription: { '@id': 'schema:MediaSubscription' }, + MedicalAudience: { '@id': 'schema:MedicalAudience' }, + MedicalAudienceType: { '@id': 'schema:MedicalAudienceType' }, + MedicalBusiness: { '@id': 'schema:MedicalBusiness' }, + MedicalCause: { '@id': 'schema:MedicalCause' }, + MedicalClinic: { '@id': 'schema:MedicalClinic' }, + MedicalCode: { '@id': 'schema:MedicalCode' }, + MedicalCondition: { '@id': 'schema:MedicalCondition' }, + MedicalConditionStage: { '@id': 'schema:MedicalConditionStage' }, + MedicalContraindication: { '@id': 'schema:MedicalContraindication' }, + MedicalDevice: { '@id': 'schema:MedicalDevice' }, + MedicalDevicePurpose: { '@id': 'schema:MedicalDevicePurpose' }, + MedicalEntity: { '@id': 'schema:MedicalEntity' }, + MedicalEnumeration: { '@id': 'schema:MedicalEnumeration' }, + MedicalEvidenceLevel: { '@id': 'schema:MedicalEvidenceLevel' }, + MedicalGuideline: { '@id': 'schema:MedicalGuideline' }, + MedicalGuidelineContraindication: { + '@id': 'schema:MedicalGuidelineContraindication', + }, + MedicalGuidelineRecommendation: { + '@id': 'schema:MedicalGuidelineRecommendation', + }, + MedicalImagingTechnique: { '@id': 'schema:MedicalImagingTechnique' }, + MedicalIndication: { '@id': 'schema:MedicalIndication' }, + MedicalIntangible: { '@id': 'schema:MedicalIntangible' }, + MedicalObservationalStudy: { '@id': 'schema:MedicalObservationalStudy' }, + MedicalObservationalStudyDesign: { + '@id': 'schema:MedicalObservationalStudyDesign', + }, + MedicalOrganization: { '@id': 'schema:MedicalOrganization' }, + MedicalProcedure: { '@id': 'schema:MedicalProcedure' }, + MedicalProcedureType: { '@id': 'schema:MedicalProcedureType' }, + MedicalResearcher: { '@id': 'schema:MedicalResearcher' }, + MedicalRiskCalculator: { '@id': 'schema:MedicalRiskCalculator' }, + MedicalRiskEstimator: { '@id': 'schema:MedicalRiskEstimator' }, + MedicalRiskFactor: { '@id': 'schema:MedicalRiskFactor' }, + MedicalRiskScore: { '@id': 'schema:MedicalRiskScore' }, + MedicalScholarlyArticle: { '@id': 'schema:MedicalScholarlyArticle' }, + MedicalSign: { '@id': 'schema:MedicalSign' }, + MedicalSignOrSymptom: { '@id': 'schema:MedicalSignOrSymptom' }, + MedicalSpecialty: { '@id': 'schema:MedicalSpecialty' }, + MedicalStudy: { '@id': 'schema:MedicalStudy' }, + MedicalStudyStatus: { '@id': 'schema:MedicalStudyStatus' }, + MedicalSymptom: { '@id': 'schema:MedicalSymptom' }, + MedicalTest: { '@id': 'schema:MedicalTest' }, + MedicalTestPanel: { '@id': 'schema:MedicalTestPanel' }, + MedicalTherapy: { '@id': 'schema:MedicalTherapy' }, + MedicalTrial: { '@id': 'schema:MedicalTrial' }, + MedicalTrialDesign: { '@id': 'schema:MedicalTrialDesign' }, + MedicalWebPage: { '@id': 'schema:MedicalWebPage' }, + MedicineSystem: { '@id': 'schema:MedicineSystem' }, + MeetingRoom: { '@id': 'schema:MeetingRoom' }, + MensClothingStore: { '@id': 'schema:MensClothingStore' }, + Menu: { '@id': 'schema:Menu' }, + MenuItem: { '@id': 'schema:MenuItem' }, + MenuSection: { '@id': 'schema:MenuSection' }, + MerchantReturnEnumeration: { '@id': 'schema:MerchantReturnEnumeration' }, + MerchantReturnFiniteReturnWindow: { + '@id': 'schema:MerchantReturnFiniteReturnWindow', + }, + MerchantReturnNotPermitted: { '@id': 'schema:MerchantReturnNotPermitted' }, + MerchantReturnPolicy: { '@id': 'schema:MerchantReturnPolicy' }, + MerchantReturnUnlimitedWindow: { + '@id': 'schema:MerchantReturnUnlimitedWindow', + }, + MerchantReturnUnspecified: { '@id': 'schema:MerchantReturnUnspecified' }, + Message: { '@id': 'schema:Message' }, + MiddleSchool: { '@id': 'schema:MiddleSchool' }, + Midwifery: { '@id': 'schema:Midwifery' }, + MinimumAdvertisedPrice: { '@id': 'schema:MinimumAdvertisedPrice' }, + MisconceptionsHealthAspect: { '@id': 'schema:MisconceptionsHealthAspect' }, + MixedEventAttendanceMode: { '@id': 'schema:MixedEventAttendanceMode' }, + MixtapeAlbum: { '@id': 'schema:MixtapeAlbum' }, + MobileApplication: { '@id': 'schema:MobileApplication' }, + MobilePhoneStore: { '@id': 'schema:MobilePhoneStore' }, + Monday: { '@id': 'schema:Monday' }, + MonetaryAmount: { '@id': 'schema:MonetaryAmount' }, + MonetaryAmountDistribution: { '@id': 'schema:MonetaryAmountDistribution' }, + MonetaryGrant: { '@id': 'schema:MonetaryGrant' }, + MoneyTransfer: { '@id': 'schema:MoneyTransfer' }, + MortgageLoan: { '@id': 'schema:MortgageLoan' }, + Mosque: { '@id': 'schema:Mosque' }, + Motel: { '@id': 'schema:Motel' }, + Motorcycle: { '@id': 'schema:Motorcycle' }, + MotorcycleDealer: { '@id': 'schema:MotorcycleDealer' }, + MotorcycleRepair: { '@id': 'schema:MotorcycleRepair' }, + MotorizedBicycle: { '@id': 'schema:MotorizedBicycle' }, + Mountain: { '@id': 'schema:Mountain' }, + MoveAction: { '@id': 'schema:MoveAction' }, + Movie: { '@id': 'schema:Movie' }, + MovieClip: { '@id': 'schema:MovieClip' }, + MovieRentalStore: { '@id': 'schema:MovieRentalStore' }, + MovieSeries: { '@id': 'schema:MovieSeries' }, + MovieTheater: { '@id': 'schema:MovieTheater' }, + MovingCompany: { '@id': 'schema:MovingCompany' }, + MultiCenterTrial: { '@id': 'schema:MultiCenterTrial' }, + MultiPlayer: { '@id': 'schema:MultiPlayer' }, + MulticellularParasite: { '@id': 'schema:MulticellularParasite' }, + Muscle: { '@id': 'schema:Muscle' }, + Musculoskeletal: { '@id': 'schema:Musculoskeletal' }, + MusculoskeletalExam: { '@id': 'schema:MusculoskeletalExam' }, + Museum: { '@id': 'schema:Museum' }, + MusicAlbum: { '@id': 'schema:MusicAlbum' }, + MusicAlbumProductionType: { '@id': 'schema:MusicAlbumProductionType' }, + MusicAlbumReleaseType: { '@id': 'schema:MusicAlbumReleaseType' }, + MusicComposition: { '@id': 'schema:MusicComposition' }, + MusicEvent: { '@id': 'schema:MusicEvent' }, + MusicGroup: { '@id': 'schema:MusicGroup' }, + MusicPlaylist: { '@id': 'schema:MusicPlaylist' }, + MusicRecording: { '@id': 'schema:MusicRecording' }, + MusicRelease: { '@id': 'schema:MusicRelease' }, + MusicReleaseFormatType: { '@id': 'schema:MusicReleaseFormatType' }, + MusicStore: { '@id': 'schema:MusicStore' }, + MusicVenue: { '@id': 'schema:MusicVenue' }, + MusicVideoObject: { '@id': 'schema:MusicVideoObject' }, + NGO: { '@id': 'schema:NGO' }, + NLNonprofitType: { '@id': 'schema:NLNonprofitType' }, + NailSalon: { '@id': 'schema:NailSalon' }, + Neck: { '@id': 'schema:Neck' }, + Nerve: { '@id': 'schema:Nerve' }, + Neuro: { '@id': 'schema:Neuro' }, + Neurologic: { '@id': 'schema:Neurologic' }, + NewCondition: { '@id': 'schema:NewCondition' }, + NewsArticle: { '@id': 'schema:NewsArticle' }, + NewsMediaOrganization: { '@id': 'schema:NewsMediaOrganization' }, + Newspaper: { '@id': 'schema:Newspaper' }, + NightClub: { '@id': 'schema:NightClub' }, + NoninvasiveProcedure: { '@id': 'schema:NoninvasiveProcedure' }, + Nonprofit501a: { '@id': 'schema:Nonprofit501a' }, + Nonprofit501c1: { '@id': 'schema:Nonprofit501c1' }, + Nonprofit501c10: { '@id': 'schema:Nonprofit501c10' }, + Nonprofit501c11: { '@id': 'schema:Nonprofit501c11' }, + Nonprofit501c12: { '@id': 'schema:Nonprofit501c12' }, + Nonprofit501c13: { '@id': 'schema:Nonprofit501c13' }, + Nonprofit501c14: { '@id': 'schema:Nonprofit501c14' }, + Nonprofit501c15: { '@id': 'schema:Nonprofit501c15' }, + Nonprofit501c16: { '@id': 'schema:Nonprofit501c16' }, + Nonprofit501c17: { '@id': 'schema:Nonprofit501c17' }, + Nonprofit501c18: { '@id': 'schema:Nonprofit501c18' }, + Nonprofit501c19: { '@id': 'schema:Nonprofit501c19' }, + Nonprofit501c2: { '@id': 'schema:Nonprofit501c2' }, + Nonprofit501c20: { '@id': 'schema:Nonprofit501c20' }, + Nonprofit501c21: { '@id': 'schema:Nonprofit501c21' }, + Nonprofit501c22: { '@id': 'schema:Nonprofit501c22' }, + Nonprofit501c23: { '@id': 'schema:Nonprofit501c23' }, + Nonprofit501c24: { '@id': 'schema:Nonprofit501c24' }, + Nonprofit501c25: { '@id': 'schema:Nonprofit501c25' }, + Nonprofit501c26: { '@id': 'schema:Nonprofit501c26' }, + Nonprofit501c27: { '@id': 'schema:Nonprofit501c27' }, + Nonprofit501c28: { '@id': 'schema:Nonprofit501c28' }, + Nonprofit501c3: { '@id': 'schema:Nonprofit501c3' }, + Nonprofit501c4: { '@id': 'schema:Nonprofit501c4' }, + Nonprofit501c5: { '@id': 'schema:Nonprofit501c5' }, + Nonprofit501c6: { '@id': 'schema:Nonprofit501c6' }, + Nonprofit501c7: { '@id': 'schema:Nonprofit501c7' }, + Nonprofit501c8: { '@id': 'schema:Nonprofit501c8' }, + Nonprofit501c9: { '@id': 'schema:Nonprofit501c9' }, + Nonprofit501d: { '@id': 'schema:Nonprofit501d' }, + Nonprofit501e: { '@id': 'schema:Nonprofit501e' }, + Nonprofit501f: { '@id': 'schema:Nonprofit501f' }, + Nonprofit501k: { '@id': 'schema:Nonprofit501k' }, + Nonprofit501n: { '@id': 'schema:Nonprofit501n' }, + Nonprofit501q: { '@id': 'schema:Nonprofit501q' }, + Nonprofit527: { '@id': 'schema:Nonprofit527' }, + NonprofitANBI: { '@id': 'schema:NonprofitANBI' }, + NonprofitSBBI: { '@id': 'schema:NonprofitSBBI' }, + NonprofitType: { '@id': 'schema:NonprofitType' }, + Nose: { '@id': 'schema:Nose' }, + NotInForce: { '@id': 'schema:NotInForce' }, + NotYetRecruiting: { '@id': 'schema:NotYetRecruiting' }, + Notary: { '@id': 'schema:Notary' }, + NoteDigitalDocument: { '@id': 'schema:NoteDigitalDocument' }, + Number: { '@id': 'schema:Number' }, + Nursing: { '@id': 'schema:Nursing' }, + NutritionInformation: { '@id': 'schema:NutritionInformation' }, + OTC: { '@id': 'schema:OTC' }, + Observation: { '@id': 'schema:Observation' }, + Observational: { '@id': 'schema:Observational' }, + Obstetric: { '@id': 'schema:Obstetric' }, + Occupation: { '@id': 'schema:Occupation' }, + OccupationalActivity: { '@id': 'schema:OccupationalActivity' }, + OccupationalExperienceRequirements: { + '@id': 'schema:OccupationalExperienceRequirements', + }, + OccupationalTherapy: { '@id': 'schema:OccupationalTherapy' }, + OceanBodyOfWater: { '@id': 'schema:OceanBodyOfWater' }, + Offer: { '@id': 'schema:Offer' }, + OfferCatalog: { '@id': 'schema:OfferCatalog' }, + OfferForLease: { '@id': 'schema:OfferForLease' }, + OfferForPurchase: { '@id': 'schema:OfferForPurchase' }, + OfferItemCondition: { '@id': 'schema:OfferItemCondition' }, + OfferShippingDetails: { '@id': 'schema:OfferShippingDetails' }, + OfficeEquipmentStore: { '@id': 'schema:OfficeEquipmentStore' }, + OfficialLegalValue: { '@id': 'schema:OfficialLegalValue' }, + OfflineEventAttendanceMode: { '@id': 'schema:OfflineEventAttendanceMode' }, + OfflinePermanently: { '@id': 'schema:OfflinePermanently' }, + OfflineTemporarily: { '@id': 'schema:OfflineTemporarily' }, + OnDemandEvent: { '@id': 'schema:OnDemandEvent' }, + OnSitePickup: { '@id': 'schema:OnSitePickup' }, + Oncologic: { '@id': 'schema:Oncologic' }, + OneTimePayments: { '@id': 'schema:OneTimePayments' }, + Online: { '@id': 'schema:Online' }, + OnlineEventAttendanceMode: { '@id': 'schema:OnlineEventAttendanceMode' }, + OnlineFull: { '@id': 'schema:OnlineFull' }, + OnlineOnly: { '@id': 'schema:OnlineOnly' }, + OpenTrial: { '@id': 'schema:OpenTrial' }, + OpeningHoursSpecification: { '@id': 'schema:OpeningHoursSpecification' }, + OpinionNewsArticle: { '@id': 'schema:OpinionNewsArticle' }, + Optician: { '@id': 'schema:Optician' }, + Optometric: { '@id': 'schema:Optometric' }, + Order: { '@id': 'schema:Order' }, + OrderAction: { '@id': 'schema:OrderAction' }, + OrderCancelled: { '@id': 'schema:OrderCancelled' }, + OrderDelivered: { '@id': 'schema:OrderDelivered' }, + OrderInTransit: { '@id': 'schema:OrderInTransit' }, + OrderItem: { '@id': 'schema:OrderItem' }, + OrderPaymentDue: { '@id': 'schema:OrderPaymentDue' }, + OrderPickupAvailable: { '@id': 'schema:OrderPickupAvailable' }, + OrderProblem: { '@id': 'schema:OrderProblem' }, + OrderProcessing: { '@id': 'schema:OrderProcessing' }, + OrderReturned: { '@id': 'schema:OrderReturned' }, + OrderStatus: { '@id': 'schema:OrderStatus' }, + Organization: { '@id': 'schema:Organization' }, + OrganizationRole: { '@id': 'schema:OrganizationRole' }, + OrganizeAction: { '@id': 'schema:OrganizeAction' }, + OriginalMediaContent: { '@id': 'schema:OriginalMediaContent' }, + OriginalShippingFees: { '@id': 'schema:OriginalShippingFees' }, + Osteopathic: { '@id': 'schema:Osteopathic' }, + Otolaryngologic: { '@id': 'schema:Otolaryngologic' }, + OutOfStock: { '@id': 'schema:OutOfStock' }, + OutletStore: { '@id': 'schema:OutletStore' }, + OverviewHealthAspect: { '@id': 'schema:OverviewHealthAspect' }, + OwnershipInfo: { '@id': 'schema:OwnershipInfo' }, + PET: { '@id': 'schema:PET' }, + PaidLeave: { '@id': 'schema:PaidLeave' }, + PaintAction: { '@id': 'schema:PaintAction' }, + Painting: { '@id': 'schema:Painting' }, + PalliativeProcedure: { '@id': 'schema:PalliativeProcedure' }, + Paperback: { '@id': 'schema:Paperback' }, + ParcelDelivery: { '@id': 'schema:ParcelDelivery' }, + ParcelService: { '@id': 'schema:ParcelService' }, + ParentAudience: { '@id': 'schema:ParentAudience' }, + ParentalSupport: { '@id': 'schema:ParentalSupport' }, + Park: { '@id': 'schema:Park' }, + ParkingFacility: { '@id': 'schema:ParkingFacility' }, + ParkingMap: { '@id': 'schema:ParkingMap' }, + PartiallyInForce: { '@id': 'schema:PartiallyInForce' }, + Pathology: { '@id': 'schema:Pathology' }, + PathologyTest: { '@id': 'schema:PathologyTest' }, + Patient: { '@id': 'schema:Patient' }, + PatientExperienceHealthAspect: { + '@id': 'schema:PatientExperienceHealthAspect', + }, + PawnShop: { '@id': 'schema:PawnShop' }, + PayAction: { '@id': 'schema:PayAction' }, + PaymentAutomaticallyApplied: { '@id': 'schema:PaymentAutomaticallyApplied' }, + PaymentCard: { '@id': 'schema:PaymentCard' }, + PaymentChargeSpecification: { '@id': 'schema:PaymentChargeSpecification' }, + PaymentComplete: { '@id': 'schema:PaymentComplete' }, + PaymentDeclined: { '@id': 'schema:PaymentDeclined' }, + PaymentDue: { '@id': 'schema:PaymentDue' }, + PaymentMethod: { '@id': 'schema:PaymentMethod' }, + PaymentPastDue: { '@id': 'schema:PaymentPastDue' }, + PaymentService: { '@id': 'schema:PaymentService' }, + PaymentStatusType: { '@id': 'schema:PaymentStatusType' }, + Pediatric: { '@id': 'schema:Pediatric' }, + PeopleAudience: { '@id': 'schema:PeopleAudience' }, + PercutaneousProcedure: { '@id': 'schema:PercutaneousProcedure' }, + PerformAction: { '@id': 'schema:PerformAction' }, + PerformanceRole: { '@id': 'schema:PerformanceRole' }, + PerformingArtsTheater: { '@id': 'schema:PerformingArtsTheater' }, + PerformingGroup: { '@id': 'schema:PerformingGroup' }, + Periodical: { '@id': 'schema:Periodical' }, + Permit: { '@id': 'schema:Permit' }, + Person: { '@id': 'schema:Person' }, + PetStore: { '@id': 'schema:PetStore' }, + Pharmacy: { '@id': 'schema:Pharmacy' }, + PharmacySpecialty: { '@id': 'schema:PharmacySpecialty' }, + Photograph: { '@id': 'schema:Photograph' }, + PhotographAction: { '@id': 'schema:PhotographAction' }, + PhysicalActivity: { '@id': 'schema:PhysicalActivity' }, + PhysicalActivityCategory: { '@id': 'schema:PhysicalActivityCategory' }, + PhysicalExam: { '@id': 'schema:PhysicalExam' }, + PhysicalTherapy: { '@id': 'schema:PhysicalTherapy' }, + Physician: { '@id': 'schema:Physician' }, + Physiotherapy: { '@id': 'schema:Physiotherapy' }, + Place: { '@id': 'schema:Place' }, + PlaceOfWorship: { '@id': 'schema:PlaceOfWorship' }, + PlaceboControlledTrial: { '@id': 'schema:PlaceboControlledTrial' }, + PlanAction: { '@id': 'schema:PlanAction' }, + PlasticSurgery: { '@id': 'schema:PlasticSurgery' }, + Play: { '@id': 'schema:Play' }, + PlayAction: { '@id': 'schema:PlayAction' }, + Playground: { '@id': 'schema:Playground' }, + Plumber: { '@id': 'schema:Plumber' }, + PodcastEpisode: { '@id': 'schema:PodcastEpisode' }, + PodcastSeason: { '@id': 'schema:PodcastSeason' }, + PodcastSeries: { '@id': 'schema:PodcastSeries' }, + Podiatric: { '@id': 'schema:Podiatric' }, + PoliceStation: { '@id': 'schema:PoliceStation' }, + Pond: { '@id': 'schema:Pond' }, + PostOffice: { '@id': 'schema:PostOffice' }, + PostalAddress: { '@id': 'schema:PostalAddress' }, + PostalCodeRangeSpecification: { '@id': 'schema:PostalCodeRangeSpecification' }, + Poster: { '@id': 'schema:Poster' }, + PotentialActionStatus: { '@id': 'schema:PotentialActionStatus' }, + PreOrder: { '@id': 'schema:PreOrder' }, + PreOrderAction: { '@id': 'schema:PreOrderAction' }, + PreSale: { '@id': 'schema:PreSale' }, + PregnancyHealthAspect: { '@id': 'schema:PregnancyHealthAspect' }, + PrependAction: { '@id': 'schema:PrependAction' }, + Preschool: { '@id': 'schema:Preschool' }, + PrescriptionOnly: { '@id': 'schema:PrescriptionOnly' }, + PresentationDigitalDocument: { '@id': 'schema:PresentationDigitalDocument' }, + PreventionHealthAspect: { '@id': 'schema:PreventionHealthAspect' }, + PreventionIndication: { '@id': 'schema:PreventionIndication' }, + PriceComponentTypeEnumeration: { + '@id': 'schema:PriceComponentTypeEnumeration', + }, + PriceSpecification: { '@id': 'schema:PriceSpecification' }, + PriceTypeEnumeration: { '@id': 'schema:PriceTypeEnumeration' }, + PrimaryCare: { '@id': 'schema:PrimaryCare' }, + Prion: { '@id': 'schema:Prion' }, + Product: { '@id': 'schema:Product' }, + ProductCollection: { '@id': 'schema:ProductCollection' }, + ProductGroup: { '@id': 'schema:ProductGroup' }, + ProductModel: { '@id': 'schema:ProductModel' }, + ProductReturnEnumeration: { '@id': 'schema:ProductReturnEnumeration' }, + ProductReturnFiniteReturnWindow: { + '@id': 'schema:ProductReturnFiniteReturnWindow', + }, + ProductReturnNotPermitted: { '@id': 'schema:ProductReturnNotPermitted' }, + ProductReturnPolicy: { '@id': 'schema:ProductReturnPolicy' }, + ProductReturnUnlimitedWindow: { '@id': 'schema:ProductReturnUnlimitedWindow' }, + ProductReturnUnspecified: { '@id': 'schema:ProductReturnUnspecified' }, + ProfessionalService: { '@id': 'schema:ProfessionalService' }, + ProfilePage: { '@id': 'schema:ProfilePage' }, + PrognosisHealthAspect: { '@id': 'schema:PrognosisHealthAspect' }, + ProgramMembership: { '@id': 'schema:ProgramMembership' }, + Project: { '@id': 'schema:Project' }, + PronounceableText: { '@id': 'schema:PronounceableText' }, + Property: { '@id': 'schema:Property' }, + PropertyValue: { '@id': 'schema:PropertyValue' }, + PropertyValueSpecification: { '@id': 'schema:PropertyValueSpecification' }, + Protozoa: { '@id': 'schema:Protozoa' }, + Psychiatric: { '@id': 'schema:Psychiatric' }, + PsychologicalTreatment: { '@id': 'schema:PsychologicalTreatment' }, + PublicHealth: { '@id': 'schema:PublicHealth' }, + PublicHolidays: { '@id': 'schema:PublicHolidays' }, + PublicSwimmingPool: { '@id': 'schema:PublicSwimmingPool' }, + PublicToilet: { '@id': 'schema:PublicToilet' }, + PublicationEvent: { '@id': 'schema:PublicationEvent' }, + PublicationIssue: { '@id': 'schema:PublicationIssue' }, + PublicationVolume: { '@id': 'schema:PublicationVolume' }, + Pulmonary: { '@id': 'schema:Pulmonary' }, + QAPage: { '@id': 'schema:QAPage' }, + QualitativeValue: { '@id': 'schema:QualitativeValue' }, + QuantitativeValue: { '@id': 'schema:QuantitativeValue' }, + QuantitativeValueDistribution: { + '@id': 'schema:QuantitativeValueDistribution', + }, + Quantity: { '@id': 'schema:Quantity' }, + Question: { '@id': 'schema:Question' }, + Quiz: { '@id': 'schema:Quiz' }, + Quotation: { '@id': 'schema:Quotation' }, + QuoteAction: { '@id': 'schema:QuoteAction' }, + RVPark: { '@id': 'schema:RVPark' }, + RadiationTherapy: { '@id': 'schema:RadiationTherapy' }, + RadioBroadcastService: { '@id': 'schema:RadioBroadcastService' }, + RadioChannel: { '@id': 'schema:RadioChannel' }, + RadioClip: { '@id': 'schema:RadioClip' }, + RadioEpisode: { '@id': 'schema:RadioEpisode' }, + RadioSeason: { '@id': 'schema:RadioSeason' }, + RadioSeries: { '@id': 'schema:RadioSeries' }, + RadioStation: { '@id': 'schema:RadioStation' }, + Radiography: { '@id': 'schema:Radiography' }, + RandomizedTrial: { '@id': 'schema:RandomizedTrial' }, + Rating: { '@id': 'schema:Rating' }, + ReactAction: { '@id': 'schema:ReactAction' }, + ReadAction: { '@id': 'schema:ReadAction' }, + ReadPermission: { '@id': 'schema:ReadPermission' }, + RealEstateAgent: { '@id': 'schema:RealEstateAgent' }, + RealEstateListing: { '@id': 'schema:RealEstateListing' }, + RearWheelDriveConfiguration: { '@id': 'schema:RearWheelDriveConfiguration' }, + ReceiveAction: { '@id': 'schema:ReceiveAction' }, + Recipe: { '@id': 'schema:Recipe' }, + Recommendation: { '@id': 'schema:Recommendation' }, + RecommendedDoseSchedule: { '@id': 'schema:RecommendedDoseSchedule' }, + Recruiting: { '@id': 'schema:Recruiting' }, + RecyclingCenter: { '@id': 'schema:RecyclingCenter' }, + RefundTypeEnumeration: { '@id': 'schema:RefundTypeEnumeration' }, + RefurbishedCondition: { '@id': 'schema:RefurbishedCondition' }, + RegisterAction: { '@id': 'schema:RegisterAction' }, + Registry: { '@id': 'schema:Registry' }, + ReimbursementCap: { '@id': 'schema:ReimbursementCap' }, + RejectAction: { '@id': 'schema:RejectAction' }, + RelatedTopicsHealthAspect: { '@id': 'schema:RelatedTopicsHealthAspect' }, + RemixAlbum: { '@id': 'schema:RemixAlbum' }, + Renal: { '@id': 'schema:Renal' }, + RentAction: { '@id': 'schema:RentAction' }, + RentalCarReservation: { '@id': 'schema:RentalCarReservation' }, + RentalVehicleUsage: { '@id': 'schema:RentalVehicleUsage' }, + RepaymentSpecification: { '@id': 'schema:RepaymentSpecification' }, + ReplaceAction: { '@id': 'schema:ReplaceAction' }, + ReplyAction: { '@id': 'schema:ReplyAction' }, + Report: { '@id': 'schema:Report' }, + ReportageNewsArticle: { '@id': 'schema:ReportageNewsArticle' }, + ReportedDoseSchedule: { '@id': 'schema:ReportedDoseSchedule' }, + ResearchProject: { '@id': 'schema:ResearchProject' }, + Researcher: { '@id': 'schema:Researcher' }, + Reservation: { '@id': 'schema:Reservation' }, + ReservationCancelled: { '@id': 'schema:ReservationCancelled' }, + ReservationConfirmed: { '@id': 'schema:ReservationConfirmed' }, + ReservationHold: { '@id': 'schema:ReservationHold' }, + ReservationPackage: { '@id': 'schema:ReservationPackage' }, + ReservationPending: { '@id': 'schema:ReservationPending' }, + ReservationStatusType: { '@id': 'schema:ReservationStatusType' }, + ReserveAction: { '@id': 'schema:ReserveAction' }, + Reservoir: { '@id': 'schema:Reservoir' }, + Residence: { '@id': 'schema:Residence' }, + Resort: { '@id': 'schema:Resort' }, + RespiratoryTherapy: { '@id': 'schema:RespiratoryTherapy' }, + Restaurant: { '@id': 'schema:Restaurant' }, + RestockingFees: { '@id': 'schema:RestockingFees' }, + RestrictedDiet: { '@id': 'schema:RestrictedDiet' }, + ResultsAvailable: { '@id': 'schema:ResultsAvailable' }, + ResultsNotAvailable: { '@id': 'schema:ResultsNotAvailable' }, + ResumeAction: { '@id': 'schema:ResumeAction' }, + Retail: { '@id': 'schema:Retail' }, + ReturnAction: { '@id': 'schema:ReturnAction' }, + ReturnFeesEnumeration: { '@id': 'schema:ReturnFeesEnumeration' }, + ReturnShippingFees: { '@id': 'schema:ReturnShippingFees' }, + Review: { '@id': 'schema:Review' }, + ReviewAction: { '@id': 'schema:ReviewAction' }, + ReviewNewsArticle: { '@id': 'schema:ReviewNewsArticle' }, + Rheumatologic: { '@id': 'schema:Rheumatologic' }, + RightHandDriving: { '@id': 'schema:RightHandDriving' }, + RisksOrComplicationsHealthAspect: { + '@id': 'schema:RisksOrComplicationsHealthAspect', + }, + RiverBodyOfWater: { '@id': 'schema:RiverBodyOfWater' }, + Role: { '@id': 'schema:Role' }, + RoofingContractor: { '@id': 'schema:RoofingContractor' }, + Room: { '@id': 'schema:Room' }, + RsvpAction: { '@id': 'schema:RsvpAction' }, + RsvpResponseMaybe: { '@id': 'schema:RsvpResponseMaybe' }, + RsvpResponseNo: { '@id': 'schema:RsvpResponseNo' }, + RsvpResponseType: { '@id': 'schema:RsvpResponseType' }, + RsvpResponseYes: { '@id': 'schema:RsvpResponseYes' }, + SRP: { '@id': 'schema:SRP' }, + SafetyHealthAspect: { '@id': 'schema:SafetyHealthAspect' }, + SaleEvent: { '@id': 'schema:SaleEvent' }, + SalePrice: { '@id': 'schema:SalePrice' }, + SatireOrParodyContent: { '@id': 'schema:SatireOrParodyContent' }, + SatiricalArticle: { '@id': 'schema:SatiricalArticle' }, + Saturday: { '@id': 'schema:Saturday' }, + Schedule: { '@id': 'schema:Schedule' }, + ScheduleAction: { '@id': 'schema:ScheduleAction' }, + ScholarlyArticle: { '@id': 'schema:ScholarlyArticle' }, + School: { '@id': 'schema:School' }, + SchoolDistrict: { '@id': 'schema:SchoolDistrict' }, + ScreeningEvent: { '@id': 'schema:ScreeningEvent' }, + ScreeningHealthAspect: { '@id': 'schema:ScreeningHealthAspect' }, + Sculpture: { '@id': 'schema:Sculpture' }, + SeaBodyOfWater: { '@id': 'schema:SeaBodyOfWater' }, + SearchAction: { '@id': 'schema:SearchAction' }, + SearchResultsPage: { '@id': 'schema:SearchResultsPage' }, + Season: { '@id': 'schema:Season' }, + Seat: { '@id': 'schema:Seat' }, + SeatingMap: { '@id': 'schema:SeatingMap' }, + SeeDoctorHealthAspect: { '@id': 'schema:SeeDoctorHealthAspect' }, + SeekToAction: { '@id': 'schema:SeekToAction' }, + SelfCareHealthAspect: { '@id': 'schema:SelfCareHealthAspect' }, + SelfStorage: { '@id': 'schema:SelfStorage' }, + SellAction: { '@id': 'schema:SellAction' }, + SendAction: { '@id': 'schema:SendAction' }, + Series: { '@id': 'schema:Series' }, + Service: { '@id': 'schema:Service' }, + ServiceChannel: { '@id': 'schema:ServiceChannel' }, + ShareAction: { '@id': 'schema:ShareAction' }, + SheetMusic: { '@id': 'schema:SheetMusic' }, + ShippingDeliveryTime: { '@id': 'schema:ShippingDeliveryTime' }, + ShippingRateSettings: { '@id': 'schema:ShippingRateSettings' }, + ShoeStore: { '@id': 'schema:ShoeStore' }, + ShoppingCenter: { '@id': 'schema:ShoppingCenter' }, + ShortStory: { '@id': 'schema:ShortStory' }, + SideEffectsHealthAspect: { '@id': 'schema:SideEffectsHealthAspect' }, + SingleBlindedTrial: { '@id': 'schema:SingleBlindedTrial' }, + SingleCenterTrial: { '@id': 'schema:SingleCenterTrial' }, + SingleFamilyResidence: { '@id': 'schema:SingleFamilyResidence' }, + SinglePlayer: { '@id': 'schema:SinglePlayer' }, + SingleRelease: { '@id': 'schema:SingleRelease' }, + SiteNavigationElement: { '@id': 'schema:SiteNavigationElement' }, + SizeGroupEnumeration: { '@id': 'schema:SizeGroupEnumeration' }, + SizeSpecification: { '@id': 'schema:SizeSpecification' }, + SizeSystemEnumeration: { '@id': 'schema:SizeSystemEnumeration' }, + SizeSystemImperial: { '@id': 'schema:SizeSystemImperial' }, + SizeSystemMetric: { '@id': 'schema:SizeSystemMetric' }, + SkiResort: { '@id': 'schema:SkiResort' }, + Skin: { '@id': 'schema:Skin' }, + SocialEvent: { '@id': 'schema:SocialEvent' }, + SocialMediaPosting: { '@id': 'schema:SocialMediaPosting' }, + SoftwareApplication: { '@id': 'schema:SoftwareApplication' }, + SoftwareSourceCode: { '@id': 'schema:SoftwareSourceCode' }, + SoldOut: { '@id': 'schema:SoldOut' }, + SolveMathAction: { '@id': 'schema:SolveMathAction' }, + SomeProducts: { '@id': 'schema:SomeProducts' }, + SoundtrackAlbum: { '@id': 'schema:SoundtrackAlbum' }, + SpeakableSpecification: { '@id': 'schema:SpeakableSpecification' }, + SpecialAnnouncement: { '@id': 'schema:SpecialAnnouncement' }, + Specialty: { '@id': 'schema:Specialty' }, + SpeechPathology: { '@id': 'schema:SpeechPathology' }, + SpokenWordAlbum: { '@id': 'schema:SpokenWordAlbum' }, + SportingGoodsStore: { '@id': 'schema:SportingGoodsStore' }, + SportsActivityLocation: { '@id': 'schema:SportsActivityLocation' }, + SportsClub: { '@id': 'schema:SportsClub' }, + SportsEvent: { '@id': 'schema:SportsEvent' }, + SportsOrganization: { '@id': 'schema:SportsOrganization' }, + SportsTeam: { '@id': 'schema:SportsTeam' }, + SpreadsheetDigitalDocument: { '@id': 'schema:SpreadsheetDigitalDocument' }, + StadiumOrArena: { '@id': 'schema:StadiumOrArena' }, + StagedContent: { '@id': 'schema:StagedContent' }, + StagesHealthAspect: { '@id': 'schema:StagesHealthAspect' }, + State: { '@id': 'schema:State' }, + StatisticalPopulation: { '@id': 'schema:StatisticalPopulation' }, + StatusEnumeration: { '@id': 'schema:StatusEnumeration' }, + SteeringPositionValue: { '@id': 'schema:SteeringPositionValue' }, + Store: { '@id': 'schema:Store' }, + StoreCreditRefund: { '@id': 'schema:StoreCreditRefund' }, + StrengthTraining: { '@id': 'schema:StrengthTraining' }, + StructuredValue: { '@id': 'schema:StructuredValue' }, + StudioAlbum: { '@id': 'schema:StudioAlbum' }, + StupidType: { '@id': 'schema:StupidType' }, + SubscribeAction: { '@id': 'schema:SubscribeAction' }, + Subscription: { '@id': 'schema:Subscription' }, + Substance: { '@id': 'schema:Substance' }, + SubwayStation: { '@id': 'schema:SubwayStation' }, + Suite: { '@id': 'schema:Suite' }, + Sunday: { '@id': 'schema:Sunday' }, + SuperficialAnatomy: { '@id': 'schema:SuperficialAnatomy' }, + Surgical: { '@id': 'schema:Surgical' }, + SurgicalProcedure: { '@id': 'schema:SurgicalProcedure' }, + SuspendAction: { '@id': 'schema:SuspendAction' }, + Suspended: { '@id': 'schema:Suspended' }, + SymptomsHealthAspect: { '@id': 'schema:SymptomsHealthAspect' }, + Synagogue: { '@id': 'schema:Synagogue' }, + TVClip: { '@id': 'schema:TVClip' }, + TVEpisode: { '@id': 'schema:TVEpisode' }, + TVSeason: { '@id': 'schema:TVSeason' }, + TVSeries: { '@id': 'schema:TVSeries' }, + Table: { '@id': 'schema:Table' }, + TakeAction: { '@id': 'schema:TakeAction' }, + TattooParlor: { '@id': 'schema:TattooParlor' }, + Taxi: { '@id': 'schema:Taxi' }, + TaxiReservation: { '@id': 'schema:TaxiReservation' }, + TaxiService: { '@id': 'schema:TaxiService' }, + TaxiStand: { '@id': 'schema:TaxiStand' }, + TaxiVehicleUsage: { '@id': 'schema:TaxiVehicleUsage' }, + TechArticle: { '@id': 'schema:TechArticle' }, + TelevisionChannel: { '@id': 'schema:TelevisionChannel' }, + TelevisionStation: { '@id': 'schema:TelevisionStation' }, + TennisComplex: { '@id': 'schema:TennisComplex' }, + Terminated: { '@id': 'schema:Terminated' }, + Text: { '@id': 'schema:Text' }, + TextDigitalDocument: { '@id': 'schema:TextDigitalDocument' }, + TheaterEvent: { '@id': 'schema:TheaterEvent' }, + TheaterGroup: { '@id': 'schema:TheaterGroup' }, + Therapeutic: { '@id': 'schema:Therapeutic' }, + TherapeuticProcedure: { '@id': 'schema:TherapeuticProcedure' }, + Thesis: { '@id': 'schema:Thesis' }, + Thing: { '@id': 'schema:Thing' }, + Throat: { '@id': 'schema:Throat' }, + Thursday: { '@id': 'schema:Thursday' }, + Ticket: { '@id': 'schema:Ticket' }, + TieAction: { '@id': 'schema:TieAction' }, + Time: { '@id': 'schema:Time' }, + TipAction: { '@id': 'schema:TipAction' }, + TireShop: { '@id': 'schema:TireShop' }, + TollFree: { '@id': 'schema:TollFree' }, + TouristAttraction: { '@id': 'schema:TouristAttraction' }, + TouristDestination: { '@id': 'schema:TouristDestination' }, + TouristInformationCenter: { '@id': 'schema:TouristInformationCenter' }, + TouristTrip: { '@id': 'schema:TouristTrip' }, + Toxicologic: { '@id': 'schema:Toxicologic' }, + ToyStore: { '@id': 'schema:ToyStore' }, + TrackAction: { '@id': 'schema:TrackAction' }, + TradeAction: { '@id': 'schema:TradeAction' }, + TraditionalChinese: { '@id': 'schema:TraditionalChinese' }, + TrainReservation: { '@id': 'schema:TrainReservation' }, + TrainStation: { '@id': 'schema:TrainStation' }, + TrainTrip: { '@id': 'schema:TrainTrip' }, + TransferAction: { '@id': 'schema:TransferAction' }, + TransformedContent: { '@id': 'schema:TransformedContent' }, + TransitMap: { '@id': 'schema:TransitMap' }, + TravelAction: { '@id': 'schema:TravelAction' }, + TravelAgency: { '@id': 'schema:TravelAgency' }, + TreatmentIndication: { '@id': 'schema:TreatmentIndication' }, + TreatmentsHealthAspect: { '@id': 'schema:TreatmentsHealthAspect' }, + Trip: { '@id': 'schema:Trip' }, + TripleBlindedTrial: { '@id': 'schema:TripleBlindedTrial' }, + True: { '@id': 'schema:True' }, + Tuesday: { '@id': 'schema:Tuesday' }, + TypeAndQuantityNode: { '@id': 'schema:TypeAndQuantityNode' }, + TypesHealthAspect: { '@id': 'schema:TypesHealthAspect' }, + UKNonprofitType: { '@id': 'schema:UKNonprofitType' }, + UKTrust: { '@id': 'schema:UKTrust' }, + URL: { '@id': 'schema:URL' }, + USNonprofitType: { '@id': 'schema:USNonprofitType' }, + Ultrasound: { '@id': 'schema:Ultrasound' }, + UnRegisterAction: { '@id': 'schema:UnRegisterAction' }, + UnemploymentSupport: { '@id': 'schema:UnemploymentSupport' }, + UnincorporatedAssociationCharity: { + '@id': 'schema:UnincorporatedAssociationCharity', + }, + UnitPriceSpecification: { '@id': 'schema:UnitPriceSpecification' }, + UnofficialLegalValue: { '@id': 'schema:UnofficialLegalValue' }, + UpdateAction: { '@id': 'schema:UpdateAction' }, + Urologic: { '@id': 'schema:Urologic' }, + UsageOrScheduleHealthAspect: { '@id': 'schema:UsageOrScheduleHealthAspect' }, + UseAction: { '@id': 'schema:UseAction' }, + UsedCondition: { '@id': 'schema:UsedCondition' }, + UserBlocks: { '@id': 'schema:UserBlocks' }, + UserCheckins: { '@id': 'schema:UserCheckins' }, + UserComments: { '@id': 'schema:UserComments' }, + UserDownloads: { '@id': 'schema:UserDownloads' }, + UserInteraction: { '@id': 'schema:UserInteraction' }, + UserLikes: { '@id': 'schema:UserLikes' }, + UserPageVisits: { '@id': 'schema:UserPageVisits' }, + UserPlays: { '@id': 'schema:UserPlays' }, + UserPlusOnes: { '@id': 'schema:UserPlusOnes' }, + UserReview: { '@id': 'schema:UserReview' }, + UserTweets: { '@id': 'schema:UserTweets' }, + VeganDiet: { '@id': 'schema:VeganDiet' }, + VegetarianDiet: { '@id': 'schema:VegetarianDiet' }, + Vehicle: { '@id': 'schema:Vehicle' }, + Vein: { '@id': 'schema:Vein' }, + VenueMap: { '@id': 'schema:VenueMap' }, + Vessel: { '@id': 'schema:Vessel' }, + VeterinaryCare: { '@id': 'schema:VeterinaryCare' }, + VideoGallery: { '@id': 'schema:VideoGallery' }, + VideoGame: { '@id': 'schema:VideoGame' }, + VideoGameClip: { '@id': 'schema:VideoGameClip' }, + VideoGameSeries: { '@id': 'schema:VideoGameSeries' }, + VideoObject: { '@id': 'schema:VideoObject' }, + ViewAction: { '@id': 'schema:ViewAction' }, + VinylFormat: { '@id': 'schema:VinylFormat' }, + VirtualLocation: { '@id': 'schema:VirtualLocation' }, + Virus: { '@id': 'schema:Virus' }, + VisualArtsEvent: { '@id': 'schema:VisualArtsEvent' }, + VisualArtwork: { '@id': 'schema:VisualArtwork' }, + VitalSign: { '@id': 'schema:VitalSign' }, + Volcano: { '@id': 'schema:Volcano' }, + VoteAction: { '@id': 'schema:VoteAction' }, + WPAdBlock: { '@id': 'schema:WPAdBlock' }, + WPFooter: { '@id': 'schema:WPFooter' }, + WPHeader: { '@id': 'schema:WPHeader' }, + WPSideBar: { '@id': 'schema:WPSideBar' }, + WantAction: { '@id': 'schema:WantAction' }, + WarrantyPromise: { '@id': 'schema:WarrantyPromise' }, + WarrantyScope: { '@id': 'schema:WarrantyScope' }, + WatchAction: { '@id': 'schema:WatchAction' }, + Waterfall: { '@id': 'schema:Waterfall' }, + WearAction: { '@id': 'schema:WearAction' }, + WearableMeasurementBack: { '@id': 'schema:WearableMeasurementBack' }, + WearableMeasurementChestOrBust: { + '@id': 'schema:WearableMeasurementChestOrBust', + }, + WearableMeasurementCollar: { '@id': 'schema:WearableMeasurementCollar' }, + WearableMeasurementCup: { '@id': 'schema:WearableMeasurementCup' }, + WearableMeasurementHeight: { '@id': 'schema:WearableMeasurementHeight' }, + WearableMeasurementHips: { '@id': 'schema:WearableMeasurementHips' }, + WearableMeasurementInseam: { '@id': 'schema:WearableMeasurementInseam' }, + WearableMeasurementLength: { '@id': 'schema:WearableMeasurementLength' }, + WearableMeasurementOutsideLeg: { + '@id': 'schema:WearableMeasurementOutsideLeg', + }, + WearableMeasurementSleeve: { '@id': 'schema:WearableMeasurementSleeve' }, + WearableMeasurementTypeEnumeration: { + '@id': 'schema:WearableMeasurementTypeEnumeration', + }, + WearableMeasurementWaist: { '@id': 'schema:WearableMeasurementWaist' }, + WearableMeasurementWidth: { '@id': 'schema:WearableMeasurementWidth' }, + WearableSizeGroupBig: { '@id': 'schema:WearableSizeGroupBig' }, + WearableSizeGroupBoys: { '@id': 'schema:WearableSizeGroupBoys' }, + WearableSizeGroupEnumeration: { '@id': 'schema:WearableSizeGroupEnumeration' }, + WearableSizeGroupExtraShort: { '@id': 'schema:WearableSizeGroupExtraShort' }, + WearableSizeGroupExtraTall: { '@id': 'schema:WearableSizeGroupExtraTall' }, + WearableSizeGroupGirls: { '@id': 'schema:WearableSizeGroupGirls' }, + WearableSizeGroupHusky: { '@id': 'schema:WearableSizeGroupHusky' }, + WearableSizeGroupInfants: { '@id': 'schema:WearableSizeGroupInfants' }, + WearableSizeGroupJuniors: { '@id': 'schema:WearableSizeGroupJuniors' }, + WearableSizeGroupMaternity: { '@id': 'schema:WearableSizeGroupMaternity' }, + WearableSizeGroupMens: { '@id': 'schema:WearableSizeGroupMens' }, + WearableSizeGroupMisses: { '@id': 'schema:WearableSizeGroupMisses' }, + WearableSizeGroupPetite: { '@id': 'schema:WearableSizeGroupPetite' }, + WearableSizeGroupPlus: { '@id': 'schema:WearableSizeGroupPlus' }, + WearableSizeGroupRegular: { '@id': 'schema:WearableSizeGroupRegular' }, + WearableSizeGroupShort: { '@id': 'schema:WearableSizeGroupShort' }, + WearableSizeGroupTall: { '@id': 'schema:WearableSizeGroupTall' }, + WearableSizeGroupWomens: { '@id': 'schema:WearableSizeGroupWomens' }, + WearableSizeSystemAU: { '@id': 'schema:WearableSizeSystemAU' }, + WearableSizeSystemBR: { '@id': 'schema:WearableSizeSystemBR' }, + WearableSizeSystemCN: { '@id': 'schema:WearableSizeSystemCN' }, + WearableSizeSystemContinental: { + '@id': 'schema:WearableSizeSystemContinental', + }, + WearableSizeSystemDE: { '@id': 'schema:WearableSizeSystemDE' }, + WearableSizeSystemEN13402: { '@id': 'schema:WearableSizeSystemEN13402' }, + WearableSizeSystemEnumeration: { + '@id': 'schema:WearableSizeSystemEnumeration', + }, + WearableSizeSystemEurope: { '@id': 'schema:WearableSizeSystemEurope' }, + WearableSizeSystemFR: { '@id': 'schema:WearableSizeSystemFR' }, + WearableSizeSystemGS1: { '@id': 'schema:WearableSizeSystemGS1' }, + WearableSizeSystemIT: { '@id': 'schema:WearableSizeSystemIT' }, + WearableSizeSystemJP: { '@id': 'schema:WearableSizeSystemJP' }, + WearableSizeSystemMX: { '@id': 'schema:WearableSizeSystemMX' }, + WearableSizeSystemUK: { '@id': 'schema:WearableSizeSystemUK' }, + WearableSizeSystemUS: { '@id': 'schema:WearableSizeSystemUS' }, + WebAPI: { '@id': 'schema:WebAPI' }, + WebApplication: { '@id': 'schema:WebApplication' }, + WebContent: { '@id': 'schema:WebContent' }, + WebPage: { '@id': 'schema:WebPage' }, + WebPageElement: { '@id': 'schema:WebPageElement' }, + WebSite: { '@id': 'schema:WebSite' }, + Wednesday: { '@id': 'schema:Wednesday' }, + WesternConventional: { '@id': 'schema:WesternConventional' }, + Wholesale: { '@id': 'schema:Wholesale' }, + WholesaleStore: { '@id': 'schema:WholesaleStore' }, + WinAction: { '@id': 'schema:WinAction' }, + Winery: { '@id': 'schema:Winery' }, + Withdrawn: { '@id': 'schema:Withdrawn' }, + WorkBasedProgram: { '@id': 'schema:WorkBasedProgram' }, + WorkersUnion: { '@id': 'schema:WorkersUnion' }, + WriteAction: { '@id': 'schema:WriteAction' }, + WritePermission: { '@id': 'schema:WritePermission' }, + XPathType: { '@id': 'schema:XPathType' }, + XRay: { '@id': 'schema:XRay' }, + ZoneBoardingPolicy: { '@id': 'schema:ZoneBoardingPolicy' }, + Zoo: { '@id': 'schema:Zoo' }, + about: { '@id': 'schema:about' }, + abridged: { '@id': 'schema:abridged' }, + abstract: { '@id': 'schema:abstract' }, + accelerationTime: { '@id': 'schema:accelerationTime' }, + acceptedAnswer: { '@id': 'schema:acceptedAnswer' }, + acceptedOffer: { '@id': 'schema:acceptedOffer' }, + acceptedPaymentMethod: { '@id': 'schema:acceptedPaymentMethod' }, + acceptsReservations: { '@id': 'schema:acceptsReservations' }, + accessCode: { '@id': 'schema:accessCode' }, + accessMode: { '@id': 'schema:accessMode' }, + accessModeSufficient: { '@id': 'schema:accessModeSufficient' }, + accessibilityAPI: { '@id': 'schema:accessibilityAPI' }, + accessibilityControl: { '@id': 'schema:accessibilityControl' }, + accessibilityFeature: { '@id': 'schema:accessibilityFeature' }, + accessibilityHazard: { '@id': 'schema:accessibilityHazard' }, + accessibilitySummary: { '@id': 'schema:accessibilitySummary' }, + accommodationCategory: { '@id': 'schema:accommodationCategory' }, + accommodationFloorPlan: { '@id': 'schema:accommodationFloorPlan' }, + accountId: { '@id': 'schema:accountId' }, + accountMinimumInflow: { '@id': 'schema:accountMinimumInflow' }, + accountOverdraftLimit: { '@id': 'schema:accountOverdraftLimit' }, + accountablePerson: { '@id': 'schema:accountablePerson' }, + acquireLicensePage: { '@id': 'schema:acquireLicensePage', '@type': '@id' }, + acquiredFrom: { '@id': 'schema:acquiredFrom' }, + acrissCode: { '@id': 'schema:acrissCode' }, + actionAccessibilityRequirement: { + '@id': 'schema:actionAccessibilityRequirement', + }, + actionApplication: { '@id': 'schema:actionApplication' }, + actionOption: { '@id': 'schema:actionOption' }, + actionPlatform: { '@id': 'schema:actionPlatform' }, + actionStatus: { '@id': 'schema:actionStatus' }, + actionableFeedbackPolicy: { + '@id': 'schema:actionableFeedbackPolicy', + '@type': '@id', + }, + activeIngredient: { '@id': 'schema:activeIngredient' }, + activityDuration: { '@id': 'schema:activityDuration' }, + activityFrequency: { '@id': 'schema:activityFrequency' }, + actor: { '@id': 'schema:actor' }, + actors: { '@id': 'schema:actors' }, + addOn: { '@id': 'schema:addOn' }, + additionalName: { '@id': 'schema:additionalName' }, + additionalNumberOfGuests: { '@id': 'schema:additionalNumberOfGuests' }, + additionalProperty: { '@id': 'schema:additionalProperty' }, + additionalType: { '@id': 'schema:additionalType', '@type': '@id' }, + additionalVariable: { '@id': 'schema:additionalVariable' }, + address: { '@id': 'schema:address' }, + addressCountry: { '@id': 'schema:addressCountry' }, + addressLocality: { '@id': 'schema:addressLocality' }, + addressRegion: { '@id': 'schema:addressRegion' }, + administrationRoute: { '@id': 'schema:administrationRoute' }, + advanceBookingRequirement: { '@id': 'schema:advanceBookingRequirement' }, + adverseOutcome: { '@id': 'schema:adverseOutcome' }, + affectedBy: { '@id': 'schema:affectedBy' }, + affiliation: { '@id': 'schema:affiliation' }, + afterMedia: { '@id': 'schema:afterMedia', '@type': '@id' }, + agent: { '@id': 'schema:agent' }, + aggregateRating: { '@id': 'schema:aggregateRating' }, + aircraft: { '@id': 'schema:aircraft' }, + album: { '@id': 'schema:album' }, + albumProductionType: { '@id': 'schema:albumProductionType' }, + albumRelease: { '@id': 'schema:albumRelease' }, + albumReleaseType: { '@id': 'schema:albumReleaseType' }, + albums: { '@id': 'schema:albums' }, + alcoholWarning: { '@id': 'schema:alcoholWarning' }, + algorithm: { '@id': 'schema:algorithm' }, + alignmentType: { '@id': 'schema:alignmentType' }, + alternateName: { '@id': 'schema:alternateName' }, + alternativeHeadline: { '@id': 'schema:alternativeHeadline' }, + alumni: { '@id': 'schema:alumni' }, + alumniOf: { '@id': 'schema:alumniOf' }, + amenityFeature: { '@id': 'schema:amenityFeature' }, + amount: { '@id': 'schema:amount' }, + amountOfThisGood: { '@id': 'schema:amountOfThisGood' }, + announcementLocation: { '@id': 'schema:announcementLocation' }, + annualPercentageRate: { '@id': 'schema:annualPercentageRate' }, + answerCount: { '@id': 'schema:answerCount' }, + answerExplanation: { '@id': 'schema:answerExplanation' }, + antagonist: { '@id': 'schema:antagonist' }, + appearance: { '@id': 'schema:appearance' }, + applicableLocation: { '@id': 'schema:applicableLocation' }, + applicantLocationRequirements: { + '@id': 'schema:applicantLocationRequirements', + }, + application: { '@id': 'schema:application' }, + applicationCategory: { '@id': 'schema:applicationCategory' }, + applicationContact: { '@id': 'schema:applicationContact' }, + applicationDeadline: { '@id': 'schema:applicationDeadline', '@type': 'Date' }, + applicationStartDate: { '@id': 'schema:applicationStartDate', '@type': 'Date' }, + applicationSubCategory: { '@id': 'schema:applicationSubCategory' }, + applicationSuite: { '@id': 'schema:applicationSuite' }, + appliesToDeliveryMethod: { '@id': 'schema:appliesToDeliveryMethod' }, + appliesToPaymentMethod: { '@id': 'schema:appliesToPaymentMethod' }, + archiveHeld: { '@id': 'schema:archiveHeld' }, + area: { '@id': 'schema:area' }, + areaServed: { '@id': 'schema:areaServed' }, + arrivalAirport: { '@id': 'schema:arrivalAirport' }, + arrivalBoatTerminal: { '@id': 'schema:arrivalBoatTerminal' }, + arrivalBusStop: { '@id': 'schema:arrivalBusStop' }, + arrivalGate: { '@id': 'schema:arrivalGate' }, + arrivalPlatform: { '@id': 'schema:arrivalPlatform' }, + arrivalStation: { '@id': 'schema:arrivalStation' }, + arrivalTerminal: { '@id': 'schema:arrivalTerminal' }, + arrivalTime: { '@id': 'schema:arrivalTime' }, + artEdition: { '@id': 'schema:artEdition' }, + artMedium: { '@id': 'schema:artMedium' }, + arterialBranch: { '@id': 'schema:arterialBranch' }, + artform: { '@id': 'schema:artform' }, + articleBody: { '@id': 'schema:articleBody' }, + articleSection: { '@id': 'schema:articleSection' }, + artist: { '@id': 'schema:artist' }, + artworkSurface: { '@id': 'schema:artworkSurface' }, + aspect: { '@id': 'schema:aspect' }, + assembly: { '@id': 'schema:assembly' }, + assemblyVersion: { '@id': 'schema:assemblyVersion' }, + assesses: { '@id': 'schema:assesses' }, + associatedAnatomy: { '@id': 'schema:associatedAnatomy' }, + associatedArticle: { '@id': 'schema:associatedArticle' }, + associatedMedia: { '@id': 'schema:associatedMedia' }, + associatedPathophysiology: { '@id': 'schema:associatedPathophysiology' }, + athlete: { '@id': 'schema:athlete' }, + attendee: { '@id': 'schema:attendee' }, + attendees: { '@id': 'schema:attendees' }, + audience: { '@id': 'schema:audience' }, + audienceType: { '@id': 'schema:audienceType' }, + audio: { '@id': 'schema:audio' }, + authenticator: { '@id': 'schema:authenticator' }, + author: { '@id': 'schema:author' }, + availability: { '@id': 'schema:availability' }, + availabilityEnds: { '@id': 'schema:availabilityEnds', '@type': 'Date' }, + availabilityStarts: { '@id': 'schema:availabilityStarts', '@type': 'Date' }, + availableAtOrFrom: { '@id': 'schema:availableAtOrFrom' }, + availableChannel: { '@id': 'schema:availableChannel' }, + availableDeliveryMethod: { '@id': 'schema:availableDeliveryMethod' }, + availableFrom: { '@id': 'schema:availableFrom' }, + availableIn: { '@id': 'schema:availableIn' }, + availableLanguage: { '@id': 'schema:availableLanguage' }, + availableOnDevice: { '@id': 'schema:availableOnDevice' }, + availableService: { '@id': 'schema:availableService' }, + availableStrength: { '@id': 'schema:availableStrength' }, + availableTest: { '@id': 'schema:availableTest' }, + availableThrough: { '@id': 'schema:availableThrough' }, + award: { '@id': 'schema:award' }, + awards: { '@id': 'schema:awards' }, + awayTeam: { '@id': 'schema:awayTeam' }, + backstory: { '@id': 'schema:backstory' }, + bankAccountType: { '@id': 'schema:bankAccountType' }, + baseSalary: { '@id': 'schema:baseSalary' }, + bccRecipient: { '@id': 'schema:bccRecipient' }, + bed: { '@id': 'schema:bed' }, + beforeMedia: { '@id': 'schema:beforeMedia', '@type': '@id' }, + beneficiaryBank: { '@id': 'schema:beneficiaryBank' }, + benefits: { '@id': 'schema:benefits' }, + benefitsSummaryUrl: { '@id': 'schema:benefitsSummaryUrl', '@type': '@id' }, + bestRating: { '@id': 'schema:bestRating' }, + billingAddress: { '@id': 'schema:billingAddress' }, + billingDuration: { '@id': 'schema:billingDuration' }, + billingIncrement: { '@id': 'schema:billingIncrement' }, + billingPeriod: { '@id': 'schema:billingPeriod' }, + billingStart: { '@id': 'schema:billingStart' }, + biomechnicalClass: { '@id': 'schema:biomechnicalClass' }, + birthDate: { '@id': 'schema:birthDate', '@type': 'Date' }, + birthPlace: { '@id': 'schema:birthPlace' }, + bitrate: { '@id': 'schema:bitrate' }, + blogPost: { '@id': 'schema:blogPost' }, + blogPosts: { '@id': 'schema:blogPosts' }, + bloodSupply: { '@id': 'schema:bloodSupply' }, + boardingGroup: { '@id': 'schema:boardingGroup' }, + boardingPolicy: { '@id': 'schema:boardingPolicy' }, + bodyLocation: { '@id': 'schema:bodyLocation' }, + bodyType: { '@id': 'schema:bodyType' }, + bookEdition: { '@id': 'schema:bookEdition' }, + bookFormat: { '@id': 'schema:bookFormat' }, + bookingAgent: { '@id': 'schema:bookingAgent' }, + bookingTime: { '@id': 'schema:bookingTime' }, + borrower: { '@id': 'schema:borrower' }, + box: { '@id': 'schema:box' }, + branch: { '@id': 'schema:branch' }, + branchCode: { '@id': 'schema:branchCode' }, + branchOf: { '@id': 'schema:branchOf' }, + brand: { '@id': 'schema:brand' }, + breadcrumb: { '@id': 'schema:breadcrumb' }, + breastfeedingWarning: { '@id': 'schema:breastfeedingWarning' }, + broadcastAffiliateOf: { '@id': 'schema:broadcastAffiliateOf' }, + broadcastChannelId: { '@id': 'schema:broadcastChannelId' }, + broadcastDisplayName: { '@id': 'schema:broadcastDisplayName' }, + broadcastFrequency: { '@id': 'schema:broadcastFrequency' }, + broadcastFrequencyValue: { '@id': 'schema:broadcastFrequencyValue' }, + broadcastOfEvent: { '@id': 'schema:broadcastOfEvent' }, + broadcastServiceTier: { '@id': 'schema:broadcastServiceTier' }, + broadcastSignalModulation: { '@id': 'schema:broadcastSignalModulation' }, + broadcastSubChannel: { '@id': 'schema:broadcastSubChannel' }, + broadcastTimezone: { '@id': 'schema:broadcastTimezone' }, + broadcaster: { '@id': 'schema:broadcaster' }, + broker: { '@id': 'schema:broker' }, + browserRequirements: { '@id': 'schema:browserRequirements' }, + busName: { '@id': 'schema:busName' }, + busNumber: { '@id': 'schema:busNumber' }, + businessDays: { '@id': 'schema:businessDays' }, + businessFunction: { '@id': 'schema:businessFunction' }, + buyer: { '@id': 'schema:buyer' }, + byArtist: { '@id': 'schema:byArtist' }, + byDay: { '@id': 'schema:byDay' }, + byMonth: { '@id': 'schema:byMonth' }, + byMonthDay: { '@id': 'schema:byMonthDay' }, + byMonthWeek: { '@id': 'schema:byMonthWeek' }, + callSign: { '@id': 'schema:callSign' }, + calories: { '@id': 'schema:calories' }, + candidate: { '@id': 'schema:candidate' }, + caption: { '@id': 'schema:caption' }, + carbohydrateContent: { '@id': 'schema:carbohydrateContent' }, + cargoVolume: { '@id': 'schema:cargoVolume' }, + carrier: { '@id': 'schema:carrier' }, + carrierRequirements: { '@id': 'schema:carrierRequirements' }, + cashBack: { '@id': 'schema:cashBack' }, + catalog: { '@id': 'schema:catalog' }, + catalogNumber: { '@id': 'schema:catalogNumber' }, + category: { '@id': 'schema:category' }, + causeOf: { '@id': 'schema:causeOf' }, + ccRecipient: { '@id': 'schema:ccRecipient' }, + character: { '@id': 'schema:character' }, + characterAttribute: { '@id': 'schema:characterAttribute' }, + characterName: { '@id': 'schema:characterName' }, + cheatCode: { '@id': 'schema:cheatCode' }, + checkinTime: { '@id': 'schema:checkinTime' }, + checkoutTime: { '@id': 'schema:checkoutTime' }, + childMaxAge: { '@id': 'schema:childMaxAge' }, + childMinAge: { '@id': 'schema:childMinAge' }, + children: { '@id': 'schema:children' }, + cholesterolContent: { '@id': 'schema:cholesterolContent' }, + circle: { '@id': 'schema:circle' }, + citation: { '@id': 'schema:citation' }, + claimReviewed: { '@id': 'schema:claimReviewed' }, + clincalPharmacology: { '@id': 'schema:clincalPharmacology' }, + clinicalPharmacology: { '@id': 'schema:clinicalPharmacology' }, + clipNumber: { '@id': 'schema:clipNumber' }, + closes: { '@id': 'schema:closes' }, + coach: { '@id': 'schema:coach' }, + code: { '@id': 'schema:code' }, + codeRepository: { '@id': 'schema:codeRepository', '@type': '@id' }, + codeSampleType: { '@id': 'schema:codeSampleType' }, + codeValue: { '@id': 'schema:codeValue' }, + codingSystem: { '@id': 'schema:codingSystem' }, + colleague: { '@id': 'schema:colleague', '@type': '@id' }, + colleagues: { '@id': 'schema:colleagues' }, + collection: { '@id': 'schema:collection' }, + collectionSize: { '@id': 'schema:collectionSize' }, + color: { '@id': 'schema:color' }, + colorist: { '@id': 'schema:colorist' }, + comment: { '@id': 'schema:comment' }, + commentCount: { '@id': 'schema:commentCount' }, + commentText: { '@id': 'schema:commentText' }, + commentTime: { '@id': 'schema:commentTime', '@type': 'Date' }, + competencyRequired: { '@id': 'schema:competencyRequired' }, + competitor: { '@id': 'schema:competitor' }, + composer: { '@id': 'schema:composer' }, + comprisedOf: { '@id': 'schema:comprisedOf' }, + conditionsOfAccess: { '@id': 'schema:conditionsOfAccess' }, + confirmationNumber: { '@id': 'schema:confirmationNumber' }, + connectedTo: { '@id': 'schema:connectedTo' }, + constrainingProperty: { '@id': 'schema:constrainingProperty' }, + contactOption: { '@id': 'schema:contactOption' }, + contactPoint: { '@id': 'schema:contactPoint' }, + contactPoints: { '@id': 'schema:contactPoints' }, + contactType: { '@id': 'schema:contactType' }, + contactlessPayment: { '@id': 'schema:contactlessPayment' }, + containedIn: { '@id': 'schema:containedIn' }, + containedInPlace: { '@id': 'schema:containedInPlace' }, + containsPlace: { '@id': 'schema:containsPlace' }, + containsSeason: { '@id': 'schema:containsSeason' }, + contentLocation: { '@id': 'schema:contentLocation' }, + contentRating: { '@id': 'schema:contentRating' }, + contentReferenceTime: { '@id': 'schema:contentReferenceTime' }, + contentSize: { '@id': 'schema:contentSize' }, + contentType: { '@id': 'schema:contentType' }, + contentUrl: { '@id': 'schema:contentUrl', '@type': '@id' }, + contraindication: { '@id': 'schema:contraindication' }, + contributor: { '@id': 'schema:contributor' }, + cookTime: { '@id': 'schema:cookTime' }, + cookingMethod: { '@id': 'schema:cookingMethod' }, + copyrightHolder: { '@id': 'schema:copyrightHolder' }, + copyrightNotice: { '@id': 'schema:copyrightNotice' }, + copyrightYear: { '@id': 'schema:copyrightYear' }, + correction: { '@id': 'schema:correction' }, + correctionsPolicy: { '@id': 'schema:correctionsPolicy', '@type': '@id' }, + costCategory: { '@id': 'schema:costCategory' }, + costCurrency: { '@id': 'schema:costCurrency' }, + costOrigin: { '@id': 'schema:costOrigin' }, + costPerUnit: { '@id': 'schema:costPerUnit' }, + countriesNotSupported: { '@id': 'schema:countriesNotSupported' }, + countriesSupported: { '@id': 'schema:countriesSupported' }, + countryOfOrigin: { '@id': 'schema:countryOfOrigin' }, + course: { '@id': 'schema:course' }, + courseCode: { '@id': 'schema:courseCode' }, + courseMode: { '@id': 'schema:courseMode' }, + coursePrerequisites: { '@id': 'schema:coursePrerequisites' }, + courseWorkload: { '@id': 'schema:courseWorkload' }, + coverageEndTime: { '@id': 'schema:coverageEndTime' }, + coverageStartTime: { '@id': 'schema:coverageStartTime' }, + creativeWorkStatus: { '@id': 'schema:creativeWorkStatus' }, + creator: { '@id': 'schema:creator' }, + credentialCategory: { '@id': 'schema:credentialCategory' }, + creditText: { '@id': 'schema:creditText' }, + creditedTo: { '@id': 'schema:creditedTo' }, + cssSelector: { '@id': 'schema:cssSelector' }, + currenciesAccepted: { '@id': 'schema:currenciesAccepted' }, + currency: { '@id': 'schema:currency' }, + currentExchangeRate: { '@id': 'schema:currentExchangeRate' }, + customer: { '@id': 'schema:customer' }, + cutoffTime: { '@id': 'schema:cutoffTime' }, + cvdCollectionDate: { '@id': 'schema:cvdCollectionDate' }, + cvdFacilityCounty: { '@id': 'schema:cvdFacilityCounty' }, + cvdFacilityId: { '@id': 'schema:cvdFacilityId' }, + cvdNumBeds: { '@id': 'schema:cvdNumBeds' }, + cvdNumBedsOcc: { '@id': 'schema:cvdNumBedsOcc' }, + cvdNumC19Died: { '@id': 'schema:cvdNumC19Died' }, + cvdNumC19HOPats: { '@id': 'schema:cvdNumC19HOPats' }, + cvdNumC19HospPats: { '@id': 'schema:cvdNumC19HospPats' }, + cvdNumC19MechVentPats: { '@id': 'schema:cvdNumC19MechVentPats' }, + cvdNumC19OFMechVentPats: { '@id': 'schema:cvdNumC19OFMechVentPats' }, + cvdNumC19OverflowPats: { '@id': 'schema:cvdNumC19OverflowPats' }, + cvdNumICUBeds: { '@id': 'schema:cvdNumICUBeds' }, + cvdNumICUBedsOcc: { '@id': 'schema:cvdNumICUBedsOcc' }, + cvdNumTotBeds: { '@id': 'schema:cvdNumTotBeds' }, + cvdNumVent: { '@id': 'schema:cvdNumVent' }, + cvdNumVentUse: { '@id': 'schema:cvdNumVentUse' }, + dataFeedElement: { '@id': 'schema:dataFeedElement' }, + dataset: { '@id': 'schema:dataset' }, + datasetTimeInterval: { '@id': 'schema:datasetTimeInterval' }, + dateCreated: { '@id': 'schema:dateCreated', '@type': 'Date' }, + dateDeleted: { '@id': 'schema:dateDeleted', '@type': 'Date' }, + dateIssued: { '@id': 'schema:dateIssued', '@type': 'Date' }, + dateModified: { '@id': 'schema:dateModified', '@type': 'Date' }, + datePosted: { '@id': 'schema:datePosted', '@type': 'Date' }, + datePublished: { '@id': 'schema:datePublished', '@type': 'Date' }, + dateRead: { '@id': 'schema:dateRead', '@type': 'Date' }, + dateReceived: { '@id': 'schema:dateReceived' }, + dateSent: { '@id': 'schema:dateSent' }, + dateVehicleFirstRegistered: { + '@id': 'schema:dateVehicleFirstRegistered', + '@type': 'Date', + }, + dateline: { '@id': 'schema:dateline' }, + dayOfWeek: { '@id': 'schema:dayOfWeek' }, + deathDate: { '@id': 'schema:deathDate', '@type': 'Date' }, + deathPlace: { '@id': 'schema:deathPlace' }, + defaultValue: { '@id': 'schema:defaultValue' }, + deliveryAddress: { '@id': 'schema:deliveryAddress' }, + deliveryLeadTime: { '@id': 'schema:deliveryLeadTime' }, + deliveryMethod: { '@id': 'schema:deliveryMethod' }, + deliveryStatus: { '@id': 'schema:deliveryStatus' }, + deliveryTime: { '@id': 'schema:deliveryTime' }, + department: { '@id': 'schema:department' }, + departureAirport: { '@id': 'schema:departureAirport' }, + departureBoatTerminal: { '@id': 'schema:departureBoatTerminal' }, + departureBusStop: { '@id': 'schema:departureBusStop' }, + departureGate: { '@id': 'schema:departureGate' }, + departurePlatform: { '@id': 'schema:departurePlatform' }, + departureStation: { '@id': 'schema:departureStation' }, + departureTerminal: { '@id': 'schema:departureTerminal' }, + departureTime: { '@id': 'schema:departureTime' }, + dependencies: { '@id': 'schema:dependencies' }, + depth: { '@id': 'schema:depth' }, + description: { '@id': 'schema:description' }, + device: { '@id': 'schema:device' }, + diagnosis: { '@id': 'schema:diagnosis' }, + diagram: { '@id': 'schema:diagram' }, + diet: { '@id': 'schema:diet' }, + dietFeatures: { '@id': 'schema:dietFeatures' }, + differentialDiagnosis: { '@id': 'schema:differentialDiagnosis' }, + director: { '@id': 'schema:director' }, + directors: { '@id': 'schema:directors' }, + disambiguatingDescription: { '@id': 'schema:disambiguatingDescription' }, + discount: { '@id': 'schema:discount' }, + discountCode: { '@id': 'schema:discountCode' }, + discountCurrency: { '@id': 'schema:discountCurrency' }, + discusses: { '@id': 'schema:discusses' }, + discussionUrl: { '@id': 'schema:discussionUrl', '@type': '@id' }, + diseasePreventionInfo: { + '@id': 'schema:diseasePreventionInfo', + '@type': '@id', + }, + diseaseSpreadStatistics: { + '@id': 'schema:diseaseSpreadStatistics', + '@type': '@id', + }, + dissolutionDate: { '@id': 'schema:dissolutionDate', '@type': 'Date' }, + distance: { '@id': 'schema:distance' }, + distinguishingSign: { '@id': 'schema:distinguishingSign' }, + distribution: { '@id': 'schema:distribution' }, + diversityPolicy: { '@id': 'schema:diversityPolicy', '@type': '@id' }, + diversityStaffingReport: { + '@id': 'schema:diversityStaffingReport', + '@type': '@id', + }, + documentation: { '@id': 'schema:documentation', '@type': '@id' }, + doesNotShip: { '@id': 'schema:doesNotShip' }, + domainIncludes: { '@id': 'schema:domainIncludes' }, + domiciledMortgage: { '@id': 'schema:domiciledMortgage' }, + doorTime: { '@id': 'schema:doorTime' }, + dosageForm: { '@id': 'schema:dosageForm' }, + doseSchedule: { '@id': 'schema:doseSchedule' }, + doseUnit: { '@id': 'schema:doseUnit' }, + doseValue: { '@id': 'schema:doseValue' }, + downPayment: { '@id': 'schema:downPayment' }, + downloadUrl: { '@id': 'schema:downloadUrl', '@type': '@id' }, + downvoteCount: { '@id': 'schema:downvoteCount' }, + drainsTo: { '@id': 'schema:drainsTo' }, + driveWheelConfiguration: { '@id': 'schema:driveWheelConfiguration' }, + dropoffLocation: { '@id': 'schema:dropoffLocation' }, + dropoffTime: { '@id': 'schema:dropoffTime' }, + drug: { '@id': 'schema:drug' }, + drugClass: { '@id': 'schema:drugClass' }, + drugUnit: { '@id': 'schema:drugUnit' }, + duns: { '@id': 'schema:duns' }, + duplicateTherapy: { '@id': 'schema:duplicateTherapy' }, + duration: { '@id': 'schema:duration' }, + durationOfWarranty: { '@id': 'schema:durationOfWarranty' }, + duringMedia: { '@id': 'schema:duringMedia', '@type': '@id' }, + earlyPrepaymentPenalty: { '@id': 'schema:earlyPrepaymentPenalty' }, + editEIDR: { '@id': 'schema:editEIDR' }, + editor: { '@id': 'schema:editor' }, + eduQuestionType: { '@id': 'schema:eduQuestionType' }, + educationRequirements: { '@id': 'schema:educationRequirements' }, + educationalAlignment: { '@id': 'schema:educationalAlignment' }, + educationalCredentialAwarded: { '@id': 'schema:educationalCredentialAwarded' }, + educationalFramework: { '@id': 'schema:educationalFramework' }, + educationalLevel: { '@id': 'schema:educationalLevel' }, + educationalProgramMode: { '@id': 'schema:educationalProgramMode' }, + educationalRole: { '@id': 'schema:educationalRole' }, + educationalUse: { '@id': 'schema:educationalUse' }, + elevation: { '@id': 'schema:elevation' }, + eligibilityToWorkRequirement: { '@id': 'schema:eligibilityToWorkRequirement' }, + eligibleCustomerType: { '@id': 'schema:eligibleCustomerType' }, + eligibleDuration: { '@id': 'schema:eligibleDuration' }, + eligibleQuantity: { '@id': 'schema:eligibleQuantity' }, + eligibleRegion: { '@id': 'schema:eligibleRegion' }, + eligibleTransactionVolume: { '@id': 'schema:eligibleTransactionVolume' }, + email: { '@id': 'schema:email' }, + embedUrl: { '@id': 'schema:embedUrl', '@type': '@id' }, + emissionsCO2: { '@id': 'schema:emissionsCO2' }, + employee: { '@id': 'schema:employee' }, + employees: { '@id': 'schema:employees' }, + employerOverview: { '@id': 'schema:employerOverview' }, + employmentType: { '@id': 'schema:employmentType' }, + employmentUnit: { '@id': 'schema:employmentUnit' }, + encodesCreativeWork: { '@id': 'schema:encodesCreativeWork' }, + encoding: { '@id': 'schema:encoding' }, + encodingFormat: { '@id': 'schema:encodingFormat' }, + encodingType: { '@id': 'schema:encodingType' }, + encodings: { '@id': 'schema:encodings' }, + endDate: { '@id': 'schema:endDate', '@type': 'Date' }, + endOffset: { '@id': 'schema:endOffset' }, + endTime: { '@id': 'schema:endTime' }, + endorsee: { '@id': 'schema:endorsee' }, + endorsers: { '@id': 'schema:endorsers' }, + energyEfficiencyScaleMax: { '@id': 'schema:energyEfficiencyScaleMax' }, + energyEfficiencyScaleMin: { '@id': 'schema:energyEfficiencyScaleMin' }, + engineDisplacement: { '@id': 'schema:engineDisplacement' }, + enginePower: { '@id': 'schema:enginePower' }, + engineType: { '@id': 'schema:engineType' }, + entertainmentBusiness: { '@id': 'schema:entertainmentBusiness' }, + epidemiology: { '@id': 'schema:epidemiology' }, + episode: { '@id': 'schema:episode' }, + episodeNumber: { '@id': 'schema:episodeNumber' }, + episodes: { '@id': 'schema:episodes' }, + equal: { '@id': 'schema:equal' }, + error: { '@id': 'schema:error' }, + estimatedCost: { '@id': 'schema:estimatedCost' }, + estimatedFlightDuration: { '@id': 'schema:estimatedFlightDuration' }, + estimatedSalary: { '@id': 'schema:estimatedSalary' }, + estimatesRiskOf: { '@id': 'schema:estimatesRiskOf' }, + ethicsPolicy: { '@id': 'schema:ethicsPolicy', '@type': '@id' }, + event: { '@id': 'schema:event' }, + eventAttendanceMode: { '@id': 'schema:eventAttendanceMode' }, + eventSchedule: { '@id': 'schema:eventSchedule' }, + eventStatus: { '@id': 'schema:eventStatus' }, + events: { '@id': 'schema:events' }, + evidenceLevel: { '@id': 'schema:evidenceLevel' }, + evidenceOrigin: { '@id': 'schema:evidenceOrigin' }, + exampleOfWork: { '@id': 'schema:exampleOfWork' }, + exceptDate: { '@id': 'schema:exceptDate', '@type': 'Date' }, + exchangeRateSpread: { '@id': 'schema:exchangeRateSpread' }, + executableLibraryName: { '@id': 'schema:executableLibraryName' }, + exerciseCourse: { '@id': 'schema:exerciseCourse' }, + exercisePlan: { '@id': 'schema:exercisePlan' }, + exerciseRelatedDiet: { '@id': 'schema:exerciseRelatedDiet' }, + exerciseType: { '@id': 'schema:exerciseType' }, + exifData: { '@id': 'schema:exifData' }, + expectedArrivalFrom: { '@id': 'schema:expectedArrivalFrom', '@type': 'Date' }, + expectedArrivalUntil: { '@id': 'schema:expectedArrivalUntil', '@type': 'Date' }, + expectedPrognosis: { '@id': 'schema:expectedPrognosis' }, + expectsAcceptanceOf: { '@id': 'schema:expectsAcceptanceOf' }, + experienceInPlaceOfEducation: { '@id': 'schema:experienceInPlaceOfEducation' }, + experienceRequirements: { '@id': 'schema:experienceRequirements' }, + expertConsiderations: { '@id': 'schema:expertConsiderations' }, + expires: { '@id': 'schema:expires', '@type': 'Date' }, + familyName: { '@id': 'schema:familyName' }, + fatContent: { '@id': 'schema:fatContent' }, + faxNumber: { '@id': 'schema:faxNumber' }, + featureList: { '@id': 'schema:featureList' }, + feesAndCommissionsSpecification: { + '@id': 'schema:feesAndCommissionsSpecification', + }, + fiberContent: { '@id': 'schema:fiberContent' }, + fileFormat: { '@id': 'schema:fileFormat' }, + fileSize: { '@id': 'schema:fileSize' }, + financialAidEligible: { '@id': 'schema:financialAidEligible' }, + firstAppearance: { '@id': 'schema:firstAppearance' }, + firstPerformance: { '@id': 'schema:firstPerformance' }, + flightDistance: { '@id': 'schema:flightDistance' }, + flightNumber: { '@id': 'schema:flightNumber' }, + floorLevel: { '@id': 'schema:floorLevel' }, + floorLimit: { '@id': 'schema:floorLimit' }, + floorSize: { '@id': 'schema:floorSize' }, + followee: { '@id': 'schema:followee' }, + follows: { '@id': 'schema:follows' }, + followup: { '@id': 'schema:followup' }, + foodEstablishment: { '@id': 'schema:foodEstablishment' }, + foodEvent: { '@id': 'schema:foodEvent' }, + foodWarning: { '@id': 'schema:foodWarning' }, + founder: { '@id': 'schema:founder' }, + founders: { '@id': 'schema:founders' }, + foundingDate: { '@id': 'schema:foundingDate', '@type': 'Date' }, + foundingLocation: { '@id': 'schema:foundingLocation' }, + free: { '@id': 'schema:free' }, + freeShippingThreshold: { '@id': 'schema:freeShippingThreshold' }, + frequency: { '@id': 'schema:frequency' }, + fromLocation: { '@id': 'schema:fromLocation' }, + fuelCapacity: { '@id': 'schema:fuelCapacity' }, + fuelConsumption: { '@id': 'schema:fuelConsumption' }, + fuelEfficiency: { '@id': 'schema:fuelEfficiency' }, + fuelType: { '@id': 'schema:fuelType' }, + functionalClass: { '@id': 'schema:functionalClass' }, + fundedItem: { '@id': 'schema:fundedItem' }, + funder: { '@id': 'schema:funder' }, + game: { '@id': 'schema:game' }, + gameItem: { '@id': 'schema:gameItem' }, + gameLocation: { '@id': 'schema:gameLocation', '@type': '@id' }, + gamePlatform: { '@id': 'schema:gamePlatform' }, + gameServer: { '@id': 'schema:gameServer' }, + gameTip: { '@id': 'schema:gameTip' }, + gender: { '@id': 'schema:gender' }, + genre: { '@id': 'schema:genre' }, + geo: { '@id': 'schema:geo' }, + geoContains: { '@id': 'schema:geoContains' }, + geoCoveredBy: { '@id': 'schema:geoCoveredBy' }, + geoCovers: { '@id': 'schema:geoCovers' }, + geoCrosses: { '@id': 'schema:geoCrosses' }, + geoDisjoint: { '@id': 'schema:geoDisjoint' }, + geoEquals: { '@id': 'schema:geoEquals' }, + geoIntersects: { '@id': 'schema:geoIntersects' }, + geoMidpoint: { '@id': 'schema:geoMidpoint' }, + geoOverlaps: { '@id': 'schema:geoOverlaps' }, + geoRadius: { '@id': 'schema:geoRadius' }, + geoTouches: { '@id': 'schema:geoTouches' }, + geoWithin: { '@id': 'schema:geoWithin' }, + geographicArea: { '@id': 'schema:geographicArea' }, + gettingTestedInfo: { '@id': 'schema:gettingTestedInfo', '@type': '@id' }, + givenName: { '@id': 'schema:givenName' }, + globalLocationNumber: { '@id': 'schema:globalLocationNumber' }, + governmentBenefitsInfo: { '@id': 'schema:governmentBenefitsInfo' }, + gracePeriod: { '@id': 'schema:gracePeriod' }, + grantee: { '@id': 'schema:grantee' }, + greater: { '@id': 'schema:greater' }, + greaterOrEqual: { '@id': 'schema:greaterOrEqual' }, + gtin: { '@id': 'schema:gtin' }, + gtin12: { '@id': 'schema:gtin12' }, + gtin13: { '@id': 'schema:gtin13' }, + gtin14: { '@id': 'schema:gtin14' }, + gtin8: { '@id': 'schema:gtin8' }, + guideline: { '@id': 'schema:guideline' }, + guidelineDate: { '@id': 'schema:guidelineDate', '@type': 'Date' }, + guidelineSubject: { '@id': 'schema:guidelineSubject' }, + handlingTime: { '@id': 'schema:handlingTime' }, + hasBroadcastChannel: { '@id': 'schema:hasBroadcastChannel' }, + hasCategoryCode: { '@id': 'schema:hasCategoryCode' }, + hasCourse: { '@id': 'schema:hasCourse' }, + hasCourseInstance: { '@id': 'schema:hasCourseInstance' }, + hasCredential: { '@id': 'schema:hasCredential' }, + hasDefinedTerm: { '@id': 'schema:hasDefinedTerm' }, + hasDeliveryMethod: { '@id': 'schema:hasDeliveryMethod' }, + hasDigitalDocumentPermission: { '@id': 'schema:hasDigitalDocumentPermission' }, + hasDriveThroughService: { '@id': 'schema:hasDriveThroughService' }, + hasEnergyConsumptionDetails: { '@id': 'schema:hasEnergyConsumptionDetails' }, + hasEnergyEfficiencyCategory: { '@id': 'schema:hasEnergyEfficiencyCategory' }, + hasHealthAspect: { '@id': 'schema:hasHealthAspect' }, + hasMap: { '@id': 'schema:hasMap', '@type': '@id' }, + hasMeasurement: { '@id': 'schema:hasMeasurement' }, + hasMenu: { '@id': 'schema:hasMenu' }, + hasMenuItem: { '@id': 'schema:hasMenuItem' }, + hasMenuSection: { '@id': 'schema:hasMenuSection' }, + hasMerchantReturnPolicy: { '@id': 'schema:hasMerchantReturnPolicy' }, + hasOccupation: { '@id': 'schema:hasOccupation' }, + hasOfferCatalog: { '@id': 'schema:hasOfferCatalog' }, + hasPOS: { '@id': 'schema:hasPOS' }, + hasPart: { '@id': 'schema:hasPart' }, + hasProductReturnPolicy: { '@id': 'schema:hasProductReturnPolicy' }, + hasVariant: { '@id': 'schema:hasVariant' }, + headline: { '@id': 'schema:headline' }, + healthCondition: { '@id': 'schema:healthCondition' }, + healthPlanCoinsuranceOption: { '@id': 'schema:healthPlanCoinsuranceOption' }, + healthPlanCoinsuranceRate: { '@id': 'schema:healthPlanCoinsuranceRate' }, + healthPlanCopay: { '@id': 'schema:healthPlanCopay' }, + healthPlanCopayOption: { '@id': 'schema:healthPlanCopayOption' }, + healthPlanCostSharing: { '@id': 'schema:healthPlanCostSharing' }, + healthPlanDrugOption: { '@id': 'schema:healthPlanDrugOption' }, + healthPlanDrugTier: { '@id': 'schema:healthPlanDrugTier' }, + healthPlanId: { '@id': 'schema:healthPlanId' }, + healthPlanMarketingUrl: { + '@id': 'schema:healthPlanMarketingUrl', + '@type': '@id', + }, + healthPlanNetworkId: { '@id': 'schema:healthPlanNetworkId' }, + healthPlanNetworkTier: { '@id': 'schema:healthPlanNetworkTier' }, + healthPlanPharmacyCategory: { '@id': 'schema:healthPlanPharmacyCategory' }, + healthcareReportingData: { '@id': 'schema:healthcareReportingData' }, + height: { '@id': 'schema:height' }, + highPrice: { '@id': 'schema:highPrice' }, + hiringOrganization: { '@id': 'schema:hiringOrganization' }, + holdingArchive: { '@id': 'schema:holdingArchive' }, + homeLocation: { '@id': 'schema:homeLocation' }, + homeTeam: { '@id': 'schema:homeTeam' }, + honorificPrefix: { '@id': 'schema:honorificPrefix' }, + honorificSuffix: { '@id': 'schema:honorificSuffix' }, + hospitalAffiliation: { '@id': 'schema:hospitalAffiliation' }, + hostingOrganization: { '@id': 'schema:hostingOrganization' }, + hoursAvailable: { '@id': 'schema:hoursAvailable' }, + howPerformed: { '@id': 'schema:howPerformed' }, + httpMethod: { '@id': 'schema:httpMethod' }, + iataCode: { '@id': 'schema:iataCode' }, + icaoCode: { '@id': 'schema:icaoCode' }, + identifier: { '@id': 'schema:identifier' }, + identifyingExam: { '@id': 'schema:identifyingExam' }, + identifyingTest: { '@id': 'schema:identifyingTest' }, + illustrator: { '@id': 'schema:illustrator' }, + image: { '@id': 'schema:image', '@type': '@id' }, + imagingTechnique: { '@id': 'schema:imagingTechnique' }, + inAlbum: { '@id': 'schema:inAlbum' }, + inBroadcastLineup: { '@id': 'schema:inBroadcastLineup' }, + inCodeSet: { '@id': 'schema:inCodeSet', '@type': '@id' }, + inDefinedTermSet: { '@id': 'schema:inDefinedTermSet', '@type': '@id' }, + inLanguage: { '@id': 'schema:inLanguage' }, + inPlaylist: { '@id': 'schema:inPlaylist' }, + inProductGroupWithID: { '@id': 'schema:inProductGroupWithID' }, + inStoreReturnsOffered: { '@id': 'schema:inStoreReturnsOffered' }, + inSupportOf: { '@id': 'schema:inSupportOf' }, + incentiveCompensation: { '@id': 'schema:incentiveCompensation' }, + incentives: { '@id': 'schema:incentives' }, + includedComposition: { '@id': 'schema:includedComposition' }, + includedDataCatalog: { '@id': 'schema:includedDataCatalog' }, + includedInDataCatalog: { '@id': 'schema:includedInDataCatalog' }, + includedInHealthInsurancePlan: { + '@id': 'schema:includedInHealthInsurancePlan', + }, + includedRiskFactor: { '@id': 'schema:includedRiskFactor' }, + includesAttraction: { '@id': 'schema:includesAttraction' }, + includesHealthPlanFormulary: { '@id': 'schema:includesHealthPlanFormulary' }, + includesHealthPlanNetwork: { '@id': 'schema:includesHealthPlanNetwork' }, + includesObject: { '@id': 'schema:includesObject' }, + increasesRiskOf: { '@id': 'schema:increasesRiskOf' }, + industry: { '@id': 'schema:industry' }, + ineligibleRegion: { '@id': 'schema:ineligibleRegion' }, + infectiousAgent: { '@id': 'schema:infectiousAgent' }, + infectiousAgentClass: { '@id': 'schema:infectiousAgentClass' }, + ingredients: { '@id': 'schema:ingredients' }, + inker: { '@id': 'schema:inker' }, + insertion: { '@id': 'schema:insertion' }, + installUrl: { '@id': 'schema:installUrl', '@type': '@id' }, + instructor: { '@id': 'schema:instructor' }, + instrument: { '@id': 'schema:instrument' }, + intensity: { '@id': 'schema:intensity' }, + interactingDrug: { '@id': 'schema:interactingDrug' }, + interactionCount: { '@id': 'schema:interactionCount' }, + interactionService: { '@id': 'schema:interactionService' }, + interactionStatistic: { '@id': 'schema:interactionStatistic' }, + interactionType: { '@id': 'schema:interactionType' }, + interactivityType: { '@id': 'schema:interactivityType' }, + interestRate: { '@id': 'schema:interestRate' }, + inventoryLevel: { '@id': 'schema:inventoryLevel' }, + inverseOf: { '@id': 'schema:inverseOf' }, + isAcceptingNewPatients: { '@id': 'schema:isAcceptingNewPatients' }, + isAccessibleForFree: { '@id': 'schema:isAccessibleForFree' }, + isAccessoryOrSparePartFor: { '@id': 'schema:isAccessoryOrSparePartFor' }, + isAvailableGenerically: { '@id': 'schema:isAvailableGenerically' }, + isBasedOn: { '@id': 'schema:isBasedOn', '@type': '@id' }, + isBasedOnUrl: { '@id': 'schema:isBasedOnUrl', '@type': '@id' }, + isConsumableFor: { '@id': 'schema:isConsumableFor' }, + isFamilyFriendly: { '@id': 'schema:isFamilyFriendly' }, + isGift: { '@id': 'schema:isGift' }, + isLiveBroadcast: { '@id': 'schema:isLiveBroadcast' }, + isPartOf: { '@id': 'schema:isPartOf', '@type': '@id' }, + isPlanForApartment: { '@id': 'schema:isPlanForApartment' }, + isProprietary: { '@id': 'schema:isProprietary' }, + isRelatedTo: { '@id': 'schema:isRelatedTo' }, + isResizable: { '@id': 'schema:isResizable' }, + isSimilarTo: { '@id': 'schema:isSimilarTo' }, + isUnlabelledFallback: { '@id': 'schema:isUnlabelledFallback' }, + isVariantOf: { '@id': 'schema:isVariantOf' }, + isbn: { '@id': 'schema:isbn' }, + isicV4: { '@id': 'schema:isicV4' }, + isrcCode: { '@id': 'schema:isrcCode' }, + issn: { '@id': 'schema:issn' }, + issueNumber: { '@id': 'schema:issueNumber' }, + issuedBy: { '@id': 'schema:issuedBy' }, + issuedThrough: { '@id': 'schema:issuedThrough' }, + iswcCode: { '@id': 'schema:iswcCode' }, + item: { '@id': 'schema:item' }, + itemCondition: { '@id': 'schema:itemCondition' }, + itemListElement: { '@id': 'schema:itemListElement' }, + itemListOrder: { '@id': 'schema:itemListOrder' }, + itemLocation: { '@id': 'schema:itemLocation' }, + itemOffered: { '@id': 'schema:itemOffered' }, + itemReviewed: { '@id': 'schema:itemReviewed' }, + itemShipped: { '@id': 'schema:itemShipped' }, + itinerary: { '@id': 'schema:itinerary' }, + jobBenefits: { '@id': 'schema:jobBenefits' }, + jobImmediateStart: { '@id': 'schema:jobImmediateStart' }, + jobLocation: { '@id': 'schema:jobLocation' }, + jobLocationType: { '@id': 'schema:jobLocationType' }, + jobStartDate: { '@id': 'schema:jobStartDate' }, + jobTitle: { '@id': 'schema:jobTitle' }, + jurisdiction: { '@id': 'schema:jurisdiction' }, + keywords: { '@id': 'schema:keywords' }, + knownVehicleDamages: { '@id': 'schema:knownVehicleDamages' }, + knows: { '@id': 'schema:knows' }, + knowsAbout: { '@id': 'schema:knowsAbout' }, + knowsLanguage: { '@id': 'schema:knowsLanguage' }, + labelDetails: { '@id': 'schema:labelDetails', '@type': '@id' }, + landlord: { '@id': 'schema:landlord' }, + language: { '@id': 'schema:language' }, + lastReviewed: { '@id': 'schema:lastReviewed', '@type': 'Date' }, + latitude: { '@id': 'schema:latitude' }, + layoutImage: { '@id': 'schema:layoutImage', '@type': '@id' }, + learningResourceType: { '@id': 'schema:learningResourceType' }, + leaseLength: { '@id': 'schema:leaseLength' }, + legalName: { '@id': 'schema:legalName' }, + legalStatus: { '@id': 'schema:legalStatus' }, + legislationApplies: { '@id': 'schema:legislationApplies' }, + legislationChanges: { '@id': 'schema:legislationChanges' }, + legislationConsolidates: { '@id': 'schema:legislationConsolidates' }, + legislationDate: { '@id': 'schema:legislationDate', '@type': 'Date' }, + legislationDateVersion: { + '@id': 'schema:legislationDateVersion', + '@type': 'Date', + }, + legislationIdentifier: { '@id': 'schema:legislationIdentifier' }, + legislationJurisdiction: { '@id': 'schema:legislationJurisdiction' }, + legislationLegalForce: { '@id': 'schema:legislationLegalForce' }, + legislationLegalValue: { '@id': 'schema:legislationLegalValue' }, + legislationPassedBy: { '@id': 'schema:legislationPassedBy' }, + legislationResponsible: { '@id': 'schema:legislationResponsible' }, + legislationTransposes: { '@id': 'schema:legislationTransposes' }, + legislationType: { '@id': 'schema:legislationType' }, + leiCode: { '@id': 'schema:leiCode' }, + lender: { '@id': 'schema:lender' }, + lesser: { '@id': 'schema:lesser' }, + lesserOrEqual: { '@id': 'schema:lesserOrEqual' }, + letterer: { '@id': 'schema:letterer' }, + license: { '@id': 'schema:license', '@type': '@id' }, + line: { '@id': 'schema:line' }, + linkRelationship: { '@id': 'schema:linkRelationship' }, + liveBlogUpdate: { '@id': 'schema:liveBlogUpdate' }, + loanMortgageMandateAmount: { '@id': 'schema:loanMortgageMandateAmount' }, + loanPaymentAmount: { '@id': 'schema:loanPaymentAmount' }, + loanPaymentFrequency: { '@id': 'schema:loanPaymentFrequency' }, + loanRepaymentForm: { '@id': 'schema:loanRepaymentForm' }, + loanTerm: { '@id': 'schema:loanTerm' }, + loanType: { '@id': 'schema:loanType' }, + location: { '@id': 'schema:location' }, + locationCreated: { '@id': 'schema:locationCreated' }, + lodgingUnitDescription: { '@id': 'schema:lodgingUnitDescription' }, + lodgingUnitType: { '@id': 'schema:lodgingUnitType' }, + logo: { '@id': 'schema:logo', '@type': '@id' }, + longitude: { '@id': 'schema:longitude' }, + loser: { '@id': 'schema:loser' }, + lowPrice: { '@id': 'schema:lowPrice' }, + lyricist: { '@id': 'schema:lyricist' }, + lyrics: { '@id': 'schema:lyrics' }, + mainContentOfPage: { '@id': 'schema:mainContentOfPage' }, + mainEntity: { '@id': 'schema:mainEntity' }, + mainEntityOfPage: { '@id': 'schema:mainEntityOfPage', '@type': '@id' }, + maintainer: { '@id': 'schema:maintainer' }, + makesOffer: { '@id': 'schema:makesOffer' }, + manufacturer: { '@id': 'schema:manufacturer' }, + map: { '@id': 'schema:map', '@type': '@id' }, + mapType: { '@id': 'schema:mapType' }, + maps: { '@id': 'schema:maps', '@type': '@id' }, + marginOfError: { '@id': 'schema:marginOfError' }, + masthead: { '@id': 'schema:masthead', '@type': '@id' }, + material: { '@id': 'schema:material' }, + materialExtent: { '@id': 'schema:materialExtent' }, + mathExpression: { '@id': 'schema:mathExpression' }, + maxPrice: { '@id': 'schema:maxPrice' }, + maxValue: { '@id': 'schema:maxValue' }, + maximumAttendeeCapacity: { '@id': 'schema:maximumAttendeeCapacity' }, + maximumEnrollment: { '@id': 'schema:maximumEnrollment' }, + maximumIntake: { '@id': 'schema:maximumIntake' }, + maximumPhysicalAttendeeCapacity: { + '@id': 'schema:maximumPhysicalAttendeeCapacity', + }, + maximumVirtualAttendeeCapacity: { + '@id': 'schema:maximumVirtualAttendeeCapacity', + }, + mealService: { '@id': 'schema:mealService' }, + measuredProperty: { '@id': 'schema:measuredProperty' }, + measuredValue: { '@id': 'schema:measuredValue' }, + measurementTechnique: { '@id': 'schema:measurementTechnique' }, + mechanismOfAction: { '@id': 'schema:mechanismOfAction' }, + mediaAuthenticityCategory: { '@id': 'schema:mediaAuthenticityCategory' }, + median: { '@id': 'schema:median' }, + medicalAudience: { '@id': 'schema:medicalAudience' }, + medicalSpecialty: { '@id': 'schema:medicalSpecialty' }, + medicineSystem: { '@id': 'schema:medicineSystem' }, + meetsEmissionStandard: { '@id': 'schema:meetsEmissionStandard' }, + member: { '@id': 'schema:member' }, + memberOf: { '@id': 'schema:memberOf' }, + members: { '@id': 'schema:members' }, + membershipNumber: { '@id': 'schema:membershipNumber' }, + membershipPointsEarned: { '@id': 'schema:membershipPointsEarned' }, + memoryRequirements: { '@id': 'schema:memoryRequirements' }, + mentions: { '@id': 'schema:mentions' }, + menu: { '@id': 'schema:menu' }, + menuAddOn: { '@id': 'schema:menuAddOn' }, + merchant: { '@id': 'schema:merchant' }, + merchantReturnDays: { '@id': 'schema:merchantReturnDays' }, + merchantReturnLink: { '@id': 'schema:merchantReturnLink', '@type': '@id' }, + messageAttachment: { '@id': 'schema:messageAttachment' }, + mileageFromOdometer: { '@id': 'schema:mileageFromOdometer' }, + minPrice: { '@id': 'schema:minPrice' }, + minValue: { '@id': 'schema:minValue' }, + minimumPaymentDue: { '@id': 'schema:minimumPaymentDue' }, + missionCoveragePrioritiesPolicy: { + '@id': 'schema:missionCoveragePrioritiesPolicy', + '@type': '@id', + }, + model: { '@id': 'schema:model' }, + modelDate: { '@id': 'schema:modelDate', '@type': 'Date' }, + modifiedTime: { '@id': 'schema:modifiedTime' }, + monthlyMinimumRepaymentAmount: { + '@id': 'schema:monthlyMinimumRepaymentAmount', + }, + monthsOfExperience: { '@id': 'schema:monthsOfExperience' }, + mpn: { '@id': 'schema:mpn' }, + multipleValues: { '@id': 'schema:multipleValues' }, + muscleAction: { '@id': 'schema:muscleAction' }, + musicArrangement: { '@id': 'schema:musicArrangement' }, + musicBy: { '@id': 'schema:musicBy' }, + musicCompositionForm: { '@id': 'schema:musicCompositionForm' }, + musicGroupMember: { '@id': 'schema:musicGroupMember' }, + musicReleaseFormat: { '@id': 'schema:musicReleaseFormat' }, + musicalKey: { '@id': 'schema:musicalKey' }, + naics: { '@id': 'schema:naics' }, + name: { '@id': 'schema:name' }, + namedPosition: { '@id': 'schema:namedPosition' }, + nationality: { '@id': 'schema:nationality' }, + naturalProgression: { '@id': 'schema:naturalProgression' }, + nerve: { '@id': 'schema:nerve' }, + nerveMotor: { '@id': 'schema:nerveMotor' }, + netWorth: { '@id': 'schema:netWorth' }, + newsUpdatesAndGuidelines: { + '@id': 'schema:newsUpdatesAndGuidelines', + '@type': '@id', + }, + nextItem: { '@id': 'schema:nextItem' }, + noBylinesPolicy: { '@id': 'schema:noBylinesPolicy', '@type': '@id' }, + nonEqual: { '@id': 'schema:nonEqual' }, + nonProprietaryName: { '@id': 'schema:nonProprietaryName' }, + nonprofitStatus: { '@id': 'schema:nonprofitStatus' }, + normalRange: { '@id': 'schema:normalRange' }, + nsn: { '@id': 'schema:nsn' }, + numAdults: { '@id': 'schema:numAdults' }, + numChildren: { '@id': 'schema:numChildren' }, + numConstraints: { '@id': 'schema:numConstraints' }, + numTracks: { '@id': 'schema:numTracks' }, + numberOfAccommodationUnits: { '@id': 'schema:numberOfAccommodationUnits' }, + numberOfAirbags: { '@id': 'schema:numberOfAirbags' }, + numberOfAvailableAccommodationUnits: { + '@id': 'schema:numberOfAvailableAccommodationUnits', + }, + numberOfAxles: { '@id': 'schema:numberOfAxles' }, + numberOfBathroomsTotal: { '@id': 'schema:numberOfBathroomsTotal' }, + numberOfBedrooms: { '@id': 'schema:numberOfBedrooms' }, + numberOfBeds: { '@id': 'schema:numberOfBeds' }, + numberOfCredits: { '@id': 'schema:numberOfCredits' }, + numberOfDoors: { '@id': 'schema:numberOfDoors' }, + numberOfEmployees: { '@id': 'schema:numberOfEmployees' }, + numberOfEpisodes: { '@id': 'schema:numberOfEpisodes' }, + numberOfForwardGears: { '@id': 'schema:numberOfForwardGears' }, + numberOfFullBathrooms: { '@id': 'schema:numberOfFullBathrooms' }, + numberOfItems: { '@id': 'schema:numberOfItems' }, + numberOfLoanPayments: { '@id': 'schema:numberOfLoanPayments' }, + numberOfPages: { '@id': 'schema:numberOfPages' }, + numberOfPartialBathrooms: { '@id': 'schema:numberOfPartialBathrooms' }, + numberOfPlayers: { '@id': 'schema:numberOfPlayers' }, + numberOfPreviousOwners: { '@id': 'schema:numberOfPreviousOwners' }, + numberOfRooms: { '@id': 'schema:numberOfRooms' }, + numberOfSeasons: { '@id': 'schema:numberOfSeasons' }, + numberedPosition: { '@id': 'schema:numberedPosition' }, + nutrition: { '@id': 'schema:nutrition' }, + object: { '@id': 'schema:object' }, + observationDate: { '@id': 'schema:observationDate' }, + observedNode: { '@id': 'schema:observedNode' }, + occupancy: { '@id': 'schema:occupancy' }, + occupationLocation: { '@id': 'schema:occupationLocation' }, + occupationalCategory: { '@id': 'schema:occupationalCategory' }, + occupationalCredentialAwarded: { + '@id': 'schema:occupationalCredentialAwarded', + }, + offerCount: { '@id': 'schema:offerCount' }, + offeredBy: { '@id': 'schema:offeredBy' }, + offers: { '@id': 'schema:offers' }, + offersPrescriptionByMail: { '@id': 'schema:offersPrescriptionByMail' }, + openingHours: { '@id': 'schema:openingHours' }, + openingHoursSpecification: { '@id': 'schema:openingHoursSpecification' }, + opens: { '@id': 'schema:opens' }, + operatingSystem: { '@id': 'schema:operatingSystem' }, + opponent: { '@id': 'schema:opponent' }, + option: { '@id': 'schema:option' }, + orderDate: { '@id': 'schema:orderDate', '@type': 'Date' }, + orderDelivery: { '@id': 'schema:orderDelivery' }, + orderItemNumber: { '@id': 'schema:orderItemNumber' }, + orderItemStatus: { '@id': 'schema:orderItemStatus' }, + orderNumber: { '@id': 'schema:orderNumber' }, + orderQuantity: { '@id': 'schema:orderQuantity' }, + orderStatus: { '@id': 'schema:orderStatus' }, + orderedItem: { '@id': 'schema:orderedItem' }, + organizer: { '@id': 'schema:organizer' }, + originAddress: { '@id': 'schema:originAddress' }, + originatesFrom: { '@id': 'schema:originatesFrom' }, + overdosage: { '@id': 'schema:overdosage' }, + ownedFrom: { '@id': 'schema:ownedFrom' }, + ownedThrough: { '@id': 'schema:ownedThrough' }, + ownershipFundingInfo: { '@id': 'schema:ownershipFundingInfo' }, + owns: { '@id': 'schema:owns' }, + pageEnd: { '@id': 'schema:pageEnd' }, + pageStart: { '@id': 'schema:pageStart' }, + pagination: { '@id': 'schema:pagination' }, + parent: { '@id': 'schema:parent' }, + parentItem: { '@id': 'schema:parentItem' }, + parentOrganization: { '@id': 'schema:parentOrganization' }, + parentService: { '@id': 'schema:parentService' }, + parents: { '@id': 'schema:parents' }, + partOfEpisode: { '@id': 'schema:partOfEpisode' }, + partOfInvoice: { '@id': 'schema:partOfInvoice' }, + partOfOrder: { '@id': 'schema:partOfOrder' }, + partOfSeason: { '@id': 'schema:partOfSeason' }, + partOfSeries: { '@id': 'schema:partOfSeries' }, + partOfSystem: { '@id': 'schema:partOfSystem' }, + partOfTVSeries: { '@id': 'schema:partOfTVSeries' }, + partOfTrip: { '@id': 'schema:partOfTrip' }, + participant: { '@id': 'schema:participant' }, + partySize: { '@id': 'schema:partySize' }, + passengerPriorityStatus: { '@id': 'schema:passengerPriorityStatus' }, + passengerSequenceNumber: { '@id': 'schema:passengerSequenceNumber' }, + pathophysiology: { '@id': 'schema:pathophysiology' }, + pattern: { '@id': 'schema:pattern' }, + payload: { '@id': 'schema:payload' }, + paymentAccepted: { '@id': 'schema:paymentAccepted' }, + paymentDue: { '@id': 'schema:paymentDue' }, + paymentDueDate: { '@id': 'schema:paymentDueDate', '@type': 'Date' }, + paymentMethod: { '@id': 'schema:paymentMethod' }, + paymentMethodId: { '@id': 'schema:paymentMethodId' }, + paymentStatus: { '@id': 'schema:paymentStatus' }, + paymentUrl: { '@id': 'schema:paymentUrl', '@type': '@id' }, + penciler: { '@id': 'schema:penciler' }, + percentile10: { '@id': 'schema:percentile10' }, + percentile25: { '@id': 'schema:percentile25' }, + percentile75: { '@id': 'schema:percentile75' }, + percentile90: { '@id': 'schema:percentile90' }, + performTime: { '@id': 'schema:performTime' }, + performer: { '@id': 'schema:performer' }, + performerIn: { '@id': 'schema:performerIn' }, + performers: { '@id': 'schema:performers' }, + permissionType: { '@id': 'schema:permissionType' }, + permissions: { '@id': 'schema:permissions' }, + permitAudience: { '@id': 'schema:permitAudience' }, + permittedUsage: { '@id': 'schema:permittedUsage' }, + petsAllowed: { '@id': 'schema:petsAllowed' }, + phoneticText: { '@id': 'schema:phoneticText' }, + photo: { '@id': 'schema:photo' }, + photos: { '@id': 'schema:photos' }, + physicalRequirement: { '@id': 'schema:physicalRequirement' }, + physiologicalBenefits: { '@id': 'schema:physiologicalBenefits' }, + pickupLocation: { '@id': 'schema:pickupLocation' }, + pickupTime: { '@id': 'schema:pickupTime' }, + playMode: { '@id': 'schema:playMode' }, + playerType: { '@id': 'schema:playerType' }, + playersOnline: { '@id': 'schema:playersOnline' }, + polygon: { '@id': 'schema:polygon' }, + populationType: { '@id': 'schema:populationType' }, + position: { '@id': 'schema:position' }, + possibleComplication: { '@id': 'schema:possibleComplication' }, + possibleTreatment: { '@id': 'schema:possibleTreatment' }, + postOfficeBoxNumber: { '@id': 'schema:postOfficeBoxNumber' }, + postOp: { '@id': 'schema:postOp' }, + postalCode: { '@id': 'schema:postalCode' }, + postalCodeBegin: { '@id': 'schema:postalCodeBegin' }, + postalCodeEnd: { '@id': 'schema:postalCodeEnd' }, + postalCodePrefix: { '@id': 'schema:postalCodePrefix' }, + postalCodeRange: { '@id': 'schema:postalCodeRange' }, + potentialAction: { '@id': 'schema:potentialAction' }, + preOp: { '@id': 'schema:preOp' }, + predecessorOf: { '@id': 'schema:predecessorOf' }, + pregnancyCategory: { '@id': 'schema:pregnancyCategory' }, + pregnancyWarning: { '@id': 'schema:pregnancyWarning' }, + prepTime: { '@id': 'schema:prepTime' }, + preparation: { '@id': 'schema:preparation' }, + prescribingInfo: { '@id': 'schema:prescribingInfo', '@type': '@id' }, + prescriptionStatus: { '@id': 'schema:prescriptionStatus' }, + previousItem: { '@id': 'schema:previousItem' }, + previousStartDate: { '@id': 'schema:previousStartDate', '@type': 'Date' }, + price: { '@id': 'schema:price' }, + priceComponent: { '@id': 'schema:priceComponent' }, + priceComponentType: { '@id': 'schema:priceComponentType' }, + priceCurrency: { '@id': 'schema:priceCurrency' }, + priceRange: { '@id': 'schema:priceRange' }, + priceSpecification: { '@id': 'schema:priceSpecification' }, + priceType: { '@id': 'schema:priceType' }, + priceValidUntil: { '@id': 'schema:priceValidUntil', '@type': 'Date' }, + primaryImageOfPage: { '@id': 'schema:primaryImageOfPage' }, + primaryPrevention: { '@id': 'schema:primaryPrevention' }, + printColumn: { '@id': 'schema:printColumn' }, + printEdition: { '@id': 'schema:printEdition' }, + printPage: { '@id': 'schema:printPage' }, + printSection: { '@id': 'schema:printSection' }, + procedure: { '@id': 'schema:procedure' }, + procedureType: { '@id': 'schema:procedureType' }, + processingTime: { '@id': 'schema:processingTime' }, + processorRequirements: { '@id': 'schema:processorRequirements' }, + producer: { '@id': 'schema:producer' }, + produces: { '@id': 'schema:produces' }, + productGroupID: { '@id': 'schema:productGroupID' }, + productID: { '@id': 'schema:productID' }, + productReturnDays: { '@id': 'schema:productReturnDays' }, + productReturnLink: { '@id': 'schema:productReturnLink', '@type': '@id' }, + productSupported: { '@id': 'schema:productSupported' }, + productionCompany: { '@id': 'schema:productionCompany' }, + productionDate: { '@id': 'schema:productionDate', '@type': 'Date' }, + proficiencyLevel: { '@id': 'schema:proficiencyLevel' }, + programMembershipUsed: { '@id': 'schema:programMembershipUsed' }, + programName: { '@id': 'schema:programName' }, + programPrerequisites: { '@id': 'schema:programPrerequisites' }, + programType: { '@id': 'schema:programType' }, + programmingLanguage: { '@id': 'schema:programmingLanguage' }, + programmingModel: { '@id': 'schema:programmingModel' }, + propertyID: { '@id': 'schema:propertyID' }, + proprietaryName: { '@id': 'schema:proprietaryName' }, + proteinContent: { '@id': 'schema:proteinContent' }, + provider: { '@id': 'schema:provider' }, + providerMobility: { '@id': 'schema:providerMobility' }, + providesBroadcastService: { '@id': 'schema:providesBroadcastService' }, + providesService: { '@id': 'schema:providesService' }, + publicAccess: { '@id': 'schema:publicAccess' }, + publicTransportClosuresInfo: { + '@id': 'schema:publicTransportClosuresInfo', + '@type': '@id', + }, + publication: { '@id': 'schema:publication' }, + publicationType: { '@id': 'schema:publicationType' }, + publishedBy: { '@id': 'schema:publishedBy' }, + publishedOn: { '@id': 'schema:publishedOn' }, + publisher: { '@id': 'schema:publisher' }, + publisherImprint: { '@id': 'schema:publisherImprint' }, + publishingPrinciples: { '@id': 'schema:publishingPrinciples', '@type': '@id' }, + purchaseDate: { '@id': 'schema:purchaseDate', '@type': 'Date' }, + qualifications: { '@id': 'schema:qualifications' }, + quarantineGuidelines: { '@id': 'schema:quarantineGuidelines', '@type': '@id' }, + query: { '@id': 'schema:query' }, + quest: { '@id': 'schema:quest' }, + question: { '@id': 'schema:question' }, + rangeIncludes: { '@id': 'schema:rangeIncludes' }, + ratingCount: { '@id': 'schema:ratingCount' }, + ratingExplanation: { '@id': 'schema:ratingExplanation' }, + ratingValue: { '@id': 'schema:ratingValue' }, + readBy: { '@id': 'schema:readBy' }, + readonlyValue: { '@id': 'schema:readonlyValue' }, + realEstateAgent: { '@id': 'schema:realEstateAgent' }, + recipe: { '@id': 'schema:recipe' }, + recipeCategory: { '@id': 'schema:recipeCategory' }, + recipeCuisine: { '@id': 'schema:recipeCuisine' }, + recipeIngredient: { '@id': 'schema:recipeIngredient' }, + recipeInstructions: { '@id': 'schema:recipeInstructions' }, + recipeYield: { '@id': 'schema:recipeYield' }, + recipient: { '@id': 'schema:recipient' }, + recognizedBy: { '@id': 'schema:recognizedBy' }, + recognizingAuthority: { '@id': 'schema:recognizingAuthority' }, + recommendationStrength: { '@id': 'schema:recommendationStrength' }, + recommendedIntake: { '@id': 'schema:recommendedIntake' }, + recordLabel: { '@id': 'schema:recordLabel' }, + recordedAs: { '@id': 'schema:recordedAs' }, + recordedAt: { '@id': 'schema:recordedAt' }, + recordedIn: { '@id': 'schema:recordedIn' }, + recordingOf: { '@id': 'schema:recordingOf' }, + recourseLoan: { '@id': 'schema:recourseLoan' }, + referenceQuantity: { '@id': 'schema:referenceQuantity' }, + referencesOrder: { '@id': 'schema:referencesOrder' }, + refundType: { '@id': 'schema:refundType' }, + regionDrained: { '@id': 'schema:regionDrained' }, + regionsAllowed: { '@id': 'schema:regionsAllowed' }, + relatedAnatomy: { '@id': 'schema:relatedAnatomy' }, + relatedCondition: { '@id': 'schema:relatedCondition' }, + relatedDrug: { '@id': 'schema:relatedDrug' }, + relatedLink: { '@id': 'schema:relatedLink', '@type': '@id' }, + relatedStructure: { '@id': 'schema:relatedStructure' }, + relatedTherapy: { '@id': 'schema:relatedTherapy' }, + relatedTo: { '@id': 'schema:relatedTo' }, + releaseDate: { '@id': 'schema:releaseDate', '@type': 'Date' }, + releaseNotes: { '@id': 'schema:releaseNotes' }, + releaseOf: { '@id': 'schema:releaseOf' }, + releasedEvent: { '@id': 'schema:releasedEvent' }, + relevantOccupation: { '@id': 'schema:relevantOccupation' }, + relevantSpecialty: { '@id': 'schema:relevantSpecialty' }, + remainingAttendeeCapacity: { '@id': 'schema:remainingAttendeeCapacity' }, + renegotiableLoan: { '@id': 'schema:renegotiableLoan' }, + repeatCount: { '@id': 'schema:repeatCount' }, + repeatFrequency: { '@id': 'schema:repeatFrequency' }, + repetitions: { '@id': 'schema:repetitions' }, + replacee: { '@id': 'schema:replacee' }, + replacer: { '@id': 'schema:replacer' }, + replyToUrl: { '@id': 'schema:replyToUrl', '@type': '@id' }, + reportNumber: { '@id': 'schema:reportNumber' }, + representativeOfPage: { '@id': 'schema:representativeOfPage' }, + requiredCollateral: { '@id': 'schema:requiredCollateral' }, + requiredGender: { '@id': 'schema:requiredGender' }, + requiredMaxAge: { '@id': 'schema:requiredMaxAge' }, + requiredMinAge: { '@id': 'schema:requiredMinAge' }, + requiredQuantity: { '@id': 'schema:requiredQuantity' }, + requirements: { '@id': 'schema:requirements' }, + requiresSubscription: { '@id': 'schema:requiresSubscription' }, + reservationFor: { '@id': 'schema:reservationFor' }, + reservationId: { '@id': 'schema:reservationId' }, + reservationStatus: { '@id': 'schema:reservationStatus' }, + reservedTicket: { '@id': 'schema:reservedTicket' }, + responsibilities: { '@id': 'schema:responsibilities' }, + restPeriods: { '@id': 'schema:restPeriods' }, + result: { '@id': 'schema:result' }, + resultComment: { '@id': 'schema:resultComment' }, + resultReview: { '@id': 'schema:resultReview' }, + returnFees: { '@id': 'schema:returnFees' }, + returnPolicyCategory: { '@id': 'schema:returnPolicyCategory' }, + review: { '@id': 'schema:review' }, + reviewAspect: { '@id': 'schema:reviewAspect' }, + reviewBody: { '@id': 'schema:reviewBody' }, + reviewCount: { '@id': 'schema:reviewCount' }, + reviewRating: { '@id': 'schema:reviewRating' }, + reviewedBy: { '@id': 'schema:reviewedBy' }, + reviews: { '@id': 'schema:reviews' }, + riskFactor: { '@id': 'schema:riskFactor' }, + risks: { '@id': 'schema:risks' }, + roleName: { '@id': 'schema:roleName' }, + roofLoad: { '@id': 'schema:roofLoad' }, + rsvpResponse: { '@id': 'schema:rsvpResponse' }, + runsTo: { '@id': 'schema:runsTo' }, + runtime: { '@id': 'schema:runtime' }, + runtimePlatform: { '@id': 'schema:runtimePlatform' }, + rxcui: { '@id': 'schema:rxcui' }, + safetyConsideration: { '@id': 'schema:safetyConsideration' }, + salaryCurrency: { '@id': 'schema:salaryCurrency' }, + salaryUponCompletion: { '@id': 'schema:salaryUponCompletion' }, + sameAs: { '@id': 'schema:sameAs', '@type': '@id' }, + sampleType: { '@id': 'schema:sampleType' }, + saturatedFatContent: { '@id': 'schema:saturatedFatContent' }, + scheduleTimezone: { '@id': 'schema:scheduleTimezone' }, + scheduledPaymentDate: { '@id': 'schema:scheduledPaymentDate', '@type': 'Date' }, + scheduledTime: { '@id': 'schema:scheduledTime' }, + schemaVersion: { '@id': 'schema:schemaVersion' }, + schoolClosuresInfo: { '@id': 'schema:schoolClosuresInfo', '@type': '@id' }, + screenCount: { '@id': 'schema:screenCount' }, + screenshot: { '@id': 'schema:screenshot', '@type': '@id' }, + sdDatePublished: { '@id': 'schema:sdDatePublished', '@type': 'Date' }, + sdLicense: { '@id': 'schema:sdLicense', '@type': '@id' }, + sdPublisher: { '@id': 'schema:sdPublisher' }, + season: { '@id': 'schema:season', '@type': '@id' }, + seasonNumber: { '@id': 'schema:seasonNumber' }, + seasons: { '@id': 'schema:seasons' }, + seatNumber: { '@id': 'schema:seatNumber' }, + seatRow: { '@id': 'schema:seatRow' }, + seatSection: { '@id': 'schema:seatSection' }, + seatingCapacity: { '@id': 'schema:seatingCapacity' }, + seatingType: { '@id': 'schema:seatingType' }, + secondaryPrevention: { '@id': 'schema:secondaryPrevention' }, + securityClearanceRequirement: { '@id': 'schema:securityClearanceRequirement' }, + securityScreening: { '@id': 'schema:securityScreening' }, + seeks: { '@id': 'schema:seeks' }, + seller: { '@id': 'schema:seller' }, + sender: { '@id': 'schema:sender' }, + sensoryRequirement: { '@id': 'schema:sensoryRequirement' }, + sensoryUnit: { '@id': 'schema:sensoryUnit' }, + serialNumber: { '@id': 'schema:serialNumber' }, + seriousAdverseOutcome: { '@id': 'schema:seriousAdverseOutcome' }, + serverStatus: { '@id': 'schema:serverStatus' }, + servesCuisine: { '@id': 'schema:servesCuisine' }, + serviceArea: { '@id': 'schema:serviceArea' }, + serviceAudience: { '@id': 'schema:serviceAudience' }, + serviceLocation: { '@id': 'schema:serviceLocation' }, + serviceOperator: { '@id': 'schema:serviceOperator' }, + serviceOutput: { '@id': 'schema:serviceOutput' }, + servicePhone: { '@id': 'schema:servicePhone' }, + servicePostalAddress: { '@id': 'schema:servicePostalAddress' }, + serviceSmsNumber: { '@id': 'schema:serviceSmsNumber' }, + serviceType: { '@id': 'schema:serviceType' }, + serviceUrl: { '@id': 'schema:serviceUrl', '@type': '@id' }, + servingSize: { '@id': 'schema:servingSize' }, + sharedContent: { '@id': 'schema:sharedContent' }, + shippingDestination: { '@id': 'schema:shippingDestination' }, + shippingDetails: { '@id': 'schema:shippingDetails' }, + shippingLabel: { '@id': 'schema:shippingLabel' }, + shippingRate: { '@id': 'schema:shippingRate' }, + shippingSettingsLink: { '@id': 'schema:shippingSettingsLink', '@type': '@id' }, + sibling: { '@id': 'schema:sibling' }, + siblings: { '@id': 'schema:siblings' }, + signDetected: { '@id': 'schema:signDetected' }, + signOrSymptom: { '@id': 'schema:signOrSymptom' }, + significance: { '@id': 'schema:significance' }, + significantLink: { '@id': 'schema:significantLink', '@type': '@id' }, + significantLinks: { '@id': 'schema:significantLinks', '@type': '@id' }, + size: { '@id': 'schema:size' }, + sizeGroup: { '@id': 'schema:sizeGroup' }, + sizeSystem: { '@id': 'schema:sizeSystem' }, + skills: { '@id': 'schema:skills' }, + sku: { '@id': 'schema:sku' }, + slogan: { '@id': 'schema:slogan' }, + smokingAllowed: { '@id': 'schema:smokingAllowed' }, + sodiumContent: { '@id': 'schema:sodiumContent' }, + softwareAddOn: { '@id': 'schema:softwareAddOn' }, + softwareHelp: { '@id': 'schema:softwareHelp' }, + softwareRequirements: { '@id': 'schema:softwareRequirements' }, + softwareVersion: { '@id': 'schema:softwareVersion' }, + sourceOrganization: { '@id': 'schema:sourceOrganization' }, + sourcedFrom: { '@id': 'schema:sourcedFrom' }, + spatial: { '@id': 'schema:spatial' }, + spatialCoverage: { '@id': 'schema:spatialCoverage' }, + speakable: { '@id': 'schema:speakable', '@type': '@id' }, + specialCommitments: { '@id': 'schema:specialCommitments' }, + specialOpeningHoursSpecification: { + '@id': 'schema:specialOpeningHoursSpecification', + }, + specialty: { '@id': 'schema:specialty' }, + speechToTextMarkup: { '@id': 'schema:speechToTextMarkup' }, + speed: { '@id': 'schema:speed' }, + spokenByCharacter: { '@id': 'schema:spokenByCharacter' }, + sponsor: { '@id': 'schema:sponsor' }, + sport: { '@id': 'schema:sport' }, + sportsActivityLocation: { '@id': 'schema:sportsActivityLocation' }, + sportsEvent: { '@id': 'schema:sportsEvent' }, + sportsTeam: { '@id': 'schema:sportsTeam' }, + spouse: { '@id': 'schema:spouse' }, + stage: { '@id': 'schema:stage' }, + stageAsNumber: { '@id': 'schema:stageAsNumber' }, + starRating: { '@id': 'schema:starRating' }, + startDate: { '@id': 'schema:startDate', '@type': 'Date' }, + startOffset: { '@id': 'schema:startOffset' }, + startTime: { '@id': 'schema:startTime' }, + status: { '@id': 'schema:status' }, + steeringPosition: { '@id': 'schema:steeringPosition' }, + step: { '@id': 'schema:step' }, + stepValue: { '@id': 'schema:stepValue' }, + steps: { '@id': 'schema:steps' }, + storageRequirements: { '@id': 'schema:storageRequirements' }, + streetAddress: { '@id': 'schema:streetAddress' }, + strengthUnit: { '@id': 'schema:strengthUnit' }, + strengthValue: { '@id': 'schema:strengthValue' }, + structuralClass: { '@id': 'schema:structuralClass' }, + study: { '@id': 'schema:study' }, + studyDesign: { '@id': 'schema:studyDesign' }, + studyLocation: { '@id': 'schema:studyLocation' }, + studySubject: { '@id': 'schema:studySubject' }, + stupidProperty: { '@id': 'schema:stupidProperty' }, + subEvent: { '@id': 'schema:subEvent' }, + subEvents: { '@id': 'schema:subEvents' }, + subOrganization: { '@id': 'schema:subOrganization' }, + subReservation: { '@id': 'schema:subReservation' }, + subStageSuffix: { '@id': 'schema:subStageSuffix' }, + subStructure: { '@id': 'schema:subStructure' }, + subTest: { '@id': 'schema:subTest' }, + subTrip: { '@id': 'schema:subTrip' }, + subjectOf: { '@id': 'schema:subjectOf' }, + subtitleLanguage: { '@id': 'schema:subtitleLanguage' }, + successorOf: { '@id': 'schema:successorOf' }, + sugarContent: { '@id': 'schema:sugarContent' }, + suggestedAge: { '@id': 'schema:suggestedAge' }, + suggestedAnswer: { '@id': 'schema:suggestedAnswer' }, + suggestedGender: { '@id': 'schema:suggestedGender' }, + suggestedMaxAge: { '@id': 'schema:suggestedMaxAge' }, + suggestedMeasurement: { '@id': 'schema:suggestedMeasurement' }, + suggestedMinAge: { '@id': 'schema:suggestedMinAge' }, + suitableForDiet: { '@id': 'schema:suitableForDiet' }, + superEvent: { '@id': 'schema:superEvent' }, + supersededBy: { '@id': 'schema:supersededBy' }, + supply: { '@id': 'schema:supply' }, + supplyTo: { '@id': 'schema:supplyTo' }, + supportingData: { '@id': 'schema:supportingData' }, + surface: { '@id': 'schema:surface' }, + target: { '@id': 'schema:target' }, + targetCollection: { '@id': 'schema:targetCollection' }, + targetDescription: { '@id': 'schema:targetDescription' }, + targetName: { '@id': 'schema:targetName' }, + targetPlatform: { '@id': 'schema:targetPlatform' }, + targetPopulation: { '@id': 'schema:targetPopulation' }, + targetProduct: { '@id': 'schema:targetProduct' }, + targetUrl: { '@id': 'schema:targetUrl', '@type': '@id' }, + taxID: { '@id': 'schema:taxID' }, + teaches: { '@id': 'schema:teaches' }, + telephone: { '@id': 'schema:telephone' }, + temporal: { '@id': 'schema:temporal' }, + temporalCoverage: { '@id': 'schema:temporalCoverage' }, + termCode: { '@id': 'schema:termCode' }, + termDuration: { '@id': 'schema:termDuration' }, + termsOfService: { '@id': 'schema:termsOfService' }, + termsPerYear: { '@id': 'schema:termsPerYear' }, + text: { '@id': 'schema:text' }, + textValue: { '@id': 'schema:textValue' }, + thumbnail: { '@id': 'schema:thumbnail' }, + thumbnailUrl: { '@id': 'schema:thumbnailUrl', '@type': '@id' }, + tickerSymbol: { '@id': 'schema:tickerSymbol' }, + ticketNumber: { '@id': 'schema:ticketNumber' }, + ticketToken: { '@id': 'schema:ticketToken' }, + ticketedSeat: { '@id': 'schema:ticketedSeat' }, + timeOfDay: { '@id': 'schema:timeOfDay' }, + timeRequired: { '@id': 'schema:timeRequired' }, + timeToComplete: { '@id': 'schema:timeToComplete' }, + tissueSample: { '@id': 'schema:tissueSample' }, + title: { '@id': 'schema:title' }, + titleEIDR: { '@id': 'schema:titleEIDR' }, + toLocation: { '@id': 'schema:toLocation' }, + toRecipient: { '@id': 'schema:toRecipient' }, + tocContinuation: { '@id': 'schema:tocContinuation' }, + tocEntry: { '@id': 'schema:tocEntry' }, + tongueWeight: { '@id': 'schema:tongueWeight' }, + tool: { '@id': 'schema:tool' }, + torque: { '@id': 'schema:torque' }, + totalJobOpenings: { '@id': 'schema:totalJobOpenings' }, + totalPaymentDue: { '@id': 'schema:totalPaymentDue' }, + totalPrice: { '@id': 'schema:totalPrice' }, + totalTime: { '@id': 'schema:totalTime' }, + tourBookingPage: { '@id': 'schema:tourBookingPage', '@type': '@id' }, + touristType: { '@id': 'schema:touristType' }, + track: { '@id': 'schema:track' }, + trackingNumber: { '@id': 'schema:trackingNumber' }, + trackingUrl: { '@id': 'schema:trackingUrl', '@type': '@id' }, + tracks: { '@id': 'schema:tracks' }, + trailer: { '@id': 'schema:trailer' }, + trailerWeight: { '@id': 'schema:trailerWeight' }, + trainName: { '@id': 'schema:trainName' }, + trainNumber: { '@id': 'schema:trainNumber' }, + trainingSalary: { '@id': 'schema:trainingSalary' }, + transFatContent: { '@id': 'schema:transFatContent' }, + transcript: { '@id': 'schema:transcript' }, + transitTime: { '@id': 'schema:transitTime' }, + transitTimeLabel: { '@id': 'schema:transitTimeLabel' }, + translationOfWork: { '@id': 'schema:translationOfWork' }, + translator: { '@id': 'schema:translator' }, + transmissionMethod: { '@id': 'schema:transmissionMethod' }, + travelBans: { '@id': 'schema:travelBans', '@type': '@id' }, + trialDesign: { '@id': 'schema:trialDesign' }, + tributary: { '@id': 'schema:tributary' }, + typeOfBed: { '@id': 'schema:typeOfBed' }, + typeOfGood: { '@id': 'schema:typeOfGood' }, + typicalAgeRange: { '@id': 'schema:typicalAgeRange' }, + typicalCreditsPerTerm: { '@id': 'schema:typicalCreditsPerTerm' }, + typicalTest: { '@id': 'schema:typicalTest' }, + underName: { '@id': 'schema:underName' }, + unitCode: { '@id': 'schema:unitCode' }, + unitText: { '@id': 'schema:unitText' }, + unnamedSourcesPolicy: { '@id': 'schema:unnamedSourcesPolicy', '@type': '@id' }, + unsaturatedFatContent: { '@id': 'schema:unsaturatedFatContent' }, + uploadDate: { '@id': 'schema:uploadDate', '@type': 'Date' }, + upvoteCount: { '@id': 'schema:upvoteCount' }, + url: { '@id': 'schema:url', '@type': '@id' }, + urlTemplate: { '@id': 'schema:urlTemplate' }, + usageInfo: { '@id': 'schema:usageInfo', '@type': '@id' }, + usedToDiagnose: { '@id': 'schema:usedToDiagnose' }, + userInteractionCount: { '@id': 'schema:userInteractionCount' }, + usesDevice: { '@id': 'schema:usesDevice' }, + usesHealthPlanIdStandard: { '@id': 'schema:usesHealthPlanIdStandard' }, + utterances: { '@id': 'schema:utterances' }, + validFor: { '@id': 'schema:validFor' }, + validFrom: { '@id': 'schema:validFrom', '@type': 'Date' }, + validIn: { '@id': 'schema:validIn' }, + validThrough: { '@id': 'schema:validThrough', '@type': 'Date' }, + validUntil: { '@id': 'schema:validUntil', '@type': 'Date' }, + value: { '@id': 'schema:value' }, + valueAddedTaxIncluded: { '@id': 'schema:valueAddedTaxIncluded' }, + valueMaxLength: { '@id': 'schema:valueMaxLength' }, + valueMinLength: { '@id': 'schema:valueMinLength' }, + valueName: { '@id': 'schema:valueName' }, + valuePattern: { '@id': 'schema:valuePattern' }, + valueReference: { '@id': 'schema:valueReference' }, + valueRequired: { '@id': 'schema:valueRequired' }, + variableMeasured: { '@id': 'schema:variableMeasured' }, + variablesMeasured: { '@id': 'schema:variablesMeasured' }, + variantCover: { '@id': 'schema:variantCover' }, + variesBy: { '@id': 'schema:variesBy' }, + vatID: { '@id': 'schema:vatID' }, + vehicleConfiguration: { '@id': 'schema:vehicleConfiguration' }, + vehicleEngine: { '@id': 'schema:vehicleEngine' }, + vehicleIdentificationNumber: { '@id': 'schema:vehicleIdentificationNumber' }, + vehicleInteriorColor: { '@id': 'schema:vehicleInteriorColor' }, + vehicleInteriorType: { '@id': 'schema:vehicleInteriorType' }, + vehicleModelDate: { '@id': 'schema:vehicleModelDate', '@type': 'Date' }, + vehicleSeatingCapacity: { '@id': 'schema:vehicleSeatingCapacity' }, + vehicleSpecialUsage: { '@id': 'schema:vehicleSpecialUsage' }, + vehicleTransmission: { '@id': 'schema:vehicleTransmission' }, + vendor: { '@id': 'schema:vendor' }, + verificationFactCheckingPolicy: { + '@id': 'schema:verificationFactCheckingPolicy', + '@type': '@id', + }, + version: { '@id': 'schema:version' }, + video: { '@id': 'schema:video' }, + videoFormat: { '@id': 'schema:videoFormat' }, + videoFrameSize: { '@id': 'schema:videoFrameSize' }, + videoQuality: { '@id': 'schema:videoQuality' }, + volumeNumber: { '@id': 'schema:volumeNumber' }, + warning: { '@id': 'schema:warning' }, + warranty: { '@id': 'schema:warranty' }, + warrantyPromise: { '@id': 'schema:warrantyPromise' }, + warrantyScope: { '@id': 'schema:warrantyScope' }, + webCheckinTime: { '@id': 'schema:webCheckinTime' }, + webFeed: { '@id': 'schema:webFeed', '@type': '@id' }, + weight: { '@id': 'schema:weight' }, + weightTotal: { '@id': 'schema:weightTotal' }, + wheelbase: { '@id': 'schema:wheelbase' }, + width: { '@id': 'schema:width' }, + winner: { '@id': 'schema:winner' }, + wordCount: { '@id': 'schema:wordCount' }, + workExample: { '@id': 'schema:workExample' }, + workFeatured: { '@id': 'schema:workFeatured' }, + workHours: { '@id': 'schema:workHours' }, + workLocation: { '@id': 'schema:workLocation' }, + workPerformed: { '@id': 'schema:workPerformed' }, + workPresented: { '@id': 'schema:workPresented' }, + workTranslation: { '@id': 'schema:workTranslation' }, + workload: { '@id': 'schema:workload' }, + worksFor: { '@id': 'schema:worksFor' }, + worstRating: { '@id': 'schema:worstRating' }, + xpath: { '@id': 'schema:xpath' }, + yearBuilt: { '@id': 'schema:yearBuilt' }, + yearlyRevenue: { '@id': 'schema:yearlyRevenue' }, + yearsInOperation: { '@id': 'schema:yearsInOperation' }, + yield: { '@id': 'schema:yield' }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/security_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/security_v1.ts new file mode 100644 index 0000000000..070779f190 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/security_v1.ts @@ -0,0 +1,47 @@ +export const SECURITY_V1 = { + '@context': { + id: '@id', + type: '@type', + dc: 'http://purl.org/dc/terms/', + sec: 'https://w3id.org/security#', + xsd: 'http://www.w3.org/2001/XMLSchema#', + EcdsaKoblitzSignature2016: 'sec:EcdsaKoblitzSignature2016', + Ed25519Signature2018: 'sec:Ed25519Signature2018', + EncryptedMessage: 'sec:EncryptedMessage', + GraphSignature2012: 'sec:GraphSignature2012', + LinkedDataSignature2015: 'sec:LinkedDataSignature2015', + LinkedDataSignature2016: 'sec:LinkedDataSignature2016', + CryptographicKey: 'sec:Key', + authenticationTag: 'sec:authenticationTag', + canonicalizationAlgorithm: 'sec:canonicalizationAlgorithm', + cipherAlgorithm: 'sec:cipherAlgorithm', + cipherData: 'sec:cipherData', + cipherKey: 'sec:cipherKey', + created: { '@id': 'dc:created', '@type': 'xsd:dateTime' }, + creator: { '@id': 'dc:creator', '@type': '@id' }, + digestAlgorithm: 'sec:digestAlgorithm', + digestValue: 'sec:digestValue', + domain: 'sec:domain', + encryptionKey: 'sec:encryptionKey', + expiration: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, + initializationVector: 'sec:initializationVector', + iterationCount: 'sec:iterationCount', + nonce: 'sec:nonce', + normalizationAlgorithm: 'sec:normalizationAlgorithm', + owner: { '@id': 'sec:owner', '@type': '@id' }, + password: 'sec:password', + privateKey: { '@id': 'sec:privateKey', '@type': '@id' }, + privateKeyPem: 'sec:privateKeyPem', + publicKey: { '@id': 'sec:publicKey', '@type': '@id' }, + publicKeyBase58: 'sec:publicKeyBase58', + publicKeyPem: 'sec:publicKeyPem', + publicKeyWif: 'sec:publicKeyWif', + publicKeyService: { '@id': 'sec:publicKeyService', '@type': '@id' }, + revoked: { '@id': 'sec:revoked', '@type': 'xsd:dateTime' }, + salt: 'sec:salt', + signature: 'sec:signature', + signatureAlgorithm: 'sec:signingAlgorithm', + signatureValue: 'sec:signatureValue', + }, +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/security_v2.ts b/packages/core/src/modules/vc/__tests__/contexts/security_v2.ts new file mode 100644 index 0000000000..5da116cae1 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/security_v2.ts @@ -0,0 +1,90 @@ +export const SECURITY_V2 = { + '@context': [ + { '@version': 1.1 }, + 'https://w3id.org/security/v1', + { + AesKeyWrappingKey2019: 'sec:AesKeyWrappingKey2019', + DeleteKeyOperation: 'sec:DeleteKeyOperation', + DeriveSecretOperation: 'sec:DeriveSecretOperation', + EcdsaSecp256k1Signature2019: 'sec:EcdsaSecp256k1Signature2019', + EcdsaSecp256r1Signature2019: 'sec:EcdsaSecp256r1Signature2019', + EcdsaSecp256k1VerificationKey2019: 'sec:EcdsaSecp256k1VerificationKey2019', + EcdsaSecp256r1VerificationKey2019: 'sec:EcdsaSecp256r1VerificationKey2019', + Ed25519Signature2018: 'sec:Ed25519Signature2018', + Ed25519VerificationKey2018: 'sec:Ed25519VerificationKey2018', + EquihashProof2018: 'sec:EquihashProof2018', + ExportKeyOperation: 'sec:ExportKeyOperation', + GenerateKeyOperation: 'sec:GenerateKeyOperation', + KmsOperation: 'sec:KmsOperation', + RevokeKeyOperation: 'sec:RevokeKeyOperation', + RsaSignature2018: 'sec:RsaSignature2018', + RsaVerificationKey2018: 'sec:RsaVerificationKey2018', + Sha256HmacKey2019: 'sec:Sha256HmacKey2019', + SignOperation: 'sec:SignOperation', + UnwrapKeyOperation: 'sec:UnwrapKeyOperation', + VerifyOperation: 'sec:VerifyOperation', + WrapKeyOperation: 'sec:WrapKeyOperation', + X25519KeyAgreementKey2019: 'sec:X25519KeyAgreementKey2019', + allowedAction: 'sec:allowedAction', + assertionMethod: { + '@id': 'sec:assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'sec:authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capability: { '@id': 'sec:capability', '@type': '@id' }, + capabilityAction: 'sec:capabilityAction', + capabilityChain: { + '@id': 'sec:capabilityChain', + '@type': '@id', + '@container': '@list', + }, + capabilityDelegation: { + '@id': 'sec:capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'sec:capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + caveat: { '@id': 'sec:caveat', '@type': '@id', '@container': '@set' }, + challenge: 'sec:challenge', + ciphertext: 'sec:ciphertext', + controller: { '@id': 'sec:controller', '@type': '@id' }, + delegator: { '@id': 'sec:delegator', '@type': '@id' }, + equihashParameterK: { + '@id': 'sec:equihashParameterK', + '@type': 'xsd:integer', + }, + equihashParameterN: { + '@id': 'sec:equihashParameterN', + '@type': 'xsd:integer', + }, + invocationTarget: { '@id': 'sec:invocationTarget', '@type': '@id' }, + invoker: { '@id': 'sec:invoker', '@type': '@id' }, + jws: 'sec:jws', + keyAgreement: { + '@id': 'sec:keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + kmsModule: { '@id': 'sec:kmsModule' }, + parentCapability: { '@id': 'sec:parentCapability', '@type': '@id' }, + plaintext: 'sec:plaintext', + proof: { '@id': 'sec:proof', '@type': '@id', '@container': '@graph' }, + proofPurpose: { '@id': 'sec:proofPurpose', '@type': '@vocab' }, + proofValue: 'sec:proofValue', + referenceId: 'sec:referenceId', + unwrappedKey: 'sec:unwrappedKey', + verificationMethod: { '@id': 'sec:verificationMethod', '@type': '@id' }, + verifyData: 'sec:verifyData', + wrappedKey: 'sec:wrappedKey', + }, + ], +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/security_v3_unstable.ts b/packages/core/src/modules/vc/__tests__/contexts/security_v3_unstable.ts new file mode 100644 index 0000000000..e24d51defe --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/security_v3_unstable.ts @@ -0,0 +1,680 @@ +export const SECURITY_V3_UNSTABLE = { + '@context': [ + { + '@version': 1.1, + id: '@id', + type: '@type', + '@protected': true, + JsonWebKey2020: { '@id': 'https://w3id.org/security#JsonWebKey2020' }, + JsonWebSignature2020: { + '@id': 'https://w3id.org/security#JsonWebSignature2020', + '@context': { + '@version': 1.1, + id: '@id', + type: '@type', + '@protected': true, + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + Ed25519VerificationKey2020: { + '@id': 'https://w3id.org/security#Ed25519VerificationKey2020', + }, + Ed25519Signature2020: { + '@id': 'https://w3id.org/security#Ed25519Signature2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: { + '@id': 'https://w3id.org/security#proofValue', + '@type': 'https://w3id.org/security#multibase', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + publicKeyJwk: { + '@id': 'https://w3id.org/security#publicKeyJwk', + '@type': '@json', + }, + ethereumAddress: { '@id': 'https://w3id.org/security#ethereumAddress' }, + publicKeyHex: { '@id': 'https://w3id.org/security#publicKeyHex' }, + blockchainAccountId: { + '@id': 'https://w3id.org/security#blockchainAccountId', + }, + MerkleProof2019: { '@id': 'https://w3id.org/security#MerkleProof2019' }, + Bls12381G1Key2020: { '@id': 'https://w3id.org/security#Bls12381G1Key2020' }, + Bls12381G2Key2020: { '@id': 'https://w3id.org/security#Bls12381G2Key2020' }, + BbsBlsSignature2020: { + '@id': 'https://w3id.org/security#BbsBlsSignature2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + BbsBlsSignatureProof2020: { + '@id': 'https://w3id.org/security#BbsBlsSignatureProof2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + EcdsaKoblitzSignature2016: 'https://w3id.org/security#EcdsaKoblitzSignature2016', + Ed25519Signature2018: { + '@id': 'https://w3id.org/security#Ed25519Signature2018', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + EncryptedMessage: 'https://w3id.org/security#EncryptedMessage', + GraphSignature2012: 'https://w3id.org/security#GraphSignature2012', + LinkedDataSignature2015: 'https://w3id.org/security#LinkedDataSignature2015', + LinkedDataSignature2016: 'https://w3id.org/security#LinkedDataSignature2016', + CryptographicKey: 'https://w3id.org/security#Key', + authenticationTag: 'https://w3id.org/security#authenticationTag', + canonicalizationAlgorithm: 'https://w3id.org/security#canonicalizationAlgorithm', + cipherAlgorithm: 'https://w3id.org/security#cipherAlgorithm', + cipherData: 'https://w3id.org/security#cipherData', + cipherKey: 'https://w3id.org/security#cipherKey', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + creator: { '@id': 'http://purl.org/dc/terms/creator', '@type': '@id' }, + digestAlgorithm: 'https://w3id.org/security#digestAlgorithm', + digestValue: 'https://w3id.org/security#digestValue', + domain: 'https://w3id.org/security#domain', + encryptionKey: 'https://w3id.org/security#encryptionKey', + expiration: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + initializationVector: 'https://w3id.org/security#initializationVector', + iterationCount: 'https://w3id.org/security#iterationCount', + nonce: 'https://w3id.org/security#nonce', + normalizationAlgorithm: 'https://w3id.org/security#normalizationAlgorithm', + owner: 'https://w3id.org/security#owner', + password: 'https://w3id.org/security#password', + privateKey: 'https://w3id.org/security#privateKey', + privateKeyPem: 'https://w3id.org/security#privateKeyPem', + publicKey: 'https://w3id.org/security#publicKey', + publicKeyBase58: 'https://w3id.org/security#publicKeyBase58', + publicKeyPem: 'https://w3id.org/security#publicKeyPem', + publicKeyWif: 'https://w3id.org/security#publicKeyWif', + publicKeyService: 'https://w3id.org/security#publicKeyService', + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + salt: 'https://w3id.org/security#salt', + signature: 'https://w3id.org/security#signature', + signatureAlgorithm: 'https://w3id.org/security#signingAlgorithm', + signatureValue: 'https://w3id.org/security#signatureValue', + proofValue: 'https://w3id.org/security#proofValue', + AesKeyWrappingKey2019: 'https://w3id.org/security#AesKeyWrappingKey2019', + DeleteKeyOperation: 'https://w3id.org/security#DeleteKeyOperation', + DeriveSecretOperation: 'https://w3id.org/security#DeriveSecretOperation', + EcdsaSecp256k1Signature2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256k1Signature2019', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + EcdsaSecp256r1Signature2019: { + '@id': 'https://w3id.org/security#EcdsaSecp256r1Signature2019', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + EcdsaSecp256k1VerificationKey2019: 'https://w3id.org/security#EcdsaSecp256k1VerificationKey2019', + EcdsaSecp256r1VerificationKey2019: 'https://w3id.org/security#EcdsaSecp256r1VerificationKey2019', + Ed25519VerificationKey2018: 'https://w3id.org/security#Ed25519VerificationKey2018', + EquihashProof2018: 'https://w3id.org/security#EquihashProof2018', + ExportKeyOperation: 'https://w3id.org/security#ExportKeyOperation', + GenerateKeyOperation: 'https://w3id.org/security#GenerateKeyOperation', + KmsOperation: 'https://w3id.org/security#KmsOperation', + RevokeKeyOperation: 'https://w3id.org/security#RevokeKeyOperation', + RsaSignature2018: { + '@id': 'https://w3id.org/security#RsaSignature2018', + '@context': { + '@protected': true, + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + jws: 'https://w3id.org/security#jws', + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: 'https://w3id.org/security#proofValue', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + RsaVerificationKey2018: 'https://w3id.org/security#RsaVerificationKey2018', + Sha256HmacKey2019: 'https://w3id.org/security#Sha256HmacKey2019', + SignOperation: 'https://w3id.org/security#SignOperation', + UnwrapKeyOperation: 'https://w3id.org/security#UnwrapKeyOperation', + VerifyOperation: 'https://w3id.org/security#VerifyOperation', + WrapKeyOperation: 'https://w3id.org/security#WrapKeyOperation', + X25519KeyAgreementKey2019: 'https://w3id.org/security#X25519KeyAgreementKey2019', + allowedAction: 'https://w3id.org/security#allowedAction', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capability: { + '@id': 'https://w3id.org/security#capability', + '@type': '@id', + }, + capabilityAction: 'https://w3id.org/security#capabilityAction', + capabilityChain: { + '@id': 'https://w3id.org/security#capabilityChain', + '@type': '@id', + '@container': '@list', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + caveat: { + '@id': 'https://w3id.org/security#caveat', + '@type': '@id', + '@container': '@set', + }, + challenge: 'https://w3id.org/security#challenge', + ciphertext: 'https://w3id.org/security#ciphertext', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + delegator: { '@id': 'https://w3id.org/security#delegator', '@type': '@id' }, + equihashParameterK: { + '@id': 'https://w3id.org/security#equihashParameterK', + '@type': 'http://www.w3.org/2001/XMLSchema#:integer', + }, + equihashParameterN: { + '@id': 'https://w3id.org/security#equihashParameterN', + '@type': 'http://www.w3.org/2001/XMLSchema#:integer', + }, + invocationTarget: { + '@id': 'https://w3id.org/security#invocationTarget', + '@type': '@id', + }, + invoker: { '@id': 'https://w3id.org/security#invoker', '@type': '@id' }, + jws: 'https://w3id.org/security#jws', + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + kmsModule: { '@id': 'https://w3id.org/security#kmsModule' }, + parentCapability: { + '@id': 'https://w3id.org/security#parentCapability', + '@type': '@id', + }, + plaintext: 'https://w3id.org/security#plaintext', + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + referenceId: 'https://w3id.org/security#referenceId', + unwrappedKey: 'https://w3id.org/security#unwrappedKey', + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + verifyData: 'https://w3id.org/security#verifyData', + wrappedKey: 'https://w3id.org/security#wrappedKey', + }, + ], +} diff --git a/packages/core/src/modules/vc/__tests__/contexts/vaccination_v1.ts b/packages/core/src/modules/vc/__tests__/contexts/vaccination_v1.ts new file mode 100644 index 0000000000..ce7d65f499 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/contexts/vaccination_v1.ts @@ -0,0 +1,88 @@ +export const VACCINATION_V1 = { + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + name: 'http://schema.org/name', + image: 'http://schema.org/image', + VaccinationCertificate: { + '@id': 'https://w3id.org/vaccination#VaccinationCertificate', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + description: 'http://schema.org/description', + identifier: 'http://schema.org/identifier', + name: 'http://schema.org/name', + image: 'http://schema.org/image', + }, + }, + VaccinationEvent: { + '@id': 'https://w3id.org/vaccination#VaccinationEvent', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + administeringCentre: 'https://w3id.org/vaccination#administeringCentre', + batchNumber: 'https://w3id.org/vaccination#batchNumber', + countryOfVaccination: 'https://w3id.org/vaccination#countryOfVaccination', + dateOfVaccination: { + '@id': 'https://w3id.org/vaccination#dateOfVaccination', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + healthProfessional: 'https://w3id.org/vaccination#healthProfessional', + nextVaccinationDate: { + '@id': 'https://w3id.org/vaccination#nextVaccinationDate', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + order: 'https://w3id.org/vaccination#order', + recipient: { + '@id': 'https://w3id.org/vaccination#recipient', + '@type': 'https://w3id.org/vaccination#VaccineRecipient', + }, + vaccine: { + '@id': 'https://w3id.org/vaccination#VaccineEventVaccine', + '@type': 'https://w3id.org/vaccination#Vaccine', + }, + }, + }, + VaccineRecipient: { + '@id': 'https://w3id.org/vaccination#VaccineRecipient', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + birthDate: { + '@id': 'http://schema.org/birthDate', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + familyName: 'http://schema.org/familyName', + gender: 'http://schema.org/gender', + givenName: 'http://schema.org/givenName', + }, + }, + Vaccine: { + '@id': 'https://w3id.org/vaccination#Vaccine', + '@context': { + '@version': 1.1, + '@protected': true, + id: '@id', + type: '@type', + atcCode: 'https://w3id.org/vaccination#atc-code', + disease: 'https://w3id.org/vaccination#disease', + event: { + '@id': 'https://w3id.org/vaccination#VaccineRecipientVaccineEvent', + '@type': 'https://w3id.org/vaccination#VaccineEvent', + }, + marketingAuthorizationHolder: 'https://w3id.org/vaccination#marketingAuthorizationHolder', + medicinalProductName: 'https://w3id.org/vaccination#medicinalProductName', + }, + }, + }, +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_example_489398593.ts b/packages/core/src/modules/vc/__tests__/dids/did_example_489398593.ts new file mode 100644 index 0000000000..9cee4d0e2c --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_example_489398593.ts @@ -0,0 +1,13 @@ +export const DID_EXAMPLE_48939859 = { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:example:489398593', + assertionMethod: [ + { + id: 'did:example:489398593#test', + type: 'Bls12381G2Key2020', + controller: 'did:example:489398593', + publicKeyBase58: + 'oqpWYKaZD9M1Kbe94BVXpr8WTdFBNZyKv48cziTiQUeuhm7sBhCABMyYG4kcMrseC68YTFFgyhiNeBKjzdKk9MiRWuLv5H4FFujQsQK2KTAtzU8qTBiZqBHMmnLF4PL7Ytu', + }, + ], +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_sov_QqEfJxe752NCmWqR5TssZ5.ts b/packages/core/src/modules/vc/__tests__/dids/did_sov_QqEfJxe752NCmWqR5TssZ5.ts new file mode 100644 index 0000000000..0246796da6 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_sov_QqEfJxe752NCmWqR5TssZ5.ts @@ -0,0 +1,24 @@ +export const DID_SOV_QqEfJxe752NCmWqR5TssZ5 = { + '@context': 'https://www.w3.org/ns/did/v1', + id: 'did:sov:QqEfJxe752NCmWqR5TssZ5', + verificationMethod: [ + { + id: 'did:sov:QqEfJxe752NCmWqR5TssZ5#key-1', + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:QqEfJxe752NCmWqR5TssZ5', + publicKeyBase58: 'DzNC1pbarUzgGXmxRsccNJDBjWgCaiy6uSXgPPJZGWCL', + }, + ], + authentication: ['did:sov:QqEfJxe752NCmWqR5TssZ5#key-1'], + assertionMethod: ['did:sov:QqEfJxe752NCmWqR5TssZ5#key-1'], + service: [ + { + id: 'did:sov:QqEfJxe752NCmWqR5TssZ5#did-communication', + type: 'did-communication', + serviceEndpoint: 'http://localhost:3002', + recipientKeys: ['did:sov:QqEfJxe752NCmWqR5TssZ5#key-1'], + routingKeys: [], + priority: 0, + }, + ], +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL.ts b/packages/core/src/modules/vc/__tests__/dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL.ts new file mode 100644 index 0000000000..f3bb1e0b1d --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL.ts @@ -0,0 +1,45 @@ +export const DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL = { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + alsoKnownAs: [], + controller: [], + verificationMethod: [ + { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + publicKeyBase58: '3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx', + }, + ], + service: [], + authentication: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + assertionMethod: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + keyAgreement: [ + { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + publicKeyBase58: '3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx', + }, + { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6LSbkodSr6SU2trs8VUgnrnWtSm7BAPG245ggrBmSrxbv1R', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + publicKeyBase58: '5dTvYHaNaB7mk7iA9LqCJEHG2dGZQsvoi8WGzDRtYEf', + }, + ], + capabilityInvocation: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + capabilityDelegation: [ + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + ], + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV.ts b/packages/core/src/modules/vc/__tests__/dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV.ts new file mode 100644 index 0000000000..2c8d443940 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV.ts @@ -0,0 +1,45 @@ +export const DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV = { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + alsoKnownAs: [], + controller: [], + verificationMethod: [ + { + id: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + publicKeyBase58: 'HC8vuuvP8x9kVJizh2eujQjo2JwFQJz6w63szzdbu1Q7', + }, + ], + service: [], + authentication: [ + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + ], + assertionMethod: [ + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + ], + keyAgreement: [ + { + id: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + publicKeyBase58: 'HC8vuuvP8x9kVJizh2eujQjo2JwFQJz6w63szzdbu1Q7', + }, + { + id: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6LSsJwtXqeVHCtCR9QMyX58hfBNY62wQooE4VPYmwyyesov', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + publicKeyBase58: 'Gdmj1XqdBkATKm2bSsZBP4xtgwVpiCd5BWfsHVLSwW3A', + }, + ], + capabilityInvocation: [ + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + ], + capabilityDelegation: [ + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + ], + id: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', +} diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC729nNiUKQ4pHHNYovae25gkkuvtsZmtpjnLYUj1r8Yd4ZRn3FaswicUWs2NYNuWXxQ7MgzAX7dqXxAFZXFvn2jhqGKpjm5xLwESYfhcDGdSrc9mgfu51w939BjmKmng5HvYK.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC729nNiUKQ4pHHNYovae25gkkuvtsZmtpjnLYUj1r8Yd4ZRn3FaswicUWs2NYNuWXxQ7MgzAX7dqXxAFZXFvn2jhqGKpjm5xLwESYfhcDGdSrc9mgfu51w939BjmKmng5HvYK.ts new file mode 100644 index 0000000000..46ae84b94e --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC729nNiUKQ4pHHNYovae25gkkuvtsZmtpjnLYUj1r8Yd4ZRn3FaswicUWs2NYNuWXxQ7MgzAX7dqXxAFZXFvn2jhqGKpjm5xLwESYfhcDGdSrc9mgfu51w939BjmKmng5HvYK.ts @@ -0,0 +1,30 @@ +export const DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4 = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + verificationMethod: [ + { + id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'rvmIn58iMglCOixwxv7snWjuu8ooQteghivgqrchuIDH8DbG7pzF5io_k2t5HOW1DjcsVioEXLnIdSdUz8jJQq2r-B8zyw4CEiWAM9LUPnmmRDeVFVtA0YVaLo7DdkOn', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + authentication: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + capabilityInvocation: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + capabilityDelegation: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.ts new file mode 100644 index 0000000000..472bc1e84c --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa.ts @@ -0,0 +1,54 @@ +export const DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa = + { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/bbs/v1'], + alsoKnownAs: [], + controller: [], + verificationMethod: [ + { + id: 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + type: 'Bls12381G2Key2020', + controller: + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + publicKeyBase58: + 'nZZe9Nizhaz9JGpgjysaNkWGg5TNEhpib5j6WjTUHJ5K46dedUrZ57PUFZBq9Xckv8mFJjx6G6Vvj2rPspq22BagdADEEEy2F8AVLE1DhuwWC5vHFa4fUhUwxMkH7B6joqG', + publicKeyBase64: undefined, + publicKeyJwk: undefined, + publicKeyHex: undefined, + publicKeyMultibase: undefined, + publicKeyPem: undefined, + blockchainAccountId: undefined, + ethereumAddress: undefined, + }, + ], + service: [], + authentication: [ + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + ], + assertionMethod: [ + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + ], + keyAgreement: [ + { + id: 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + type: 'Bls12381G2Key2020', + controller: + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + publicKeyBase58: + 'nZZe9Nizhaz9JGpgjysaNkWGg5TNEhpib5j6WjTUHJ5K46dedUrZ57PUFZBq9Xckv8mFJjx6G6Vvj2rPspq22BagdADEEEy2F8AVLE1DhuwWC5vHFa4fUhUwxMkH7B6joqG', + publicKeyBase64: undefined, + publicKeyJwk: undefined, + publicKeyHex: undefined, + publicKeyMultibase: undefined, + publicKeyPem: undefined, + blockchainAccountId: undefined, + ethereumAddress: undefined, + }, + ], + capabilityInvocation: [ + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + ], + capabilityDelegation: [ + 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + ], + id: 'did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa', + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.ts new file mode 100644 index 0000000000..968aec92bc --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD.ts @@ -0,0 +1,30 @@ +export const DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + verificationMethod: [ + { + id: 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'hbLuuV4otX1HEALBmUGy_ryyTIcY4TsoZYm_UZPCPgITbXvn8YlvlVM_T6_D0ZrUByvZELEX6wXzKhSkCwEqawZOEhUk4iWFID4MR6nRD4icGm97LC4d58WHTfCZ5bXw', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + ], + authentication: [ + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + ], + capabilityInvocation: [ + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + ], + capabilityDelegation: [ + 'did:key:zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD#zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.ts new file mode 100644 index 0000000000..b3072fa575 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh.ts @@ -0,0 +1,30 @@ +export const DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + verificationMethod: [ + { + id: 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'huBQv7qpuF5FI5bvaku1B8JSPHeHKPI-hhvcJ97I5vNdGtafbPfrPncV4NNXidkzDDASYgt22eMSVKX9Kc9iWFnPmprzDNUt1HhvtBrldXLlRegT93LOogEh7BwoKVGW', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + ], + authentication: [ + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + ], + capabilityInvocation: [ + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + ], + capabilityDelegation: [ + 'did:key:zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh#zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.ts new file mode 100644 index 0000000000..c2861e2a1a --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn.ts @@ -0,0 +1,30 @@ +export const DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + verificationMethod: [ + { + id: 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'h5pno-Wq71ExNSbjZ91OJavpe0tA871-20TigCvQAs9jHtIV6KjXtX17Cmoz01dQBlPUFPOB5ILw2JeZ2MYtMOzCCYtnuour5XDuyYs6KTAXgYQ2nAlIFfmXXr9Jc48z', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + ], + authentication: [ + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + ], + capabilityInvocation: [ + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + ], + capabilityDelegation: [ + 'did:key:zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn#zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.ts new file mode 100644 index 0000000000..3991dcd28b --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN.ts @@ -0,0 +1,27 @@ +export const DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/bls12381-2020/v1'], + id: 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + verificationMethod: [ + { + id: 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + type: 'Bls12381G2Key2020', + controller: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + publicKeyBase58: + 'pegxn1a43zphf3uqGT4cx1bz8Ebb9QmoSWhQyP1qYTSeRuvWLGKJ5KcqaymnSj53YhCFbjr3tJAhqcaxxZ4Lry7KxkpLeA6GVf3Zb1x999dYp3k4jQzYa1PQXC6x1uCd9s4', + }, + ], + assertionMethod: [ + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + ], + authentication: [ + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + ], + capabilityInvocation: [ + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + ], + capabilityDelegation: [ + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.ts new file mode 100644 index 0000000000..d369808fc9 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ.ts @@ -0,0 +1,30 @@ +export const DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + verificationMethod: [ + { + id: 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'kSN7z0XGmPGn81aqNhL4zE-jF799YUzc7nl730o0nBsMZiZzwlqyNvemMYrWAGq5FCoaN0jpCkefgdRrMRPPD_6IK3w0g3ieFxNxdwX7NcGR8aihA9stCdTe0kx-ePJr', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + ], + authentication: [ + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + ], + capabilityInvocation: [ + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + ], + capabilityDelegation: [ + 'did:key:zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ#zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.ts new file mode 100644 index 0000000000..5288ec249c --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox.ts @@ -0,0 +1,30 @@ +export const DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + verificationMethod: [ + { + id: 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'pA1LXe8EGRU8PTpXfnG3fpJoIW394wpGpx8Q3V5Keh3PUM7j_PRLbk6XN3KJTv7cFesQeo_Q-knymniIm0Ugk9-RGKn65pRIy65aMa1ACfKfGTnnnTuJP4tWRHW2BaHb', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + ], + authentication: [ + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + ], + capabilityInvocation: [ + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + ], + capabilityDelegation: [ + 'did:key:zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox#zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.ts new file mode 100644 index 0000000000..3e4bac3b13 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F.ts @@ -0,0 +1,30 @@ +export const DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + verificationMethod: [ + { + id: 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'qULVOptm5i4PfW7r6Hu6wzw6BZRywAQcCi3V0q1VDidrf0bZ-rFUaP72vXRa1WkPAoWpjMjM-uYbDQJBQbgVXoFm4L5Qz3YG5ziHRGdVWChY_5TX8yV3fQOsLJDSnfZy', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + ], + authentication: [ + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + ], + capabilityInvocation: [ + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + ], + capabilityDelegation: [ + 'did:key:zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F#zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.ts b/packages/core/src/modules/vc/__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.ts new file mode 100644 index 0000000000..46ae84b94e --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4.ts @@ -0,0 +1,30 @@ +export const DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4 = + { + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + verificationMethod: [ + { + id: 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + type: 'JsonWebKey2020', + controller: + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + publicKeyJwk: { + kty: 'EC', + crv: 'BLS12381_G2', + x: 'rvmIn58iMglCOixwxv7snWjuu8ooQteghivgqrchuIDH8DbG7pzF5io_k2t5HOW1DjcsVioEXLnIdSdUz8jJQq2r-B8zyw4CEiWAM9LUPnmmRDeVFVtA0YVaLo7DdkOn', + }, + }, + ], + assertionMethod: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + authentication: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + capabilityInvocation: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + capabilityDelegation: [ + 'did:key:zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4#zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4', + ], + } diff --git a/packages/core/src/modules/vc/__tests__/documentLoader.ts b/packages/core/src/modules/vc/__tests__/documentLoader.ts new file mode 100644 index 0000000000..91a76bf879 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/documentLoader.ts @@ -0,0 +1,113 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line import/no-extraneous-dependencies + +import type { JsonObject } from '../../../types' +import type { DocumentLoaderResult } from '../../../utils' + +import jsonld from '../../../../types/jsonld' + +import { BBS_V1, EXAMPLES_V1, ODRL, SCHEMA_ORG, VACCINATION_V1 } from './contexts' +import { X25519_V1 } from './contexts/X25519_v1' +import { CITIZENSHIP_V1 } from './contexts/citizenship_v1' +import { CREDENTIALS_V1 } from './contexts/credentials_v1' +import { DID_V1 } from './contexts/did_v1' +import { ED25519_V1 } from './contexts/ed25519_v1' +import { SECURITY_V1 } from './contexts/security_v1' +import { SECURITY_V2 } from './contexts/security_v2' +import { SECURITY_V3_UNSTABLE } from './contexts/security_v3_unstable' +import { DID_EXAMPLE_48939859 } from './dids/did_example_489398593' +import { DID_SOV_QqEfJxe752NCmWqR5TssZ5 } from './dids/did_sov_QqEfJxe752NCmWqR5TssZ5' +import { DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL } from './dids/did_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL' +import { DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV } from './dids/did_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV' +import { DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa } from './dids/did_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa' +import { DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD } from './dids/did_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD' +import { DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh } from './dids/did_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh' +import { DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn } from './dids/did_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn' +import { DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN } from './dids/did_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN' +import { DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ } from './dids/did_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ' +import { DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox } from './dids/did_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox' +import { DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F } from './dids/did_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F' +import { DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4 } from './dids/did_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4' + +export const DOCUMENTS = { + [DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL['id']]: DID_z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL, + [DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV['id']]: DID_z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV, + [DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa[ + 'id' + ]]: DID_zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa, + [DID_EXAMPLE_48939859['id']]: DID_EXAMPLE_48939859, + [DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh[ + 'id' + ]]: DID_zUC73JKGpX1WG4CWbFM15ni3faANPet6m8WJ6vaF5xyFsM3MeoBVNgQ6jjVPCcUnTAnJy6RVKqsUXa4AvdRKwV5hhQhwhMWFT9so9jrPekKmqpikTjYBXa3RYWqRpCWHY4u4hxh, + [DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn[ + 'id' + ]]: DID_zUC73YqdRJ3t8bZsFUoxYFPNVruHzn4o7u78GSrMXVSkcb3xAYtUxRD2kSt2bDcmQpRjKfygwLJ1HEGfkosSN7gr4acjGkXLbLRXREueknFN4AU19m8BxEgWnLM84CAvsw6bhYn, + [DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ[ + 'id' + ]]: DID_zUC76qMTDAaupy19pEk8JKH5LJwPwmscNQn24SYpqrgqEoYWPFgCSm4CnTfupADRfbB6CxdwYhVaTFjT4fmPvMh7gWY87LauhaLmNpPamCv4LAepcRfBDndSdtCpZKSTELMjzGJ, + [DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox[ + 'id' + ]]: DID_zUC7DMETzdZM6woUjvs2fieEyFTbHABXwBvLYPBs4NDWKut4H41h8V3KTqGNRUziXLYqa1sFYYw9Zjpt6pFUf7hra4Q1zXMA9JjXcXxDpxuDNpUKEpiDPSYYUztVchUJHQJJhox, + [DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F[ + 'id' + ]]: DID_zUC7F9Jt6YzVW9fGhwYjVrjdS8Xzg7oQc2CeDcVNgEcEAaJXAtPz3eXu2sewq4xtwRK3DAhQRYwwoYiT3nNzLCPsrKoP72UGZKhh4cNuZD7RkmwzAa1Bye4C5a9DcyYBGKZrE5F, + [DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4[ + 'id' + ]]: DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4, + [DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4[ + 'id' + ]]: DID_zUC7H7TxvhWmvfptpu2zSwo5EZ1kr3MPNsjovaD2ipbuzj6zi1vk4FHTiunCJrFvUYV77Mk3QcWUUAHojPZdU8oG476cvMK2ozP1gVq63x5ovj6e4oQ9qg9eF4YjPhWJs6FPuT4, + [DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD[ + 'id' + ]]: DID_zUC72to2eJiFMrt8a89LoaEPHC76QcfAxQdFys3nFGCmDKAmLbdE4ByyQ54kh42XgECCyZfVKe3m41Kk35nzrBKYbk6s9K7EjyLJcGGPkA7N15tDNBQJaY7cHD4RRaTwF6qXpmD, + [DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN[ + 'id' + ]]: DID_zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN, + [DID_SOV_QqEfJxe752NCmWqR5TssZ5['id']]: DID_SOV_QqEfJxe752NCmWqR5TssZ5, + SECURITY_CONTEXT_V1_URL: SECURITY_V1, + SECURITY_CONTEXT_V2_URL: SECURITY_V2, + SECURITY_CONTEXT_V3_URL: SECURITY_V3_UNSTABLE, + DID_V1_CONTEXT_URL: DID_V1, + CREDENTIALS_CONTEXT_V1_URL: CREDENTIALS_V1, + SECURITY_CONTEXT_BBS_URL: BBS_V1, + 'https://w3id.org/security/bbs/v1': BBS_V1, + 'https://w3id.org/security/v1': SECURITY_V1, + 'https://w3id.org/security/v2': SECURITY_V2, + 'https://w3id.org/security/suites/x25519-2019/v1': X25519_V1, + 'https://w3id.org/security/suites/ed25519-2018/v1': ED25519_V1, + 'https://www.w3.org/2018/credentials/examples/v1': EXAMPLES_V1, + 'https://www.w3.org/2018/credentials/v1': CREDENTIALS_V1, + 'https://w3id.org/did/v1': DID_V1, + 'https://w3id.org/citizenship/v1': CITIZENSHIP_V1, + 'https://www.w3.org/ns/odrl.jsonld': ODRL, + 'http://schema.org/': SCHEMA_ORG, + 'https://w3id.org/vaccination/v1': VACCINATION_V1, +} + +export const customDocumentLoader = async (url: string): Promise => { + let result = DOCUMENTS[url] + + if (!result) { + const withoutFragment = url.split('#')[0] + result = DOCUMENTS[withoutFragment] + } + + if (!result) { + throw new Error(`Document not found: ${url}`) + } + + if (url.startsWith('did:')) { + result = await jsonld.frame(result, { + '@context': result['@context'], + '@embed': '@never', + id: url, + }) + } + + return { + contextUrl: null, + documentUrl: url, + document: result as JsonObject, + } +} diff --git a/packages/core/src/modules/vc/__tests__/fixtures.ts b/packages/core/src/modules/vc/__tests__/fixtures.ts new file mode 100644 index 0000000000..a40b8499c1 --- /dev/null +++ b/packages/core/src/modules/vc/__tests__/fixtures.ts @@ -0,0 +1,326 @@ +import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_BBS_URL } from '../constants' + +export const Ed25519Signature2018Fixtures = { + TEST_LD_DOCUMENT: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + }, + TEST_LD_DOCUMENT_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-18T23:13:10Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw', + }, + }, + TEST_LD_DOCUMENT_BAD_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: + 'did:key:z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV#z6MkvePyWAApUVeDboZhNbckaWHnqtD6pCETd6xoqGbcpEBV', + type: 'Ed25519Signature2018', + created: '2022-03-28T15:54:59Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..Ej5aEUBTgeNm3_a4uO_AuNnisldnYTMMGMom4xLb-_TmoYe7467Yo046Bw2QqdfdBja6y-HBbBj4SonOlwswAg', + }, + }, + TEST_VP_DOCUMENT: { + '@context': [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: [ + { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-18T23:13:10Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw', + }, + }, + ], + }, + TEST_VP_DOCUMENT_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: [ + { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-18T23:13:10Z', + proofPurpose: 'assertionMethod', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..ECQsj_lABelr1jkehSkqaYpc5CBvbSjbi3ZvgiVVKxZFDYfj5xZmeXb_awa4aw_cGEVaoypeN2uCFmeG6WKkBw', + }, + }, + ], + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-20T17:31:49Z', + proofPurpose: 'authentication', + challenge: '7bf32d0b-39d4-41f3-96b6-45de52988e4c', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..yNSkNCfVv6_1-P6CtldiqS2bDe_8DPKBIP3Do9qi0LF2DU_d70pWajevJIBH5NZ8K4AawDYx_irlhdz4aiH3Bw', + }, + }, +} + +export const BbsBlsSignature2020Fixtures = { + TEST_LD_DOCUMENT: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: '', + identifier: '83627465', + name: 'Permanent Resident Card', + description: 'Government of Example Permanent Resident Card.', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + }, + + TEST_LD_DOCUMENT_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + identifier: '83627465', + name: 'Permanent Resident Card', + description: 'Government of Example Permanent Resident Card.', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + proof: { + type: 'BbsBlsSignature2020', + created: '2022-04-13T13:47:47Z', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proofPurpose: 'assertionMethod', + proofValue: + 'hoNNnnRIoEoaY9Fvg3pGVG2eWTAHnR1kIM01nObEL2FdI2IkkpM3246jn3VBD8KBYUHlKfzccE4m7waZyoLEkBLFiK2g54Q2i+CdtYBgDdkUDsoULSBMcH1MwGHwdjfXpldFNFrHFx/IAvLVniyeMQ==', + }, + }, + TEST_LD_DOCUMENT_BAD_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + identifier: '83627465', + name: 'Permanent Resident Card', + description: 'Government of Example Permanent Resident Card.', + issuanceDate: '2019-12-03T12:19:52Z', + expirationDate: '2029-12-03T12:19:52Z', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['PermanentResident', 'Person'], + givenName: 'JOHN', + familyName: 'SMITH', + gender: 'Male', + image: 'data:image/png;base64,iVBORw0KGgokJggg==', + residentSince: '2015-01-01', + lprCategory: 'C09', + lprNumber: '999-999-999', + commuterClassification: 'C1', + birthCountry: 'Bahamas', + birthDate: '1958-07-17', + }, + proof: { + type: 'BbsBlsSignature2020', + created: '2022-04-13T13:47:47Z', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proofPurpose: 'assertionMethod', + proofValue: + 'gU44r/fmvGpkOyMRZX4nwRB6IsbrL7zbVTs+yu6bZGeCNJuiJqS5U6fCPuvGQ+iNYUHlKfzccE4m7waZyoLEkBLFiK2g54Q2i+CdtYBgDdkUDsoULSBMcH1MwGHwdjfXpldFNFrHFx/IAvLVniyeMQ==', + }, + }, + + TEST_VALID_DERIVED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['PermanentResidentCard', 'VerifiableCredential'], + description: 'Government of Example Permanent Resident Card.', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['Person', 'PermanentResident'], + familyName: 'SMITH', + gender: 'Male', + givenName: 'JOHN', + }, + expirationDate: '2029-12-03T12:19:52Z', + issuanceDate: '2019-12-03T12:19:52Z', + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proof: { + type: 'BbsBlsSignatureProof2020', + created: '2022-04-13T13:47:47Z', + nonce: 'GfuRhH8hSAcWm5RWgUQYNQNWjQBsWuVgMCJrhTCD3kSpnHmQOkHcnNAoBsgyMAT4UUI=', + proofPurpose: 'assertionMethod', + proofValue: + 'ABkB/wbvkcCcbPRE5vrXc++orru4MsgrS4ESsZ30RNCs3noqLwm94/RZNp62I6Hyf0Kmht0Vog70HDtnNzbnMAj/zD9oT/N53pOADrtn5v+xZgP3cK4N2d6amg6h3LXem29gidW9hMrROPLit5cWEIL4/HOzxPxQQGYiwEXdW++Aja5ZuwJoMsIx7ysn4C4ekN7JXZtnAAAAdJR/oeDShxRdSBlnCSUHkE4Ol+Z3AhXBKkxb4AxiMKOiNmBreMTjJUGwNAPNU2aKnAAAAAIBUuKV0W0YBQZY/mwLmwCcyOWMiaEpjnVhYip4jhBBZw1aPBe8GzsG9zv3Sf9XAyGEAvVFe3OvwvMwYY5nZYdYoLSR4TLl1aLw0oChiPm2zb6ApXypCEEVd8KhJMATyssTlY48bEljDNixAD2rVDaoAAAACWjyrWp3b62M5Onuwo9EItCrBjPD68xC12q1agqgwFTnOI0+MfEwVGNZsA0IqkCGrZmo3AyRpcRm51IYDWYorM4hued5EcVHeCGd6NrnLSxTFPEu8lnmCoMXcxBWDCZFRGb//M5WlncbsYiz01itHbSs1nmpj3o+DYlF2ZyOYphvLo5A9T4rWVwHRK1+LeCDEawOnI03DWLyN8U4ZpbpcdZNK421IwNjseYY+ptvvL3juZ2uQR84maAZYy/OjMuHNyzqHPXNgsLLqtrvPo0kncefp+x1jgA0J/b5xfT72+vhKZAN1R48/uPf+DySC3avwD3T+YHjePn1bBOidhCWMjwzI9LYO8VvhcWXzH7nBWh5MeUch+Wkl777KrsLhrXnCg==', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + }, + }, + + TEST_VP_DOCUMENT: { + '@context': [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: [ + { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['PermanentResidentCard', 'VerifiableCredential'], + description: 'Government of Example Permanent Resident Card.', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['Person', 'PermanentResident'], + familyName: 'SMITH', + gender: 'Male', + givenName: 'JOHN', + }, + expirationDate: '2029-12-03T12:19:52Z', + issuanceDate: '2019-12-03T12:19:52Z', + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proof: { + type: 'BbsBlsSignatureProof2020', + created: '2022-04-13T13:47:47Z', + nonce: 'GfuRhH8hSAcWm5RWgUQYNQNWjQBsWuVgMCJrhTCD3kSpnHmQOkHcnNAoBsgyMAT4UUI=', + proofPurpose: 'assertionMethod', + proofValue: + 'ABkB/wbvkcCcbPRE5vrXc++orru4MsgrS4ESsZ30RNCs3noqLwm94/RZNp62I6Hyf0Kmht0Vog70HDtnNzbnMAj/zD9oT/N53pOADrtn5v+xZgP3cK4N2d6amg6h3LXem29gidW9hMrROPLit5cWEIL4/HOzxPxQQGYiwEXdW++Aja5ZuwJoMsIx7ysn4C4ekN7JXZtnAAAAdJR/oeDShxRdSBlnCSUHkE4Ol+Z3AhXBKkxb4AxiMKOiNmBreMTjJUGwNAPNU2aKnAAAAAIBUuKV0W0YBQZY/mwLmwCcyOWMiaEpjnVhYip4jhBBZw1aPBe8GzsG9zv3Sf9XAyGEAvVFe3OvwvMwYY5nZYdYoLSR4TLl1aLw0oChiPm2zb6ApXypCEEVd8KhJMATyssTlY48bEljDNixAD2rVDaoAAAACWjyrWp3b62M5Onuwo9EItCrBjPD68xC12q1agqgwFTnOI0+MfEwVGNZsA0IqkCGrZmo3AyRpcRm51IYDWYorM4hued5EcVHeCGd6NrnLSxTFPEu8lnmCoMXcxBWDCZFRGb//M5WlncbsYiz01itHbSs1nmpj3o+DYlF2ZyOYphvLo5A9T4rWVwHRK1+LeCDEawOnI03DWLyN8U4ZpbpcdZNK421IwNjseYY+ptvvL3juZ2uQR84maAZYy/OjMuHNyzqHPXNgsLLqtrvPo0kncefp+x1jgA0J/b5xfT72+vhKZAN1R48/uPf+DySC3avwD3T+YHjePn1bBOidhCWMjwzI9LYO8VvhcWXzH7nBWh5MeUch+Wkl777KrsLhrXnCg==', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + }, + }, + ], + }, + TEST_VP_DOCUMENT_SIGNED: { + '@context': [CREDENTIALS_CONTEXT_V1_URL], + type: ['VerifiablePresentation'], + verifiableCredential: [ + { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://w3id.org/citizenship/v1', SECURITY_CONTEXT_BBS_URL], + id: 'https://issuer.oidp.uscis.gov/credentials/83627465', + type: ['PermanentResidentCard', 'VerifiableCredential'], + description: 'Government of Example Permanent Resident Card.', + identifier: '83627465', + name: 'Permanent Resident Card', + credentialSubject: { + id: 'did:example:b34ca6cd37bbf23', + type: ['Person', 'PermanentResident'], + familyName: 'SMITH', + gender: 'Male', + givenName: 'JOHN', + }, + expirationDate: '2029-12-03T12:19:52Z', + issuanceDate: '2019-12-03T12:19:52Z', + issuer: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + proof: { + type: 'BbsBlsSignatureProof2020', + created: '2022-04-13T13:47:47Z', + nonce: 'GfuRhH8hSAcWm5RWgUQYNQNWjQBsWuVgMCJrhTCD3kSpnHmQOkHcnNAoBsgyMAT4UUI=', + proofPurpose: 'assertionMethod', + proofValue: + 'ABkB/wbvkcCcbPRE5vrXc++orru4MsgrS4ESsZ30RNCs3noqLwm94/RZNp62I6Hyf0Kmht0Vog70HDtnNzbnMAj/zD9oT/N53pOADrtn5v+xZgP3cK4N2d6amg6h3LXem29gidW9hMrROPLit5cWEIL4/HOzxPxQQGYiwEXdW++Aja5ZuwJoMsIx7ysn4C4ekN7JXZtnAAAAdJR/oeDShxRdSBlnCSUHkE4Ol+Z3AhXBKkxb4AxiMKOiNmBreMTjJUGwNAPNU2aKnAAAAAIBUuKV0W0YBQZY/mwLmwCcyOWMiaEpjnVhYip4jhBBZw1aPBe8GzsG9zv3Sf9XAyGEAvVFe3OvwvMwYY5nZYdYoLSR4TLl1aLw0oChiPm2zb6ApXypCEEVd8KhJMATyssTlY48bEljDNixAD2rVDaoAAAACWjyrWp3b62M5Onuwo9EItCrBjPD68xC12q1agqgwFTnOI0+MfEwVGNZsA0IqkCGrZmo3AyRpcRm51IYDWYorM4hued5EcVHeCGd6NrnLSxTFPEu8lnmCoMXcxBWDCZFRGb//M5WlncbsYiz01itHbSs1nmpj3o+DYlF2ZyOYphvLo5A9T4rWVwHRK1+LeCDEawOnI03DWLyN8U4ZpbpcdZNK421IwNjseYY+ptvvL3juZ2uQR84maAZYy/OjMuHNyzqHPXNgsLLqtrvPo0kncefp+x1jgA0J/b5xfT72+vhKZAN1R48/uPf+DySC3avwD3T+YHjePn1bBOidhCWMjwzI9LYO8VvhcWXzH7nBWh5MeUch+Wkl777KrsLhrXnCg==', + verificationMethod: + 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN#zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y2cgguc8e9hsGBifnVK67pQ4gve3m6iSboDkmJjxVEb1d6mRAx5fpMAejooNzNqqbTMVeUN', + }, + }, + ], + proof: { + verificationMethod: + 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + type: 'Ed25519Signature2018', + created: '2022-04-21T10:15:38Z', + proofPurpose: 'authentication', + challenge: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..wGtR9yuTRfhrsvCthUOn-fg_lK0mZIe2IOO2Lv21aOXo5YUAbk50qMBLk4C1iqoOx-Jz6R0g4aa4cuqpdXzkBw', + }, + }, +} diff --git a/packages/core/src/modules/vc/constants.ts b/packages/core/src/modules/vc/constants.ts new file mode 100644 index 0000000000..6b298625df --- /dev/null +++ b/packages/core/src/modules/vc/constants.ts @@ -0,0 +1,14 @@ +export const SECURITY_CONTEXT_V1_URL = 'https://w3id.org/security/v1' +export const SECURITY_CONTEXT_V2_URL = 'https://w3id.org/security/v2' +export const SECURITY_CONTEXT_V3_URL = 'https://w3id.org/security/v3-unstable' +export const SECURITY_CONTEXT_URL = SECURITY_CONTEXT_V2_URL +export const SECURITY_X25519_CONTEXT_URL = 'https://w3id.org/security/suites/x25519-2019/v1' +export const DID_V1_CONTEXT_URL = 'https://www.w3.org/ns/did/v1' +export const CREDENTIALS_CONTEXT_V1_URL = 'https://www.w3.org/2018/credentials/v1' +export const SECURITY_CONTEXT_BBS_URL = 'https://w3id.org/security/bbs/v1' +export const CREDENTIALS_ISSUER_URL = 'https://www.w3.org/2018/credentials#issuer' +export const SECURITY_PROOF_URL = 'https://w3id.org/security#proof' +export const SECURITY_SIGNATURE_URL = 'https://w3id.org/security#signature' +export const VERIFIABLE_CREDENTIAL_TYPE = 'VerifiableCredential' +export const VERIFIABLE_PRESENTATION_TYPE = 'VerifiablePresentation' +export const EXPANDED_TYPE_CREDENTIALS_CONTEXT_V1_VC_TYPE = 'https://www.w3.org/2018/credentials#VerifiableCredential' diff --git a/packages/core/src/modules/vc/index.ts b/packages/core/src/modules/vc/index.ts new file mode 100644 index 0000000000..7f8b432491 --- /dev/null +++ b/packages/core/src/modules/vc/index.ts @@ -0,0 +1 @@ +export * from './W3cCredentialService' diff --git a/packages/core/src/modules/vc/models/LinkedDataProof.ts b/packages/core/src/modules/vc/models/LinkedDataProof.ts new file mode 100644 index 0000000000..3dd7209dcb --- /dev/null +++ b/packages/core/src/modules/vc/models/LinkedDataProof.ts @@ -0,0 +1,86 @@ +import type { SingleOrArray } from '../../../utils/type' + +import { Transform, TransformationType, plainToInstance, instanceToPlain } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' + +export interface LinkedDataProofOptions { + type: string + proofPurpose: string + verificationMethod: string + created: string + domain?: string + challenge?: string + jws?: string + proofValue?: string + nonce?: string +} + +/** + * Linked Data Proof + * @see https://w3c.github.io/vc-data-model/#proofs-signatures + * + * @class LinkedDataProof + */ +export class LinkedDataProof { + public constructor(options: LinkedDataProofOptions) { + if (options) { + this.type = options.type + this.proofPurpose = options.proofPurpose + this.verificationMethod = options.verificationMethod + this.created = options.created + this.domain = options.domain + this.challenge = options.challenge + this.jws = options.jws + this.proofValue = options.proofValue + this.nonce = options.nonce + } + } + + @IsString() + public type!: string + + @IsString() + public proofPurpose!: string + + @IsString() + public verificationMethod!: string + + @IsString() + public created!: string + + @IsString() + @IsOptional() + public domain?: string + + @IsString() + @IsOptional() + public challenge?: string + + @IsString() + @IsOptional() + public jws?: string + + @IsString() + @IsOptional() + public proofValue?: string + + @IsString() + @IsOptional() + public nonce?: string +} + +// Custom transformers + +export function LinkedDataProofTransformer() { + return Transform(({ value, type }: { value: SingleOrArray; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + if (Array.isArray(value)) return value.map((v) => plainToInstance(LinkedDataProof, v)) + return plainToInstance(LinkedDataProof, value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + if (Array.isArray(value)) return value.map((v) => instanceToPlain(v)) + return instanceToPlain(value) + } + // PLAIN_TO_PLAIN + return value + }) +} diff --git a/packages/core/src/modules/vc/models/W3cCredentialServiceOptions.ts b/packages/core/src/modules/vc/models/W3cCredentialServiceOptions.ts new file mode 100644 index 0000000000..b683677576 --- /dev/null +++ b/packages/core/src/modules/vc/models/W3cCredentialServiceOptions.ts @@ -0,0 +1,57 @@ +import type { JsonObject } from '../../../types' +import type { SingleOrArray } from '../../../utils/type' +import type { ProofPurpose } from '../proof-purposes/ProofPurpose' +import type { W3cCredential } from './credential/W3cCredential' +import type { W3cVerifiableCredential } from './credential/W3cVerifiableCredential' +import type { W3cPresentation } from './presentation/W3Presentation' +import type { W3cVerifiablePresentation } from './presentation/W3cVerifiablePresentation' + +export interface SignCredentialOptions { + credential: W3cCredential + proofType: string + verificationMethod: string + proofPurpose?: ProofPurpose + created?: string + domain?: string + challenge?: string + credentialStatus?: { + type: string + } +} + +export interface VerifyCredentialOptions { + credential: W3cVerifiableCredential + proofPurpose?: ProofPurpose +} + +export interface StoreCredentialOptions { + record: W3cVerifiableCredential +} + +export interface CreatePresentationOptions { + credentials: SingleOrArray + id?: string + holderUrl?: string +} + +export interface SignPresentationOptions { + presentation: W3cPresentation + signatureType: string + purpose: ProofPurpose + verificationMethod: string + challenge: string +} + +export interface VerifyPresentationOptions { + presentation: W3cVerifiablePresentation + proofType: string + verificationMethod: string + purpose?: ProofPurpose + challenge?: string +} + +export interface DeriveProofOptions { + credential: W3cVerifiableCredential + revealDocument: JsonObject + verificationMethod: string +} diff --git a/packages/core/src/modules/vc/models/credential/CredentialSchema.ts b/packages/core/src/modules/vc/models/credential/CredentialSchema.ts new file mode 100644 index 0000000000..c98ba30fea --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/CredentialSchema.ts @@ -0,0 +1,23 @@ +import { IsString } from 'class-validator' + +import { IsUri } from '../../../../utils/validators' + +export interface CredentialSchemaOptions { + id: string + type: string +} + +export class CredentialSchema { + public constructor(options: CredentialSchemaOptions) { + if (options) { + this.id = options.id + this.type = options.type + } + } + + @IsUri() + public id!: string + + @IsString() + public type!: string +} diff --git a/packages/core/src/modules/vc/models/credential/CredentialSubject.ts b/packages/core/src/modules/vc/models/credential/CredentialSubject.ts new file mode 100644 index 0000000000..7462a68066 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/CredentialSubject.ts @@ -0,0 +1,40 @@ +import { Transform, TransformationType, plainToInstance, instanceToPlain } from 'class-transformer' +import { isString } from 'class-validator' + +import { IsUri } from '../../../../utils/validators' + +/** + * TODO: check how to support arbitrary data in class + * @see https://www.w3.org/TR/vc-data-model/#credential-subject + */ + +export interface CredentialSubjectOptions { + id: string +} + +export class CredentialSubject { + public constructor(options: CredentialSubjectOptions) { + if (options) { + this.id = options.id + } + } + + @IsUri() + public id!: string +} + +// Custom transformers + +export function CredentialSubjectTransformer() { + return Transform(({ value, type }: { value: string | CredentialSubjectOptions; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + if (isString(value)) return value + return plainToInstance(CredentialSubject, value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + if (isString(value)) return value + return instanceToPlain(value) + } + // PLAIN_TO_PLAIN + return value + }) +} diff --git a/packages/core/src/modules/vc/models/credential/Issuer.ts b/packages/core/src/modules/vc/models/credential/Issuer.ts new file mode 100644 index 0000000000..6eb4359087 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/Issuer.ts @@ -0,0 +1,68 @@ +import type { ValidationOptions } from 'class-validator' + +import { Transform, TransformationType, plainToInstance, instanceToPlain } from 'class-transformer' +import { buildMessage, isInstance, isString, ValidateBy } from 'class-validator' + +import { IsUri, UriValidator } from '../../../../utils/validators' + +/** + * TODO: check how to support arbitrary data in class + * @see https://www.w3.org/TR/vc-data-model/#credential-subject + */ + +export interface IssuerOptions { + id: string +} + +export class Issuer { + public constructor(options: IssuerOptions) { + if (options) { + this.id = options.id + } + } + + @IsUri() + public id!: string +} + +// Custom transformers + +export function IssuerTransformer() { + return Transform(({ value, type }: { value: string | IssuerOptions; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + if (isString(value)) return value + return plainToInstance(Issuer, value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + if (isString(value)) return value + return instanceToPlain(value) + } + // PLAIN_TO_PLAIN + return value + }) +} + +// Custom validators + +export function IsIssuer(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsIssuer', + validator: { + validate: (value): boolean => { + if (typeof value === 'string') { + return UriValidator.test(value) + } + if (isInstance(value, Issuer)) { + return UriValidator.test(value.id) + } + return false + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be a string or an object with an id property', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/vc/models/credential/W3cCredential.ts b/packages/core/src/modules/vc/models/credential/W3cCredential.ts new file mode 100644 index 0000000000..beed50d700 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cCredential.ts @@ -0,0 +1,128 @@ +import type { JsonObject } from '../../../../types' +import type { CredentialSubjectOptions } from './CredentialSubject' +import type { IssuerOptions } from './Issuer' +import type { ValidationOptions } from 'class-validator' + +import { Expose, Type } from 'class-transformer' +import { buildMessage, IsOptional, IsString, ValidateBy } from 'class-validator' + +import { SingleOrArray } from '../../../../utils/type' +import { IsInstanceOrArrayOfInstances, IsUri } from '../../../../utils/validators' +import { CREDENTIALS_CONTEXT_V1_URL, VERIFIABLE_CREDENTIAL_TYPE } from '../../constants' +import { IsJsonLdContext } from '../../validators' + +import { CredentialSchema } from './CredentialSchema' +import { CredentialSubject } from './CredentialSubject' +import { Issuer, IsIssuer, IssuerTransformer } from './Issuer' + +export interface W3cCredentialOptions { + context: Array | JsonObject + id?: string + type: Array + issuer: string | IssuerOptions + issuanceDate: string + expirationDate?: string + credentialSubject: SingleOrArray +} + +export class W3cCredential { + public constructor(options: W3cCredentialOptions) { + if (options) { + this.context = options.context ?? [CREDENTIALS_CONTEXT_V1_URL] + this.id = options.id + this.type = options.type || [] + this.issuer = options.issuer + this.issuanceDate = options.issuanceDate + this.expirationDate = options.expirationDate + this.credentialSubject = options.credentialSubject + } + } + + @Expose({ name: '@context' }) + @IsJsonLdContext() + public context!: Array | JsonObject + + @IsOptional() + @IsUri() + public id?: string + + @IsCredentialType() + public type!: Array + + @IssuerTransformer() + @IsIssuer() + public issuer!: string | Issuer + + @IsString() + public issuanceDate!: string + + @IsString() + @IsOptional() + public expirationDate?: string + + @Type(() => CredentialSubject) + @IsInstanceOrArrayOfInstances({ classType: CredentialSubject }) + public credentialSubject!: SingleOrArray + + @IsOptional() + @Type(() => CredentialSchema) + @IsInstanceOrArrayOfInstances({ classType: CredentialSchema }) + public credentialSchema?: SingleOrArray + + public get issuerId(): string { + return this.issuer instanceof Issuer ? this.issuer.id : this.issuer + } + + public get credentialSchemaIds(): string[] { + if (!this.credentialSchema) return [] + + if (Array.isArray(this.credentialSchema)) { + return this.credentialSchema.map((credentialSchema) => credentialSchema.id) + } + + return [this.credentialSchema.id] + } + + public get credentialSubjectIds(): string[] { + if (Array.isArray(this.credentialSubject)) { + return this.credentialSubject.map((credentialSubject) => credentialSubject.id) + } + + return [this.credentialSubject.id] + } + + public get contexts(): Array { + if (Array.isArray(this.context)) { + return this.context.filter((x) => typeof x === 'string') + } + + if (typeof this.context === 'string') { + return [this.context] + } + + return [this.context.id as string] + } +} + +// Custom validator + +export function IsCredentialType(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsVerifiableCredentialType', + validator: { + validate: (value): boolean => { + if (Array.isArray(value)) { + return value.includes(VERIFIABLE_CREDENTIAL_TYPE) + } + return false + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be an array of strings which includes "VerifiableCredential"', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialRecord.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialRecord.ts new file mode 100644 index 0000000000..a69fa03739 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialRecord.ts @@ -0,0 +1,58 @@ +import type { TagsBase } from '../../../../storage/BaseRecord' + +import { Type } from 'class-transformer' + +import { BaseRecord } from '../../../../storage/BaseRecord' +import { uuid } from '../../../../utils/uuid' + +import { W3cVerifiableCredential } from './W3cVerifiableCredential' + +export interface W3cCredentialRecordOptions { + id?: string + createdAt?: Date + credential: W3cVerifiableCredential + tags: CustomW3cCredentialTags +} + +export type CustomW3cCredentialTags = TagsBase & { + expandedTypes?: Array +} + +export type DefaultCredentialTags = { + issuerId: string + subjectIds: Array + schemaIds: Array + contexts: Array + proofTypes: Array + givenId?: string +} + +export class W3cCredentialRecord extends BaseRecord { + public static readonly type = 'W3cCredentialRecord' + public readonly type = W3cCredentialRecord.type + + @Type(() => W3cVerifiableCredential) + public credential!: W3cVerifiableCredential + + public constructor(props: W3cCredentialRecordOptions) { + super() + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags + this.credential = props.credential + } + } + + public getTags() { + return { + ...this._tags, + issuerId: this.credential.issuerId, + subjectIds: this.credential.credentialSubjectIds, + schemaIds: this.credential.credentialSchemaIds, + contexts: this.credential.contexts, + proofTypes: this.credential.proofTypes, + givedId: this.credential.id, + } + } +} diff --git a/packages/core/src/modules/vc/models/credential/W3cCredentialRepository.ts b/packages/core/src/modules/vc/models/credential/W3cCredentialRepository.ts new file mode 100644 index 0000000000..baf11e4dba --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cCredentialRepository.ts @@ -0,0 +1,14 @@ +import { inject, scoped, Lifecycle } from 'tsyringe' + +import { InjectionSymbols } from '../../../../constants' +import { Repository } from '../../../../storage/Repository' +import { StorageService } from '../../../../storage/StorageService' + +import { W3cCredentialRecord } from './W3cCredentialRecord' + +@scoped(Lifecycle.ContainerScoped) +export class W3cCredentialRepository extends Repository { + public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { + super(W3cCredentialRecord, storageService) + } +} diff --git a/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts b/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts new file mode 100644 index 0000000000..e2d1b1afe3 --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts @@ -0,0 +1,53 @@ +import type { LinkedDataProofOptions } from '../LinkedDataProof' +import type { W3cCredentialOptions } from './W3cCredential' + +import { instanceToPlain, plainToInstance, Transform, TransformationType } from 'class-transformer' + +import { orArrayToArray } from '../../../../utils' +import { SingleOrArray } from '../../../../utils/type' +import { IsInstanceOrArrayOfInstances } from '../../../../utils/validators' +import { LinkedDataProof, LinkedDataProofTransformer } from '../LinkedDataProof' + +import { W3cCredential } from './W3cCredential' + +export interface W3cVerifiableCredentialOptions extends W3cCredentialOptions { + proof: SingleOrArray +} + +export class W3cVerifiableCredential extends W3cCredential { + public constructor(options: W3cVerifiableCredentialOptions) { + super(options) + if (options) { + this.proof = Array.isArray(options.proof) + ? options.proof.map((proof) => new LinkedDataProof(proof)) + : new LinkedDataProof(options.proof) + } + } + + @LinkedDataProofTransformer() + @IsInstanceOrArrayOfInstances({ classType: LinkedDataProof }) + public proof!: SingleOrArray + + public get proofTypes(): Array { + const proofArray = orArrayToArray(this.proof) + return proofArray?.map((x) => x.type) ?? [] + } +} + +// Custom transformers + +export function VerifiableCredentialTransformer() { + return Transform( + ({ value, type }: { value: SingleOrArray; type: TransformationType }) => { + if (type === TransformationType.PLAIN_TO_CLASS) { + if (Array.isArray(value)) return value.map((v) => plainToInstance(W3cVerifiableCredential, v)) + return plainToInstance(W3cVerifiableCredential, value) + } else if (type === TransformationType.CLASS_TO_PLAIN) { + if (Array.isArray(value)) return value.map((v) => instanceToPlain(v)) + return instanceToPlain(value) + } + // PLAIN_TO_PLAIN + return value + } + ) +} diff --git a/packages/core/src/modules/vc/models/credential/W3cVerifyCredentialResult.ts b/packages/core/src/modules/vc/models/credential/W3cVerifyCredentialResult.ts new file mode 100644 index 0000000000..9f8880467a --- /dev/null +++ b/packages/core/src/modules/vc/models/credential/W3cVerifyCredentialResult.ts @@ -0,0 +1,15 @@ +import type { JsonObject } from '../../../../types' +import type { W3cVerifiableCredential } from './W3cVerifiableCredential' + +export interface VerifyCredentialResult { + credential: W3cVerifiableCredential + verified: boolean + error?: Error +} + +export interface W3cVerifyCredentialResult { + verified: boolean + statusResult: JsonObject + results: Array + error?: Error +} diff --git a/packages/core/src/modules/vc/models/index.ts b/packages/core/src/modules/vc/models/index.ts new file mode 100644 index 0000000000..37c71ef25d --- /dev/null +++ b/packages/core/src/modules/vc/models/index.ts @@ -0,0 +1,3 @@ +export * from './credential/W3cCredential' +export * from './credential/W3cVerifiableCredential' +export * from './credential/W3cVerifyCredentialResult' diff --git a/packages/core/src/modules/vc/models/presentation/VerifyPresentationResult.ts b/packages/core/src/modules/vc/models/presentation/VerifyPresentationResult.ts new file mode 100644 index 0000000000..41a9041e04 --- /dev/null +++ b/packages/core/src/modules/vc/models/presentation/VerifyPresentationResult.ts @@ -0,0 +1,9 @@ +import type { JsonObject } from '../../../../types' +import type { VerifyCredentialResult } from '../credential/W3cVerifyCredentialResult' + +export interface VerifyPresentationResult { + verified: boolean + presentationResult: JsonObject // the precise interface of this object is still unclear + credentialResults: Array + error?: Error +} diff --git a/packages/core/src/modules/vc/models/presentation/W3Presentation.ts b/packages/core/src/modules/vc/models/presentation/W3Presentation.ts new file mode 100644 index 0000000000..1ee6ae677f --- /dev/null +++ b/packages/core/src/modules/vc/models/presentation/W3Presentation.ts @@ -0,0 +1,77 @@ +import type { JsonObject } from '../../../../types' +import type { W3cVerifiableCredentialOptions } from '../credential/W3cVerifiableCredential' +import type { ValidationOptions } from 'class-validator' + +import { Expose } from 'class-transformer' +import { buildMessage, IsOptional, IsString, ValidateBy } from 'class-validator' + +import { SingleOrArray } from '../../../../utils/type' +import { IsUri, IsInstanceOrArrayOfInstances } from '../../../../utils/validators' +import { VERIFIABLE_PRESENTATION_TYPE } from '../../constants' +import { IsJsonLdContext } from '../../validators' +import { VerifiableCredentialTransformer, W3cVerifiableCredential } from '../credential/W3cVerifiableCredential' + +export interface W3cPresentationOptions { + id?: string + context: Array | JsonObject + verifiableCredential: SingleOrArray + type: Array + holder?: string +} + +export class W3cPresentation { + public constructor(options: W3cPresentationOptions) { + if (options) { + this.id = options.id + this.context = options.context + this.type = options.type + this.verifiableCredential = Array.isArray(options.verifiableCredential) + ? options.verifiableCredential.map((vc) => new W3cVerifiableCredential(vc)) + : new W3cVerifiableCredential(options.verifiableCredential) + this.holder = options.holder + } + } + + @Expose({ name: '@context' }) + @IsJsonLdContext() + public context!: Array | JsonObject + + @IsOptional() + @IsUri() + public id?: string + + @IsVerifiablePresentationType() + public type!: Array + + @IsOptional() + @IsString() + @IsUri() + public holder?: string + + @VerifiableCredentialTransformer() + @IsInstanceOrArrayOfInstances({ classType: W3cVerifiableCredential }) + public verifiableCredential!: SingleOrArray +} + +// Custom validators + +export function IsVerifiablePresentationType(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsVerifiablePresentationType', + validator: { + validate: (value): boolean => { + if (Array.isArray(value)) { + return value.includes(VERIFIABLE_PRESENTATION_TYPE) + } + return false + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be an array of strings which includes "VerifiablePresentation"', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts new file mode 100644 index 0000000000..2645fb9cc5 --- /dev/null +++ b/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts @@ -0,0 +1,25 @@ +import type { LinkedDataProofOptions } from '../LinkedDataProof' +import type { W3cPresentationOptions } from './W3Presentation' + +import { SingleOrArray } from '../../../../utils/type' +import { IsInstanceOrArrayOfInstances } from '../../../../utils/validators' +import { LinkedDataProof, LinkedDataProofTransformer } from '../LinkedDataProof' + +import { W3cPresentation } from './W3Presentation' + +export interface W3cVerifiablePresentationOptions extends W3cPresentationOptions { + proof: LinkedDataProofOptions +} + +export class W3cVerifiablePresentation extends W3cPresentation { + public constructor(options: W3cVerifiablePresentationOptions) { + super(options) + if (options) { + this.proof = new LinkedDataProof(options.proof) + } + } + + @LinkedDataProofTransformer() + @IsInstanceOrArrayOfInstances({ classType: LinkedDataProof }) + public proof!: SingleOrArray +} diff --git a/packages/core/src/modules/vc/proof-purposes/CredentialIssuancePurpose.ts b/packages/core/src/modules/vc/proof-purposes/CredentialIssuancePurpose.ts new file mode 100644 index 0000000000..c4127374e7 --- /dev/null +++ b/packages/core/src/modules/vc/proof-purposes/CredentialIssuancePurpose.ts @@ -0,0 +1,89 @@ +import type { JsonObject } from '../../../types' +import type { DocumentLoader, Proof } from '../../../utils' + +import jsonld from '../../../../types/jsonld' +import { suites, purposes } from '../../../../types/jsonld-signatures' + +const AssertionProofPurpose = purposes.AssertionProofPurpose +const LinkedDataProof = suites.LinkedDataProof +/** + * Creates a proof purpose that will validate whether or not the verification + * method in a proof was authorized by its declared controller for the + * proof's purpose. + */ +export class CredentialIssuancePurpose extends AssertionProofPurpose { + /** + * @param {object} options - The options to use. + * @param {object} [options.controller] - The description of the controller, + * if it is not to be dereferenced via a `documentLoader`. + * @param {string|Date|number} [options.date] - The expected date for + * the creation of the proof. + * @param {number} [options.maxTimestampDelta=Infinity] - A maximum number + * of seconds that the date on the signature can deviate from. + */ + public constructor(options: { controller?: Record; date: string; maxTimestampDelta?: number }) { + options.maxTimestampDelta = options.maxTimestampDelta || Infinity + super(options) + } + + /** + * Validates the purpose of a proof. This method is called during + * proof verification, after the proof value has been checked against the + * given verification method (in the case of a digital signature, the + * signature has been cryptographically verified against the public key). + * + * @param {object} proof - The proof to validate. + * @param {object} options - The options to use. + * @param {object} options.document - The document whose signature is + * being verified. + * @param {object} options.suite - Signature suite used in + * the proof. + * @param {string} options.verificationMethod - Key id URL to the paired + * public key. + * @param {object} [options.documentLoader] - A document loader. + * @param {object} [options.expansionMap] - An expansion map. + * + * @throws {Error} If verification method not authorized by controller. + * @throws {Error} If proof's created timestamp is out of range. + * + * @returns {Promise<{valid: boolean, error: Error}>} Resolves on completion. + */ + public async validate( + proof: Proof, + options?: { + document: JsonObject + suite: typeof LinkedDataProof + verificationMethod: string + documentLoader?: DocumentLoader + expansionMap?: () => void + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise<{ valid: boolean; error?: any }> { + try { + const result = await super.validate(proof, options) + + if (!result.valid) { + throw result.error + } + + // This @ts-ignore is necessary because the .getValues() method is not part of the public API. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const issuer = jsonld.util.getValues(options.document, 'issuer') + + if (!issuer || issuer.length === 0) { + throw new Error('Credential issuer is required.') + } + + const issuerId = typeof issuer[0] === 'string' ? issuer[0] : issuer[0].id + + if (result.controller.id !== issuerId) { + throw new Error('Credential issuer must match the verification method controller.') + } + + return { valid: true } + } catch (error) { + return { valid: false, error } + } + } +} diff --git a/packages/core/src/modules/vc/proof-purposes/ProofPurpose.ts b/packages/core/src/modules/vc/proof-purposes/ProofPurpose.ts new file mode 100644 index 0000000000..2695f3276c --- /dev/null +++ b/packages/core/src/modules/vc/proof-purposes/ProofPurpose.ts @@ -0,0 +1 @@ +export type ProofPurpose = any diff --git a/packages/core/src/modules/vc/validators.ts b/packages/core/src/modules/vc/validators.ts new file mode 100644 index 0000000000..0bce78fa79 --- /dev/null +++ b/packages/core/src/modules/vc/validators.ts @@ -0,0 +1,32 @@ +import type { ValidationOptions } from 'class-validator' + +import { buildMessage, isString, isURL, ValidateBy } from 'class-validator' + +import { CREDENTIALS_CONTEXT_V1_URL } from './constants' + +export function IsJsonLdContext(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'IsJsonLdContext', + validator: { + validate: (value): boolean => { + // If value is an array, check if all items are strings, are URLs and that + // the first entry is a verifiable credential context + if (Array.isArray(value)) { + return value.every((v) => isString(v) && isURL(v)) && value[0] === CREDENTIALS_CONTEXT_V1_URL + } + // If value is not an array, check if it is an object (assuming it's a JSON-LD context definition) + if (typeof value === 'object') { + return true + } + return false + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be an array of strings or a JSON-LD context definition', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/storage/BaseRecord.ts b/packages/core/src/storage/BaseRecord.ts index 6532b0c362..6ff450c2a5 100644 --- a/packages/core/src/storage/BaseRecord.ts +++ b/packages/core/src/storage/BaseRecord.ts @@ -1,4 +1,4 @@ -import { Exclude, Type } from 'class-transformer' +import { Exclude, Transform, TransformationType } from 'class-transformer' import { JsonTransformer } from '../utils/JsonTransformer' import { MetadataTransformer } from '../utils/transformers' @@ -15,15 +15,23 @@ export type Tags = Cu export type RecordTags = ReturnType -export abstract class BaseRecord { +export abstract class BaseRecord< + DefaultTags extends TagsBase = TagsBase, + CustomTags extends TagsBase = TagsBase, + MetadataValues = undefined +> { protected _tags: CustomTags = {} as CustomTags public id!: string - @Type(() => Date) + @Transform(({ value, type }) => + type === TransformationType.CLASS_TO_PLAIN ? value.toISOString(value) : new Date(value) + ) public createdAt!: Date - @Type(() => Date) + @Transform(({ value, type }) => + type === TransformationType.CLASS_TO_PLAIN ? value.toISOString(value) : new Date(value) + ) public updatedAt?: Date @Exclude() @@ -32,7 +40,7 @@ export abstract class BaseRecord = new Metadata({}) /** * Get all tags. This is includes custom and default tags diff --git a/packages/core/src/storage/FileSystem.ts b/packages/core/src/storage/FileSystem.ts index 7fdeb53c52..6673bc333c 100644 --- a/packages/core/src/storage/FileSystem.ts +++ b/packages/core/src/storage/FileSystem.ts @@ -4,4 +4,5 @@ export interface FileSystem { exists(path: string): Promise write(path: string, data: string): Promise read(path: string): Promise + downloadToFile(url: string, path: string): Promise } diff --git a/packages/core/src/storage/InMemoryMessageRepository.ts b/packages/core/src/storage/InMemoryMessageRepository.ts index cde7f94a52..28496065f0 100644 --- a/packages/core/src/storage/InMemoryMessageRepository.ts +++ b/packages/core/src/storage/InMemoryMessageRepository.ts @@ -1,5 +1,5 @@ import type { Logger } from '../logger' -import type { WireMessage } from '../types' +import type { EncryptedMessage } from '../types' import type { MessageRepository } from './MessageRepository' import { Lifecycle, scoped } from 'tsyringe' @@ -9,7 +9,7 @@ import { AgentConfig } from '../agent/AgentConfig' @scoped(Lifecycle.ContainerScoped) export class InMemoryMessageRepository implements MessageRepository { private logger: Logger - private messages: { [key: string]: WireMessage[] } = {} + private messages: { [key: string]: EncryptedMessage[] } = {} public constructor(agentConfig: AgentConfig) { this.logger = agentConfig.logger @@ -26,7 +26,7 @@ export class InMemoryMessageRepository implements MessageRepository { return this.messages[connectionId].splice(0, messagesToTake) } - public add(connectionId: string, payload: WireMessage) { + public add(connectionId: string, payload: EncryptedMessage) { if (!this.messages[connectionId]) { this.messages[connectionId] = [] } diff --git a/packages/core/src/storage/IndyStorageService.ts b/packages/core/src/storage/IndyStorageService.ts index c976f18c35..469f6b1c3e 100644 --- a/packages/core/src/storage/IndyStorageService.ts +++ b/packages/core/src/storage/IndyStorageService.ts @@ -32,8 +32,8 @@ export class IndyStorageService implements StorageService< for (const [key, value] of Object.entries(tags)) { // If the value is a boolean string ('1' or '0') // use the boolean val - if (value === '1' && value?.includes(':')) { - const [tagName, tagValue] = value.split(':') + if (value === '1' && key?.includes(':')) { + const [tagName, tagValue] = key.split(':') const transformedValue = transformedTags[tagName] @@ -42,9 +42,16 @@ export class IndyStorageService implements StorageService< } else { transformedTags[tagName] = [tagValue] } - } else if (value === '1' || value === '0') { + } + // Transform '1' and '0' to boolean + else if (value === '1' || value === '0') { transformedTags[key] = value === '1' } + // If 1 or 0 is prefixed with 'n__' we need to remove it. This is to prevent + // casting the value to a boolean + else if (value === 'n__1' || value === 'n__0') { + transformedTags[key] = value === 'n__1' ? '1' : '0' + } // Otherwise just use the value else { transformedTags[key] = value @@ -63,6 +70,11 @@ export class IndyStorageService implements StorageService< if (isBoolean(value)) { transformedTags[key] = value ? '1' : '0' } + // If the value is 1 or 0, we need to add something to the value, otherwise + // the next time we deserialize the tag values it will be converted to boolean + else if (value === '1' || value === '0') { + transformedTags[key] = `n__${value}` + } // If the value is an array we create a tag for each array // item ("tagName:arrayItem" = "1") else if (Array.isArray(value)) { @@ -94,7 +106,6 @@ export class IndyStorageService implements StorageService< /** @inheritDoc */ public async save(record: T) { const value = JsonTransformer.serialize(record) - // FIXME: update @types/indy-sdk to be of type Record const tags = this.transformFromRecordTagValues(record.getTags()) as Record try { @@ -112,7 +123,6 @@ export class IndyStorageService implements StorageService< /** @inheritDoc */ public async update(record: T): Promise { const value = JsonTransformer.serialize(record) - // FIXME: update @types/indy-sdk to be of type Record const tags = this.transformFromRecordTagValues(record.getTags()) as Record try { @@ -213,7 +223,6 @@ export class IndyStorageService implements StorageService< // Retrieve records const recordsJson = await this.indy.fetchWalletSearchNextRecords(this.wallet.handle, searchHandle, chunk) - // FIXME: update @types/indy-sdk: records can be null (if last reached) if (recordsJson.records) { records = [...records, ...recordsJson.records] @@ -224,14 +233,14 @@ export class IndyStorageService implements StorageService< // If the number of records returned is less than chunk // It means we reached the end of the iterator (no more records) - if (!records.length || recordsJson.records.length < chunk) { + if (!records.length || !recordsJson.records || recordsJson.records.length < chunk) { await this.indy.closeWalletSearch(searchHandle) return } } } catch (error) { - throw new IndySdkError(error) + throw new IndySdkError(error, `Searching '${type}' records for query '${JSON.stringify(query)}' failed`) } } } diff --git a/packages/core/src/storage/MessageRepository.ts b/packages/core/src/storage/MessageRepository.ts index ec89b71b7c..e56830bc4a 100644 --- a/packages/core/src/storage/MessageRepository.ts +++ b/packages/core/src/storage/MessageRepository.ts @@ -1,6 +1,6 @@ -import type { WireMessage } from '../types' +import type { EncryptedMessage } from '../types' export interface MessageRepository { - takeFromQueue(connectionId: string, limit?: number): WireMessage[] - add(connectionId: string, payload: WireMessage): void + takeFromQueue(connectionId: string, limit?: number): EncryptedMessage[] + add(connectionId: string, payload: EncryptedMessage): void } diff --git a/packages/core/src/storage/Metadata.ts b/packages/core/src/storage/Metadata.ts index a4914782cb..87c3e0d298 100644 --- a/packages/core/src/storage/Metadata.ts +++ b/packages/core/src/storage/Metadata.ts @@ -3,22 +3,19 @@ export type MetadataBase = { } /** - * Metadata access class to get, set (create and update) and delete - * metadata on any record. + * Metadata access class to get, set (create and update), add (append to a record) and delete metadata on any record. * * set will override the previous value if it already exists * - * note: To add persistence to these records, you have to - * update the record in the correct repository + * note: To add persistence to these records, you have to update the record in the correct repository * * @example * * ```ts - * connectionRecord.metadata.set('foo', { bar: 'baz' }) - * connectionRepository.update(connectionRecord) + * connectionRecord.metadata.set('foo', { bar: 'baz' }) connectionRepository.update(connectionRecord) * ``` */ -export class Metadata { +export class Metadata { public readonly data: MetadataBase public constructor(data: MetadataBase) { @@ -28,22 +25,29 @@ export class Metadata { /** * Gets the value by key in the metadata * + * Any extension of the `BaseRecord` can implement their own typed metadata + * * @param key the key to retrieve the metadata by * @returns the value saved in the key value pair * @returns null when the key could not be found */ - public get>(key: string): T | null { - return (this.data[key] as T) ?? null + public get, Key extends string = string>( + key: Key + ): (Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value) | null { + return (this.data[key] as Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value) ?? null } /** - * Will set, or override, a key value pair on the metadata + * Will set, or override, a key-value pair on the metadata * * @param key the key to set the metadata by * @param value the value to set in the metadata */ - public set(key: string, value: Record): void { - this.data[key] = value + public set, Key extends string = string>( + key: Key, + value: Key extends keyof MetadataTypes ? MetadataTypes[Key] : Value + ): void { + this.data[key] = value as Record } /** @@ -52,7 +56,10 @@ export class Metadata { * @param key the key to add the metadata at * @param value the value to add in the metadata */ - public add(key: string, value: Record): void { + public add, Key extends string = string>( + key: Key, + value: Partial + ): void { this.data[key] = { ...this.data[key], ...value, @@ -64,8 +71,8 @@ export class Metadata { * * @returns all the metadata that exists on the record */ - public getAll(): MetadataBase { - return this.data + public get keys(): string[] { + return Object.keys(this.data) } /** @@ -73,7 +80,7 @@ export class Metadata { * * @param key the key to delete the data by */ - public delete(key: string): void { + public delete(key: Key): void { delete this.data[key] } } diff --git a/packages/core/src/storage/Repository.ts b/packages/core/src/storage/Repository.ts index 445cb1bad3..bc4266bf7e 100644 --- a/packages/core/src/storage/Repository.ts +++ b/packages/core/src/storage/Repository.ts @@ -4,7 +4,7 @@ import type { BaseRecordConstructor, Query, StorageService } from './StorageServ import { RecordDuplicateError, RecordNotFoundError } from '../error' // eslint-disable-next-line @typescript-eslint/no-explicit-any -export class Repository> { +export class Repository> { private storageService: StorageService private recordClass: BaseRecordConstructor diff --git a/packages/core/src/storage/StorageService.ts b/packages/core/src/storage/StorageService.ts index 93a3cdf5a9..c9b3e8bfee 100644 --- a/packages/core/src/storage/StorageService.ts +++ b/packages/core/src/storage/StorageService.ts @@ -8,7 +8,7 @@ export interface BaseRecordConstructor extends Constructor { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface StorageService> { +export interface StorageService> { /** * Save record in storage * diff --git a/packages/core/src/storage/__tests__/DidCommMessageRecord.test.ts b/packages/core/src/storage/__tests__/DidCommMessageRecord.test.ts new file mode 100644 index 0000000000..30198e7f00 --- /dev/null +++ b/packages/core/src/storage/__tests__/DidCommMessageRecord.test.ts @@ -0,0 +1,55 @@ +import { ConnectionInvitationMessage } from '../../modules/connections' +import { DidCommMessageRecord, DidCommMessageRole } from '../didcomm' + +describe('DidCommMessageRecord', () => { + it('correctly computes message type tags', () => { + const didCommMessage = { + '@id': '7eb74118-7f91-4ba9-9960-c709b036aa86', + '@type': 'https://didcomm.org/test-protocol/1.0/send-test', + some: { other: 'property' }, + '~thread': { + thid: 'ea24e14a-4fc4-40f4-85a0-f6fcf02bfc1c', + }, + } + + const didCommeMessageRecord = new DidCommMessageRecord({ + message: didCommMessage, + role: DidCommMessageRole.Receiver, + associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0', + }) + + expect(didCommeMessageRecord.getTags()).toEqual({ + role: DidCommMessageRole.Receiver, + associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0', + + // Computed properties based on message id and type + threadId: 'ea24e14a-4fc4-40f4-85a0-f6fcf02bfc1c', + protocolName: 'test-protocol', + messageName: 'send-test', + protocolMajorVersion: '1', + protocolMinorVersion: '0', + messageType: 'https://didcomm.org/test-protocol/1.0/send-test', + messageId: '7eb74118-7f91-4ba9-9960-c709b036aa86', + }) + }) + + it('correctly returns a message class instance', () => { + const invitationJson = { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], + serviceEndpoint: 'https://example.com', + label: 'test', + } + + const didCommeMessageRecord = new DidCommMessageRecord({ + message: invitationJson, + role: DidCommMessageRole.Receiver, + associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0', + }) + + const invitation = didCommeMessageRecord.getMessageInstance(ConnectionInvitationMessage) + + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) + }) +}) diff --git a/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts b/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts new file mode 100644 index 0000000000..732e1c16fd --- /dev/null +++ b/packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts @@ -0,0 +1,139 @@ +import { mockFunction } from '../../../tests/helpers' +import { ConnectionInvitationMessage } from '../../modules/connections' +import { JsonTransformer } from '../../utils/JsonTransformer' +import { IndyStorageService } from '../IndyStorageService' +import { DidCommMessageRecord, DidCommMessageRepository, DidCommMessageRole } from '../didcomm' + +jest.mock('../IndyStorageService') + +const StorageMock = IndyStorageService as unknown as jest.Mock> + +const invitationJson = { + '@type': 'https://didcomm.org/connections/1.0/invitation', + '@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'], + serviceEndpoint: 'https://example.com', + label: 'test', +} + +describe('Repository', () => { + let repository: DidCommMessageRepository + let storageMock: IndyStorageService + + beforeEach(async () => { + storageMock = new StorageMock() + repository = new DidCommMessageRepository(storageMock) + }) + + const getRecord = ({ id }: { id?: string } = {}) => { + return new DidCommMessageRecord({ + id, + message: invitationJson, + role: DidCommMessageRole.Receiver, + associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0', + }) + } + + describe('getAgentMessage()', () => { + it('should get the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record])) + + const invitation = await repository.findAgentMessage({ + messageClass: ConnectionInvitationMessage, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.findByQuery).toBeCalledWith(DidCommMessageRecord, { + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + messageName: 'invitation', + protocolName: 'connections', + protocolMajorVersion: '1', + }) + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) + }) + }) + describe('findAgentMessage()', () => { + it('should get the record using the storage service', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record])) + + const invitation = await repository.findAgentMessage({ + messageClass: ConnectionInvitationMessage, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.findByQuery).toBeCalledWith(DidCommMessageRecord, { + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + messageName: 'invitation', + protocolName: 'connections', + protocolMajorVersion: '1', + }) + expect(invitation).toBeInstanceOf(ConnectionInvitationMessage) + }) + it("should return null because the record doesn't exist", async () => { + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([])) + + const invitation = await repository.findAgentMessage({ + messageClass: ConnectionInvitationMessage, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.findByQuery).toBeCalledWith(DidCommMessageRecord, { + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + messageName: 'invitation', + protocolName: 'connections', + protocolMajorVersion: '1', + }) + expect(invitation).toBeNull() + }) + }) + + describe('saveAgentMessage()', () => { + it('should transform and save the agent message', async () => { + await repository.saveAgentMessage({ + role: DidCommMessageRole.Receiver, + agentMessage: JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage), + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.save).toBeCalledWith( + expect.objectContaining({ + role: DidCommMessageRole.Receiver, + message: invitationJson, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + ) + }) + }) + + describe('saveOrUpdateAgentMessage()', () => { + it('should transform and save the agent message', async () => { + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([])) + await repository.saveOrUpdateAgentMessage({ + role: DidCommMessageRole.Receiver, + agentMessage: JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage), + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.save).toBeCalledWith( + expect.objectContaining({ + role: DidCommMessageRole.Receiver, + message: invitationJson, + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + ) + }) + it('should transform and update the agent message', async () => { + const record = getRecord({ id: 'test-id' }) + mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record])) + await repository.saveOrUpdateAgentMessage({ + role: DidCommMessageRole.Receiver, + agentMessage: JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage), + associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', + }) + + expect(storageMock.update).toBeCalledWith(record) + }) + }) +}) diff --git a/packages/core/src/storage/__tests__/IndyStorageService.test.ts b/packages/core/src/storage/__tests__/IndyStorageService.test.ts index 921ff9c90f..378f8a655c 100644 --- a/packages/core/src/storage/__tests__/IndyStorageService.test.ts +++ b/packages/core/src/storage/__tests__/IndyStorageService.test.ts @@ -18,7 +18,7 @@ describe('IndyStorageService', () => { indy = config.agentDependencies.indy wallet = new IndyWallet(config) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await wallet.initialize(config.walletConfig!) + await wallet.createAndOpen(config.walletConfig!) storageService = new IndyStorageService(wallet, config) }) @@ -45,6 +45,10 @@ describe('IndyStorageService', () => { someBoolean: true, someOtherBoolean: false, someStringValue: 'string', + anArrayValue: ['foo', 'bar'], + // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' + someStringNumberValue: '1', + anotherStringNumberValue: '0', }, }) @@ -57,6 +61,10 @@ describe('IndyStorageService', () => { someBoolean: '1', someOtherBoolean: '0', someStringValue: 'string', + 'anArrayValue:foo': '1', + 'anArrayValue:bar': '1', + someStringNumberValue: 'n__1', + anotherStringNumberValue: 'n__0', }) }) @@ -65,6 +73,11 @@ describe('IndyStorageService', () => { someBoolean: '1', someOtherBoolean: '0', someStringValue: 'string', + 'anArrayValue:foo': '1', + 'anArrayValue:bar': '1', + // booleans are stored as '1' and '0' so we store the string values '1' and '0' as 'n__1' and 'n__0' + someStringNumberValue: 'n__1', + anotherStringNumberValue: 'n__0', }) const record = await storageService.getById(TestRecord, 'some-id') @@ -73,6 +86,9 @@ describe('IndyStorageService', () => { someBoolean: true, someOtherBoolean: false, someStringValue: 'string', + anArrayValue: expect.arrayContaining(['bar', 'foo']), + someStringNumberValue: '1', + anotherStringNumberValue: '0', }) }) }) diff --git a/packages/core/src/storage/__tests__/Metadata.test.ts b/packages/core/src/storage/__tests__/Metadata.test.ts index f61317ce37..847f6be318 100644 --- a/packages/core/src/storage/__tests__/Metadata.test.ts +++ b/packages/core/src/storage/__tests__/Metadata.test.ts @@ -1,7 +1,11 @@ import { TestRecord } from './TestRecord' describe('Metadata', () => { - const testRecord = new TestRecord() + let testRecord: TestRecord + + beforeEach(() => { + testRecord = new TestRecord() + }) test('set() as create', () => { testRecord.metadata.set('bar', { aries: { framework: 'javascript' } }) @@ -12,12 +16,12 @@ describe('Metadata', () => { }) test('set() as update ', () => { + testRecord.metadata.set('bar', { baz: 'abc' }) expect(testRecord.toJSON()).toMatchObject({ - metadata: { bar: { aries: { framework: 'javascript' } } }, + metadata: { bar: { baz: 'abc' } }, }) testRecord.metadata.set('bar', { baz: 'foo' }) - expect(testRecord.toJSON()).toMatchObject({ metadata: { bar: { baz: 'foo' } }, }) @@ -33,12 +37,14 @@ describe('Metadata', () => { }) test('get()', () => { + testRecord.metadata.set('bar', { baz: 'foo' }) const record = testRecord.metadata.get<{ baz: 'foo' }>('bar') expect(record).toMatchObject({ baz: 'foo' }) }) test('delete()', () => { + testRecord.metadata.set('bar', { baz: 'foo' }) testRecord.metadata.delete('bar') expect(testRecord.toJSON()).toMatchObject({ @@ -46,17 +52,13 @@ describe('Metadata', () => { }) }) - test('getAll()', () => { + test('keys()', () => { testRecord.metadata.set('bar', { baz: 'foo' }) testRecord.metadata.set('bazz', { blub: 'foo' }) testRecord.metadata.set('test', { abc: { def: 'hij' } }) - const record = testRecord.metadata.getAll() + const keys = testRecord.metadata.keys - expect(record).toMatchObject({ - bar: { baz: 'foo' }, - bazz: { blub: 'foo' }, - test: { abc: { def: 'hij' } }, - }) + expect(keys).toMatchObject(['bar', 'bazz', 'test']) }) }) diff --git a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts new file mode 100644 index 0000000000..e7c28a84a8 --- /dev/null +++ b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts @@ -0,0 +1,101 @@ +import type { ConstructableAgentMessage } from '../../agent/AgentMessage' +import type { JsonObject } from '../../types' +import type { DidCommMessageRole } from './DidCommMessageRole' + +import { AriesFrameworkError } from '../../error' +import { JsonTransformer } from '../../utils/JsonTransformer' +import { canHandleMessageType, parseMessageType } from '../../utils/messageType' +import { isJsonObject } from '../../utils/type' +import { uuid } from '../../utils/uuid' +import { BaseRecord } from '../BaseRecord' + +export type DefaultDidCommMessageTags = { + role: DidCommMessageRole + associatedRecordId?: string + + // Computed + protocolName: string + messageName: string + protocolMajorVersion: string + protocolMinorVersion: string + messageType: string + messageId: string + threadId: string +} + +export interface DidCommMessageRecordProps { + role: DidCommMessageRole + message: JsonObject + id?: string + createdAt?: Date + associatedRecordId?: string +} + +export class DidCommMessageRecord extends BaseRecord { + public message!: JsonObject + public role!: DidCommMessageRole + + /** + * The id of the record that is associated with this message record. + * + * E.g. if the connection record wants to store an invitation message + * the associatedRecordId will be the id of the connection record. + */ + public associatedRecordId?: string + + public static readonly type = 'DidCommMessageRecord' + public readonly type = DidCommMessageRecord.type + + public constructor(props: DidCommMessageRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.associatedRecordId = props.associatedRecordId + this.role = props.role + this.message = props.message + } + } + + public getTags() { + const messageId = this.message['@id'] as string + const messageType = this.message['@type'] as string + + const { protocolName, protocolMajorVersion, protocolMinorVersion, messageName } = parseMessageType(messageType) + + const thread = this.message['~thread'] + let threadId = messageId + + if (isJsonObject(thread) && typeof thread.thid === 'string') { + threadId = thread.thid + } + + return { + ...this._tags, + role: this.role, + associatedRecordId: this.associatedRecordId, + + // Computed properties based on message id and type + threadId, + protocolName, + messageName, + protocolMajorVersion: protocolMajorVersion.toString(), + protocolMinorVersion: protocolMinorVersion.toString(), + messageType, + messageId, + } + } + + public getMessageInstance( + messageClass: MessageClass + ): InstanceType { + const messageType = parseMessageType(this.message['@type'] as string) + + if (!canHandleMessageType(messageClass, messageType)) { + throw new AriesFrameworkError('Provided message class type does not match type of stored message') + } + + return JsonTransformer.fromJSON(this.message, messageClass) as InstanceType + } +} diff --git a/packages/core/src/storage/didcomm/DidCommMessageRepository.ts b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts new file mode 100644 index 0000000000..6051145e68 --- /dev/null +++ b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts @@ -0,0 +1,82 @@ +import type { AgentMessage, ConstructableAgentMessage } from '../../agent/AgentMessage' +import type { JsonObject } from '../../types' +import type { DidCommMessageRole } from './DidCommMessageRole' + +import { inject, scoped, Lifecycle } from 'tsyringe' + +import { InjectionSymbols } from '../../constants' +import { Repository } from '../Repository' +import { StorageService } from '../StorageService' + +import { DidCommMessageRecord } from './DidCommMessageRecord' + +@scoped(Lifecycle.ContainerScoped) +export class DidCommMessageRepository extends Repository { + public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { + super(DidCommMessageRecord, storageService) + } + + public async saveAgentMessage({ role, agentMessage, associatedRecordId }: SaveAgentMessageOptions) { + const didCommMessageRecord = new DidCommMessageRecord({ + message: agentMessage.toJSON() as JsonObject, + role, + associatedRecordId, + }) + + await this.save(didCommMessageRecord) + } + + public async saveOrUpdateAgentMessage(options: SaveAgentMessageOptions) { + const record = await this.findSingleByQuery({ + associatedRecordId: options.associatedRecordId, + messageType: options.agentMessage.type, + }) + + if (record) { + record.message = options.agentMessage.toJSON() as JsonObject + record.role = options.role + await this.update(record) + return + } + + await this.saveAgentMessage(options) + } + + public async getAgentMessage({ + associatedRecordId, + messageClass, + }: GetAgentMessageOptions): Promise> { + const record = await this.getSingleByQuery({ + associatedRecordId, + messageName: messageClass.type.messageName, + protocolName: messageClass.type.protocolName, + protocolMajorVersion: String(messageClass.type.protocolMajorVersion), + }) + + return record.getMessageInstance(messageClass) + } + public async findAgentMessage({ + associatedRecordId, + messageClass, + }: GetAgentMessageOptions): Promise | null> { + const record = await this.findSingleByQuery({ + associatedRecordId, + messageName: messageClass.type.messageName, + protocolName: messageClass.type.protocolName, + protocolMajorVersion: String(messageClass.type.protocolMajorVersion), + }) + + return record?.getMessageInstance(messageClass) ?? null + } +} + +export interface SaveAgentMessageOptions { + role: DidCommMessageRole + agentMessage: AgentMessage + associatedRecordId: string +} + +export interface GetAgentMessageOptions { + associatedRecordId: string + messageClass: MessageClass +} diff --git a/packages/core/src/storage/didcomm/DidCommMessageRole.ts b/packages/core/src/storage/didcomm/DidCommMessageRole.ts new file mode 100644 index 0000000000..0404647f76 --- /dev/null +++ b/packages/core/src/storage/didcomm/DidCommMessageRole.ts @@ -0,0 +1,4 @@ +export enum DidCommMessageRole { + Sender = 'sender', + Receiver = 'receiver', +} diff --git a/packages/core/src/storage/didcomm/index.ts b/packages/core/src/storage/didcomm/index.ts new file mode 100644 index 0000000000..a658508c7b --- /dev/null +++ b/packages/core/src/storage/didcomm/index.ts @@ -0,0 +1,3 @@ +export * from './DidCommMessageRecord' +export * from './DidCommMessageRepository' +export * from './DidCommMessageRole' diff --git a/packages/core/src/storage/index.ts b/packages/core/src/storage/index.ts new file mode 100644 index 0000000000..deb5cb0901 --- /dev/null +++ b/packages/core/src/storage/index.ts @@ -0,0 +1,2 @@ +export * from './didcomm' +export * from './migration' diff --git a/packages/core/src/storage/migration/StorageUpdateService.ts b/packages/core/src/storage/migration/StorageUpdateService.ts new file mode 100644 index 0000000000..fa05241d0f --- /dev/null +++ b/packages/core/src/storage/migration/StorageUpdateService.ts @@ -0,0 +1,80 @@ +import type { Logger } from '../../logger' +import type { VersionString } from '../../utils/version' + +import { scoped, Lifecycle } from 'tsyringe' + +import { AgentConfig } from '../../agent/AgentConfig' + +import { StorageVersionRecord } from './repository/StorageVersionRecord' +import { StorageVersionRepository } from './repository/StorageVersionRepository' +import { CURRENT_FRAMEWORK_STORAGE_VERSION, INITIAL_STORAGE_VERSION } from './updates' + +@scoped(Lifecycle.ContainerScoped) +export class StorageUpdateService { + private static STORAGE_VERSION_RECORD_ID = 'STORAGE_VERSION_RECORD_ID' + + private logger: Logger + private storageVersionRepository: StorageVersionRepository + + public constructor(agentConfig: AgentConfig, storageVersionRepository: StorageVersionRepository) { + this.storageVersionRepository = storageVersionRepository + this.logger = agentConfig.logger + } + + public async isUpToDate() { + const currentStorageVersion = await this.getCurrentStorageVersion() + + const isUpToDate = CURRENT_FRAMEWORK_STORAGE_VERSION === currentStorageVersion + + return isUpToDate + } + + public async getCurrentStorageVersion(): Promise { + const storageVersionRecord = await this.getStorageVersionRecord() + + return storageVersionRecord.storageVersion + } + + public async setCurrentStorageVersion(storageVersion: VersionString) { + this.logger.debug(`Setting current agent storage version to ${storageVersion}`) + const storageVersionRecord = await this.storageVersionRepository.findById( + StorageUpdateService.STORAGE_VERSION_RECORD_ID + ) + + if (!storageVersionRecord) { + this.logger.trace('Storage upgrade record does not exist yet. Creating.') + await this.storageVersionRepository.save( + new StorageVersionRecord({ + id: StorageUpdateService.STORAGE_VERSION_RECORD_ID, + storageVersion, + }) + ) + } else { + this.logger.trace('Storage upgrade record already exists. Updating.') + storageVersionRecord.storageVersion = storageVersion + await this.storageVersionRepository.update(storageVersionRecord) + } + } + + /** + * Retrieve the update record, creating it if it doesn't exist already. + * + * The storageVersion will be set to the INITIAL_STORAGE_VERSION if it doesn't exist yet, + * as we can assume the wallet was created before the udpate record existed + */ + public async getStorageVersionRecord() { + let storageVersionRecord = await this.storageVersionRepository.findById( + StorageUpdateService.STORAGE_VERSION_RECORD_ID + ) + + if (!storageVersionRecord) { + storageVersionRecord = new StorageVersionRecord({ + id: StorageUpdateService.STORAGE_VERSION_RECORD_ID, + storageVersion: INITIAL_STORAGE_VERSION, + }) + await this.storageVersionRepository.save(storageVersionRecord) + } + + return storageVersionRecord + } +} diff --git a/packages/core/src/storage/migration/UpdateAssistant.ts b/packages/core/src/storage/migration/UpdateAssistant.ts new file mode 100644 index 0000000000..c0e2891c72 --- /dev/null +++ b/packages/core/src/storage/migration/UpdateAssistant.ts @@ -0,0 +1,173 @@ +import type { Agent } from '../../agent/Agent' +import type { UpdateConfig } from './updates' + +import { AriesFrameworkError } from '../../error' +import { isFirstVersionHigherThanSecond, parseVersionString } from '../../utils/version' +import { WalletError } from '../../wallet/error/WalletError' + +import { StorageUpdateService } from './StorageUpdateService' +import { StorageUpdateError } from './error/StorageUpdateError' +import { CURRENT_FRAMEWORK_STORAGE_VERSION, supportedUpdates } from './updates' + +export class UpdateAssistant { + private agent: Agent + private storageUpdateService: StorageUpdateService + private updateConfig: UpdateConfig + + public constructor(agent: Agent, updateConfig: UpdateConfig) { + this.agent = agent + this.updateConfig = updateConfig + + this.storageUpdateService = this.agent.injectionContainer.resolve(StorageUpdateService) + } + + public async initialize() { + if (this.agent.isInitialized) { + throw new AriesFrameworkError("Can't initialize UpdateAssistant after agent is initialized") + } + + // Initialize the wallet if not already done + if (!this.agent.wallet.isInitialized && this.agent.config.walletConfig) { + await this.agent.wallet.initialize(this.agent.config.walletConfig) + } else if (!this.agent.wallet.isInitialized) { + throw new WalletError( + 'Wallet config has not been set on the agent config. ' + + 'Make sure to initialize the wallet yourself before initializing the update assistant, ' + + 'or provide the required wallet configuration in the agent constructor' + ) + } + } + + public async isUpToDate() { + return this.storageUpdateService.isUpToDate() + } + + public async getCurrentAgentStorageVersion() { + return this.storageUpdateService.getCurrentStorageVersion() + } + + public static get frameworkStorageVersion() { + return CURRENT_FRAMEWORK_STORAGE_VERSION + } + + public async getNeededUpdates() { + const currentStorageVersion = parseVersionString(await this.storageUpdateService.getCurrentStorageVersion()) + + // Filter updates. We don't want older updates we already applied + // or aren't needed because the wallet was created after the update script was made + const neededUpdates = supportedUpdates.filter((update) => { + const toVersion = parseVersionString(update.toVersion) + + // if an update toVersion is higher than currentStorageVersion we want to to include the update + return isFirstVersionHigherThanSecond(toVersion, currentStorageVersion) + }) + + // The current storage version is too old to update + if ( + neededUpdates.length > 0 && + isFirstVersionHigherThanSecond(parseVersionString(neededUpdates[0].fromVersion), currentStorageVersion) + ) { + throw new AriesFrameworkError( + `First fromVersion is higher than current storage version. You need to use an older version of the framework to update to at least version ${neededUpdates[0].fromVersion}` + ) + } + + return neededUpdates + } + + public async update() { + const updateIdentifier = Date.now().toString() + + try { + this.agent.config.logger.info(`Starting update of agent storage with updateIdentifier ${updateIdentifier}`) + const neededUpdates = await this.getNeededUpdates() + + if (neededUpdates.length == 0) { + this.agent.config.logger.info('No update needed. Agent storage is up to date.') + return + } + + const fromVersion = neededUpdates[0].fromVersion + const toVersion = neededUpdates[neededUpdates.length - 1].toVersion + this.agent.config.logger.info( + `Starting update process. Total of ${neededUpdates.length} update(s) will be applied to update the agent storage from version ${fromVersion} to version ${toVersion}` + ) + + // Create backup in case migration goes wrong + await this.createBackup(updateIdentifier) + + try { + for (const update of neededUpdates) { + this.agent.config.logger.info( + `Starting update of agent storage from version ${update.fromVersion} to version ${update.toVersion}` + ) + await update.doUpdate(this.agent, this.updateConfig) + + // Update the framework version in storage + await this.storageUpdateService.setCurrentStorageVersion(update.toVersion) + this.agent.config.logger.info( + `Successfully updated agent storage from version ${update.fromVersion} to version ${update.toVersion}` + ) + } + } catch (error) { + this.agent.config.logger.fatal('An error occurred while updating the wallet. Restoring backup', { + error, + }) + // In the case of an error we want to restore the backup + await this.restoreBackup(updateIdentifier) + + throw error + } + } catch (error) { + this.agent.config.logger.error(`Error updating storage (updateIdentifier: ${updateIdentifier})`, { + cause: error, + }) + + throw new StorageUpdateError(`Error updating storage (updateIdentifier: ${updateIdentifier}): ${error.message}`, { + cause: error, + }) + } + + return updateIdentifier + } + + private getBackupPath(backupIdentifier: string) { + const fileSystem = this.agent.config.fileSystem + return `${fileSystem.basePath}/afj/migration/backup/${backupIdentifier}` + } + + private async createBackup(backupIdentifier: string) { + const backupPath = this.getBackupPath(backupIdentifier) + + const walletKey = this.agent.wallet.walletConfig?.key + if (!walletKey) { + throw new AriesFrameworkError("Could not extract wallet key from wallet module. Can't create backup") + } + + await this.agent.wallet.export({ key: walletKey, path: backupPath }) + this.agent.config.logger.info('Created backup of the wallet', { + backupPath, + }) + } + + private async restoreBackup(backupIdentifier: string) { + const backupPath = this.getBackupPath(backupIdentifier) + + const walletConfig = this.agent.wallet.walletConfig + if (!walletConfig) { + throw new AriesFrameworkError('Could not extract wallet config from wallet module. Cannot restore backup') + } + + // Export and delete current wallet + await this.agent.wallet.export({ key: walletConfig.key, path: `${backupPath}-error` }) + await this.agent.wallet.delete() + + // Import backup + await this.agent.wallet.import(walletConfig, { key: walletConfig.key, path: backupPath }) + await this.agent.wallet.initialize(walletConfig) + + this.agent.config.logger.info(`Successfully restored wallet from backup ${backupIdentifier}`, { + backupPath, + }) + } +} diff --git a/packages/core/src/storage/migration/__tests__/0.1.test.ts b/packages/core/src/storage/migration/__tests__/0.1.test.ts new file mode 100644 index 0000000000..6c6acff475 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/0.1.test.ts @@ -0,0 +1,182 @@ +import type { V0_1ToV0_2UpdateConfig } from '../updates/0.1-0.2' + +import { unlinkSync, readFileSync } from 'fs' +import path from 'path' +import { container as baseContainer } from 'tsyringe' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { Agent } from '../../../../src' +import { agentDependencies } from '../../../../tests/helpers' +import { InjectionSymbols } from '../../../constants' +import { UpdateAssistant } from '../UpdateAssistant' + +const backupDate = new Date('2022-01-21T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) +const backupIdentifier = backupDate.getTime() + +const walletConfig = { + id: `Wallet: 0.1 Update`, + key: `Key: 0.1 Update`, +} + +const mediationRoleUpdateStrategies: V0_1ToV0_2UpdateConfig['mediationRoleUpdateStrategy'][] = [ + 'allMediator', + 'allRecipient', + 'doNotChange', + 'recipientIfEndpoint', +] + +describe('UpdateAssistant | v0.1 - v0.2', () => { + it(`should correctly update the role in the mediation record`, async () => { + const aliceMediationRecordsString = await readFileSync( + path.join(__dirname, '__fixtures__/alice-4-mediators-0.1.json'), + 'utf8' + ) + + for (const mediationRoleUpdateStrategy of mediationRoleUpdateStrategies) { + const container = baseContainer.createChildContainer() + const storageService = new InMemoryStorageService() + container.registerInstance(InjectionSymbols.StorageService, storageService) + + const agent = new Agent( + { + label: 'Test Agent', + walletConfig, + }, + agentDependencies, + container + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy, + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.records = JSON.parse(aliceMediationRecordsString) + + expect(await updateAssistant.getNeededUpdates()).toEqual([ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update() + + expect(await updateAssistant.isUpToDate()).toBe(true) + expect(await updateAssistant.getNeededUpdates()).toEqual([]) + expect(storageService.records).toMatchSnapshot(mediationRoleUpdateStrategy) + + // Need to remove backupFiles after each run so we don't get IOErrors + const backupPath = `${agent.config.fileSystem.basePath}/afj/migration/backup/${backupIdentifier}` + unlinkSync(backupPath) + + await agent.shutdown() + await agent.wallet.delete() + } + }) + + it(`should correctly update the metadata in credential records`, async () => { + const aliceCredentialRecordsString = await readFileSync( + path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), + 'utf8' + ) + + const container = baseContainer.createChildContainer() + const storageService = new InMemoryStorageService() + + container.registerInstance(InjectionSymbols.StorageService, storageService) + + const agent = new Agent( + { + label: 'Test Agent', + walletConfig, + }, + agentDependencies, + container + ) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'doNotChange', + }, + }) + + await updateAssistant.initialize() + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.records = JSON.parse(aliceCredentialRecordsString) + + expect(await updateAssistant.isUpToDate()).toBe(false) + expect(await updateAssistant.getNeededUpdates()).toEqual([ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: expect.any(Function), + }, + ]) + + await updateAssistant.update() + + expect(await updateAssistant.isUpToDate()).toBe(true) + expect(await updateAssistant.getNeededUpdates()).toEqual([]) + expect(storageService.records).toMatchSnapshot() + + // Need to remove backupFiles after each run so we don't get IOErrors + const backupPath = `${agent.config.fileSystem.basePath}/afj/migration/backup/${backupIdentifier}` + unlinkSync(backupPath) + + await agent.shutdown() + await agent.wallet.delete() + }) + + it(`should correctly update the metadata in credential records with auto update`, async () => { + const aliceCredentialRecordsString = await readFileSync( + path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), + 'utf8' + ) + + const container = baseContainer.createChildContainer() + const storageService = new InMemoryStorageService() + + container.registerInstance(InjectionSymbols.StorageService, storageService) + + const agent = new Agent( + { + label: 'Test Agent', + walletConfig, + autoUpdateStorageOnStartup: true, + }, + agentDependencies, + container + ) + + // We need to manually initialize the wallet as we're using the in memory wallet service + // When we call agent.initialize() it will create the wallet and store the current framework + // version in the in memory storage service. We need to manually set the records between initializing + // the wallet and calling agent.initialize() + await agent.wallet.initialize(walletConfig) + + // Set storage after initialization. This mimics as if this wallet + // is opened as an existing wallet instead of a new wallet + storageService.records = JSON.parse(aliceCredentialRecordsString) + + await agent.initialize() + + expect(storageService.records).toMatchSnapshot() + + // Need to remove backupFiles after each run so we don't get IOErrors + const backupPath = `${agent.config.fileSystem.basePath}/afj/migration/backup/${backupIdentifier}` + unlinkSync(backupPath) + + await agent.shutdown() + await agent.wallet.delete() + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts new file mode 100644 index 0000000000..316d78d648 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/UpdateAssistant.test.ts @@ -0,0 +1,73 @@ +import type { BaseRecord } from '../../BaseRecord' +import type { DependencyContainer } from 'tsyringe' + +import { container as baseContainer } from 'tsyringe' + +import { InMemoryStorageService } from '../../../../../../tests/InMemoryStorageService' +import { getBaseConfig } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { InjectionSymbols } from '../../../constants' +import { UpdateAssistant } from '../UpdateAssistant' + +const { agentDependencies, config } = getBaseConfig('UpdateAssistant') + +describe('UpdateAssistant', () => { + let updateAssistant: UpdateAssistant + let agent: Agent + let container: DependencyContainer + let storageService: InMemoryStorageService + + beforeEach(async () => { + container = baseContainer.createChildContainer() + storageService = new InMemoryStorageService() + container.registerInstance(InjectionSymbols.StorageService, storageService) + + agent = new Agent(config, agentDependencies, container) + + updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'allMediator', + }, + }) + + await updateAssistant.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + describe('upgrade()', () => { + it('should not upgrade records when upgrading after a new wallet is created', async () => { + const beforeStorage = JSON.stringify(storageService.records) + await updateAssistant.update() + + expect(JSON.parse(beforeStorage)).toEqual(storageService.records) + }) + }) + + describe('isUpToDate()', () => { + it('should return true when a new wallet is created', async () => { + expect(await updateAssistant.isUpToDate()).toBe(true) + }) + }) + + describe('isUpToDate()', () => { + it('should return true when a new wallet is created', async () => { + expect(await updateAssistant.isUpToDate()).toBe(true) + }) + }) + + describe('UpdateAssistant.frameworkStorageVersion', () => { + it('should return 0.2', async () => { + expect(UpdateAssistant.frameworkStorageVersion).toBe('0.2') + }) + }) + + describe('getCurrentAgentStorageVersion()', () => { + it('should return 0.2 when a new wallet is created', async () => { + expect(await updateAssistant.getCurrentAgentStorageVersion()).toBe('0.2') + }) + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-credentials-0.1.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-credentials-0.1.json new file mode 100644 index 0000000000..aff0d48799 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-credentials-0.1.json @@ -0,0 +1,442 @@ +{ + "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a": { + "value": { + "metadata": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0" + }, + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "createdAt": "2022-03-21T22:50:20.522Z", + "state": "done", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "offerMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9" + } + } + ] + }, + "requestMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==" + } + } + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae" + } + }, + "credentialMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9" + } + } + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae" + }, + "~please_ack": {} + }, + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ], + "autoAcceptCredential": "contentApproved" + }, + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "type": "CredentialRecord", + "tags": { + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "state": "done" + } + }, + "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7": { + "value": { + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0" + } + }, + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "createdAt": "2022-03-21T22:50:20.740Z", + "state": "done", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "offerMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=" + } + } + ] + }, + "requestMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==" + } + } + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb" + } + }, + "credentialMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=" + } + } + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb" + }, + "~please_ack": {} + }, + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ], + "autoAcceptCredential": "contentApproved" + }, + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "type": "CredentialRecord", + "tags": { + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "state": "done" + } + }, + "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a": { + "value": { + "metadata": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + "requestMetadata": { + "master_secret_blinding_data": { + "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", + "vr_prime": null + }, + "nonce": "373984270150786864433163", + "master_secret_name": "Wallet: PopulateWallet2" + } + }, + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "createdAt": "2022-03-21T22:50:20.535Z", + "state": "done", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "offerMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9" + } + } + ] + }, + "requestMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==" + } + } + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae" + } + }, + "credentialMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9" + } + } + ], + "~thread": { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae" + }, + "~please_ack": {} + }, + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ], + "autoAcceptCredential": "contentApproved" + }, + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "type": "CredentialRecord", + "tags": { + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "state": "done", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278" + } + }, + "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c": { + "value": { + "metadata": { + "_internal/indyCredential": { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0" + }, + "_internal/indyRequest": { + "master_secret_blinding_data": { + "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", + "vr_prime": null + }, + "nonce": "698370616023883730498375", + "master_secret_name": "Wallet: PopulateWallet2" + } + }, + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "createdAt": "2022-03-21T22:50:20.746Z", + "state": "done", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "offerMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ] + }, + "offers~attach": [ + { + "@id": "libindy-cred-offer-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=" + } + } + ] + }, + "requestMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "requests~attach": [ + { + "@id": "libindy-cred-request-0", + "mime-type": "application/json", + "data": { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==" + } + } + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb" + } + }, + "credentialMessage": { + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "credentials~attach": [ + { + "@id": "libindy-cred-0", + "mime-type": "application/json", + "data": { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=" + } + } + ], + "~thread": { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb" + }, + "~please_ack": {} + }, + "credentialAttributes": [ + { + "mime-type": "text/plain", + "name": "name", + "value": "Alice" + }, + { + "mime-type": "text/plain", + "name": "age", + "value": "25" + }, + { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01" + } + ], + "autoAcceptCredential": "contentApproved" + }, + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "type": "CredentialRecord", + "tags": { + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "state": "done", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9" + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-mediators-0.1.json b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-mediators-0.1.json new file mode 100644 index 0000000000..edc6a8728a --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__fixtures__/alice-4-mediators-0.1.json @@ -0,0 +1,92 @@ +{ + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": { + "value": { + "metadata": {}, + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "createdAt": "2022-03-21T22:50:17.132Z", + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "recipientKeys": [], + "routingKeys": [], + "state": "granted", + "role": "MEDIATOR" + }, + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "type": "MediationRecord", + "tags": { + "state": "granted", + "role": "MEDIATOR", + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "recipientKeys": [] + } + }, + "802ef124-36b7-490f-b152-e9d090ddf073": { + "value": { + "metadata": {}, + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "createdAt": "2022-03-21T22:50:17.161Z", + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "recipientKeys": [], + "routingKeys": [], + "state": "granted", + "role": "MEDIATOR" + }, + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "type": "MediationRecord", + "tags": { + "state": "granted", + "role": "MEDIATOR", + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "recipientKeys": [] + } + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": { + "value": { + "metadata": {}, + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "createdAt": "2022-03-21T22:50:17.126Z", + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "recipientKeys": [], + "routingKeys": ["D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu"], + "state": "granted", + "role": "MEDIATOR", + "endpoint": "rxjs:alice" + }, + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "type": "MediationRecord", + "tags": { + "state": "granted", + "role": "MEDIATOR", + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + "recipientKeys": [] + } + }, + "0b47db94-c0fa-4476-87cf-a5f664440412": { + "value": { + "metadata": {}, + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "createdAt": "2022-03-21T22:50:17.157Z", + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "recipientKeys": [], + "routingKeys": ["D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu"], + "state": "granted", + "role": "MEDIATOR", + "endpoint": "rxjs:alice" + }, + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "type": "MediationRecord", + "tags": { + "state": "granted", + "role": "MEDIATOR", + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + "recipientKeys": [] + } + } +} diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap new file mode 100644 index 0000000000..d8eb689ffe --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/0.1.test.ts.snap @@ -0,0 +1,1385 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the metadata in credential records 1`] = ` +Object { + "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a": Object { + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "tags": Object { + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "credentialIds": Array [], + "indyCredentialRevocationId": undefined, + "indyRevocationRegistryId": undefined, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "CredentialRecord", + "value": Object { + "autoAcceptCredential": "contentApproved", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "createdAt": "2022-03-21T22:50:20.522Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialMessage": Object { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "offerMessage": Object { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a": Object { + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "tags": Object { + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialIds": Array [], + "indyCredentialRevocationId": undefined, + "indyRevocationRegistryId": undefined, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "CredentialRecord", + "value": Object { + "autoAcceptCredential": "contentApproved", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "createdAt": "2022-03-21T22:50:20.535Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialMessage": Object { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": Object { + "master_secret_blinding_data": Object { + "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "373984270150786864433163", + }, + }, + "offerMessage": Object { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "STORAGE_VERSION_RECORD_ID": Object { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": Object {}, + "type": "StorageVersionRecord", + "value": Object { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": Object {}, + "storageVersion": "0.2", + }, + }, + "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7": Object { + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "tags": Object { + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "credentialIds": Array [], + "indyCredentialRevocationId": undefined, + "indyRevocationRegistryId": undefined, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "CredentialRecord", + "value": Object { + "autoAcceptCredential": "contentApproved", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "createdAt": "2022-03-21T22:50:20.740Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialMessage": Object { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "offerMessage": Object { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c": Object { + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "tags": Object { + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialIds": Array [], + "indyCredentialRevocationId": undefined, + "indyRevocationRegistryId": undefined, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "CredentialRecord", + "value": Object { + "autoAcceptCredential": "contentApproved", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "createdAt": "2022-03-21T22:50:20.746Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialMessage": Object { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": Object { + "master_secret_blinding_data": Object { + "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "698370616023883730498375", + }, + }, + "offerMessage": Object { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the metadata in credential records with auto update 1`] = ` +Object { + "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a": Object { + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "tags": Object { + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "credentialIds": Array [], + "indyCredentialRevocationId": undefined, + "indyRevocationRegistryId": undefined, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "CredentialRecord", + "value": Object { + "autoAcceptCredential": "contentApproved", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "createdAt": "2022-03-21T22:50:20.522Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialMessage": Object { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "offerMessage": Object { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a": Object { + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "tags": Object { + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialIds": Array [], + "indyCredentialRevocationId": undefined, + "indyRevocationRegistryId": undefined, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "type": "CredentialRecord", + "value": Object { + "autoAcceptCredential": "contentApproved", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "createdAt": "2022-03-21T22:50:20.535Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialMessage": Object { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": Object { + "master_secret_blinding_data": Object { + "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "373984270150786864433163", + }, + }, + "offerMessage": Object { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "STORAGE_VERSION_RECORD_ID": Object { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": Object {}, + "type": "StorageVersionRecord", + "value": Object { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": Object {}, + "storageVersion": "0.2", + }, + }, + "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7": Object { + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "tags": Object { + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "credentialIds": Array [], + "indyCredentialRevocationId": undefined, + "indyRevocationRegistryId": undefined, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "CredentialRecord", + "value": Object { + "autoAcceptCredential": "contentApproved", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "createdAt": "2022-03-21T22:50:20.740Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialMessage": Object { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "offerMessage": Object { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c": Object { + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "tags": Object { + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialIds": Array [], + "indyCredentialRevocationId": undefined, + "indyRevocationRegistryId": undefined, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "type": "CredentialRecord", + "value": Object { + "autoAcceptCredential": "contentApproved", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "createdAt": "2022-03-21T22:50:20.746Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialMessage": Object { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": Object { + "master_secret_blinding_data": Object { + "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "698370616023883730498375", + }, + }, + "offerMessage": Object { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the role in the mediation record: allMediator 1`] = ` +Object { + "0b47db94-c0fa-4476-87cf-a5f664440412": Object { + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "tags": Object { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "createdAt": "2022-03-21T22:50:17.157Z", + "endpoint": "rxjs:alice", + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": Object { + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "tags": Object { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "createdAt": "2022-03-21T22:50:17.126Z", + "endpoint": "rxjs:alice", + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + }, + "802ef124-36b7-490f-b152-e9d090ddf073": Object { + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "tags": Object { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "createdAt": "2022-03-21T22:50:17.161Z", + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + }, + "STORAGE_VERSION_RECORD_ID": Object { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": Object {}, + "type": "StorageVersionRecord", + "value": Object { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": Object {}, + "storageVersion": "0.2", + }, + }, + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": Object { + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "tags": Object { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "createdAt": "2022-03-21T22:50:17.132Z", + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the role in the mediation record: allRecipient 1`] = ` +Object { + "0b47db94-c0fa-4476-87cf-a5f664440412": Object { + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "tags": Object { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "recipientKeys": Array [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "createdAt": "2022-03-21T22:50:17.157Z", + "endpoint": "rxjs:alice", + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "RECIPIENT", + "routingKeys": Array [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": Object { + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "tags": Object { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "recipientKeys": Array [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "createdAt": "2022-03-21T22:50:17.126Z", + "endpoint": "rxjs:alice", + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "RECIPIENT", + "routingKeys": Array [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + }, + "802ef124-36b7-490f-b152-e9d090ddf073": Object { + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "tags": Object { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "recipientKeys": Array [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "createdAt": "2022-03-21T22:50:17.161Z", + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "RECIPIENT", + "routingKeys": Array [], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + }, + "STORAGE_VERSION_RECORD_ID": Object { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": Object {}, + "type": "StorageVersionRecord", + "value": Object { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": Object {}, + "storageVersion": "0.2", + }, + }, + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": Object { + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "tags": Object { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "recipientKeys": Array [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "createdAt": "2022-03-21T22:50:17.132Z", + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "RECIPIENT", + "routingKeys": Array [], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the role in the mediation record: doNotChange 1`] = ` +Object { + "0b47db94-c0fa-4476-87cf-a5f664440412": Object { + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "tags": Object { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "createdAt": "2022-03-21T22:50:17.157Z", + "endpoint": "rxjs:alice", + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": Object { + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "tags": Object { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "createdAt": "2022-03-21T22:50:17.126Z", + "endpoint": "rxjs:alice", + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + }, + "802ef124-36b7-490f-b152-e9d090ddf073": Object { + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "tags": Object { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "createdAt": "2022-03-21T22:50:17.161Z", + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + }, + "STORAGE_VERSION_RECORD_ID": Object { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": Object {}, + "type": "StorageVersionRecord", + "value": Object { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": Object {}, + "storageVersion": "0.2", + }, + }, + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": Object { + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "tags": Object { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "createdAt": "2022-03-21T22:50:17.132Z", + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + }, +} +`; + +exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the role in the mediation record: recipientIfEndpoint 1`] = ` +Object { + "0b47db94-c0fa-4476-87cf-a5f664440412": Object { + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "tags": Object { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "recipientKeys": Array [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "88e2093e-97b9-4665-aff0-ffdcb4afee60", + "createdAt": "2022-03-21T22:50:17.157Z", + "endpoint": "rxjs:alice", + "id": "0b47db94-c0fa-4476-87cf-a5f664440412", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "RECIPIENT", + "routingKeys": Array [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + }, + "7f14c1ec-514c-49b2-a00b-04af7e600060": Object { + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "tags": Object { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "recipientKeys": Array [], + "role": "RECIPIENT", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "85a78484-105d-4844-8c01-9f9877362708", + "createdAt": "2022-03-21T22:50:17.126Z", + "endpoint": "rxjs:alice", + "id": "7f14c1ec-514c-49b2-a00b-04af7e600060", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "RECIPIENT", + "routingKeys": Array [ + "D86mPByntjjYMuoaVwxotLY8RMwyRSkRkUL3XPrpwcDu", + ], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + }, + "802ef124-36b7-490f-b152-e9d090ddf073": Object { + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "tags": Object { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "ca5b148a-250f-46b0-8537-ae88014d8bd7", + "createdAt": "2022-03-21T22:50:17.161Z", + "id": "802ef124-36b7-490f-b152-e9d090ddf073", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [], + "state": "granted", + "threadId": "e9aeea8f-2c7a-4fd0-9353-f8b5b76094e7", + }, + }, + "STORAGE_VERSION_RECORD_ID": Object { + "id": "STORAGE_VERSION_RECORD_ID", + "tags": Object {}, + "type": "StorageVersionRecord", + "value": Object { + "createdAt": "2022-01-21T22:50:20.522Z", + "id": "STORAGE_VERSION_RECORD_ID", + "metadata": Object {}, + "storageVersion": "0.2", + }, + }, + "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd": Object { + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "tags": Object { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "recipientKeys": Array [], + "role": "MEDIATOR", + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + "type": "MediationRecord", + "value": Object { + "connectionId": "dbb3367e-55aa-4c03-b10a-d1fc34392bea", + "createdAt": "2022-03-21T22:50:17.132Z", + "id": "a29b39fb-f030-41ac-b6e1-ed7f3f6a05cd", + "metadata": Object {}, + "recipientKeys": Array [], + "role": "MEDIATOR", + "routingKeys": Array [], + "state": "granted", + "threadId": "a401880b-8129-4ed9-bcaa-57d0e38026cd", + }, + }, +} +`; diff --git a/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap b/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap new file mode 100644 index 0000000000..d99afc4e98 --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/__snapshots__/backup.test.ts.snap @@ -0,0 +1,434 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdateAssistant | Backup should create a backup 1`] = ` +Array [ + Object { + "_tags": Object { + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "0b6de73d-b376-430f-b2b4-f6e51407bb66", + "createdAt": "2022-03-21T22:50:20.522Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialMessage": Object { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "id": "574b2a37-1db1-4af1-a3bf-35c6cb9e1d7a", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "offerMessage": Object { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + Object { + "_tags": Object { + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "54b61a2c-59ae-4e63-a441-7f1286350132", + "createdAt": "2022-03-21T22:50:20.535Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialId": "a77114e1-c812-4bff-a53c-3d5003fcc278", + "credentialMessage": Object { + "@id": "d14cf505-4903-4dd9-95c2-a7dbc1c048b6", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImRhdGVPZkJpcnRoIjp7InJhdyI6IjIwMjAtMDEtMDEiLCJlbmNvZGVkIjoiNDEwNjEyOTM3ODA0NjIwNzU1MTQyMDgyMjM4OTc1OTA0MDc4MDA3NDg1NDMwMTQ5OTE1ODg1MjI3MjExOTM4ODY4OTgxMTk3NTM2MjQifSwiYWdlIjp7InJhdyI6IjI1IiwiZW5jb2RlZCI6IjI1In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6Ijg5NjQzNDI5NjIzMzI5NjQ1MDM2NDU5MjU4NzU2Nzc5MDY3ODczOTc4MDg1ODc3MDM3MzU1NzMxMjk1MzA0NzY5ODAxNDM5MjI2MDI4IiwiYSI6IjUzOTQ3MDU1MTU0OTEyMTE2MzM0MzU2NTMyMjEyMTM5MjMxNjE2NTcwMzU3MjkwNzcyOTkxNjM4NTU1MjU2Mjc2NjgyODY5NDM2MTI5OTEzNzk2MzQwOTA1Nzk3Mzc2OTI5NDE0MTIwNzMwMjI5NjQzNjMwNTI2OTcyOTUxNDk1NjA5NDcwMDU0NzEzOTY1OTE5NTU3MzM3NDkwMjUyNTI1NTM5NzI4MjA0NTI2NTU0MDcyMDYyOTcxMTA0MTM3NTQ4NzEzMzIzNTUzMTYwOTEzODQ0MDk0MDczOTQ3MjM2OTQyNzgyODIzNDA2NDUyNzIzODY2NjMzMzc2MjI4Mzk2ODE5ODI0MTQ2MjgyNTI4NDIyOTIzMjM1NzEzNTI5MjQ3ODkxMTQzMDE4OTM3ODQ0ODM3NzQ2MDE4MTc5Mjk5ODI5ODQ1MTI4MTgxMDUxOTE4MjE0ODU5Mzg5MjIzOTEzNjUzMjE0MjIxMTk2NDI2OTA2NDM1NDYwNDQwNTgxNDkxNTg5MzMxMzIyMDU1MDU1NjE5NDY0OTEwNTc3OTcyODAyNDM1MDY3OTMxMDczOTI5OTgyOTQ2Njg4NDg5MTIwNTQ1MjA1MzQ0MjQ1NTIzNTExNDc5NjUzNjI5ODIxNTA4NTc3MjI2MzU5MjUyMDA5MjUyNTU2NDA5NTg0MDgwNDY5NDI4NzE1NDQ0MDkyOTA4NzAxNjMwMzE3NzI5MTE3NTYwMzg2NjAyNDA4OTE3Mzg2NjM4NzQ2MDY1NjU0NzQ2OTAxOTA1NjA4MjE5MzgzNzczNjg3NDcxODI3NjE2OTU5MDk3NDU4IiwiZSI6IjI1OTM0NDcyMzA1NTA2MjA1OTkwNzAyNTQ5MTQ4MDY5NzU3MTkzODI3Nzg4OTUxNTE1MjMwNjI0OTcyODU4MzEwNTY2NTgwMDcxMzMwNjc1OTE0OTk4MTY5MDU1OTE5Mzk4NzE0MzAxMjM2NzkxMzIwNjI5OTMyMzg5OTY5Njk0MjIxMzIzNTk1Njc0MjkzMDMwMzU1ODY2NDUxMDY0MDQ4Mzk4OTcyMjU0Nzk2ODY2NDA2OSIsInYiOiI4NzE4MTIwMTY2NjA3NTg4MjIxMDczMTE0NTgxNTcxMDA5NTEwNDUwMzkyNDk0MDM1MTg3MDk5MTQyNzA5NDcyMjYxMDAyMjg5MzI5MzcyMzk1MDUwMzgzOTA3ODUxODc1MjIxODU0NjI4ODc4OTcxNzQ0Njg3MDgxMDM4Mzk2Njc2MzgyMTI0NjEyNDY0NjQxMDQ4NDMxNDkwMTYzMDgwOTU0NTc3MjA3OTkxNjg3NzU0NjQ4MTYwNzM5MjQ2ODUyMjMxMjU2NTk0MTg4MTAxNDU2MjgzNjc3MTA4NTMwNjY1NDQwNTUwMDY0MjgxODI3NzUxNjA3NjM1ODE2MjczNDU3MTc4MzAyNjEyOTI5ODMyNDMzNDc3ODk0ODMzMDc2MDA5OTE4MTc1MzI4OTUzNjg1MjEwOTQ1ODg0MTQyMDg0Nzk4ODMzNzM2OTExNDcwMTkwOTYwMjI3MzAyMzI2NjQ2NjE2NjUwMjY4OTU3NDcyMTI3MTA2Mjk3NzQ0NDg3MDUzODY2NjI0NTk4Njg1MzA0MzA0OTMzNjAxNDczNjY4Njg1NDg5MzYyMzQ2NzE5ODA4MjgxNzYxNjc0ODQyMzE5NTY5Mzk1Nzk1NTQ5OTA4MjAyODI0MjgyOTc1MTI3MDA0MzM0NTkwNTYzMTI3NjU3NDM3MjQ2MDQ3OTUyOTk0OTIyODQ3MzcxMzY4NDM0OTE3MDM1Njc4ODA2NjM1OTQ0ODY4Njc5MDY1NDc5Njk1MDU5NzkyNzUyMzk5NDcwMzUzMDI3MjEyNTg2OTc1Mjk5MTk1NDcwNjY0NDMzMDIyNTQyODg4MzI4OTA0Mjg2NTIxMzM5Nzc2OTkxMDYzNzA1MTI2NjA4MDY4OTA0Mzg2MDc4NzA5NTE3NjU0OTE3MzI0NjExMzkzNTM4MDkyNTQ3NzQ0OTM2NTM1NDkwODcwNDU4NjQ3NjY2OTU3MjA5MDk4MDU2NzIwMjAzOTAxMjI3MjU2NDM5NTkwNTA0MzIwOTI3NTc1ODA2NjE0NzA4NTU3MjAxMTAxODczODc4NDg1NjM4MzQ2Nzk1NjE4NDQxOTQxMjQyODc5ODMyNjQ5ODEyIn0sInJfY3JlZGVudGlhbCI6bnVsbH0sInNpZ25hdHVyZV9jb3JyZWN0bmVzc19wcm9vZiI6eyJzZSI6IjIxOTkzMzcwNTI0MjIxNTM0MTM0MTc4MDM3MDIyMDEyMzE4NjQ2MDE4MTk0MDIwMjgxNzY4NTQ4OTUyNjM5ODg4MDE2NDQ1NTM2NTQ2MzczMzkwMDU5NjY1Nzg4OTc2MDE0OTUzMTU2MjA1MTA0ODU1NTM1NjEyODY5Mjg5NjgyOTQ1ODI4MDQxOTMxMzc1ODY4NjE4OTE0NjUwNTc5ODM2NzI0NDE0MDMxMjU3MjU2MzkxNjg2OTQ0NjQyMzg3NTIwNjExNDQ5ODM1NTgxNDMzMDMzOTQ4MTA4OTE0MzI2NzkyNDU5MjQ0Mzc0MTgyOTQ4MDQxODIzMTg3NjY3MjE2MDI5OTEwMTAzMTM1MjE4NjY5ODc5MDk5ODA0NTA0NjI1NTAzNDM2NTAxOTk5ODkwODIyNjcyMjYwNzc1NDIzMzIxNTQ0MDk0Mjk2NDI0Nzc2Njg2MDI1MzU2MjMwOTY0OTQyMzc3NTY0NDUwMDk4NTgyMzg2Nzg0ODc3OTQwNTI2ODg0MzgzOTcxOTE4OTE3ODIyOTkzMDIzMDU2NjU1NTg2NDI3MDAwMzQ2OTcwMTI1MjA2ODg0NTkyNzkwMDU4NTAyMzkxMzUwODIyNjg1NDYyMzY0MzE5NTMwOTI0MjM4Mzg2MjkwMDI5MTQ1ODAzNTgxODA2MDQ1MTYzNDMzNTQ2OTAwMzQzNDg0MTg2NTEyMjIzODYwODY3NTI3ODExOTkyOTQwMjMxMzgzOTM4MjI0NjEwMTk0NDc1NjczMDYyMDI3MDgwOTI5NDYzMjU0NjIxMjI1NTg3MDg0NTUyODEyMDgxMDE3IiwiYyI6Ijg2NzE3OTAzNTAxODI5MzU5NDk4MjE3NDU5NjE3NjgyNTM4NzIxNzQwMjE5MDM5MDUzNjQzNTE3NDU0Nzc2NTUzMzA3MzU1OTkxMjE5In0sInJldl9yZWciOm51bGwsIndpdG5lc3MiOm51bGx9", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "id": "5f2b7bc7-edfd-47e7-a1d4-aae050df2c4a", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": Object { + "master_secret_blinding_data": Object { + "v_prime": "36456944381549782028917743247126995038265466209293312755125557271456380841610111892515020379470931691048072348420844231863825225515560265358581756565441268878364665494094789024845049226122885121039335781567964878826549149370097276812152226343824116049855825405977949749345353074025294938300401262824951638782220004732873597724698990420932910079362747837952520524827009393981876443737452031919055976088763615615890946142630576421462920865811255312740184209214306243871230276622595183415487741608569800898909023830922654063814555128779494528740438076748829436757078504882332589744263200806138145494157659396691564807976032319024007464003538934", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "373984270150786864433163", + }, + }, + "offerMessage": Object { + "@id": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiMTE4MTE3NTM4MDU1MjM2NjMxNjAwNjM1NyJ9", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "1284ae78-f3d3-4fed-a5ff-0e2aba968c3c", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiUVZveGd3d25WUGtBQlRMVmNtQ013TCIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiMTkwNzM1MzQyMjkwNjk4Mzk3NzgyNTUyMTM2NTA3NzAxMDA4NzYwMzcxMjg3NTQ1MzI0NDM2NDIyMTMwNzQ3MDI3NTk4NDM2NTM5Mzc0NzM0MjgxOTk4MDY5Mjg1OTg4MzAzMDE1MzAzMTYxNjExMTEzMTY2MzQxMzkyOTkzMTk0ODUxNzM5Njk4NzcxNDYzNzMzMDA4MjUxMzQ5NjM4OTkzMDE5NTk5MTY1NDUyOTk4OTA4OTY4MDE5NTUxODM2NDg1NzYxMzMxNTgxNTY0MzgxNTkxNjMwOTcyNTc5NTkyODMyNDk3MDI0NTMyMjQxNzk4MDMzMjI1NDg4NjA5NjEwNjEzNTU1NDMxODc3NDQzODk2ODUyMzA4NjIxNDA1NjI5NjA5MTg5Nzg2MjYzMTcyODU0MjA1MTI3ODMxNjc5NzM5NTkxODQyMTYwOTAyNjczMDE4Mzc0MzE5NjUyODA3Njc2MDQzNzc0ODcxMjQzMzkzNTIwNTkwODE5MDgxOTI4NzY3MjU5NDQ2OTIxNTM2MjU3MjQ2NjIxMjgzNjc2NDM1MDIwNzUwODI4NDI2NTM2MTU2ODA5NDgwMTU3MTQ0NDkxNTY2MTM0ODYzNjU4Mjg5ODIyMTE1NjI4MzMxNjMxMTQ3ODM1NzQ4MjAxMzkyNjY4NzQyMTQ5NTI1OTQ0OTc1NzY3NTYwMTQyNzQ5MTU3MTY2NzE0MDY0OTM2OTQ1MzEwMzEwMzU1NjgwNTcyNDgzNDgyNTYyMzk5Nzc0OTY5NTYwMTA1Njk2MzczMDU4MDMzODgyMTAwOTY2ODUwMTk5MjEzMzAiLCJ1ciI6bnVsbCwiaGlkZGVuX2F0dHJpYnV0ZXMiOlsibWFzdGVyX3NlY3JldCJdLCJjb21taXR0ZWRfYXR0cmlidXRlcyI6e319LCJibGluZGVkX21zX2NvcnJlY3RuZXNzX3Byb29mIjp7ImMiOiI1MDg3Mzk4NDExNzQ3Mzc5NjY5MDkyNzU5ODQ5OTEwMDI2OTYzMTk2NjExNjc5MzU5NDYwNDMxMjYyMDE4NzgyNzY4NTM2NTUzNzUwMiIsInZfZGFzaF9jYXAiOiIxODU0NzEwMDA5NDM4NTg5MTc4MzkzMjgzMDk1MzM5NDUzMDQ3OTkwOTYyMjE2NzEyNzk2ODkyMzcyMzA5NjU5NTU3MDY2MzQxMTMxOTY0Mjg5NjA3MTI5MDg4MjMxMDY0NTk5ODY4NDg4MTIzNDMwMzY5OTkxMjI1OTMxMTIyMjY4NjU5MDEwMDA0NDA1OTIzNTcyMzgyMzQzODczNjkxODg3NDQzMjQ5MTcwNTQwNDk4Nzk5MTkxOTIzMjc4NDU4MzcyMzk2NzIyMjM5NDE1NjY3ODIxMDQ4ODA0NDk1NDQ5ODQ3MjcwOTg1MDcwNzY3NjU2NDU4NDM0MTYzNTI3NDAyMzA1NTg5MTg4NzcyNDg4NzE1NjcwOTgxNTc0NzQxMTI1NzYxOTIxNTE3NzYyNzg1Nzk4MjU5MTQ0OTYzMDQyMjg0NzUwMDE5MjAwMjQ4NjgxNjkxOTE5Njg3MDA1MDA4MDUzMjYwODY5NzEyNTEwNzIwNDg5NDAwMjM0NDU3Njk2MDk4NjI0Nzk1MDUwMzQ2NzM2Mjg1MDE3MTU5Mjk1OTA0NTU1NTk0MzMxNzI4MTQ5MzgyODE5NTI2NTc3MDg3NjA0ODMwNDk3MTE0Mjc3MTkyMDU3ODk1MzYxNzI5NTE3NTgxNzg5ODEyMDY3MjcxNTU5MTMyNTI1MzEzNTc0MDEwNTM3NDMxMDY2NzUwNzAzMDgxMTQxNDIyODg5MzUxOTY0MDUyMDU0NjY0MTA0ODE0MzY1Njg3NTcxNjU5MTk3ODQxMjU5NjE3OTI4MDg4NjM0ODY1NTI2MjI4NTkyNTI2NTgwODgzMzIzNjEwNTc5NzU4ODgzMjgwNDcyNTA0OTQ0MjM2ODY5MTYyNzM3NzUwNTI0NTIyMjE5NTM4NjE4OTk2ODQzODU3MzU3MjUxODI5NTIyODgxOTA0NTAwMDU2MjU1NTMwNDgyIiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMjM5OTQ3ODE4MDA1MTQ3MjQ5MjcyODIxNDI2OTgwNjQ2NTIwMjAwMTc0MzUwMzkzMzQ0NzU1NTU0NDAxNjg1NzQ2NzExMzkzMjEzMjA3NjI3ODI5MTczMjc2OTA2MDg4MDAxMDIxMTMxMzY4NzY4MjI0ODgxNTExMjEwNjY0NzA5OTAxOTQwODA0MzA0OTc1NTUyMzExNzAyMjU3MTYwOTAxNTE0MzIxNzYyNzM0ODUwMSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIzNzM5ODQyNzAxNTA3ODY4NjQ0MzMxNjMifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + }, + "state": "done", + "threadId": "578e73da-c3be-43d4-949b-7aadfd5a6eae", + }, + Object { + "_tags": Object { + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "cd66cbf1-5721-449e-8724-f4d8dcef1bc4", + "createdAt": "2022-03-21T22:50:20.740Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialMessage": Object { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "id": "ad644d8a-48a2-4c55-b46d-7a7f1a9278c7", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + }, + "offerMessage": Object { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + Object { + "_tags": Object { + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + "autoAcceptCredential": "contentApproved", + "connectionId": "d8f23338-9e99-469a-bd57-1c9a26c0080f", + "createdAt": "2022-03-21T22:50:20.746Z", + "credentialAttributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + "credentialId": "19c1f29f-d2df-486c-b8c6-950c403fa7d9", + "credentialMessage": Object { + "@id": "a340a7b9-f4d4-4892-b7d2-1d3d40e4be48", + "@type": "https://didcomm.org/issue-credential/1.0/issue-credential", + "credentials~attach": Array [ + Object { + "@id": "libindy-cred-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsInJldl9yZWdfaWQiOm51bGwsInZhbHVlcyI6eyJuYW1lIjp7InJhdyI6IkFsaWNlIiwiZW5jb2RlZCI6IjI3MDM0NjQwMDI0MTE3MzMxMDMzMDYzMTI4MDQ0MDA0MzE4MjE4NDg2ODE2OTMxNTIwODg2NDA1NTM1NjU5OTM0NDE3NDM4NzgxNTA3In0sImFnZSI6eyJyYXciOiIyNSIsImVuY29kZWQiOiIyNSJ9LCJkYXRlT2ZCaXJ0aCI6eyJyYXciOiIyMDIwLTAxLTAxIiwiZW5jb2RlZCI6IjQxMDYxMjkzNzgwNDYyMDc1NTE0MjA4MjIzODk3NTkwNDA3ODAwNzQ4NTQzMDE0OTkxNTg4NTIyNzIxMTkzODg2ODk4MTE5NzUzNjI0In19LCJzaWduYXR1cmUiOnsicF9jcmVkZW50aWFsIjp7Im1fMiI6IjEwMzE4MTcwNjEzMDMzODg1ODkxNDkzMzk2MjU5NDYyNDIxMzM3MjY3ODcyNzkyNzczNjgwMDQwMTgyNTE4NDM1MzEzMDYyNjE3MTcxMCIsImEiOiI3MTM3NDExNDU3NjI3MDE5MDcyNjY0ODMzNTQ5MjAzMDQyMTg0MDQ0OTUxNTU5MzYwMDczMzk1MjM4NjkwMzkwNjgxNzA3ODAyNzY0MTE2MzQ4NjE4NjgxOTgzNTM2MTczNTE3MzgxODc5NzQ0MzQyODIzMDkzNzc4MTk1NzYxNzgzMjQ3NDcxOTQ2NDgwODc0OTY2OTQyNjY0NzU4NjIwNzEyNzExODExNTY5NTc1NzMwNTg4NTQwNDI1MjEzNjY0OTg0OTQyOTIzODU5NzQ3MjIzNjA5ODQ1NjIwMjE5NTY1NzYxODY2OTMwMzI3OTYzNTIwNzE5MTg2NjMyNTQ4NzkzNDI3MTQ0NTY0NTAxNjE1NTg1MTI2NzkxNzM5Njg3ODc2MzIxMTQ1NTAzNDU1OTM0MzUxODc0MjQ3NjA0NzAwODYxMTgxNjY4NjUxNzQ5NTExMjY0MzExMDU2NjI5MjM4NjQwNDY2NzM4ODA0NjI0NzU1MzEzODgxMjQzMjkwNDM5ODI4MDE0NzY0MTQ3NDM1MzE3NTY3MjY3MzQ2NTQxMTY2ODIwNTI2MDkyMDAyMDE0NzY5NjE1MzI3Mjg4MTYwMzM0MDg1MTQ1MzQ0Mzc5MzAxMDg1NDc1MDEyNzUzNDIzNzkxMjI4NzM5NDE3MzA0OTM2NDUzNDEyMDYxNjY0MTUzNTM4MjM5MDA0OTUxMDgyODQxMTE1MTIyNDQ1MzkzNzc5Mzc5NDI5NjQzMjMxNDE2NDQzNDM4NDQ1NzI1NTAzMDc0MTM5MzEwOTc2MjkwMTQ2MTIwMDI2MDI1NTczNzIyNTUyOCIsImUiOiIyNTkzNDQ3MjMwNTUwNjIwNTk5MDcwMjU0OTE0ODA2OTc1NzE5MzgyNzc4ODk1MTUxNTIzMDYyNDk3Mjg1ODMxMDU2NjU4MDA3MTMzMDY3NTkxNDk5ODE2OTA1NTkxOTM5ODcxNDMwMTIzNjc5MTMyMDYyOTkzMjM4OTk2OTY5NDIyMTMyMzU5NTY3NDI5MzAxNTQ3NTU5NjM2MjQ0MTcyOTU5NjM1MjY1ODc1MTkyNjIwNTEiLCJ2IjoiODMxMTU0NzI2ODU4Mzg3ODc5NTU5NDUzNzcwNjg3MzIyMzE1NTgyMjYzMTk3NDUzMjMxNTI1ODIzNDIzNjkxMDk5Mzc2NTExNzI0ODAxMzA1MTU0NzY2Mjc0NjQ1OTMzMTAwOTIzMjIxOTgwNDkyNzE0NDQxMTY0NTYxNTIwMDIzMjYzMDYzMzQ4NjQ4NzkzMjkxMzEwNDc0NTU5NDIwMTkzMTA1MjE5NzMxNTAwMTc0ODg0NzQ0MDk1MjU3MDYyMjczODA2OTYzNjg5MjY3NzA1NTg4NTQ4MzU4NzQ2NDc1Mzc1MzAzMjI1OTI3MDkxMzA0NzQ2NTg5MzA1MDEzNjc1ODk0MzIxMDkzNjE0NzIxMjQwNDAzNDE5OTM5OTk0Mjg5NTU2MzY0MDExMTg2ODQ2NjMxNTA2OTU0NDg0NjM4NjgwMzEyMDA2Njk0MjcwOTkwNDU3NTk3NjAyNzc5MjUzMDc3MTg3NDgwNTg5NDMyNzU4ODgwMjY3NzA1NzMyMjg3Nzc0ODczOTI3MDExMTQ1MDE1NzgyNjE5NzI4NTAxNjI5MTE4ODE1ODM2NjU2OTMzMzcwMzgwNDk4NDk5MDE0MDEzNDI1NDMwMjMwODQzODc0OTk3NTg0NTY3NTA0Mzg3OTE4MjQxMzMxNTM1NDk5MTQxMjU1NzQzNjQ0MzgwNTQ4ODAxNDUwNDEyMzQzMTAxODc4Nzg3NzIzOTIxMDU5NjQyNDg2MjE3NjE0MzQyMDc0MTQ3ODk1ODA2MzQ1NTQ0Njk3NjI2MzcwMDY1MjYxMjQ3OTM2NzMwMzMyNTkyMjM3NDY1MjIyNTQ1MjE4ODc4MDk0ODk0NTE0ODU2ODUyNTU1NjI4MjIwNDg2MTU5MjcyMjIxMjM3MDkwMjc4NjUyNDM2MzcyNTE4MDgzOTUxNjAwNzI1ODA1MDYwNjExMTkyNzEyOTI3MjQ2MTUxMDU4NzU5NDk2MzQ3NjA0NDQwNDcxODcwODEyNzMwODE3MTU4NzQxNDQ1MDA0OTg2MTgyNDk5NDA4OTQxMzk2NjcwMzEyOTU0NDQ1MTk1NTM5MTM5MzIwMDI4MTI3NzMyMDQ0MSJ9LCJyX2NyZWRlbnRpYWwiOm51bGx9LCJzaWduYXR1cmVfY29ycmVjdG5lc3NfcHJvb2YiOnsic2UiOiIyOTQ0ODU2NTEyNDk3MjM2MTc2OTQ3OTMxNzM1Mjk0NDc4MTU5NTE2ODM4OTExNDEyMDE3MTI1NTAyNjc1ODM2Nzk1NTQ4OTczNDMyMTg2NjI0MTc2NjQwNzAxNjczOTk4MjI3NTEzMTA0OTU5OTgzMjk1NjI0MTA0MjkwNjgzMjU0OTI4NTg1MDkwMTI2Mjk5ODczMjY4NDYyODA5NTY4NjMxNDg3MDk2ODgzMDE0OTcwMTcyMzM0MzIwMTM4OTg5ODkzMzcyODIyODU2MTQxMTM5NTQ0MzQyMzExNDEzNDgyMDU0OTYyNjE5NTgzNjU5NjMwMzYxNTc3Nzg0MTQ4NjY3NTgwNjMxODc4NDIwNTgzMDk5OTM1NzE0NDYyMzE5Njg4NDE5MTM4NjY3Nzk2Nzc2MzQwMDk2MDIxMTgwMTU0ODU0Njg1MDEzNzY1MTM0ODMwNjA0MzEyMjI0MjIwNzA5MzQwODExMTI4MDczMDk3NjEyMDA0MTE4NjA0MDI3MTczNjExNTE2ODA0NTQxMzUzODA2OTkxMTg0MDY3MzY1MDE5Nzk2NDM2MDA3NDI0NTM5NzQ4ODQxMjU4NjA1MTUxNTQxNzQzMDk4MTE5NzI1Mzc4MTQ4MTgyNDQ2Njg3NDQ0MjE0ODk1NTE2NDc3MTM4Mjk1OTI1Mzc4Nzg1MTA3OTc5MTYxNTIyNTYyNDk2Njg2MTAzMjg3MDUxNDUyMTE3NTQzMzc4Mzc0MDM5Mjg3NzgxMjAwNzgxMTkyNDQyOTY5MDU1NjIwNTcyODg1NDM5NjU2MTU3Njk2Mzc5MTAyNDc2MTQ2IiwiYyI6IjI4NjYxMjE3ODQ5OTg3ODI4MTA2Nzk5MTAxOTIxNDcyNzgxNDMzNzgzMDE5Mzg0NzAwMDUzMjU4MTU5NDUxNjc5MjMwODI0NzA5NjIifSwicmV2X3JlZyI6bnVsbCwid2l0bmVzcyI6bnVsbH0=", + }, + "mime-type": "application/json", + }, + ], + "~please_ack": Object {}, + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "id": "c7e0a752-7f1c-41c0-b0ae-a68c2d97ca8c", + "metadata": Object { + "_internal/indyCredential": Object { + "credentialDefinitionId": "TL1EaPFCZ8Si5aUrqScBDt:3:CL:681:default", + "schemaId": "TL1EaPFCZ8Si5aUrqScBDt:2:schema-80f7eec5-8e5a-43ca-ad4d-3274fb9361b8:1.0", + }, + "_internal/indyRequest": Object { + "master_secret_blinding_data": Object { + "v_prime": "24405223168730122709164916892481085040205443709643249329100687534344659826655374235392514476392517756663433844139774514430993889493707631169979521764390851593418941181409704266182779162417466204970949168472702858363964258641437554267668466400711344128132909691514606077477555576087059339291048485225394874964325220472232903203038212033940680060605090839733163438385288769519855418153181511119637865605476043416048121313638627002888436809192752657860306784733123742838413845299796745569824223645588826964796075250758249133953560017373025169692866449286962430731916293683231375510684692358406054381559324718715654332979447698704161714028193478", + "vr_prime": null, + }, + "master_secret_name": "Wallet: PopulateWallet2", + "nonce": "698370616023883730498375", + }, + }, + "offerMessage": Object { + "@id": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + "@type": "https://didcomm.org/issue-credential/1.0/offer-credential", + "credential_preview": Object { + "@type": "https://didcomm.org/issue-credential/1.0/credential-preview", + "attributes": Array [ + Object { + "mime-type": "text/plain", + "name": "name", + "value": "Alice", + }, + Object { + "mime-type": "text/plain", + "name": "age", + "value": "25", + }, + Object { + "mime-type": "text/plain", + "name": "dateOfBirth", + "value": "2020-01-01", + }, + ], + }, + "offers~attach": Array [ + Object { + "@id": "libindy-cred-offer-0", + "data": Object { + "base64": "eyJzY2hlbWFfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjI6c2NoZW1hLTgwZjdlZWM1LThlNWEtNDNjYS1hZDRkLTMyNzRmYjkzNjFiODoxLjAiLCJjcmVkX2RlZl9pZCI6IlRMMUVhUEZDWjhTaTVhVXJxU2NCRHQ6MzpDTDo2ODE6ZGVmYXVsdCIsImtleV9jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiMTEzOTY4MTg4OTM2OTQ5MzcyNzU3NjU2NzI1MjU1MTQ3NDk1OTI5NTM0MjQ5NjU1MzY4NTMzMTY4OTIzMjU4NTA2OTUzOTk3MTI2MDEyIiwieHpfY2FwIjoiMjM5NDkwMjQ4MjE4MTExOTQ1MjIxOTQ1ODcyMTE4MjQzNzA3NjE5OTQ4MzQ0MjU1ODM5ODI4NTU3NjkyNTE3NDExNDMwNDgwNDgwMTkxMTMwMjM0OTg5ODk0NzIyNDE2Nzk1MzUzODAwMDk3NDUxMjI5NDE4MzQ0MjEyOTI3NDk1NjI2NTc4MTk2ODUxMTcwMTI0MDI1NDk1MDExMjc0NjU1NjQzNjkzNTE1ODczMjA5OTczMzgwNjA3MzQxNzQzMzIwNTY0NjkwMzcxOTgyNDIxNTQyMzQzMTMzNTMxOTcxMTk4NTA4NDk5MjYyNzUxNDMyODg0NzgzMzc1MTAzODI0OTE3NzEwODAxOTE3OTc1OTM2OTg4OTIwMzYyMzA5NjE4NTgxMzY0ODE5NTA5ODYxNTE3NjI2ODc3OTUzMzMzMTkzMDExMjA2NDA5NDQ1MzA0MzEwMjUzMTU5OTE1NzYxOTI1MDY1MTg3Mjk1OTg1MzU0NDY1NjY5MTMxNjgwMzc5MzY0ODk0Mzc3NTYxODcyMjcxNDY5Mjk1NzY4MTc4NjQ2NDMxODI3NjI1MTQ3Mzk4MDg1ODI3NTUzMDAzNjIyMDM1ODM1MDg2NzE1NjgyMDA5MzgyNjgxNDc3NDc4ODQ0MDEyNDQ5NTE2NjYwODMwNDMwODQ5ODMxNjAxNDk3MTk3MjczMTIzNjg1NTE0NDMwMjY5OTkxMzMzNDI1Nzk0NjAwMzc3Mzk3NDMwMjg0MjIyMjQ1OTgyMjI1NTE3MjQ4NzA5NTczMzEwNjM5NzQyNzc2NjMyMzM3MDM0Nzk4NDY3MTAwNDczNDUxNTMzMTg1NDg5NDU0NjUyNTgwMjcxNjgyMDQzOTc4MDY4NDc4MjM1MjM5NjMzMTk0MzE4NDcxNDM2MjMwOTg1NTQ1MzAyMDQwNiIsInhyX2NhcCI6W1siZGF0ZW9mYmlydGgiLCIyNjMyMjI0MDEyMDg0NjM3ODA0NjA4MDE5MTM2MzIyNDkzMTE1MzA1MTA2ODQ1MTA0NTE4MDE4MjY2MjU1NjMyNDM0MTMzODY0MTIwNzUxNDkyMDAyMTI4MzU3NTcwMTY4MjE0OTU3OTgzNTQ3Mjk0NTczOTExMzk3MTQwNDAxNDk4MzQ0NzE0NTA5MjEyNDA2NTYzNzgyOTc4ODUzNDgzMTM4NTA1NTA3OTcxNTY3MTgyOTQ1ODQ5ODI3MDU0Nzg4NjA3ODgwOTU5NDE1MTYwMjU0OTE2MDExODkyNTIzNjUzODA1NTk2MDQ3NjA5Mjg3ODA4ODg2MzcwOTM5NDA3Njc5NTE3MjUzOTUxOTUzNDgxNDkzOTMzNDI3NTA5OTUwNTY5NjUwMTc4MjQwNTg0ODI4NzgzOTAxOTA0MjI3MzE1OTEyNzQ0NDYyODc3NDI3Njg3MDEzMDkyNDY1NTc5NzY0OTk2MTc2NTU1NDg1Nzc2Njc0NTcxMjcwNjU4NjAyMTk3OTExNTYzMjY0MzEyNTgzNzMyNTE3NjI3MjY3MzQ1MzEwODcwMjk2NTk4NTEyMDc0NzczNDQyMTExNjY4NjM0MzYzNjkzMDkxNzQ2MjE1MDQwODg1NTcyMzAzMDU4MDUwNzc3Nzg4Mzc5NDY3MzMyNDUwOTY5NTg1NDE2NzU2NzA2MzcxMzMyOTk5MDg1NjM5MDU4Nzk4ODE4MjkwNzEyNDk2NDk0NTY1MzgxMjI0NzUyMTA2OTQyMTg2OTc3MzUwNzE2NjI0MTYxMjIwNjExMDY3MzcxNTM0NjYyNTc0MzY1NTM0MzEzNjAwMTQyODk2MDkwMDQ3MTI5NjQ3OTEzOTgzMjk0Mjc2MDI2OTA2MTA3NzM4MDM2MjI2MTA4NzU0MTE3OTIwMTg2NDM1MDkwOTU2Il0sWyJuYW1lIiwiMzE2Nzc0MzUwOTgzMjI4Nzg1MDcxNDg1NjYzOTM0MTYyNjQ1MzE2NjQzMTc5Njg2NTg2MTU2MjgwMjg3Njg5Nzg0NTI1Mjg5MzY0NDg5NDMxNjEwMTc1MzUwMDgzMzUzMTg3MTIwMTE1MTU1NjUxOTYzMjcyODM4MjcyMTgyNTI2MzUyNjMyMzI5MDY2NDIxMjM3OTM3NTc4MjY4MjkwNzU4MDgwNjI0MjE3MDM0NDU5MDUyMjY2Njk5NjQzMTg2MjkyNjcxNDk1ODEwMDU4Mjg5MzA1MjI4OTQ2MTQ2MTkzMDAzNDk0NDgwNzY0NDY5Nzc1NzM5Njg0NzcxNjMyNDIzNTM5MjE1MzIxMjc0NTQ4NDU5NzE4NTM3NTMzMzE0MjYwMjcwNzE0MTkzNjc4MTEzMDg5MDUxODI1MjA2MTAyMjQ0MTc3NzAwNDk3MTYxMDM2NjgwNDYzMDUxNDcxNDk3MTgzNzc3Nzc5Nzc2Nzc0MTUzNzQ1NjEwNzc3NzgyMDYyODA3NjY5MjE2ODA2NDgxNzAzNTU5NDk5MTAyNTc5ODAwNjgxNTQxMjg3MTk0MTAwNzg4MDMxNDE3Nzg5MzQyNjk2NTQyNTA3NjE5MTgzNDIyMTc0OTk5NzQ2ODM3NjY4NTg0Mzk0NzQxNzI5MDY2MjgwNjYyMzAyMDI3NTczMDgwMDY5NTE5MDgxOTA5OTA3MzQzODAxNzg3MjgyMTEyODY2NzkwNTI2MDIwMjk4NjM2ODY3Njg2NTE5MjQ0NTg2MTg1NzMxMTE0ODk1ODU3NjkzNTQxMTIwOTc2NTQ5MzYwNDE1MTkxMjI4ODA4MzE5NTcxOTU4NTkxNDc4NDYwNzMwMTg2NDQ4MTU3NjU3OTkyOTI4NTM2MjgxODU2NjAzNjU5NjM3OTE2NzE0OTk0NTU0NSJdLFsiYWdlIiwiMTY5MjgwMDQ2OTQyNDI3NDY1ODE5OTE3MzEyNTkzMzEyMzkyNDYzMDQxODc5MzIwMjM0NDU4NjQ4Mzg2MzI4MTE5MDQ5OTIyMzc1NjkxNzI0ODYzMDM3NDMyODU4OTQ4MTIyNzI2ODQ5NDU5NDcxODA2NTU4NzYyMzU4MTgzMzU3OTYwNDk3NTMyNjUzNzE4ODE0NzQ1NzY2ODc3OTI2NzU2NTQ3NjQwMjUzMTczMDYzMjY0Mzc0OTAzNTgwNjEwNDMxMTM2NTA2NjQ1NjE5NzYzNTE1Nzc3MTkyNjU4ODk0OTc1MDMyODAzNzM0MTE5MjM5NjcxMjgwOTQyOTkxMTg2MjYyNjYzNTM5NDU3ODc1NjY1NDcwMTAxMTEzOTUyOTY2MDQyMzU4NDQ4ODE1NTk5MDgzNTU4NTIyMDQ1OTI3NDI0NjI5Njk4MTgzMTUzNzUyMDA4MzM5NTI1NTYxMDI0ODg2MzUzOTc3NzA1ODE5Mjc1MzQzOTg3MzMzODMxNjU0NzA4ODI3NDI0NzMzNzcyNjI3MTA2OTgxNjE5NDY0MzUwNDU3NzE4NzM2MDA0NjEyNzQ0OTAyNDA5NjA0Njk4NzkzNzI0MTc1MDA4OTUzMDMyMDgxMTQ2OTE3MTM4ODc4NzQ4MDM0Mzg1NDQxNTIxMTU5ODM2NDIwMDEzNTQ1NTQyMDk2NDIwODA1MDYxMzI5MTkwNzczNzIzMDYxMjE2NDIzNjczNTM1MzU1OTc5MzY1Njg0MzM2NjEyOTU2NjkxNDA4NDQ5MjE4MjcwOTYyODUyMzQwMTQ2MDAwMDYzNzA3NzU5MzUxODM0MTQ2MDI4MTYyMTEyMzU4MzAzNDQ1OTcwMTg3MTk3OTQ5MDcwNzE4NzQ4OTI4NjM5MDkyMzY1MjExMjgyODY0NDE3OTcwOTg5OCJdLFsibWFzdGVyX3NlY3JldCIsIjk4NjY2MDAzNjA2Njc3MjkxODM1MjEwMzA1NDczNTA3NjU0NTM1OTgxNDYxODkyNjY5NjI5OTE1MzQ5NjU3NjY5MTI5NzAxNTUwNzUyNTU2NjMxMzU4MzU3NjEzNjg3OTgyNTQzNTcyNTc5ODEzOTgxMzI4MDM4NzY5OTcxMzAwNTE0NDI2NzAxNTM2ODE1ODI3MDgzNDY5MzEzOTQ0ODAzMzIzMDUzMzUyNDkxMTIwMDUwNzkyNzA4NTQzMTE2NTM1NjA2NTQyNDY3OTcxNTUxODA4MzQyOTk1OTM4NzQ2NDQ4NjMyNTY4NjU0NjA4MzI1NDk2NjM3Mzc5OTQ0NjA5MDU3Mjc2OTE0OTQxNzE4MzU2MTYzODYyMzI2MDc3MDUzNjIyMDI3OTE2MzIzNzAzMDE2MTc4NDQ3MDEwMTc1MjI1MTM0NjE3NTcxMTgzMjcyNzMwNjQxNzI3Mzk2MzM4ODk2NTUyNzM4NzUwMDA4MTQ3MDExNjkyNzIwNzI4NDY2MjcwNDQ2MDg4NjEyMDg2MDExMzg2NzQxOTMzODM4ODQ1NjkzMTM1NzcyODk0MDIwNTM4NTU3ODI1MjA3OTkxNDAwODIyNjg4OTgwNTg4MjgzMTY0MzAxNTY1NjAyNDAzNTI4MDE2MTczNTk5MTM4NzI5ODEyOTE2MTYxNDEwNjgyODU4MzU4MDE3ODI1ODUyMzY2OTgzMDQzMzM4ODY2MzI3MzM3MTE0NzcyMzM5NTUyNTYzNzU0NzQ0NTA5MTYzNzYzNjAyNTI0NjgxNTUyNjIyOTYwNjM4MzE2OTc0NjI4MjQyNjQ5NjYyMjQyMTkzODMxOTUyMDE0MTAxNTA3Njk2MjIxMDU5NDE5MTcyNzMwNjE2NDE2NzEwNTMzMzc2NjI4MjQ4NzkxMTUwNjMxMDMxOCJdXX0sIm5vbmNlIjoiNTQzODYyOTczNzUxMjk1OTk2MTAzNjA3In0=", + }, + "mime-type": "application/json", + }, + ], + }, + "requestMessage": Object { + "@id": "edba1c87-51d3-4c70-aff2-ab8016e1060e", + "@type": "https://didcomm.org/issue-credential/1.0/request-credential", + "requests~attach": Array [ + Object { + "@id": "libindy-cred-request-0", + "data": Object { + "base64": "eyJwcm92ZXJfZGlkIjoiRUg2OTVENjRRd2hWRmtyazFtcDQ5aiIsImNyZWRfZGVmX2lkIjoiVEwxRWFQRkNaOFNpNWFVcnFTY0JEdDozOkNMOjY4MTpkZWZhdWx0IiwiYmxpbmRlZF9tcyI6eyJ1IjoiOTcwNjA5MzQ1NDAxNzE0NDIxNjQzNDg0NDE0MTQwOTA0NzMzNTQ4NTA4NzY0OTk5MTgxNzY1ODI3MjM3ODg3NjQ2MzQzNDYyMTY0ODA3MTUxMjg1ODk2MDczODAyNDY1MDMwMTcxNTIyODE4ODY4Mjc2ODUwMzE0NzQzMjM3ODc3NDMyMDgxMTQwMzE5ODU5OTM5NjM0MTI4NzkzOTk4NDcwMTUzNTQ0NjgxNTM5NDg4NzEyNDE5MTk3NzcxODE1NjU5Nzg1NDE5NTA1ODEyODI4NzYxOTI4MzExODczNjA5NDYwMjExMzQ4OTAyNDk4NzYxNjc5OTIzNzA2NjUwMDIzODg4ODU4NzE1MTYxMTIyMzA5MTc1MTE3MTk0NDMwNTI1ODY5NjcwMDEzMTgxNTkzNjI4NDQzMjk2MDI0MDE5NTc4MzIzODQwNzk0OTQ0MjIyODE1MTQwNTM2Mjc3ODQwMjU2ODc2MDE1MzUwNDgzOTE2MjYzMDgyODM5NzI2NzAxNjg4NjcxNDY0MjA3MzQxMTgwMjg3Mjk0Njg4NDA3NzQ3MDk1NjA0NzQ3NzA3OTc2Nzk2MjU0MTQ2NDQ0NTY5NzQ4MTk4OTMwMjkyOTkzNjY1ODk2MTUyMDMyNzQwODY3OTgwMjczMTMxMDM3NjkwNDkzNDU1Mjc4NDc3MDc3NjE0OTU2NjgzNjgxNDc5NzY3Njg0MDI4MzU5NzE4NzM0ODEyNzcyMDIyOTIwODQ3NDIyNDYyOTAwMjczMTcwMTU2NzQyMzUyMDQ2NDYyODI4NzAxMTE2MzU0MTkwMDY5MDE0NTIwMSIsInVyIjpudWxsLCJoaWRkZW5fYXR0cmlidXRlcyI6WyJtYXN0ZXJfc2VjcmV0Il0sImNvbW1pdHRlZF9hdHRyaWJ1dGVzIjp7fX0sImJsaW5kZWRfbXNfY29ycmVjdG5lc3NfcHJvb2YiOnsiYyI6IjE4MzE5MTUyNjg0NDkyMTM0OTc4MzkzNDE3OTY5NjQ5MDIyNzMzNzQzMTQ5NTUwNzAwNzc5ODk0Nzg1MTg3MTA1OTkyNjk4Mzg5MjAzIiwidl9kYXNoX2NhcCI6IjQ0NzA4MzAwOTUyNzA3MjA3NjI3NjA3NzM4MTI2NDgxNzA3OTA1MDcwNjEyMzQ5OTIxNTAxNTYyOTA2MzgyMzE0MDE4MzQ4MTAxMTE4MDI4MDIyMjk5OTgyNTEwMjI5ODM1OTQxMzY1MTM4Njg0MTU1OTEyOTE3NzYwMjgwOTIyNDk1MjE1ODA2NTM3MzA5NDU5NDA3NjcxNDgyNDA0NDMwODU4MzU5ODU3MDgzNzg1Njk1MTYzMjkwMzEyNzMzNDAxNjY1NDk5MjUwMDQ0NzkwODk4OTA4NzIzNzE1OTc0MDYwNzgyNDEzODYzMTU0MTUxNjg2OTM3ODY4MDM5MDU1Nzc1MzA5MjQ0MTYzOTUxNzgwMTgxNDk5MDM5MDgyMjcxNzgzNTgzNTkxMDIyNjYwMDYyMDQ3MDQ2NTEyMTM0NDU5OTI1MTgyOTg2MTkxOTAwNTQwMDg4MjE3Mzc2NjM4MjEzNTI0MDUxNjcxNzg3ODY0ODQ2NzIxNjk5NjQzNDk0MTI2MjA3NTg2MjgwNjQ5OTc4ODE2ODEwMjM5OTAzMzU0NzIyOTI2NTUxODYyNTQwMjc4ODU2OTEyNDQ2MDUzMTg4MzI3Mjk4NDc0NjgzMjkwMjU4MjgwNjY0OTgyOTM2NTYyODcwNTIyODA2NzYwMjE1OTI0MDc3ODQ2NjA2NTM0NzI4MjkyODQ2MDQyOTk1NjgxMTQzOTQ5MTU0MTU0NTU2NzQzMDYzMzY1OTIzMzU2OTg1MjQ5ODI1Njk2NzE3MzE2MDk1NzE5MDU2MTE5NTE0NTYwODY0MzUyMDc4ODMyOTYzNjI3Mjk0Njk1ODQ0MTE5NDA1NTMzNTY5MTI2ODExODE3NDYyNjczMzM3OTg4MzA0MDcwMzk5ODYyNTk2ODMyNDk2OTU3NzA4Nzc0NzI5NzAyOTEyNjY3Njk0OTgwMjc3OTI5MDgzNiIsIm1fY2FwcyI6eyJtYXN0ZXJfc2VjcmV0IjoiMjIxNTAyNTExNTYzODg1MTM1NzIwNjg4MzE5Njk5NzE5ODAxNDI4NDgzMTI2MjY0NjI0NTE5OTA4MjM5NTM0MjQ3MDQ3NTIwODgyNDE4Mzk0ODMzNjUwODM2NTI0NDk2MzUzMDkwNzIzOTI1MDc2NDY2ODQ3NjIxNzE4MDA4MDAxNTQyNTMzOTk4NTU1MzA1MDYwNjUzMzkwMjc4MDc2MzE2Mjg5MzcwMzA2MDcyMDYxNCJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiI2OTgzNzA2MTYwMjM4ODM3MzA0OTgzNzUifQ==", + }, + "mime-type": "application/json", + }, + ], + "~thread": Object { + "thid": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, + }, + "state": "done", + "threadId": "e2c2194c-6ac6-4b27-9030-18887c79b5eb", + }, +] +`; diff --git a/packages/core/src/storage/migration/__tests__/backup.test.ts b/packages/core/src/storage/migration/__tests__/backup.test.ts new file mode 100644 index 0000000000..b0622903ac --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/backup.test.ts @@ -0,0 +1,148 @@ +import type { StorageUpdateError } from '../error/StorageUpdateError' + +import { readFileSync, unlinkSync } from 'fs' +import path from 'path' +import { container } from 'tsyringe' + +import { getBaseConfig } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { AriesFrameworkError } from '../../../error' +import { CredentialExchangeRecord, CredentialRepository } from '../../../modules/credentials' +import { JsonTransformer } from '../../../utils' +import { StorageUpdateService } from '../StorageUpdateService' +import { UpdateAssistant } from '../UpdateAssistant' + +const { agentDependencies, config } = getBaseConfig('UpdateAssistant | Backup') + +const aliceCredentialRecordsString = readFileSync( + path.join(__dirname, '__fixtures__/alice-4-credentials-0.1.json'), + 'utf8' +) + +const backupDate = new Date('2022-03-21T22:50:20.522Z') +jest.useFakeTimers().setSystemTime(backupDate) +const backupIdentifier = backupDate.getTime() + +describe('UpdateAssistant | Backup', () => { + let updateAssistant: UpdateAssistant + let agent: Agent + let backupPath: string + + beforeEach(async () => { + agent = new Agent(config, agentDependencies, container) + backupPath = `${agent.config.fileSystem.basePath}/afj/migration/backup/${backupIdentifier}` + + // If tests fail it's possible the cleanup has been skipped. So remove before running tests + if (await agent.config.fileSystem.exists(backupPath)) { + unlinkSync(backupPath) + } + if (await agent.config.fileSystem.exists(`${backupPath}-error`)) { + unlinkSync(`${backupPath}-error`) + } + + updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'allMediator', + }, + }) + + await updateAssistant.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('should create a backup', async () => { + const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { + const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) + + record.setTags(data.tags) + return record + }) + + const credentialRepository = agent.injectionContainer.resolve(CredentialRepository) + const storageUpdateService = agent.injectionContainer.resolve(StorageUpdateService) + + // Add 0.1 data and set version to 0.1 + for (const credentialRecord of aliceCredentialRecords) { + await credentialRepository.save(credentialRecord) + } + await storageUpdateService.setCurrentStorageVersion('0.1') + + // Expect an update is needed + expect(await updateAssistant.isUpToDate()).toBe(false) + + const fileSystem = agent.config.fileSystem + // Backup should not exist before update + expect(await fileSystem.exists(backupPath)).toBe(false) + + // Create update + await updateAssistant.update() + + // Backup should exist after update + expect(await fileSystem.exists(backupPath)).toBe(true) + + expect((await credentialRepository.getAll()).sort((a, b) => a.id.localeCompare(b.id))).toMatchSnapshot() + }) + + it('should restore the backup if an error occurs during the update', async () => { + const aliceCredentialRecordsJson = JSON.parse(aliceCredentialRecordsString) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const aliceCredentialRecords = Object.values(aliceCredentialRecordsJson).map((data: any) => { + const record = JsonTransformer.fromJSON(data.value, CredentialExchangeRecord) + + record.setTags(data.tags) + return record + }) + + const credentialRepository = agent.injectionContainer.resolve(CredentialRepository) + const storageUpdateService = agent.injectionContainer.resolve(StorageUpdateService) + + // Add 0.1 data and set version to 0.1 + for (const credentialRecord of aliceCredentialRecords) { + await credentialRepository.save(credentialRecord) + } + await storageUpdateService.setCurrentStorageVersion('0.1') + + // Expect an update is needed + expect(await updateAssistant.isUpToDate()).toBe(false) + jest.spyOn(updateAssistant, 'getNeededUpdates').mockResolvedValue([ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: async () => { + throw new AriesFrameworkError("Uh oh I'm broken") + }, + }, + ]) + + const fileSystem = agent.config.fileSystem + // Backup should not exist before update + expect(await fileSystem.exists(backupPath)).toBe(false) + + let updateError: StorageUpdateError | undefined = undefined + + try { + await updateAssistant.update() + } catch (error) { + updateError = error + } + + expect(updateError?.cause?.message).toEqual("Uh oh I'm broken") + + // Backup should exist after update + expect(await fileSystem.exists(backupPath)).toBe(true) + expect(await fileSystem.exists(`${backupPath}-error`)).toBe(true) + + // Wallet should be same as when we started because of backup + expect((await credentialRepository.getAll()).sort((a, b) => a.id.localeCompare(b.id))).toEqual( + aliceCredentialRecords.sort((a, b) => a.id.localeCompare(b.id)) + ) + }) +}) diff --git a/packages/core/src/storage/migration/__tests__/updates.ts b/packages/core/src/storage/migration/__tests__/updates.ts new file mode 100644 index 0000000000..520f4518ab --- /dev/null +++ b/packages/core/src/storage/migration/__tests__/updates.ts @@ -0,0 +1,16 @@ +import { supportedUpdates } from '../updates' +import { updateV0_1ToV0_2 } from '../updates/0.1-0.2' + +describe('supportedUpdates', () => { + // This test is intentional to be bumped explicitly when a new upgrade is added + it('supports 1 update(s)', () => { + expect(supportedUpdates.length).toBe(1) + }) + + it('supports an update from 0.1 to 0.2', () => { + const upgrade = supportedUpdates[0] + expect(upgrade.fromVersion).toBe('0.1') + expect(upgrade.toVersion).toBe('0.2') + expect(upgrade.doUpdate).toBe(updateV0_1ToV0_2) + }) +}) diff --git a/packages/core/src/storage/migration/error/StorageUpdateError.ts b/packages/core/src/storage/migration/error/StorageUpdateError.ts new file mode 100644 index 0000000000..5ecef032c3 --- /dev/null +++ b/packages/core/src/storage/migration/error/StorageUpdateError.ts @@ -0,0 +1,7 @@ +import { AriesFrameworkError } from '../../../error/AriesFrameworkError' + +export class StorageUpdateError extends AriesFrameworkError { + public constructor(message: string, { cause }: { cause?: Error } = {}) { + super(message, { cause }) + } +} diff --git a/packages/core/src/storage/migration/index.ts b/packages/core/src/storage/migration/index.ts new file mode 100644 index 0000000000..e59ac63479 --- /dev/null +++ b/packages/core/src/storage/migration/index.ts @@ -0,0 +1,3 @@ +export * from './repository/StorageVersionRecord' +export * from './repository/StorageVersionRepository' +export * from './StorageUpdateService' diff --git a/packages/core/src/storage/migration/repository/StorageVersionRecord.ts b/packages/core/src/storage/migration/repository/StorageVersionRecord.ts new file mode 100644 index 0000000000..3d39b652af --- /dev/null +++ b/packages/core/src/storage/migration/repository/StorageVersionRecord.ts @@ -0,0 +1,31 @@ +import type { VersionString } from '../../../utils/version' + +import { uuid } from '../../../utils/uuid' +import { BaseRecord } from '../../BaseRecord' + +export interface StorageVersionRecordProps { + id?: string + createdAt?: Date + storageVersion: VersionString +} + +export class StorageVersionRecord extends BaseRecord { + public storageVersion!: VersionString + + public static readonly type = 'StorageVersionRecord' + public readonly type = StorageVersionRecord.type + + public constructor(props: StorageVersionRecordProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.storageVersion = props.storageVersion + } + } + + public getTags() { + return this._tags + } +} diff --git a/packages/core/src/storage/migration/repository/StorageVersionRepository.ts b/packages/core/src/storage/migration/repository/StorageVersionRepository.ts new file mode 100644 index 0000000000..373b48381c --- /dev/null +++ b/packages/core/src/storage/migration/repository/StorageVersionRepository.ts @@ -0,0 +1,14 @@ +import { inject, scoped, Lifecycle } from 'tsyringe' + +import { InjectionSymbols } from '../../../constants' +import { Repository } from '../../Repository' +import { StorageService } from '../../StorageService' + +import { StorageVersionRecord } from './StorageVersionRecord' + +@scoped(Lifecycle.ContainerScoped) +export class StorageVersionRepository extends Repository { + public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { + super(StorageVersionRecord, storageService) + } +} diff --git a/packages/core/src/storage/migration/updates.ts b/packages/core/src/storage/migration/updates.ts new file mode 100644 index 0000000000..c2f7fabb03 --- /dev/null +++ b/packages/core/src/storage/migration/updates.ts @@ -0,0 +1,34 @@ +import type { Agent } from '../../agent/Agent' +import type { VersionString } from '../../utils/version' +import type { V0_1ToV0_2UpdateConfig } from './updates/0.1-0.2' + +import { updateV0_1ToV0_2 } from './updates/0.1-0.2' + +export const INITIAL_STORAGE_VERSION = '0.1' + +export interface Update { + fromVersion: VersionString + toVersion: VersionString + doUpdate: (agent: Agent, updateConfig: UpdateConfig) => Promise +} + +export interface UpdateConfig { + v0_1ToV0_2: V0_1ToV0_2UpdateConfig +} + +export const DEFAULT_UPDATE_CONFIG: UpdateConfig = { + v0_1ToV0_2: { + mediationRoleUpdateStrategy: 'recipientIfEndpoint', + }, +} + +export const supportedUpdates: Update[] = [ + { + fromVersion: '0.1', + toVersion: '0.2', + doUpdate: updateV0_1ToV0_2, + }, +] + +// Current version is last toVersion from the supported updates +export const CURRENT_FRAMEWORK_STORAGE_VERSION = supportedUpdates[supportedUpdates.length - 1].toVersion diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/credential.test.ts b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/credential.test.ts new file mode 100644 index 0000000000..56d9098826 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/credential.test.ts @@ -0,0 +1,179 @@ +import { CredentialExchangeRecord } from '../../../../../../src/modules/credentials' +import { getAgentConfig, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { CredentialRepository } from '../../../../../modules/credentials/repository/CredentialRepository' +import { JsonTransformer } from '../../../../../utils' +import * as testModule from '../credential' + +const agentConfig = getAgentConfig('Migration CredentialRecord 0.1-0.2') + +jest.mock('../../../../../modules/credentials/repository/CredentialRepository') +const CredentialRepositoryMock = CredentialRepository as jest.Mock +const credentialRepository = new CredentialRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + injectionContainer: { + resolve: jest.fn(() => credentialRepository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.1-0.2 | Credential', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateCredentialRecordToV0_2()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: CredentialExchangeRecord[] = [ + getCredentialWithMetadata({ + schemaId: 'schemaId', + credentialDefinitionId: 'schemaId', + anotherObject: { + someNested: 'value', + }, + requestMetadata: { + the: { + indy: { + meta: 'data', + }, + }, + }, + }), + ] + + mockFunction(credentialRepository.getAll).mockResolvedValue(records) + + await testModule.migrateCredentialRecordToV0_2(agent) + + // FIXME: I can't get a spy / mock for 'updateIndyMetadata' working... + expect(credentialRepository.getAll).toHaveBeenCalledTimes(1) + expect(credentialRepository.update).toHaveBeenCalledTimes(records.length) + + // Check first object is transformed correctly + expect(credentialRepository.update).toHaveBeenNthCalledWith( + 1, + getCredentialWithMetadata({ + '_internal/indyCredential': { + schemaId: 'schemaId', + credentialDefinitionId: 'schemaId', + }, + anotherObject: { + someNested: 'value', + }, + '_internal/indyRequest': { + the: { + indy: { + meta: 'data', + }, + }, + }, + }) + ) + }) + }) + + describe('updateIndyMetadata()', () => { + it('should correctly update the old top-level keys into the nested structure', async () => { + const credentialRecord = getCredentialWithMetadata({ + schemaId: 'schemaId', + credentialDefinitionId: 'schemaId', + anotherObject: { + someNested: 'value', + }, + requestMetadata: { + the: { + indy: { + meta: 'data', + }, + }, + }, + }) + + await testModule.updateIndyMetadata(agent, credentialRecord) + + expect(credentialRecord).toMatchObject({ + metadata: { + data: { + '_internal/indyCredential': { + schemaId: 'schemaId', + credentialDefinitionId: 'schemaId', + }, + anotherObject: { + someNested: 'value', + }, + '_internal/indyRequest': { + the: { + indy: { + meta: 'data', + }, + }, + }, + }, + }, + }) + }) + + it('should not fail if some the top-level metadata keys do not exist', async () => { + const credentialRecord = getCredentialWithMetadata({ + schemaId: 'schemaId', + anotherObject: { + someNested: 'value', + }, + }) + + await testModule.updateIndyMetadata(agent, credentialRecord) + + expect(credentialRecord).toMatchObject({ + metadata: { + data: { + '_internal/indyCredential': { + schemaId: 'schemaId', + }, + anotherObject: { + someNested: 'value', + }, + }, + }, + }) + }) + + it('should not fail if all of the top-level metadata keys do not exist', async () => { + const credentialRecord = getCredentialWithMetadata({ + anotherObject: { + someNested: 'value', + }, + }) + + await testModule.updateIndyMetadata(agent, credentialRecord) + + expect(credentialRecord).toMatchObject({ + metadata: { + data: { + anotherObject: { + someNested: 'value', + }, + }, + }, + }) + }) + }) +}) + +function getCredentialWithMetadata(metadata: Record) { + return JsonTransformer.fromJSON( + { + metadata, + }, + CredentialExchangeRecord + ) +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/mediation.test.ts b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/mediation.test.ts new file mode 100644 index 0000000000..ef1ea91062 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/mediation.test.ts @@ -0,0 +1,181 @@ +import { getAgentConfig, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { MediationRole, MediationRecord } from '../../../../../modules/routing' +import { MediationRepository } from '../../../../../modules/routing/repository/MediationRepository' +import { JsonTransformer } from '../../../../../utils' +import * as testModule from '../mediation' + +const agentConfig = getAgentConfig('Migration MediationRecord 0.1-0.2') + +jest.mock('../../../../../modules/routing/repository/MediationRepository') +const MediationRepositoryMock = MediationRepository as jest.Mock +const mediationRepository = new MediationRepositoryMock() + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + injectionContainer: { + resolve: jest.fn(() => mediationRepository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.1-0.2 | Mediation', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateMediationRecordToV0_2()', () => { + it('should fetch all records and apply the needed updates ', async () => { + const records: MediationRecord[] = [ + getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'firstEndpoint', + }), + getMediationRecord({ + role: MediationRole.Recipient, + endpoint: 'secondEndpoint', + }), + ] + + mockFunction(mediationRepository.getAll).mockResolvedValue(records) + + await testModule.migrateMediationRecordToV0_2(agent, { + mediationRoleUpdateStrategy: 'allMediator', + }) + + expect(mediationRepository.getAll).toHaveBeenCalledTimes(1) + expect(mediationRepository.update).toHaveBeenCalledTimes(records.length) + + // Check second object is transformed correctly + expect(mediationRepository.update).toHaveBeenNthCalledWith( + 2, + getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'secondEndpoint', + }) + ) + + expect(records).toMatchObject([ + { + role: MediationRole.Mediator, + endpoint: 'firstEndpoint', + }, + { + role: MediationRole.Mediator, + endpoint: 'secondEndpoint', + }, + ]) + }) + }) + + describe('updateMediationRole()', () => { + it(`should update the role to ${MediationRole.Mediator} if no endpoint exists on the record and mediationRoleUpdateStrategy is 'recipientIfEndpoint'`, async () => { + const mediationRecord = getMediationRecord({ + role: MediationRole.Recipient, + }) + + await testModule.updateMediationRole(agent, mediationRecord, { + mediationRoleUpdateStrategy: 'recipientIfEndpoint', + }) + + expect(mediationRecord).toMatchObject({ + role: MediationRole.Mediator, + }) + }) + + it(`should update the role to ${MediationRole.Recipient} if an endpoint exists on the record and mediationRoleUpdateStrategy is 'recipientIfEndpoint'`, async () => { + const mediationRecord = getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecord, { + mediationRoleUpdateStrategy: 'recipientIfEndpoint', + }) + + expect(mediationRecord).toMatchObject({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + }) + + it(`should not update the role if mediationRoleUpdateStrategy is 'doNotChange'`, async () => { + const mediationRecordMediator = getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + const mediationRecordRecipient = getMediationRecord({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecordMediator, { + mediationRoleUpdateStrategy: 'doNotChange', + }) + + expect(mediationRecordMediator).toMatchObject({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecordRecipient, { + mediationRoleUpdateStrategy: 'doNotChange', + }) + + expect(mediationRecordRecipient).toMatchObject({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + }) + + it(`should update the role to ${MediationRole.Recipient} if mediationRoleUpdateStrategy is 'allRecipient'`, async () => { + const mediationRecord = getMediationRecord({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecord, { + mediationRoleUpdateStrategy: 'allRecipient', + }) + + expect(mediationRecord).toMatchObject({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + }) + + it(`should update the role to ${MediationRole.Mediator} if mediationRoleUpdateStrategy is 'allMediator'`, async () => { + const mediationRecord = getMediationRecord({ + role: MediationRole.Recipient, + endpoint: 'something', + }) + + await testModule.updateMediationRole(agent, mediationRecord, { + mediationRoleUpdateStrategy: 'allMediator', + }) + + expect(mediationRecord).toMatchObject({ + role: MediationRole.Mediator, + endpoint: 'something', + }) + }) + }) +}) + +function getMediationRecord({ role, endpoint }: { role: MediationRole; endpoint?: string }) { + return JsonTransformer.fromJSON( + { + role, + endpoint, + }, + MediationRecord + ) +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts b/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts new file mode 100644 index 0000000000..838b1271f3 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/credential.ts @@ -0,0 +1,93 @@ +import type { Agent } from '../../../../agent/Agent' +import type { CredentialMetadata, CredentialExchangeRecord } from '../../../../modules/credentials' + +import { CredentialMetadataKeys } from '../../../../modules/credentials/repository/CredentialMetadataTypes' +import { CredentialRepository } from '../../../../modules/credentials/repository/CredentialRepository' +import { Metadata } from '../../../Metadata' + +/** + * Migrates the {@link CredentialRecord} to 0.2 compatible format. It fetches all records from storage + * and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link updateIndyMetadata} + */ +export async function migrateCredentialRecordToV0_2(agent: Agent) { + agent.config.logger.info('Migrating credential records to storage version 0.2') + const credentialRepository = agent.injectionContainer.resolve(CredentialRepository) + + agent.config.logger.debug(`Fetching all credential records from storage`) + const allCredentials = await credentialRepository.getAll() + + agent.config.logger.debug(`Found a total of ${allCredentials.length} credential records to update.`) + for (const credentialRecord of allCredentials) { + agent.config.logger.debug(`Migrating credential record with id ${credentialRecord.id} to storage version 0.2`) + + await updateIndyMetadata(agent, credentialRecord) + + await credentialRepository.update(credentialRecord) + + agent.config.logger.debug( + `Successfully migrated credential record with id ${credentialRecord.id} to storage version 0.2` + ) + } +} + +/** + * The credential record had a custom `metadata` property in pre-0.1.0 storage that contained the `requestMetadata`, `schemaId` and `credentialDefinition` + * properties. Later a generic metadata API was added that only allows objects to be stored. Therefore the properties were moved into a different structure. + * + * This migration method updates the top level properties to the new nested metadata structure. + * + * The following pre-0.1.0 structure: + * + * ```json + * { + * "requestMetadata": "", + * "schemaId": "", + * "credentialDefinitionId": "" + * } + * ``` + * + * Will be transformed into the following 0.2.0 structure: + * + * ```json + * { + * "_internal/indyRequest": , + * "_internal/indyCredential": { + * "schemaId": "", + * "credentialDefinitionId": "" + * } + * } + * ``` + */ +export async function updateIndyMetadata(agent: Agent, credentialRecord: CredentialExchangeRecord) { + agent.config.logger.debug(`Updating indy metadata to use the generic metadata api available to records.`) + + const { requestMetadata, schemaId, credentialDefinitionId, ...rest } = credentialRecord.metadata.data + const metadata = new Metadata(rest) + + if (requestMetadata) { + agent.config.logger.trace( + `Found top-level 'requestMetadata' key, moving to '${CredentialMetadataKeys.IndyRequest}'` + ) + metadata.add(CredentialMetadataKeys.IndyRequest, { ...requestMetadata }) + } + + if (schemaId && typeof schemaId === 'string') { + agent.config.logger.trace( + `Found top-level 'schemaId' key, moving to '${CredentialMetadataKeys.IndyCredential}.schemaId'` + ) + metadata.add(CredentialMetadataKeys.IndyCredential, { schemaId }) + } + + if (credentialDefinitionId && typeof credentialDefinitionId === 'string') { + agent.config.logger.trace( + `Found top-level 'credentialDefinitionId' key, moving to '${CredentialMetadataKeys.IndyCredential}.credentialDefinitionId'` + ) + metadata.add(CredentialMetadataKeys.IndyCredential, { credentialDefinitionId }) + } + + credentialRecord.metadata = metadata +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/index.ts b/packages/core/src/storage/migration/updates/0.1-0.2/index.ts new file mode 100644 index 0000000000..5d5c7df79b --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/index.ts @@ -0,0 +1,14 @@ +import type { Agent } from '../../../../agent/Agent' +import type { UpdateConfig } from '../../updates' + +import { migrateCredentialRecordToV0_2 } from './credential' +import { migrateMediationRecordToV0_2 } from './mediation' + +export interface V0_1ToV0_2UpdateConfig { + mediationRoleUpdateStrategy: 'allMediator' | 'allRecipient' | 'recipientIfEndpoint' | 'doNotChange' +} + +export async function updateV0_1ToV0_2(agent: Agent, config: UpdateConfig): Promise { + await migrateCredentialRecordToV0_2(agent) + await migrateMediationRecordToV0_2(agent, config.v0_1ToV0_2) +} diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/mediation.ts b/packages/core/src/storage/migration/updates/0.1-0.2/mediation.ts new file mode 100644 index 0000000000..7976f4037f --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.1-0.2/mediation.ts @@ -0,0 +1,79 @@ +import type { V0_1ToV0_2UpdateConfig } from '.' +import type { Agent } from '../../../../agent/Agent' +import type { MediationRecord } from '../../../../modules/routing' + +import { MediationRepository, MediationRole } from '../../../../modules/routing' + +/** + * Migrates the {@link MediationRecord} to 0.2 compatible format. It fetches all records from storage + * and applies the needed updates to the records. After a record has been transformed, it is updated + * in storage and the next record will be transformed. + * + * The following transformations are applied: + * - {@link updateMediationRole} + */ +export async function migrateMediationRecordToV0_2(agent: Agent, upgradeConfig: V0_1ToV0_2UpdateConfig) { + agent.config.logger.info('Migrating mediation records to storage version 0.2') + const mediationRepository = agent.injectionContainer.resolve(MediationRepository) + + agent.config.logger.debug(`Fetching all mediation records from storage`) + const allMediationRecords = await mediationRepository.getAll() + + agent.config.logger.debug(`Found a total of ${allMediationRecords.length} mediation records to update.`) + for (const mediationRecord of allMediationRecords) { + agent.config.logger.debug(`Migrating mediation record with id ${mediationRecord.id} to storage version 0.2`) + + await updateMediationRole(agent, mediationRecord, upgradeConfig) + + await mediationRepository.update(mediationRecord) + + agent.config.logger.debug( + `Successfully migrated mediation record with id ${mediationRecord.id} to storage version 0.2` + ) + } +} + +/** + * The role in the mediation record was always being set to {@link MediationRole.Mediator} for both mediators and recipients. This didn't cause any issues, but would return the wrong role for recipients. + * + * In 0.2 a check is added to make sure the role of a mediation record matches with actions (e.g. a recipient can't grant mediation), which means it will throw an error if the role is not set correctly. + * + * Because it's not always possible detect whether the role should actually be mediator or recipient, a number of configuration options are provided on how the role should be updated: + * + * - `allMediator`: The role is set to {@link MediationRole.Mediator} for both mediators and recipients + * - `allRecipient`: The role is set to {@link MediationRole.Recipient} for both mediators and recipients + * - `recipientIfEndpoint`: The role is set to {@link MediationRole.Recipient} if their is an `endpoint` configured on the record otherwise it is set to {@link MediationRole.Mediator}. + * The endpoint is not set when running as a mediator, so in theory this allows to determine the role of the record. + * There is one case where this could be problematic when the role should be recipient, if the mediation grant hasn't actually occurred (meaning the endpoint is not set). + * - `doNotChange`: The role is not changed + * + * Most agents only act as either the role of mediator or recipient, in which case the `allMediator` or `allRecipient` configuration is the most appropriate. If your agent acts as both a recipient and mediator, the `recipientIfEndpoint` configuration is the most appropriate. The `doNotChange` options is not recommended and can lead to errors if the role is not set correctly. + * + */ +export async function updateMediationRole( + agent: Agent, + mediationRecord: MediationRecord, + { mediationRoleUpdateStrategy }: V0_1ToV0_2UpdateConfig +) { + agent.config.logger.debug(`Updating mediation record role using strategy '${mediationRoleUpdateStrategy}'`) + + switch (mediationRoleUpdateStrategy) { + case 'allMediator': + mediationRecord.role = MediationRole.Mediator + break + case 'allRecipient': + mediationRecord.role = MediationRole.Recipient + break + case 'recipientIfEndpoint': + if (mediationRecord.endpoint) { + agent.config.logger.debug('Mediation record endpoint is set, setting mediation role to recipient') + mediationRecord.role = MediationRole.Recipient + } else { + agent.config.logger.debug('Mediation record endpoint is not set, setting mediation role to mediator') + mediationRecord.role = MediationRole.Mediator + } + break + case 'doNotChange': + break + } +} diff --git a/packages/core/src/transport/HttpOutboundTransport.ts b/packages/core/src/transport/HttpOutboundTransport.ts index 05ee409a95..0d157483c9 100644 --- a/packages/core/src/transport/HttpOutboundTransport.ts +++ b/packages/core/src/transport/HttpOutboundTransport.ts @@ -8,6 +8,7 @@ import { AbortController } from 'abort-controller' import { AgentConfig } from '../agent/AgentConfig' import { AriesFrameworkError } from '../error/AriesFrameworkError' +import { isValidJweStructure, JsonEncoder } from '../utils' export class HttpOutboundTransport implements OutboundTransport { private agent!: Agent @@ -76,8 +77,14 @@ export class HttpOutboundTransport implements OutboundTransport { this.logger.debug(`Response received`, { responseMessage, status: response.status }) try { - const wireMessage = JSON.parse(responseMessage) - this.agent.receiveMessage(wireMessage) + const encryptedMessage = JsonEncoder.fromString(responseMessage) + if (!isValidJweStructure(encryptedMessage)) { + this.logger.error( + `Received a response from the other agent but the structure of the incoming message is not a DIDComm message: ${responseMessage}` + ) + return + } + await this.agent.receiveMessage(encryptedMessage) } catch (error) { this.logger.debug('Unable to parse response message') } diff --git a/packages/core/src/transport/WsOutboundTransport.ts b/packages/core/src/transport/WsOutboundTransport.ts index b25da0e06b..594012e4e5 100644 --- a/packages/core/src/transport/WsOutboundTransport.ts +++ b/packages/core/src/transport/WsOutboundTransport.ts @@ -8,6 +8,7 @@ import type WebSocket from 'ws' import { AgentConfig } from '../agent/AgentConfig' import { EventEmitter } from '../agent/EventEmitter' import { AriesFrameworkError } from '../error/AriesFrameworkError' +import { isValidJweStructure, JsonEncoder } from '../utils' import { Buffer } from '../utils/buffer' import { TransportEventTypes } from './TransportEventTypes' @@ -101,9 +102,14 @@ export class WsOutboundTransport implements OutboundTransport { // eslint-disable-next-line @typescript-eslint/no-explicit-any private handleMessageEvent = (event: any) => { this.logger.trace('WebSocket message event received.', { url: event.target.url, data: event.data }) - const payload = JSON.parse(Buffer.from(event.data).toString('utf-8')) + const payload = JsonEncoder.fromBuffer(event.data) + if (!isValidJweStructure(payload)) { + throw new Error( + `Received a response from the other agent but the structure of the incoming message is not a DIDComm message: ${payload}` + ) + } this.logger.debug('Payload received from mediator:', payload) - this.agent.receiveMessage(payload) + void this.agent.receiveMessage(payload) } private listenOnWebSocketMessages(socket: WebSocket) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c2c09faf05..806e8b25f9 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,17 +1,47 @@ import type { AgentMessage } from './agent/AgentMessage' +import type { ResolvedDidCommService } from './agent/MessageSender' +import type { Key } from './crypto' import type { Logger } from './logger' -import type { ConnectionRecord, DidCommService } from './modules/connections' +import type { ConnectionRecord } from './modules/connections' import type { AutoAcceptCredential } from './modules/credentials/CredentialAutoAcceptType' import type { IndyPoolConfig } from './modules/ledger/IndyPool' +import type { OutOfBandRecord } from './modules/oob/repository' import type { AutoAcceptProof } from './modules/proofs' import type { MediatorPickupStrategy } from './modules/routing' +export const enum KeyDerivationMethod { + /** default value in indy-sdk. Will be used when no value is provided */ + Argon2IMod = 'ARGON2I_MOD', + /** less secure, but faster */ + Argon2IInt = 'ARGON2I_INT', + /** raw wallet master key */ + Raw = 'RAW', +} + export interface WalletConfig { id: string key: string + keyDerivationMethod?: KeyDerivationMethod + storage?: { + type: string + [key: string]: unknown + } +} + +export interface WalletConfigRekey { + id: string + key: string + rekey: string + keyDerivationMethod?: KeyDerivationMethod + rekeyDerivationMethod?: KeyDerivationMethod +} + +export interface WalletExportImportConfig { + key: string + path: string } -export type WireMessage = { +export type EncryptedMessage = { protected: unknown iv: unknown ciphertext: unknown @@ -36,6 +66,7 @@ export interface InitConfig { didCommMimeType?: DidCommMimeType indyLedgers?: IndyPoolConfig[] + connectToIndyLedgersOnStartup?: boolean autoAcceptMediationRequests?: boolean mediatorConnectionsInvite?: string @@ -43,36 +74,42 @@ export interface InitConfig { clearDefaultMediator?: boolean mediatorPollingInterval?: number mediatorPickupStrategy?: MediatorPickupStrategy + maximumMessagePickup?: number useLegacyDidSovPrefix?: boolean connectionImageUrl?: string + + autoUpdateStorageOnStartup?: boolean } -export interface UnpackedMessage { +export interface PlaintextMessage { '@type': string + '@id': string [key: string]: unknown } -export interface UnpackedMessageContext { - message: UnpackedMessage - senderVerkey?: string - recipientVerkey?: string -} - export interface OutboundMessage { payload: T connection: ConnectionRecord + sessionId?: string + outOfBand?: OutOfBandRecord } export interface OutboundServiceMessage { payload: T - service: DidCommService - senderKey: string + service: ResolvedDidCommService + senderKey: Key } export interface OutboundPackage { - payload: WireMessage + payload: EncryptedMessage responseRequested?: boolean endpoint?: string connectionId?: string } + +export type JsonValue = string | number | boolean | null | JsonObject | JsonArray +export type JsonArray = Array +export interface JsonObject { + [property: string]: JsonValue +} diff --git a/packages/core/src/utils/Hasher.ts b/packages/core/src/utils/Hasher.ts new file mode 100644 index 0000000000..023a69c708 --- /dev/null +++ b/packages/core/src/utils/Hasher.ts @@ -0,0 +1,23 @@ +import { hash as sha256 } from '@stablelib/sha256' + +export type HashName = 'sha2-256' + +type HashingMap = { + [key in HashName]: (data: Uint8Array) => Uint8Array +} + +const hashingMap: HashingMap = { + 'sha2-256': (data) => sha256(data), +} + +export class Hasher { + public static hash(data: Uint8Array, hashName: HashName): Uint8Array { + const hashFn = hashingMap[hashName] + + if (!hashFn) { + throw new Error(`Unsupported hash name '${hashName}'`) + } + + return hashFn(data) + } +} diff --git a/packages/core/src/utils/HashlinkEncoder.ts b/packages/core/src/utils/HashlinkEncoder.ts index 95d947db20..6791b514f3 100644 --- a/packages/core/src/utils/HashlinkEncoder.ts +++ b/packages/core/src/utils/HashlinkEncoder.ts @@ -1,12 +1,11 @@ +import type { HashName } from './Hasher' import type { BaseName } from './MultiBaseEncoder' import type { Buffer } from './buffer' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ts is giving me headaches because this package has no types import cbor from 'borc' -import { sha256 } from 'js-sha256' -import { BufferEncoder } from './BufferEncoder' import { MultiBaseEncoder } from './MultiBaseEncoder' import { MultiHashEncoder } from './MultiHashEncoder' @@ -38,7 +37,7 @@ export class HashlinkEncoder { */ public static encode( buffer: Buffer | Uint8Array, - hashAlgorithm: 'sha2-256', + hashAlgorithm: HashName, baseEncoding: BaseName = 'base58btc', metadata?: Metadata ) { @@ -84,15 +83,13 @@ export class HashlinkEncoder { } private static encodeMultiHash( - buffer: Buffer | Uint8Array, - hashName: 'sha2-256', + data: Buffer | Uint8Array, + hashName: HashName, baseEncoding: BaseName = 'base58btc' ): string { - // TODO: Support more hashing algorithms - const hash = new Uint8Array(sha256.array(buffer)) - const mh = MultiHashEncoder.encode(hash, hashName) + const mh = MultiHashEncoder.encode(data, hashName) const mb = MultiBaseEncoder.encode(mh, baseEncoding) - return BufferEncoder.toUtf8String(mb) + return mb } private static encodeMetadata(metadata: Metadata, baseEncoding: BaseName): string { @@ -110,7 +107,7 @@ export class HashlinkEncoder { const multibaseMetadata = MultiBaseEncoder.encode(cborData, baseEncoding) - return BufferEncoder.toUtf8String(multibaseMetadata) + return multibaseMetadata } private static decodeMetadata(mb: string): Metadata { diff --git a/packages/core/src/utils/JWE.ts b/packages/core/src/utils/JWE.ts new file mode 100644 index 0000000000..d8d7909b65 --- /dev/null +++ b/packages/core/src/utils/JWE.ts @@ -0,0 +1,6 @@ +import type { EncryptedMessage } from '../types' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isValidJweStructure(message: any): message is EncryptedMessage { + return message && typeof message === 'object' && message.protected && message.iv && message.ciphertext && message.tag +} diff --git a/packages/core/src/utils/MultiBaseEncoder.ts b/packages/core/src/utils/MultiBaseEncoder.ts index 372e6473f8..2a1e19e125 100644 --- a/packages/core/src/utils/MultiBaseEncoder.ts +++ b/packages/core/src/utils/MultiBaseEncoder.ts @@ -1,45 +1,66 @@ -import multibase from 'multibase' +import { decodeFromBase58, encodeToBase58 } from './base58' -export type BaseName = multibase.BaseName +export type BaseName = 'base58btc' + +type EncodingMap = { + [key in BaseName]: (data: Uint8Array) => string +} + +type DecodingMap = { + [key: string]: (data: string) => { data: Uint8Array; baseName: BaseName } +} + +const multibaseEncodingMap: EncodingMap = { + base58btc: (data) => `z${encodeToBase58(data)}`, +} + +const multibaseDecodingMap: DecodingMap = { + z: (data) => ({ data: decodeFromBase58(data.substring(1)), baseName: 'base58btc' }), +} export class MultiBaseEncoder { /** * * Encodes a buffer into a multibase * - * @param {Uint8Array} buffer the buffer that has to be encoded - * @param {multibase.BaseName} baseName the encoding algorithm + * @param buffer the buffer that has to be encoded + * @param baseName the encoding algorithm */ - public static encode(buffer: Uint8Array, baseName: multibase.BaseName = 'base58btc') { - return multibase.encode(baseName, buffer) + public static encode(buffer: Uint8Array, baseName: BaseName) { + const encode = multibaseEncodingMap[baseName] + + if (!encode) { + throw new Error(`Unsupported encoding '${baseName}'`) + } + + return encode(buffer) } /** * * Decodes a multibase into a Uint8Array * - * @param {string} data the multibase that has to be decoded + * @param data the multibase that has to be decoded * - * @returns {Uint8array} data the decoded multibase - * @returns {string} encodingAlgorithm name of the encoding algorithm + * @returns decoded data and the multi base name */ - public static decode(data: string | Uint8Array): { data: Uint8Array; baseName: string } { - if (this.isValid(data)) { - const baseName = multibase.encodingFromData(data).name - return { data: multibase.decode(data), baseName } + public static decode(data: string): { data: Uint8Array; baseName: string } { + const prefix = data[0] + const decode = multibaseDecodingMap[prefix] + + if (!decode) { + throw new Error(`No decoder found for multibase prefix '${prefix}'`) } - throw new Error(`Invalid multibase: ${data}`) + + return decode(data) } - /** - * - * Validates if it is a valid multibase encoded value - * - * @param {Uint8Array} data the multibase that needs to be validated - * - * @returns {boolean} bool whether the multibase value is encoded - */ - public static isValid(data: string | Uint8Array): boolean { - return multibase.isEncoded(data) ? true : false + public static isValid(data: string): boolean { + try { + MultiBaseEncoder.decode(data) + return true + } catch (error) { + return false + } } } diff --git a/packages/core/src/utils/MultiHashEncoder.ts b/packages/core/src/utils/MultiHashEncoder.ts index 8315de74f0..43a333d495 100644 --- a/packages/core/src/utils/MultiHashEncoder.ts +++ b/packages/core/src/utils/MultiHashEncoder.ts @@ -1,4 +1,25 @@ -import * as multihash from 'multihashes' +import type { HashName } from './Hasher' + +import { Hasher } from './Hasher' +import { VarintEncoder } from './VarintEncoder' +import { Buffer } from './buffer' + +type MultiHashNameMap = { + [key in HashName]: number +} + +type MultiHashCodeMap = { + [key: number]: HashName +} + +const multiHashNameMap: MultiHashNameMap = { + 'sha2-256': 0x12, +} + +const multiHashCodeMap: MultiHashCodeMap = Object.entries(multiHashNameMap).reduce( + (map, [hashName, hashCode]) => ({ ...map, [hashCode]: hashName }), + {} +) export class MultiHashEncoder { /** @@ -10,8 +31,14 @@ export class MultiHashEncoder { * * @returns a multihash */ - public static encode(buffer: Uint8Array, hashName: 'sha2-256'): Uint8Array { - return multihash.encode(buffer, hashName) + public static encode(data: Uint8Array, hashName: 'sha2-256'): Buffer { + const hash = Hasher.hash(data, hashName) + const hashCode = multiHashNameMap[hashName] + + const hashPrefix = VarintEncoder.encode(hashCode) + const hashLengthPrefix = VarintEncoder.encode(hash.length) + + return Buffer.concat([hashPrefix, hashLengthPrefix, hash]) } /** @@ -22,12 +49,23 @@ export class MultiHashEncoder { * * @returns object with the data and the hashing algorithm */ - public static decode(data: Uint8Array): { data: Uint8Array; hashName: string } { - if (this.isValid(data)) { - const decodedHash = multihash.decode(data) - return { data: decodedHash.digest, hashName: decodedHash.name } + public static decode(data: Uint8Array): { data: Buffer; hashName: string } { + const [hashPrefix, hashPrefixByteLength] = VarintEncoder.decode(data) + const withoutHashPrefix = data.slice(hashPrefixByteLength) + + const [, lengthPrefixByteLength] = VarintEncoder.decode(withoutHashPrefix) + const withoutLengthPrefix = withoutHashPrefix.slice(lengthPrefixByteLength) + + const hashName = multiHashCodeMap[hashPrefix] + + if (!hashName) { + throw new Error(`Unsupported hash code 0x${hashPrefix.toString(16)}`) + } + + return { + data: Buffer.from(withoutLengthPrefix), + hashName: multiHashCodeMap[hashPrefix], } - throw new Error(`Invalid multihash: ${data}`) } /** @@ -40,7 +78,7 @@ export class MultiHashEncoder { */ public static isValid(data: Uint8Array): boolean { try { - multihash.validate(data) + MultiHashEncoder.decode(data) return true } catch (e) { return false diff --git a/packages/core/src/utils/BufferEncoder.ts b/packages/core/src/utils/TypedArrayEncoder.ts similarity index 68% rename from packages/core/src/utils/BufferEncoder.ts rename to packages/core/src/utils/TypedArrayEncoder.ts index 18407796d2..685eac485c 100644 --- a/packages/core/src/utils/BufferEncoder.ts +++ b/packages/core/src/utils/TypedArrayEncoder.ts @@ -2,7 +2,7 @@ import { decodeFromBase58, encodeToBase58 } from './base58' import { base64ToBase64URL } from './base64' import { Buffer } from './buffer' -export class BufferEncoder { +export class TypedArrayEncoder { /** * Encode buffer into base64 string. * @@ -18,7 +18,7 @@ export class BufferEncoder { * @param buffer the buffer to encode into base64url string */ public static toBase64URL(buffer: Buffer) { - return base64ToBase64URL(BufferEncoder.toBase64(buffer)) + return base64ToBase64URL(TypedArrayEncoder.toBase64(buffer)) } /** @@ -53,11 +53,25 @@ export class BufferEncoder { * * @param str the string to decode into buffer format */ - public static fromString(str: string): Uint8Array { + public static fromString(str: string): Buffer { return Buffer.from(str) } public static toUtf8String(buffer: Buffer | Uint8Array) { return Buffer.from(buffer).toString() } + + /** + * Check whether an array is byte, or typed, array + * + * @param array unknown The array that has to be checked + * + * @returns A boolean if the array is a byte array + */ + public static isTypedArray(array: unknown): boolean { + // Checks whether the static property 'BYTES_PER_ELEMENT' exists on the provided array. + // This has to be done, since the TypedArrays, e.g. Uint8Array and Float32Array, do not + // extend a single base class + return 'BYTES_PER_ELEMENT' in (array as Record) + } } diff --git a/packages/core/src/utils/VarintEncoder.ts b/packages/core/src/utils/VarintEncoder.ts new file mode 100644 index 0000000000..d8a16699da --- /dev/null +++ b/packages/core/src/utils/VarintEncoder.ts @@ -0,0 +1,20 @@ +import { decode, encode, encodingLength } from 'varint' + +import { Buffer } from './buffer' + +export class VarintEncoder { + public static decode(data: Uint8Array | number[] | Buffer) { + const code = decode(data) + return [code, decode.bytes] as const + } + + public static encode(int: number) { + const target = new Buffer(VarintEncoder.encodingLength(int)) + encode(int, target) + return target + } + + public static encodingLength(int: number) { + return encodingLength(int) + } +} diff --git a/packages/core/src/utils/__tests__/HashlinkEncoder.test.ts b/packages/core/src/utils/__tests__/HashlinkEncoder.test.ts index 30700569b0..adf916866c 100644 --- a/packages/core/src/utils/__tests__/HashlinkEncoder.test.ts +++ b/packages/core/src/utils/__tests__/HashlinkEncoder.test.ts @@ -1,4 +1,5 @@ import { HashlinkEncoder } from '../HashlinkEncoder' +import { Buffer } from '../buffer' const validData = { data: Buffer.from('Hello World!'), diff --git a/packages/core/src/utils/__tests__/JWE.test.ts b/packages/core/src/utils/__tests__/JWE.test.ts new file mode 100644 index 0000000000..80d1af2ae8 --- /dev/null +++ b/packages/core/src/utils/__tests__/JWE.test.ts @@ -0,0 +1,19 @@ +import { isValidJweStructure } from '../JWE' + +describe('ValidJWEStructure', () => { + test('throws error when the response message has an invalid JWE structure', async () => { + const responseMessage = 'invalid JWE structure' + await expect(isValidJweStructure(responseMessage)).toBeFalsy() + }) + + test('valid JWE structure', async () => { + const responseMessage = { + protected: + 'eyJlbmMiOiJ4Y2hhY2hhMjBwb2x5MTMwNV9pZXRmIiwidHlwIjoiSldNLzEuMCIsImFsZyI6IkF1dGhjcnlwdCIsInJlY2lwaWVudHMiOlt7ImVuY3J5cHRlZF9rZXkiOiJNYUNKa3B1YzltZWxnblEtUk8teWtsQWRBWWxzY21GdFEzd1hjZ3R0R0dlSmVsZDBEc2pmTUpSWUtYUDA0cTQ2IiwiaGVhZGVyIjp7ImtpZCI6IkJid2ZCaDZ3bWdZUnJ1TlozZXhFelk2RXBLS2g4cGNob211eDJQUjg5bURlIiwiaXYiOiJOWVJGb0xoUG1EZlFhQ3czUzQ2RmM5M1lucWhDUnhKbiIsInNlbmRlciI6IkRIQ0lsdE5tcEgwRlRrd3NuVGNSWXgwZmYzTHBQTlF6VG1jbUdhRW83aGU5d19ERkFmemNTWFdhOEFnNzRHVEpfdnBpNWtzQkQ3MWYwYjI2VF9mVHBfV2FscTBlWUhmeTE4ZEszejhUTkJFQURpZ1VPWi1wR21pV3FrUT0ifX1dfQ==', + iv: 'KNezOOt7JJtuU2q1', + ciphertext: 'mwRMpVg9wkF4rIZcBeWLcc0fWhs=', + tag: '0yW0Lx8-vWevj3if91R06g==', + } + await expect(isValidJweStructure(responseMessage)).toBeTruthy() + }) +}) diff --git a/packages/core/src/utils/__tests__/JsonTransformer.test.ts b/packages/core/src/utils/__tests__/JsonTransformer.test.ts index 44c272f30a..71cce2e7d5 100644 --- a/packages/core/src/utils/__tests__/JsonTransformer.test.ts +++ b/packages/core/src/utils/__tests__/JsonTransformer.test.ts @@ -1,4 +1,4 @@ -import { ConnectionInvitationMessage, ConnectionRecord, DidDoc } from '../../modules/connections' +import { ConnectionInvitationMessage, ConnectionRecord } from '../../modules/connections' import { JsonTransformer } from '../JsonTransformer' describe('JsonTransformer', () => { @@ -69,12 +69,13 @@ describe('JsonTransformer', () => { expect(JsonTransformer.deserialize(jsonString, ConnectionInvitationMessage)).toEqual(invitation) }) - it('transforms JSON string to nested class instance', () => { + // TODO Use other testing object than connection because it does not contain `didDoc` anymore + it.skip('transforms JSON string to nested class instance', () => { const connectionString = `{"createdAt":"2021-06-06T10:16:02.740Z","did":"5AhYREdFcNAdxMhuFfMrG8","didDoc":{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"5AhYREdFcNAdxMhuFfMrG8#1","controller":"5AhYREdFcNAdxMhuFfMrG8","type":"Ed25519VerificationKey2018","publicKeyBase58":"3GjajqxDHZfD4FCpMsA6K5mey782oVJgizapkYUTkYJC"}],"service":[{"id":"5AhYREdFcNAdxMhuFfMrG8#did-communication","serviceEndpoint":"didcomm:transport/queue","type":"did-communication","priority":1,"recipientKeys":["3GjajqxDHZfD4FCpMsA6K5mey782oVJgizapkYUTkYJC"],"routingKeys":[]},{"id":"5AhYREdFcNAdxMhuFfMrG8#IndyAgentService","serviceEndpoint":"didcomm:transport/queue","type":"IndyAgent","priority":0,"recipientKeys":["3GjajqxDHZfD4FCpMsA6K5mey782oVJgizapkYUTkYJC"],"routingKeys":[]}],"authentication":[{"publicKey":"5AhYREdFcNAdxMhuFfMrG8#1","type":"Ed25519SignatureAuthentication2018"}],"id":"5AhYREdFcNAdxMhuFfMrG8"},"verkey":"3GjajqxDHZfD4FCpMsA6K5mey782oVJgizapkYUTkYJC","state":"complete","role":"invitee","alias":"Mediator","invitation":{"@type":"https://didcomm.org/connections/1.0/invitation","@id":"f2938e83-4ea4-44ef-acb1-be2351112fec","label":"RoutingMediator02","recipientKeys":["DHf1TwnRHQdkdTUFAoSdQBPrVToNK6ULHo165Cbq7woB"],"serviceEndpoint":"https://mediator.animo.id/msg","routingKeys":[]},"theirDid":"PYYVEngpK4wsWM5aQuBQt5","theirDidDoc":{"@context":"https://w3id.org/did/v1","publicKey":[{"id":"PYYVEngpK4wsWM5aQuBQt5#1","controller":"PYYVEngpK4wsWM5aQuBQt5","type":"Ed25519VerificationKey2018","publicKeyBase58":"DHf1TwnRHQdkdTUFAoSdQBPrVToNK6ULHo165Cbq7woB"}],"service":[{"id":"PYYVEngpK4wsWM5aQuBQt5#did-communication","serviceEndpoint":"https://mediator.animo.id/msg","type":"did-communication","priority":1,"recipientKeys":["DHf1TwnRHQdkdTUFAoSdQBPrVToNK6ULHo165Cbq7woB"],"routingKeys":[]},{"id":"PYYVEngpK4wsWM5aQuBQt5#IndyAgentService","serviceEndpoint":"https://mediator.animo.id/msg","type":"IndyAgent","priority":0,"recipientKeys":["DHf1TwnRHQdkdTUFAoSdQBPrVToNK6ULHo165Cbq7woB"],"routingKeys":[]}],"authentication":[{"publicKey":"PYYVEngpK4wsWM5aQuBQt5#1","type":"Ed25519SignatureAuthentication2018"}],"id":"PYYVEngpK4wsWM5aQuBQt5"}}` const connection = JsonTransformer.deserialize(connectionString, ConnectionRecord) - expect(connection.didDoc).toBeInstanceOf(DidDoc) + // expect(connection.didDoc).toBeInstanceOf(DidDoc) }) }) }) diff --git a/packages/core/src/utils/__tests__/MultibaseEncoder.test.ts b/packages/core/src/utils/__tests__/MultiBaseEncoder.test.ts similarity index 76% rename from packages/core/src/utils/__tests__/MultibaseEncoder.test.ts rename to packages/core/src/utils/__tests__/MultiBaseEncoder.test.ts index 28b0d080ed..302bac9c99 100644 --- a/packages/core/src/utils/__tests__/MultibaseEncoder.test.ts +++ b/packages/core/src/utils/__tests__/MultiBaseEncoder.test.ts @@ -1,5 +1,6 @@ -import { BufferEncoder } from '../BufferEncoder' import { MultiBaseEncoder } from '../MultiBaseEncoder' +import { TypedArrayEncoder } from '../TypedArrayEncoder' +import { Buffer } from '../buffer' const validData = Buffer.from('Hello World!') const validMultiBase = 'zKWfinQuRQ3ekD1danFHqvKRg9koFp8vpokUeREEgjSyHwweeKDFaxVHi' @@ -8,7 +9,7 @@ const invalidMultiBase = 'gKWfinQuRQ3ekD1danFHqvKRg9koFp8vpokUeREEgjSyHwweeKDFax describe('MultiBaseEncoder', () => { describe('encode()', () => { it('Encodes valid multibase', () => { - const multibase = BufferEncoder.toUtf8String(MultiBaseEncoder.encode(validData, 'base58btc')) + const multibase = MultiBaseEncoder.encode(validData, 'base58btc') expect(multibase).toEqual('z2NEpo7TZRRrLZSi2U') }) }) @@ -16,14 +17,14 @@ describe('MultiBaseEncoder', () => { describe('Decodes()', () => { it('Decodes multibase', () => { const { data, baseName } = MultiBaseEncoder.decode(validMultiBase) - expect(BufferEncoder.toUtf8String(data)).toEqual('This is a valid base58btc encoded string!') + expect(TypedArrayEncoder.toUtf8String(data)).toEqual('This is a valid base58btc encoded string!') expect(baseName).toEqual('base58btc') }) it('Decodes invalid multibase', () => { expect(() => { MultiBaseEncoder.decode(invalidMultiBase) - }).toThrow(/^Invalid multibase: /) + }).toThrow(/^No decoder found for multibase prefix/) }) }) diff --git a/packages/core/src/utils/__tests__/MultihashEncoder.test.ts b/packages/core/src/utils/__tests__/MultiHashEncoder.test.ts similarity index 66% rename from packages/core/src/utils/__tests__/MultihashEncoder.test.ts rename to packages/core/src/utils/__tests__/MultiHashEncoder.test.ts index e5bcac867d..6564f8d563 100644 --- a/packages/core/src/utils/__tests__/MultihashEncoder.test.ts +++ b/packages/core/src/utils/__tests__/MultiHashEncoder.test.ts @@ -1,15 +1,20 @@ -import { BufferEncoder } from '../BufferEncoder' +import { Hasher } from '../Hasher' import { MultiHashEncoder } from '../MultiHashEncoder' +import { Buffer } from '../buffer' const validData = Buffer.from('Hello World!') -const validMultiHash = new Uint8Array([18, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33]) +const validMultiHash = new Uint8Array([ + 18, 32, 127, 131, 177, 101, 127, 241, 252, 83, 185, 45, 193, 129, 72, 161, 214, 93, 252, 45, 75, 31, 163, 214, 119, + 40, 74, 221, 210, 0, 18, 109, 144, 105, +]) +const validHash = Hasher.hash(validData, 'sha2-256') const invalidMultiHash = new Uint8Array([99, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33]) -describe('multihash', () => { +describe('MultiHashEncoder', () => { describe('encode()', () => { it('encodes multihash', () => { const multihash = MultiHashEncoder.encode(validData, 'sha2-256') - expect(multihash).toEqual(new Uint8Array([18, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33])) + expect(multihash.equals(Buffer.from(validMultiHash))).toBe(true) }) }) @@ -17,7 +22,7 @@ describe('multihash', () => { it('Decodes multihash', () => { const { data, hashName } = MultiHashEncoder.decode(validMultiHash) expect(hashName).toEqual('sha2-256') - expect(BufferEncoder.toUtf8String(data)).toEqual('Hello World!') + expect(data.equals(Buffer.from(validHash))).toBe(true) }) it('Decodes invalid multihash', () => { diff --git a/packages/core/src/utils/__tests__/BufferEncoder.test.ts b/packages/core/src/utils/__tests__/TypedArrayEncoder.test.ts similarity index 93% rename from packages/core/src/utils/__tests__/BufferEncoder.test.ts rename to packages/core/src/utils/__tests__/TypedArrayEncoder.test.ts index 444e4c6fe4..925bf97f82 100644 --- a/packages/core/src/utils/__tests__/BufferEncoder.test.ts +++ b/packages/core/src/utils/__tests__/TypedArrayEncoder.test.ts @@ -1,7 +1,7 @@ -import { BufferEncoder } from '../BufferEncoder' +import { TypedArrayEncoder } from '../TypedArrayEncoder' import { Buffer } from '../buffer' -describe('BufferEncoder', () => { +describe('TypedArrayEncoder', () => { const mockCredentialRequestBuffer = Buffer.from( JSON.stringify({ prover_did: 'did:sov:4xRwQoKEBcLMR3ni1uEVxo', @@ -26,9 +26,26 @@ describe('BufferEncoder', () => { }) ) + describe('isTypedArray', () => { + test('is array of type typedArray', () => { + const mockArray = [0, 1, 2] + expect(TypedArrayEncoder.isTypedArray(mockArray)).toStrictEqual(false) + }) + + test('is Uint8Array of type typedArray', () => { + const mockArray = new Uint8Array([0, 1, 2]) + expect(TypedArrayEncoder.isTypedArray(mockArray)).toStrictEqual(true) + }) + + test('is Buffer of type typedArray', () => { + const mockArray = new Buffer([0, 1, 2]) + expect(TypedArrayEncoder.isTypedArray(mockArray)).toStrictEqual(true) + }) + }) + describe('toBase64', () => { test('encodes buffer to Base64 string', () => { - expect(BufferEncoder.toBase64(mockCredentialRequestBuffer)).toEqual( + expect(TypedArrayEncoder.toBase64(mockCredentialRequestBuffer)).toEqual( 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0=' ) }) @@ -36,7 +53,7 @@ describe('BufferEncoder', () => { describe('toBase64URL', () => { test('encodes buffer to Base64URL string', () => { - expect(BufferEncoder.toBase64URL(mockCredentialRequestBuffer)).toEqual( + expect(TypedArrayEncoder.toBase64URL(mockCredentialRequestBuffer)).toEqual( 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0' ) }) @@ -45,7 +62,7 @@ describe('BufferEncoder', () => { describe('fromBase64', () => { test('decodes Base64 string to buffer object', () => { expect( - BufferEncoder.fromBase64( + TypedArrayEncoder.fromBase64( 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0=' ).equals(mockCredentialRequestBuffer) ).toEqual(true) @@ -53,7 +70,7 @@ describe('BufferEncoder', () => { test('decodes Base64URL string to buffer object', () => { expect( - BufferEncoder.fromBase64( + TypedArrayEncoder.fromBase64( 'eyJwcm92ZXJfZGlkIjoiZGlkOnNvdjo0eFJ3UW9LRUJjTE1SM25pMXVFVnhvIiwiY3JlZF9kZWZfaWQiOiJUTDFFYVBGQ1o4U2k1YVVycVNjQkR0OjM6Q0w6MTMyOlRBRyIsImJsaW5kZWRfbXMiOnsidSI6IjI5OTIzMzU4OTMzMzc4NTk0ODg0MDE2OTQ5MTE2MDE1MDQ4MzYyMTk3OTEwMzEzMTE1NTE3NjE1ODg2NzEyMDAyMjQ3NTQ5ODc3MjAzNzU0MTYxMDQwNjYyMTEzMTkzNjgxMDY3OTcxNzAwODg5MDA1MDU5OTU0NjgwNTYyNTY4NTQzNzA3ODc0NDU1NDYyNzM0NjE0NzcwNzE5ODU3NDc2NTE3NTgzNzMyMTE4MTc1NDcyMTE3MjU4MjYyMjQzNTI0MzcxMDQ3MjQ0NjEyMDI3NjcwNzk1MzQxNTE3Njc4NDA0NTM4Nzc0MzExMjU0ODg4MTI1MzcyMzk1NDc4MTE3ODU5OTc3OTU3NzgxNTQ1MjAzNjE0NDIyMDU3ODcyNDI1Mzg4Mzc5MDM4MTc0MTYxOTMzMDc3MzY0MTYwNTA3ODg4MjE2NzUxMjUwMTczNDc0OTkzNDU5MDAyNDA5ODA4MTY5NDEzNzg1NTM0MjgzMjQyNTY2MTA0NjQ4OTQ0NTA4NTI4NTUzNjY1MDE1OTA3ODk4NzkxMTA2NzY2MTk5OTY2MTA3NjE5OTQ5NzYyNjY4MjcyMzM5OTMxMDY3MDUwMzk0MDAyMjU1NjM3NzcxMDM1ODU1MzY1NTgyMDk4ODYyNTYxMjUwNTc2NDcwNTA0NzQyMDM2MTA3ODY0MjkyMDYyMTE3Nzk3NjI1ODI1NDMzMjQ4NTE3OTI0NTA0NTUwMzA4OTY4MzEyNzgwMzAxMDMxOTY0NjQ1NTQ4MzMzMjQ4MDg4NTkzMDE1OTM3MzU5ODg5Njg4ODYwMTQxNzU3ODYwNDE0OTYzIiwidXIiOm51bGwsImhpZGRlbl9hdHRyaWJ1dGVzIjpbIm1hc3Rlcl9zZWNyZXQiXSwiY29tbWl0dGVkX2F0dHJpYnV0ZXMiOnt9fSwiYmxpbmRlZF9tc19jb3JyZWN0bmVzc19wcm9vZiI6eyJjIjoiNzU0NzI4NDQ3OTk4ODk3MTQ5NTcyMTIyNTI2MDQxOTg2NTQ5NTk1NjQyNTQwNDk0NzY1NzUzNjYwOTM2MTkwMDg4MDQ3MjM3ODI0NzciLCJ2X2Rhc2hfY2FwIjoiMjQxNjU4NjA1NDM1MzU5NDM5Nzg0Nzk3OTIyMTkxODE5NjQ5NDI0MjI1MjYwMzQzMDEyOTY4MDI4NDQ4ODYyNDM0MzY3ODU2NTIwMDI0ODgyODA4ODgzNTkxNTU5MTIxNTUxNDcyNjI0NjM4OTQxMTQ3MTY4NDcxMjYwMzA2OTAwODkwMzk1OTQ3MjAwNzQ2NDYyNzU5MzY2MTM0ODI2NjUyNjg3NTg3ODU1MDY1OTMxOTM0MDY5MTI1NzIyNzI4MjU2ODAyNzE0MzkzMzMzNzk5MjAxMzk1NTI5ODgxOTkxNjExNzM4NzQ5NzgyMzE2NjIwMDE5NDUwODcyMzc4NzU5MDE3NzkwMjM5OTkxOTcwNTYzMDM4NjM4MTIyNTk5NTMyMDI3MTU4NjY5NDQ4OTA2NjI3MDc0NzE3NTc4NzAzMjAxMTkyNTMxMzAwMTU5NTEwMjA3OTY5NTM3NjQ0NzgyNzQyMDQ3MjY2ODk1MTcxNTc0NjU1MzA0MDI0ODIwMDU5NjgyMjIwMzE0NDQ0ODYyNzA5ODkzMDI5NzIwNDYxOTE2NzIyMTQ3NDEzMzI5MTA4NDQ0NTY5NzYyMjg1MTY4MzE0NTkzNjk4MTkyNTAyMzM1MDQ1Mjk1MzIyMzg2MjgzMDIyMzkwMzU1MTUxMzk1MTEzMTEyNjE2Mjc3MTYyNTAxMzgwNDU2NDE1MzIxOTQyMDU1MTQzMTExNTU3MDAzNzY2MzU2MDA3NjIyMzE3Mjc1MjE1NjM3ODEwOTE4NzEwNjgyMTAzMDg3NzQzMDU3NDcxMzQ3MDk1NTg4NTQ3NzkxMTE3NzI5Nzg0MjA5OTQ2NTQ2MDQwMjE2NTIwNDQwODkxNzYwMDEwODIyNDY3NjM3MTE0Nzk2OTI3NTczMjc5NTEyOTEwMzgyNjk0NjMxNjc0NTY2MjMyNDU3NDQzMjg3NDgzOTI4NjI5IiwibV9jYXBzIjp7Im1hc3Rlcl9zZWNyZXQiOiIxMDk1ODM1MjM1OTA4NzM4NjA2MzQwMDgxMTU3NDI1MzIwODMxODQzNzY0MTkyOTg3MDg1NTM5OTMxNjE3MzgwNzk1MDE5ODU1Nzk3MjAxNzE5MDg5MjY5NzI1MDM2MDg3OTIyNDIwODA3ODgxMzQ2Mzg2ODQyNTcyNzAzMTU2MDYxMzQ1MDEwMjAwNTM5ODQwMTkxNTEzNTk3MzU4NDQxOTg5MTE3MjU5MDU4NjM1ODc5OSJ9LCJyX2NhcHMiOnt9fSwibm9uY2UiOiIxMDUwNDQ1MzQ0MzY4MDg5OTAyMDkwNzYyIn0' ).equals(mockCredentialRequestBuffer) ).toEqual(true) diff --git a/packages/core/src/utils/__tests__/indyProofRequest.test.ts b/packages/core/src/utils/__tests__/indyProofRequest.test.ts new file mode 100644 index 0000000000..547745bcc7 --- /dev/null +++ b/packages/core/src/utils/__tests__/indyProofRequest.test.ts @@ -0,0 +1,90 @@ +import { checkProofRequestForDuplicates } from '..' + +import { + AriesFrameworkError, + AttributeFilter, + PredicateType, + ProofAttributeInfo, + ProofPredicateInfo, + ProofRequest, +} from '@aries-framework/core' + +export const INDY_CREDENTIAL_OFFER_ATTACHMENT_ID = 'libindy-cred-offer-0' + +describe('Present Proof', () => { + let credDefId: string + + beforeAll(() => { + credDefId = '9vPXgSpQJPkJEALbLXueBp:3:CL:57753:tag1' + }) + + test('attribute names match, same cred def filter', () => { + const attributes = { + name: new ProofAttributeInfo({ + name: 'age', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + age: new ProofAttributeInfo({ + name: 'age', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const nonce = 'testtesttest12345' + + const proofRequest = new ProofRequest({ + name: 'proof-request', + version: '1.0', + nonce, + requestedAttributes: attributes, + }) + + expect(() => checkProofRequestForDuplicates(proofRequest)).toThrowError(AriesFrameworkError) + }) + + test('attribute names match with predicates name, same cred def filter', () => { + const attributes = { + name: new ProofAttributeInfo({ + name: 'age', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const nonce = 'testtesttest12345' + + const proofRequest = new ProofRequest({ + name: 'proof-request', + version: '1.0', + nonce, + requestedAttributes: attributes, + requestedPredicates: predicates, + }) + + expect(() => checkProofRequestForDuplicates(proofRequest)).toThrowError(AriesFrameworkError) + }) +}) diff --git a/packages/core/src/utils/__tests__/messageType.test.ts b/packages/core/src/utils/__tests__/messageType.test.ts index d091ca2d04..39333d3315 100644 --- a/packages/core/src/utils/__tests__/messageType.test.ts +++ b/packages/core/src/utils/__tests__/messageType.test.ts @@ -1,10 +1,25 @@ +import { AgentMessage } from '../../agent/AgentMessage' import { + canHandleMessageType, + parseMessageType, replaceLegacyDidSovPrefix, replaceLegacyDidSovPrefixOnMessage, replaceNewDidCommPrefixWithLegacyDidSov, replaceNewDidCommPrefixWithLegacyDidSovOnMessage, + supportsIncomingMessageType, } from '../messageType' +export class TestMessage extends AgentMessage { + public constructor() { + super() + + this.id = this.generateId() + } + + public readonly type = TestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/fake-protocol/1.5/invitation') +} + describe('messageType', () => { describe('replaceLegacyDidSovPrefixOnMessage()', () => { it('should replace the message type prefix with https://didcomm.org if it starts with did:sov:BzCbsNYhMrjHiqZDTUASHg;spec', () => { @@ -81,4 +96,137 @@ describe('messageType', () => { ) }) }) + + describe('parseMessageType()', () => { + test('correctly parses the message type', () => { + expect(parseMessageType('https://didcomm.org/connections/1.0/request')).toEqual({ + documentUri: 'https://didcomm.org', + protocolName: 'connections', + protocolVersion: '1.0', + protocolMajorVersion: 1, + protocolMinorVersion: 0, + messageName: 'request', + protocolUri: `https://didcomm.org/connections/1.0`, + messageTypeUri: 'https://didcomm.org/connections/1.0/request', + }) + + expect(parseMessageType('https://didcomm.org/issue-credential/4.5/propose-credential')).toEqual({ + documentUri: 'https://didcomm.org', + protocolName: 'issue-credential', + protocolVersion: '4.5', + protocolMajorVersion: 4, + protocolMinorVersion: 5, + messageName: 'propose-credential', + protocolUri: `https://didcomm.org/issue-credential/4.5`, + messageTypeUri: 'https://didcomm.org/issue-credential/4.5/propose-credential', + }) + }) + }) + + describe('supportsIncomingMessageType()', () => { + test('returns true when the document uri, protocol name, major version all match and the minor version is lower than the expected minor version', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.0/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(expectedMessageType, incomingMessageType)).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version all match and the minor version is higher than the expected minor version', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.8/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(expectedMessageType, incomingMessageType)).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version and minor version all match', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(expectedMessageType, incomingMessageType)).toBe(true) + }) + + test('returns false when the major version does not match', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/2.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(expectedMessageType, incomingMessageType)).toBe(false) + + const incomingMessageType2 = parseMessageType('https://didcomm.org/connections/2.0/request') + const expectedMessageType2 = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(incomingMessageType2, expectedMessageType2)).toBe(false) + }) + + test('returns false when the message name does not match', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.4/proposal') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(expectedMessageType, incomingMessageType)).toBe(false) + }) + + test('returns false when the protocol name does not match', () => { + const incomingMessageType = parseMessageType('https://didcomm.org/issue-credential/1.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(expectedMessageType, incomingMessageType)).toBe(false) + }) + + test('returns false when the document uri does not match', () => { + const incomingMessageType = parseMessageType('https://my-protocol.org/connections/1.4/request') + const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + + expect(supportsIncomingMessageType(expectedMessageType, incomingMessageType)).toBe(false) + }) + }) + + describe('canHandleMessageType()', () => { + test('returns true when the document uri, protocol name, major version all match and the minor version is lower than the expected minor version', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/1.0/invitation')) + ).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version all match and the minor version is higher than the expected minor version', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/1.8/invitation')) + ).toBe(true) + }) + + test('returns true when the document uri, protocol name, major version and minor version all match', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/1.5/invitation')) + ).toBe(true) + }) + + test('returns false when the major version does not match', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/2.5/invitation')) + ).toBe(false) + + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/2.0/invitation')) + ).toBe(false) + }) + + test('returns false when the message name does not match', () => { + expect(canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/fake-protocol/1.5/request'))).toBe( + false + ) + }) + + test('returns false when the protocol name does not match', () => { + expect( + canHandleMessageType(TestMessage, parseMessageType('https://didcomm.org/another-fake-protocol/1.5/invitation')) + ).toBe(false) + }) + + test('returns false when the document uri does not match', () => { + expect( + canHandleMessageType( + TestMessage, + parseMessageType('https://another-didcomm-site.org/fake-protocol/1.5/invitation') + ) + ).toBe(false) + }) + }) }) diff --git a/packages/core/src/utils/__tests__/regex.test.ts b/packages/core/src/utils/__tests__/regex.test.ts new file mode 100644 index 0000000000..93cbaa7ae8 --- /dev/null +++ b/packages/core/src/utils/__tests__/regex.test.ts @@ -0,0 +1,29 @@ +import { credDefIdRegex, indyDidRegex, schemaIdRegex, schemaVersionRegex } from '../regex' + +describe('Valid Regular Expression', () => { + const invalidTest = 'test' + + test('test for credDefIdRegex', async () => { + const test = 'q7ATwTYbQDgiigVijUAej:3:CL:160971:1.0.0' + expect(test).toMatch(credDefIdRegex) + expect(credDefIdRegex.test(invalidTest)).toBeFalsy() + }) + + test('test for indyDidRegex', async () => { + const test = 'did:sov:q7ATwTYbQDgiigVijUAej' + expect(test).toMatch(indyDidRegex) + expect(indyDidRegex.test(invalidTest)).toBeFalsy + }) + + test('test for schemaIdRegex', async () => { + const test = 'q7ATwTYbQDgiigVijUAej:2:test:1.0' + expect(test).toMatch(schemaIdRegex) + expect(schemaIdRegex.test(invalidTest)).toBeFalsy + }) + + test('test for schemaVersionRegex', async () => { + const test = '1.0.0' + expect(test).toMatch(schemaVersionRegex) + expect(schemaVersionRegex.test(invalidTest)).toBeFalsy + }) +}) diff --git a/packages/core/src/utils/__tests__/string.test.ts b/packages/core/src/utils/__tests__/string.test.ts new file mode 100644 index 0000000000..7bb4121d1a --- /dev/null +++ b/packages/core/src/utils/__tests__/string.test.ts @@ -0,0 +1,11 @@ +import { rightSplit } from '../string' + +describe('string', () => { + describe('rightSplit', () => { + it('correctly splits a string starting from the right', () => { + const messageType = 'https://didcomm.org/connections/1.0/invitation' + + expect(rightSplit(messageType, '/', 3)).toEqual(['https://didcomm.org', 'connections', '1.0', 'invitation']) + }) + }) +}) diff --git a/packages/core/src/utils/__tests__/transformers.test.ts b/packages/core/src/utils/__tests__/transformers.test.ts deleted file mode 100644 index 6792f13a9c..0000000000 --- a/packages/core/src/utils/__tests__/transformers.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { plainToInstance } from 'class-transformer' - -import { CredentialRecord, CredentialState } from '../../modules/credentials' - -describe('transformers', () => { - it('transforms an old credential record', () => { - // Mocked old credentialRecord - const credentialRecord = new CredentialRecord({ state: CredentialState.Done, threadId: '0' }) - const jsonCredentialRecord = credentialRecord.toJSON() - - const metadata = jsonCredentialRecord.metadata as Record | string> - metadata.requestMetadata = { cred_req: 'x' } - metadata.schemaId = 'abc:def' - metadata.credentialDefinitionId = 'abc:def:CL' - - // Converted old to new credentialRecord - const cr = plainToInstance(CredentialRecord, jsonCredentialRecord) - - expect(cr.metadata.data).toEqual({ - '_internal/indyRequest': { - cred_req: 'x', - }, - '_internal/indyCredential': { - schemaId: 'abc:def', - credentialDefinitionId: 'abc:def:CL', - }, - }) - }) -}) diff --git a/packages/core/src/utils/__tests__/version.test.ts b/packages/core/src/utils/__tests__/version.test.ts new file mode 100644 index 0000000000..408bca1ee4 --- /dev/null +++ b/packages/core/src/utils/__tests__/version.test.ts @@ -0,0 +1,38 @@ +import { isFirstVersionHigherThanSecond, parseVersionString } from '../version' + +describe('version', () => { + describe('parseVersionString()', () => { + it('parses a version string to a tuple', () => { + expect(parseVersionString('1.0')).toStrictEqual([1, 0]) + expect(parseVersionString('2.12')).toStrictEqual([2, 12]) + expect(parseVersionString('0.0')).toStrictEqual([0, 0]) + }) + }) + + describe('isFirstVersionHigherThanSecond()', () => { + it('returns true if the major version digit of the first version is higher than the second', () => { + expect(isFirstVersionHigherThanSecond([2, 0], [1, 0])).toBe(true) + expect(isFirstVersionHigherThanSecond([2, 1], [1, 10])).toBe(true) + }) + + it('returns false if the major version digit of the first version is lower than the second', () => { + expect(isFirstVersionHigherThanSecond([1, 0], [2, 0])).toBe(false) + expect(isFirstVersionHigherThanSecond([1, 10], [2, 1])).toBe(false) + }) + + it('returns true if the major version digit of both versions are equal, but the minor version of the first version is higher', () => { + expect(isFirstVersionHigherThanSecond([1, 10], [1, 0])).toBe(true) + expect(isFirstVersionHigherThanSecond([2, 11], [2, 10])).toBe(true) + }) + + it('returns false if the major version digit of both versions are equal, but the minor version of the second version is higher', () => { + expect(isFirstVersionHigherThanSecond([1, 0], [1, 10])).toBe(false) + expect(isFirstVersionHigherThanSecond([2, 10], [2, 11])).toBe(false) + }) + + it('returns false if the major and minor version digit of both versions are equal', () => { + expect(isFirstVersionHigherThanSecond([1, 0], [1, 0])).toBe(false) + expect(isFirstVersionHigherThanSecond([2, 10], [2, 10])).toBe(false) + }) + }) +}) diff --git a/packages/core/src/utils/assertNoDuplicates.ts b/packages/core/src/utils/assertNoDuplicates.ts new file mode 100644 index 0000000000..841ba261f4 --- /dev/null +++ b/packages/core/src/utils/assertNoDuplicates.ts @@ -0,0 +1,11 @@ +import { AriesFrameworkError } from '../error/AriesFrameworkError' + +export function assertNoDuplicatesInArray(arr: string[]) { + const arrayLength = arr.length + const uniqueArrayLength = new Set(arr).size + + if (arrayLength === uniqueArrayLength) return + + const duplicates = arr.filter((item, index) => arr.indexOf(item) != index) + throw new AriesFrameworkError(`The proof request contains duplicate items: ${duplicates.toString()}`) +} diff --git a/packages/core/src/utils/attachment.ts b/packages/core/src/utils/attachment.ts index ce8d4afaf1..5831e7a37c 100644 --- a/packages/core/src/utils/attachment.ts +++ b/packages/core/src/utils/attachment.ts @@ -1,10 +1,10 @@ import type { Attachment } from '../decorators/attachment/Attachment' -import type { BaseName } from 'multibase' +import type { BaseName } from './MultiBaseEncoder' import { AriesFrameworkError } from '../error/AriesFrameworkError' -import { BufferEncoder } from './BufferEncoder' import { HashlinkEncoder } from './HashlinkEncoder' +import { TypedArrayEncoder } from './TypedArrayEncoder' /** * Encodes an attachment based on the `data` property @@ -22,7 +22,7 @@ export function encodeAttachment( if (attachment.data.sha256) { return `hl:${attachment.data.sha256}` } else if (attachment.data.base64) { - return HashlinkEncoder.encode(BufferEncoder.fromBase64(attachment.data.base64), hashAlgorithm, baseName) + return HashlinkEncoder.encode(TypedArrayEncoder.fromBase64(attachment.data.base64), hashAlgorithm, baseName) } else if (attachment.data.json) { throw new AriesFrameworkError( `Attachment: (${attachment.id}) has json encoded data. This is currently not supported` diff --git a/packages/core/src/utils/base58.ts b/packages/core/src/utils/base58.ts index 1de43a0fa3..4b3b0d4338 100644 --- a/packages/core/src/utils/base58.ts +++ b/packages/core/src/utils/base58.ts @@ -1,4 +1,5 @@ import base from '@multiformats/base-x' + const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' const base58Converter = base(BASE58_ALPHABET) diff --git a/packages/core/src/utils/did.ts b/packages/core/src/utils/did.ts index bfd9dbb4e2..9c8f19cdc3 100644 --- a/packages/core/src/utils/did.ts +++ b/packages/core/src/utils/did.ts @@ -15,7 +15,8 @@ * https://github.com/hyperledger/aries-framework-dotnet/blob/f90eaf9db8548f6fc831abea917e906201755763/src/Hyperledger.Aries/Ledger/DefaultLedgerService.cs#L139-L147 */ -import { BufferEncoder } from './BufferEncoder' +import { TypedArrayEncoder } from './TypedArrayEncoder' +import { Buffer } from './buffer' export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/ export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/ @@ -37,9 +38,9 @@ export function isSelfCertifiedDid(did: string, verkey: string): boolean { return true } - const buffer = BufferEncoder.fromBase58(verkey) + const buffer = TypedArrayEncoder.fromBase58(verkey) - const didFromVerkey = BufferEncoder.toBase58(buffer.slice(0, 16)) + const didFromVerkey = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) if (didFromVerkey === did) { return true @@ -48,6 +49,34 @@ export function isSelfCertifiedDid(did: string, verkey: string): boolean { return false } +export function getFullVerkey(did: string, verkey: string) { + if (isFullVerkey(verkey)) return verkey + + // Did could have did:xxx prefix, only take the last item after : + const id = did.split(':').pop() ?? did + // Verkey is prefixed with ~ if abbreviated + const verkeyWithoutTilde = verkey.slice(1) + + // Create base58 encoded public key (32 bytes) + return TypedArrayEncoder.toBase58( + Buffer.concat([ + // Take did identifier (16 bytes) + TypedArrayEncoder.fromBase58(id), + // Concat the abbreviated verkey (16 bytes) + TypedArrayEncoder.fromBase58(verkeyWithoutTilde), + ]) + ) +} + +/** + * Extract did from schema id + */ +export function didFromSchemaId(schemaId: string) { + const [did] = schemaId.split(':') + + return did +} + /** * Extract did from credential definition id */ @@ -58,10 +87,10 @@ export function didFromCredentialDefinitionId(credentialDefinitionId: string) { } /** - * Extract did from schema id + * Extract did from revocation registry definition id */ -export function didFromSchemaId(schemaId: string) { - const [did] = schemaId.split(':') +export function didFromRevocationRegistryDefinitionId(revocationRegistryId: string) { + const [did] = revocationRegistryId.split(':') return did } diff --git a/packages/core/src/utils/environment.ts b/packages/core/src/utils/environment.ts new file mode 100644 index 0000000000..b2ebadf0dd --- /dev/null +++ b/packages/core/src/utils/environment.ts @@ -0,0 +1,9 @@ +export function isNodeJS() { + return typeof process !== 'undefined' && process.release && process.release.name === 'node' +} + +export function isReactNative() { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return typeof navigator != 'undefined' && navigator.product == 'ReactNative' +} diff --git a/packages/core/src/utils/error.ts b/packages/core/src/utils/error.ts new file mode 100644 index 0000000000..530264240a --- /dev/null +++ b/packages/core/src/utils/error.ts @@ -0,0 +1 @@ +export const isError = (value: unknown): value is Error => value instanceof Error diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 0000000000..318ad5d39f --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1,12 @@ +export * from './TypedArrayEncoder' +export * from './JsonEncoder' +export * from './JsonTransformer' +export * from './MultiBaseEncoder' +export * from './buffer' +export * from './MultiHashEncoder' +export * from './JWE' +export * from './regex' +export * from './indyProofRequest' +export * from './VarintEncoder' +export * from './Hasher' +export * from './jsonld' diff --git a/packages/core/src/utils/indyProofRequest.ts b/packages/core/src/utils/indyProofRequest.ts new file mode 100644 index 0000000000..7c24a4b05c --- /dev/null +++ b/packages/core/src/utils/indyProofRequest.ts @@ -0,0 +1,22 @@ +import type { ProofRequest } from '../modules/proofs/models/ProofRequest' + +import { assertNoDuplicatesInArray } from './assertNoDuplicates' + +export function attributeNamesToArray(proofRequest: ProofRequest) { + // Attributes can contain either a `name` string value or an `names` string array. We reduce it to a single array + // containing all attribute names from the requested attributes. + return Array.from(proofRequest.requestedAttributes.values()).reduce( + (names, a) => [...names, ...(a.name ? [a.name] : a.names ? a.names : [])], + [] + ) +} + +export function predicateNamesToArray(proofRequest: ProofRequest) { + return Array.from(proofRequest.requestedPredicates.values()).map((a) => a.name) +} + +export function checkProofRequestForDuplicates(proofRequest: ProofRequest) { + const attributes = attributeNamesToArray(proofRequest) + const predicates = predicateNamesToArray(proofRequest) + assertNoDuplicatesInArray(attributes.concat(predicates)) +} diff --git a/packages/core/src/utils/jsonld.ts b/packages/core/src/utils/jsonld.ts new file mode 100644 index 0000000000..86c1ba7aea --- /dev/null +++ b/packages/core/src/utils/jsonld.ts @@ -0,0 +1,156 @@ +import type { GetProofsOptions, GetProofsResult, GetTypeOptions } from '../crypto/signature-suites/bbs' +import type { JsonObject, JsonValue } from '../types' +import type { SingleOrArray } from './type' + +import jsonld from '../../types/jsonld' +import { SECURITY_CONTEXT_URL } from '../modules/vc/constants' + +export type JsonLdDoc = Record +export interface VerificationMethod extends JsonObject { + id: string + [key: string]: JsonValue +} + +export interface Proof extends JsonObject { + verificationMethod: string | VerificationMethod + [key: string]: JsonValue +} + +export interface DocumentLoaderResult { + contextUrl?: string | null + documentUrl: string + document: Record +} + +export type DocumentLoader = (url: string) => Promise + +export const orArrayToArray = (val?: SingleOrArray): Array => { + if (!val) return [] + if (Array.isArray(val)) return val + return [val] +} + +export const _includesContext = (options: { document: JsonLdDoc; contextUrl: string }) => { + const context = options.document['@context'] + + return context === options.contextUrl || (Array.isArray(context) && context.includes(options.contextUrl)) +} + +/* + * The code in this file originated from + * @see https://github.com/digitalbazaar/jsonld-signatures + * Hence the following copyright notice applies + * + * Copyright (c) 2017-2018 Digital Bazaar, Inc. All rights reserved. + */ + +/** + * The property identifying the linked data proof + * Note - this will not work for legacy systems that + * relying on `signature` + */ +const PROOF_PROPERTY = 'proof' + +/** + * Gets a supported linked data proof from a JSON-LD Document + * Note - unless instructed not to the document will be compacted + * against the security v2 context @see https://w3id.org/security/v2 + * + * @param options Options for extracting the proof from the document + * + * @returns {GetProofsResult} An object containing the matched proofs and the JSON-LD document + */ +export const getProofs = async (options: GetProofsOptions): Promise => { + const { proofType, skipProofCompaction, documentLoader, expansionMap } = options + let { document } = options + + let proofs + if (!skipProofCompaction) { + // If we must compact the proof then we must first compact the input + // document to find the proof + document = await jsonld.compact(document, SECURITY_CONTEXT_URL, { + documentLoader, + expansionMap, + compactToRelative: false, + }) + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - needed because getValues is not part of the public API. + proofs = jsonld.getValues(document, PROOF_PROPERTY) + delete document[PROOF_PROPERTY] + + if (typeof proofType === 'string') { + proofs = proofs.filter((_: Record) => _.type == proofType) + } + if (Array.isArray(proofType)) { + proofs = proofs.filter((_: Record) => proofType.includes(_.type)) + } + + proofs = proofs.map((matchedProof: Record) => ({ + '@context': SECURITY_CONTEXT_URL, + ...matchedProof, + })) + + return { + proofs, + document, + } +} + +/** + * Formats an input date to w3c standard date format + * @param date {number|string} Optional if not defined current date is returned + * + * @returns {string} date in a standard format as a string + */ +export const w3cDate = (date?: number | string): string => { + let result = new Date() + if (typeof date === 'number' || typeof date === 'string') { + result = new Date(date) + } + const str = result.toISOString() + return str.substr(0, str.length - 5) + 'Z' +} + +/** + * Gets the JSON-LD type information for a document + * @param document {any} JSON-LD document to extract the type information from + * @param options {GetTypeInfoOptions} Options for extracting the JSON-LD document + * + * @returns {object} Type info for the JSON-LD document + */ +export const getTypeInfo = async ( + document: JsonObject, + options: GetTypeOptions +): Promise<{ types: string[]; alias: string }> => { + const { documentLoader, expansionMap } = options + + // determine `@type` alias, if any + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - needed because getValues is not part of the public API. + const context = jsonld.getValues(document, '@context') + + const compacted = await jsonld.compact({ '@type': '_:b0' }, context, { + documentLoader, + expansionMap, + }) + + delete compacted['@context'] + + const alias = Object.keys(compacted)[0] + + // optimize: expand only `@type` and `type` values + /* eslint-disable prefer-const */ + let toExpand: Record = { '@context': context } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - needed because getValues is not part of the public API. + toExpand['@type'] = jsonld.getValues(document, '@type').concat(jsonld.getValues(document, alias)) + + const expanded = (await jsonld.expand(toExpand, { documentLoader, expansionMap }))[0] || {} + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - needed because getValues is not part of the public API. + return { types: jsonld.getValues(expanded, '@type'), alias } +} diff --git a/packages/core/src/utils/messageType.ts b/packages/core/src/utils/messageType.ts index f225123e9a..e43c7c4fe4 100644 --- a/packages/core/src/utils/messageType.ts +++ b/packages/core/src/utils/messageType.ts @@ -1,6 +1,164 @@ -import type { UnpackedMessage } from '../types' +import type { PlaintextMessage } from '../types' +import type { VersionString } from './version' +import type { ValidationOptions, ValidationArguments } from 'class-validator' -export function replaceLegacyDidSovPrefixOnMessage(message: UnpackedMessage | Record) { +import { ValidateBy, buildMessage } from 'class-validator' + +import { rightSplit } from './string' +import { parseVersionString } from './version' + +export interface ParsedMessageType { + /** + * Message name + * + * @example request + */ + messageName: string + + /** + * Version of the protocol + * + * @example 1.0 + */ + protocolVersion: string + + /** + * Major version of the protocol + * + * @example 1 + */ + protocolMajorVersion: number + + /** + * Minor version of the protocol + * + * @example 0 + */ + protocolMinorVersion: number + + /** + * Name of the protocol + * + * @example connections + */ + protocolName: string + + /** + * Document uri of the message. + * + * @example https://didcomm.org + */ + documentUri: string + + /** + * Uri identifier of the protocol. Includes the + * documentUri, protocolName and protocolVersion. + * Useful when working with feature discovery + * + * @example https://didcomm.org/connections/1.0 + */ + protocolUri: string + + /** + * Uri identifier of the message. Includes all parts + * or the message type. + * + * @example https://didcomm.org/connections/1.0/request + */ + messageTypeUri: string +} + +export function parseMessageType(messageType: string): ParsedMessageType { + const [documentUri, protocolName, protocolVersion, messageName] = rightSplit(messageType, '/', 3) + const [protocolMajorVersion, protocolMinorVersion] = parseVersionString(protocolVersion as VersionString) + + return { + documentUri, + protocolName, + protocolVersion, + protocolMajorVersion, + protocolMinorVersion, + messageName, + protocolUri: `${documentUri}/${protocolName}/${protocolVersion}`, + messageTypeUri: messageType, + } +} + +/** + * Check whether the incoming message type is a message type that can be handled by comparing it to the expected message type. + * In this case the expected message type is e.g. the type declared on an agent message class, and the incoming message type is the type + * that is parsed from the incoming JSON. + * + * The method will make sure the following fields are equal: + * - documentUri + * - protocolName + * - majorVersion + * - messageName + * + * @example + * const incomingMessageType = parseMessageType('https://didcomm.org/connections/1.0/request') + * const expectedMessageType = parseMessageType('https://didcomm.org/connections/1.4/request') + * + * // Returns true because the incoming message type is equal to the expected message type, except for + * // the minor version, which is lower + * const isIncomingMessageTypeSupported = supportsIncomingMessageType(incomingMessageType, expectedMessageType) + */ +export function supportsIncomingMessageType( + incomingMessageType: ParsedMessageType, + expectedMessageType: ParsedMessageType +) { + const documentUriMatches = expectedMessageType.documentUri === incomingMessageType.documentUri + const protocolNameMatches = expectedMessageType.protocolName === incomingMessageType.protocolName + const majorVersionMatches = expectedMessageType.protocolMajorVersion === incomingMessageType.protocolMajorVersion + const messageNameMatches = expectedMessageType.messageName === incomingMessageType.messageName + + // Everything besides the minor version must match + return documentUriMatches && protocolNameMatches && majorVersionMatches && messageNameMatches +} + +export function canHandleMessageType( + messageClass: { type: ParsedMessageType }, + messageType: ParsedMessageType +): boolean { + return supportsIncomingMessageType(messageClass.type, messageType) +} + +/** + * class-validator decorator to check if the string message type value matches with the + * expected message type. This uses {@link supportsIncomingMessageType}. + */ +export function IsValidMessageType( + messageType: ParsedMessageType, + validationOptions?: ValidationOptions +): PropertyDecorator { + return ValidateBy( + { + name: 'isValidMessageType', + constraints: [messageType], + validator: { + validate: (value, args: ValidationArguments): boolean => { + const [expectedMessageType] = args.constraints as [ParsedMessageType] + + // Type must be string + if (typeof value !== 'string') { + return false + } + + const incomingMessageType = parseMessageType(value) + return supportsIncomingMessageType(incomingMessageType, expectedMessageType) + }, + defaultMessage: buildMessage( + (eachPrefix) => + eachPrefix + '$property does not match the expected message type (only minor version may be lower)', + validationOptions + ), + }, + }, + validationOptions + ) +} + +export function replaceLegacyDidSovPrefixOnMessage(message: PlaintextMessage | Record) { message['@type'] = replaceLegacyDidSovPrefix(message['@type'] as string) } diff --git a/packages/core/src/utils/mixins.ts b/packages/core/src/utils/mixins.ts index b7341fec33..b55e1844ea 100644 --- a/packages/core/src/utils/mixins.ts +++ b/packages/core/src/utils/mixins.ts @@ -5,6 +5,9 @@ // eslint-disable-next-line @typescript-eslint/ban-types export type Constructor = new (...args: any[]) => T +export type NonConstructable = Omit +export type Constructable = T & (new (...args: any) => T) + // Turns A | B | C into A & B & C export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never diff --git a/packages/core/src/utils/regex.ts b/packages/core/src/utils/regex.ts new file mode 100644 index 0000000000..629be026df --- /dev/null +++ b/packages/core/src/utils/regex.ts @@ -0,0 +1,4 @@ +export const schemaIdRegex = /^[a-zA-Z0-9]{21,22}:2:.+:[0-9.]+$/ +export const schemaVersionRegex = /^(\d+\.)?(\d+\.)?(\*|\d+)$/ +export const credDefIdRegex = /^([a-zA-Z0-9]{21,22}):3:CL:(([1-9][0-9]*)|([a-zA-Z0-9]{21,22}:2:.+:[0-9.]+)):(.+)?$/ +export const indyDidRegex = /^(did:sov:)?[a-zA-Z0-9]{21,22}$/ diff --git a/packages/core/src/utils/string.ts b/packages/core/src/utils/string.ts new file mode 100644 index 0000000000..bddc1689e1 --- /dev/null +++ b/packages/core/src/utils/string.ts @@ -0,0 +1,4 @@ +export function rightSplit(string: string, sep: string, limit: number) { + const split = string.split(sep) + return limit ? [split.slice(0, -limit).join(sep)].concat(split.slice(-limit)) : split +} diff --git a/packages/core/src/utils/transformers.ts b/packages/core/src/utils/transformers.ts index 2bdff3d09a..622772a385 100644 --- a/packages/core/src/utils/transformers.ts +++ b/packages/core/src/utils/transformers.ts @@ -1,7 +1,7 @@ import type { ValidationOptions } from 'class-validator' import { Transform, TransformationType } from 'class-transformer' -import { ValidateBy, buildMessage } from 'class-validator' +import { isString, ValidateBy, buildMessage } from 'class-validator' import { DateTime } from 'luxon' import { Metadata } from '../storage/Metadata' @@ -47,8 +47,6 @@ export function RecordTransformer(Class: { new (...args: any[]): T }) { /* * Decorator that transforms to and from a metadata instance. - * - * @todo remove the conversion at 0.1.0 release via a migration script */ export function MetadataTransformer() { return Transform(({ value, type }) => { @@ -57,17 +55,7 @@ export function MetadataTransformer() { } if (type === TransformationType.PLAIN_TO_CLASS) { - const { requestMetadata, schemaId, credentialDefinitionId, ...rest } = value - const metadata = new Metadata(rest) - - if (requestMetadata) metadata.add('_internal/indyRequest', { ...value.requestMetadata }) - - if (schemaId) metadata.add('_internal/indyCredential', { schemaId: value.schemaId }) - - if (credentialDefinitionId) - metadata.add('_internal/indyCredential', { credentialDefinitionId: value.credentialDefinitionId }) - - return metadata + return new Metadata(value) } if (type === TransformationType.CLASS_TO_CLASS) { @@ -108,3 +96,22 @@ export function IsMap(validationOptions?: ValidationOptions): PropertyDecorator validationOptions ) } + +/** + * Checks if a given value is a string or string array. + */ +export function IsStringOrStringArray(validationOptions?: Omit): PropertyDecorator { + return ValidateBy( + { + name: 'isStringOrStringArray', + validator: { + validate: (value): boolean => isString(value) || (Array.isArray(value) && value.every((v) => isString(v))), + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be a string or string array', + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/utils/type.ts b/packages/core/src/utils/type.ts index 0a3ca9896c..2155975323 100644 --- a/packages/core/src/utils/type.ts +++ b/packages/core/src/utils/type.ts @@ -1,5 +1,13 @@ +import type { JsonObject } from '../types' + +export type SingleOrArray = T | T[] + export type Optional = Pick, K> & Omit export const isString = (value: unknown): value is string => typeof value === 'string' export const isNumber = (value: unknown): value is number => typeof value === 'number' export const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean' + +export const isJsonObject = (value: unknown): value is JsonObject => { + return value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value) +} diff --git a/packages/core/src/utils/uri.ts b/packages/core/src/utils/uri.ts new file mode 100644 index 0000000000..b25a4433fb --- /dev/null +++ b/packages/core/src/utils/uri.ts @@ -0,0 +1,4 @@ +export function getProtocolScheme(url: string) { + const [protocolScheme] = url.split(':') + return protocolScheme +} diff --git a/packages/core/src/utils/validators.ts b/packages/core/src/utils/validators.ts new file mode 100644 index 0000000000..f32fe1c08f --- /dev/null +++ b/packages/core/src/utils/validators.ts @@ -0,0 +1,85 @@ +import type { Constructor } from './mixins' +import type { ValidationOptions } from 'class-validator' + +import { isString, ValidateBy, isInstance, buildMessage } from 'class-validator' + +export interface IsInstanceOrArrayOfInstancesValidationOptions extends ValidationOptions { + classType: new (...args: any[]) => any +} + +/** + * Checks if the value is an instance of the specified object. + */ +export function IsStringOrInstance(targetType: Constructor, validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isStringOrVerificationMethod', + constraints: [targetType], + validator: { + validate: (value, args): boolean => isString(value) || isInstance(value, args?.constraints[0]), + defaultMessage: buildMessage((eachPrefix, args) => { + if (args?.constraints[0]) { + return eachPrefix + `$property must be of type string or instance of ${args.constraints[0].name as string}` + } else { + return ( + eachPrefix + `isStringOrVerificationMethod decorator expects and object as value, but got falsy value.` + ) + } + }, validationOptions), + }, + }, + validationOptions + ) +} + +export function IsInstanceOrArrayOfInstances( + validationOptions: IsInstanceOrArrayOfInstancesValidationOptions +): PropertyDecorator { + return ValidateBy( + { + name: 'isInstanceOrArrayOfInstances', + validator: { + validate: (value): boolean => { + if (Array.isArray(value)) { + value.forEach((item) => { + if (!isInstance(item, validationOptions.classType)) { + return false + } + }) + return true + } + return isInstance(value, validationOptions.classType) + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + `$property must be a string or instance of ${validationOptions.classType.name}`, + validationOptions + ), + }, + }, + validationOptions + ) +} + +export function isStringArray(value: any): value is string[] { + return Array.isArray(value) && value.every((v) => typeof v === 'string') +} + +export const UriValidator = new RegExp('w+:(/?/?)[^s]+') + +export function IsUri(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isInstanceOrArrayOfInstances', + validator: { + validate: (value): boolean => { + return UriValidator.test(value) + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + `$property must be a string that matches regex: ${UriValidator.source}`, + validationOptions + ), + }, + }, + validationOptions + ) +} diff --git a/packages/core/src/utils/version.ts b/packages/core/src/utils/version.ts new file mode 100644 index 0000000000..58a3f10a77 --- /dev/null +++ b/packages/core/src/utils/version.ts @@ -0,0 +1,14 @@ +export function parseVersionString(version: VersionString): Version { + const [major, minor] = version.split('.') + + return [Number(major), Number(minor)] +} + +export function isFirstVersionHigherThanSecond(first: Version, second: Version) { + return first[0] > second[0] || (first[0] == second[0] && first[1] > second[1]) +} + +export type VersionString = `${number}.${number}` +export type MajorVersion = number +export type MinorVersion = number +export type Version = [MajorVersion, MinorVersion] diff --git a/packages/core/src/wallet/IndyWallet.test.ts b/packages/core/src/wallet/IndyWallet.test.ts new file mode 100644 index 0000000000..a1147a6260 --- /dev/null +++ b/packages/core/src/wallet/IndyWallet.test.ts @@ -0,0 +1,142 @@ +import { BBS_SIGNATURE_LENGTH } from '@mattrglobal/bbs-signatures' +import { SIGNATURE_LENGTH as ED25519_SIGNATURE_LENGTH } from '@stablelib/ed25519' + +import { getBaseConfig } from '../../tests/helpers' +import { Agent } from '../agent/Agent' +import { KeyType } from '../crypto' +import { TypedArrayEncoder } from '../utils' + +import { IndyWallet } from './IndyWallet' +import { WalletError } from './error' + +describe('IndyWallet', () => { + let indyWallet: IndyWallet + let agent: Agent + const seed = 'sample-seed' + const message = TypedArrayEncoder.fromString('sample-message') + + beforeEach(async () => { + const { config, agentDependencies } = getBaseConfig('IndyWallettest') + agent = new Agent(config, agentDependencies) + indyWallet = agent.injectionContainer.resolve(IndyWallet) + await agent.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + test('Get the public DID', () => { + expect(indyWallet.publicDid).toMatchObject({ + did: expect.any(String), + verkey: expect.any(String), + }) + }) + + test('Get the Master Secret', () => { + expect(indyWallet.masterSecretId).toEqual('Wallet: IndyWallettest') + }) + + test('Get the wallet handle', () => { + expect(indyWallet.handle).toEqual(expect.any(Number)) + }) + + test('Initializes a public did', async () => { + await indyWallet.initPublicDid({ seed: '00000000000000000000000Forward01' }) + + expect(indyWallet.publicDid).toEqual({ + did: 'DtWRdd6C5dN5vpcN6XRAvu', + verkey: '82RBSn3heLgXzZd74UsMC8Q8YRfEEhQoAM7LUqE6bevJ', + }) + }) + + test('Create DID', async () => { + const didInfo = await indyWallet.createDid({ seed: '00000000000000000000000Forward01' }) + expect(didInfo).toMatchObject({ + did: 'DtWRdd6C5dN5vpcN6XRAvu', + verkey: '82RBSn3heLgXzZd74UsMC8Q8YRfEEhQoAM7LUqE6bevJ', + }) + }) + + test('Generate Nonce', async () => { + await expect(indyWallet.generateNonce()).resolves.toEqual(expect.any(String)) + }) + + test('Create ed25519 keypair', async () => { + await expect( + indyWallet.createKey({ seed: '2103de41b4ae37e8e28586d84a342b67', keyType: KeyType.Ed25519 }) + ).resolves.toMatchObject({ + keyType: KeyType.Ed25519, + }) + }) + + test('Create blsg12381g1 keypair', async () => { + await expect(indyWallet.createKey({ seed, keyType: KeyType.Bls12381g1 })).resolves.toMatchObject({ + publicKeyBase58: '6RhvX1RK5rA9uXdTtV6WvHWNQqcCW86BQxz1aBPr6ebBcppCYMD3LLy7QLg4cGcWaq', + keyType: KeyType.Bls12381g1, + }) + }) + + test('Create bls12381g2 keypair', async () => { + await expect(indyWallet.createKey({ seed, keyType: KeyType.Bls12381g2 })).resolves.toMatchObject({ + publicKeyBase58: + 't54oLBmhhRcDLUyWTvfYRWw8VRXRy1p43pVm62hrpShrYPuHe9WNAgS33DPfeTK6xK7iPrtJDwCHZjYgbFYDVTJHxXex9xt2XEGF8D356jBT1HtqNeucv3YsPLfTWcLcpFA', + keyType: KeyType.Bls12381g2, + }) + }) + + test('Fail to create bls12381g1g2 keypair', async () => { + await expect(indyWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 })).rejects.toThrowError(WalletError) + }) + + test('Fail to create x25519 keypair', async () => { + await expect(indyWallet.createKey({ seed, keyType: KeyType.X25519 })).rejects.toThrowError(WalletError) + }) + + test('Create a signature with a ed25519 keypair', async () => { + const ed25519Key = await indyWallet.createKey({ keyType: KeyType.Ed25519 }) + const signature = await indyWallet.sign({ + data: message, + key: ed25519Key, + }) + expect(signature.length).toStrictEqual(ED25519_SIGNATURE_LENGTH) + }) + + test('Create a signature with a bls12381g2 keypair', async () => { + const bls12381g2Key = await indyWallet.createKey({ seed, keyType: KeyType.Bls12381g2 }) + const signature = await indyWallet.sign({ + data: message, + key: bls12381g2Key, + }) + expect(signature.length).toStrictEqual(BBS_SIGNATURE_LENGTH) + }) + + test('Fail to create a signature with a bls12381g1 keypair', async () => { + const bls12381g1Key = await indyWallet.createKey({ seed, keyType: KeyType.Bls12381g1 }) + await expect( + indyWallet.sign({ + data: message, + key: bls12381g1Key, + }) + ).rejects.toThrowError(WalletError) + }) + + test('Verify a signed message with a ed25519 publicKey', async () => { + const ed25519Key = await indyWallet.createKey({ keyType: KeyType.Ed25519 }) + const signature = await indyWallet.sign({ + data: message, + key: ed25519Key, + }) + await expect(indyWallet.verify({ key: ed25519Key, data: message, signature })).resolves.toStrictEqual(true) + }) + + test('Verify a signed message with a bls12381g2 publicKey', async () => { + const bls12381g2Key = await indyWallet.createKey({ seed, keyType: KeyType.Bls12381g2 }) + const signature = await indyWallet.sign({ + data: message, + key: bls12381g2Key, + }) + await expect(indyWallet.verify({ key: bls12381g2Key, data: message, signature })).resolves.toStrictEqual(true) + }) +}) diff --git a/packages/core/src/wallet/IndyWallet.ts b/packages/core/src/wallet/IndyWallet.ts index 97cd71b30b..719f6c51c0 100644 --- a/packages/core/src/wallet/IndyWallet.ts +++ b/packages/core/src/wallet/IndyWallet.ts @@ -1,29 +1,42 @@ +import type { BlsKeyPair } from '../crypto/BbsService' import type { Logger } from '../logger' -import type { WireMessage, UnpackedMessageContext, WalletConfig } from '../types' +import type { + EncryptedMessage, + WalletConfig, + WalletExportImportConfig, + WalletConfigRekey, + KeyDerivationMethod, +} from '../types' import type { Buffer } from '../utils/buffer' -import type { Wallet, DidInfo, DidConfig } from './Wallet' -import type { default as Indy } from 'indy-sdk' +import type { + Wallet, + DidInfo, + DidConfig, + CreateKeyOptions, + VerifyOptions, + SignOptions, + UnpackedMessageContext, +} from './Wallet' +import type { default as Indy, WalletStorageConfig } from 'indy-sdk' import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../agent/AgentConfig' -import { AriesFrameworkError } from '../error' -import { JsonEncoder } from '../utils/JsonEncoder' +import { BbsService } from '../crypto/BbsService' +import { Key } from '../crypto/Key' +import { KeyType } from '../crypto/KeyType' +import { AriesFrameworkError, IndySdkError, RecordDuplicateError, RecordNotFoundError } from '../error' +import { TypedArrayEncoder, JsonEncoder } from '../utils' +import { isError } from '../utils/error' import { isIndyError } from '../utils/indyError' import { WalletDuplicateError, WalletNotFoundError, WalletError } from './error' import { WalletInvalidKeyError } from './error/WalletInvalidKeyError' -export interface IndyOpenWallet { - walletHandle: number - masterSecretId: string - walletConfig: Indy.WalletConfig - walletCredentials: Indy.WalletCredentials -} - @scoped(Lifecycle.ContainerScoped) export class IndyWallet implements Wallet { - private openWalletInfo?: IndyOpenWallet + private walletConfig?: WalletConfig + private walletHandle?: number private logger: Logger private publicDidInfo: DidInfo | undefined @@ -34,8 +47,12 @@ export class IndyWallet implements Wallet { this.indy = agentConfig.agentDependencies.indy } + public get isProvisioned() { + return this.walletConfig !== undefined + } + public get isInitialized() { - return this.openWalletInfo !== undefined + return this.walletHandle !== undefined } public get publicDid() { @@ -43,48 +60,58 @@ export class IndyWallet implements Wallet { } public get handle() { - if (!this.isInitialized || !this.openWalletInfo) { + if (!this.walletHandle) { throw new AriesFrameworkError( 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' ) } - return this.openWalletInfo.walletHandle + return this.walletHandle } public get masterSecretId() { - if (!this.isInitialized || !this.openWalletInfo) { + if (!this.isInitialized || !this.walletConfig?.id) { throw new AriesFrameworkError( 'Wallet has not been initialized yet. Make sure to await agent.initialize() before using the agent.' ) } - return this.openWalletInfo.masterSecretId + return this.walletConfig.id } - public async initialize(walletConfig: WalletConfig) { - this.logger.info(`Initializing wallet '${walletConfig.id}'`, walletConfig) + private walletStorageConfig(walletConfig: WalletConfig): Indy.WalletConfig { + const walletStorageConfig: Indy.WalletConfig = { + id: walletConfig.id, + storage_type: walletConfig.storage?.type, + } - if (this.isInitialized) { - throw new WalletError( - 'Wallet instance already initialized. Close the currently opened wallet before re-initializing the wallet' - ) + if (walletConfig.storage?.config) { + walletStorageConfig.storage_config = walletConfig.storage?.config as WalletStorageConfig } - // Open wallet, creating if it doesn't exist yet - try { - await this.open(walletConfig) - } catch (error) { - // If the wallet does not exist yet, create it and try to open again - if (error instanceof WalletNotFoundError) { - await this.create(walletConfig) - await this.open(walletConfig) - } else { - throw error - } + return walletStorageConfig + } + + private walletCredentials( + walletConfig: WalletConfig, + rekey?: string, + rekeyDerivation?: KeyDerivationMethod + ): Indy.OpenWalletCredentials { + const walletCredentials: Indy.OpenWalletCredentials = { + key: walletConfig.key, + key_derivation_method: walletConfig.keyDerivationMethod, + } + if (rekey) { + walletCredentials.rekey = rekey + } + if (rekeyDerivation) { + walletCredentials.rekey_derivation_method = rekeyDerivation + } + if (walletConfig.storage?.credentials) { + walletCredentials.storage_credentials = walletConfig.storage?.credentials as Record } - this.logger.debug(`Wallet '${walletConfig.id}' initialized with handle '${this.handle}'`) + return walletCredentials } /** @@ -92,11 +119,30 @@ export class IndyWallet implements Wallet { * @throws {WalletError} if another error occurs */ public async create(walletConfig: WalletConfig): Promise { + await this.createAndOpen(walletConfig) + await this.close() + } + + /** + * @throws {WalletDuplicateError} if the wallet already exists + * @throws {WalletError} if another error occurs + */ + public async createAndOpen(walletConfig: WalletConfig): Promise { this.logger.debug(`Creating wallet '${walletConfig.id}' using SQLite storage`) try { - await this.indy.createWallet({ id: walletConfig.id }, { key: walletConfig.key }) + await this.indy.createWallet(this.walletStorageConfig(walletConfig), this.walletCredentials(walletConfig)) + this.walletConfig = walletConfig + + // We usually want to create master secret only once, therefore, we can to do so when creating a wallet. + await this.open(walletConfig) + + // We need to open wallet before creating master secret because we need wallet handle here. + await this.createMasterSecret(this.handle, walletConfig.id) } catch (error) { + // If an error ocurred while creating the master secret, we should close the wallet + if (this.isInitialized) await this.close() + if (isIndyError(error, 'WalletAlreadyExistsError')) { const errorMessage = `Wallet '${walletConfig.id}' already exists` this.logger.debug(errorMessage) @@ -106,6 +152,9 @@ export class IndyWallet implements Wallet { cause: error, }) } else { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } const errorMessage = `Error creating wallet '${walletConfig.id}'` this.logger.error(errorMessage, { error, @@ -115,6 +164,8 @@ export class IndyWallet implements Wallet { throw new WalletError(errorMessage, { cause: error }) } } + + this.logger.debug(`Successfully created wallet '${walletConfig.id}'`) } /** @@ -122,21 +173,52 @@ export class IndyWallet implements Wallet { * @throws {WalletError} if another error occurs */ public async open(walletConfig: WalletConfig): Promise { - if (this.isInitialized) { + await this._open(walletConfig) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + public async rotateKey(walletConfig: WalletConfigRekey): Promise { + if (!walletConfig.rekey) { + throw new WalletError('Wallet rekey undefined!. Please specify the new wallet key') + } + await this._open( + { + id: walletConfig.id, + key: walletConfig.key, + keyDerivationMethod: walletConfig.keyDerivationMethod, + }, + walletConfig.rekey, + walletConfig.rekeyDerivationMethod + ) + } + + /** + * @throws {WalletNotFoundError} if the wallet does not exist + * @throws {WalletError} if another error occurs + */ + private async _open( + walletConfig: WalletConfig, + rekey?: string, + rekeyDerivation?: KeyDerivationMethod + ): Promise { + if (this.walletHandle) { throw new WalletError( - 'Wallet instance already initialized. Close the currently opened wallet before re-initializing the wallet' + 'Wallet instance already opened. Close the currently opened wallet before re-opening the wallet' ) } try { - const walletHandle = await this.indy.openWallet({ id: walletConfig.id }, { key: walletConfig.key }) - const masterSecretId = await this.createMasterSecret(walletHandle, walletConfig.id) - - this.openWalletInfo = { - walletConfig: { id: walletConfig.id }, - walletCredentials: { key: walletConfig.key }, - walletHandle, - masterSecretId, + this.walletHandle = await this.indy.openWallet( + this.walletStorageConfig(walletConfig), + this.walletCredentials(walletConfig, rekey, rekeyDerivation) + ) + if (rekey) { + this.walletConfig = { ...walletConfig, key: rekey, keyDerivationMethod: rekeyDerivation } + } else { + this.walletConfig = walletConfig } } catch (error) { if (isIndyError(error, 'WalletNotFoundError')) { @@ -155,7 +237,10 @@ export class IndyWallet implements Wallet { cause: error, }) } else { - const errorMessage = `Error opening wallet '${walletConfig.id}'` + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } + const errorMessage = `Error opening wallet '${walletConfig.id}': ${error.message}` this.logger.error(errorMessage, { error, errorMessage: error.message, @@ -164,6 +249,8 @@ export class IndyWallet implements Wallet { throw new WalletError(errorMessage, { cause: error }) } } + + this.logger.debug(`Wallet '${walletConfig.id}' opened with handle '${this.handle}'`) } /** @@ -171,23 +258,26 @@ export class IndyWallet implements Wallet { * @throws {WalletError} if another error occurs */ public async delete(): Promise { - const walletInfo = this.openWalletInfo - - if (!this.isInitialized || !walletInfo) { + if (!this.walletConfig) { throw new WalletError( - 'Can not delete wallet that is not initialized. Make sure to call initialize before deleting the wallet' + 'Can not delete wallet that does not have wallet config set. Make sure to call create wallet before deleting the wallet' ) } - this.logger.info(`Deleting wallet '${walletInfo.walletConfig.id}'`) + this.logger.info(`Deleting wallet '${this.walletConfig.id}'`) - await this.close() + if (this.walletHandle) { + await this.close() + } try { - await this.indy.deleteWallet(walletInfo.walletConfig, walletInfo.walletCredentials) + await this.indy.deleteWallet( + this.walletStorageConfig(this.walletConfig), + this.walletCredentials(this.walletConfig) + ) } catch (error) { if (isIndyError(error, 'WalletNotFoundError')) { - const errorMessage = `Error deleting wallet: wallet '${walletInfo.walletConfig.id}' not found` + const errorMessage = `Error deleting wallet: wallet '${this.walletConfig.id}' not found` this.logger.debug(errorMessage) throw new WalletNotFoundError(errorMessage, { @@ -195,7 +285,10 @@ export class IndyWallet implements Wallet { cause: error, }) } else { - const errorMessage = `Error deleting wallet '${walletInfo.walletConfig.id}': ${error.message}` + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } + const errorMessage = `Error deleting wallet '${this.walletConfig.id}': ${error.message}` this.logger.error(errorMessage, { error, errorMessage: error.message, @@ -206,13 +299,55 @@ export class IndyWallet implements Wallet { } } + public async export(exportConfig: WalletExportImportConfig) { + try { + this.logger.debug(`Exporting wallet ${this.walletConfig?.id} to path ${exportConfig.path}`) + await this.indy.exportWallet(this.handle, exportConfig) + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } + const errorMessage = `Error exporting wallet: ${error.message}` + this.logger.error(errorMessage, { + error, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + + public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig) { + try { + this.logger.debug(`Importing wallet ${walletConfig.id} from path ${importConfig.path}`) + await this.indy.importWallet( + { id: walletConfig.id }, + { key: walletConfig.key, key_derivation_method: walletConfig.keyDerivationMethod }, + importConfig + ) + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } + const errorMessage = `Error importing wallet': ${error.message}` + this.logger.error(errorMessage, { + error, + }) + + throw new WalletError(errorMessage, { cause: error }) + } + } + /** * @throws {WalletError} if the wallet is already closed or another error occurs */ public async close(): Promise { + if (!this.walletHandle) { + throw new WalletError('Wallet is in invalid state, you are trying to close wallet that has no `walletHandle`.') + } + try { - await this.indy.closeWallet(this.handle) - this.openWalletInfo = undefined + await this.indy.closeWallet(this.walletHandle) + this.walletHandle = undefined this.publicDidInfo = undefined } catch (error) { if (isIndyError(error, 'WalletInvalidHandle')) { @@ -223,6 +358,9 @@ export class IndyWallet implements Wallet { cause: error, }) } else { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } const errorMessage = `Error closing wallet': ${error.message}` this.logger.error(errorMessage, { error, @@ -262,6 +400,10 @@ export class IndyWallet implements Wallet { return masterSecretId } else { + if (!isIndyError(error)) { + throw new AriesFrameworkError('Attempted to throw Indy error, but it was not an Indy error') + } + this.logger.error(`Error creating master secret with id ${masterSecretId}`, { indyError: error.indyName, error, @@ -289,62 +431,204 @@ export class IndyWallet implements Wallet { return { did, verkey } } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } throw new WalletError('Error creating Did', { cause: error }) } } + /** + * Create a key with an optional seed and keyType. + * The keypair is also automatically stored in the wallet afterwards + * + * Bls12381g1g2 and X25519 are not supported. + * + * @param seed string The seed for creating a key + * @param keyType KeyType the type of key that should be created + * + * @returns a Key instance with a publicKeyBase58 + * + * @throws {WalletError} When an unsupported keytype is requested + * @throws {WalletError} When the key could not be created + */ + public async createKey({ seed, keyType }: CreateKeyOptions): Promise { + try { + if (keyType === KeyType.Ed25519) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const verkey = await this.indy.createKey(this.handle, { seed, crypto_type: 'ed25519' }) + return Key.fromPublicKeyBase58(verkey, keyType) + } + + if (keyType === KeyType.Bls12381g1 || keyType === KeyType.Bls12381g2) { + const blsKeyPair = await BbsService.createKey({ keyType, seed }) + await this.storeKeyPair(blsKeyPair) + return Key.fromPublicKeyBase58(blsKeyPair.publicKeyBase58, keyType) + } + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } + throw new WalletError(`Error creating key with key type '${keyType}': ${error.message}`, { cause: error }) + } + + throw new WalletError(`Unsupported key type: '${keyType}' for wallet IndyWallet`) + } + + /** + * sign a Buffer with an instance of a Key class + * + * Bls12381g1g2, Bls12381g1 and X25519 are not supported. + * + * @param data Buffer The data that needs to be signed + * @param key Key The key that is used to sign the data + * + * @returns A signature for the data + */ + public async sign({ data, key }: SignOptions): Promise { + try { + if (key.keyType === KeyType.Ed25519) { + // Checks to see if it is an not an Array of messages, but just a single one + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`${KeyType.Ed25519} does not support multiple singing of multiple messages`) + } + return await this.indy.cryptoSign(this.handle, key.publicKeyBase58, data as Buffer) + } + + if (key.keyType === KeyType.Bls12381g2) { + const blsKeyPair = await this.retrieveKeyPair(key.publicKeyBase58) + return BbsService.sign({ + messages: data, + publicKey: key.publicKey, + privateKey: TypedArrayEncoder.fromBase58(blsKeyPair.privateKeyBase58), + }) + } + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } + throw new WalletError(`Error signing data with verkey ${key.publicKeyBase58}`, { cause: error }) + } + throw new WalletError(`Unsupported keyType: ${key.keyType}`) + } + + /** + * Verify the signature with the data and the used key + * + * Bls12381g1g2, Bls12381g1 and X25519 are not supported. + * + * @param data Buffer The data that has to be confirmed to be signed + * @param key Key The key that was used in the signing process + * @param signature Buffer The signature that was created by the signing process + * + * @returns A boolean whether the signature was created with the supplied data and key + * + * @throws {WalletError} When it could not do the verification + * @throws {WalletError} When an unsupported keytype is used + */ + public async verify({ data, key, signature }: VerifyOptions): Promise { + try { + if (key.keyType === KeyType.Ed25519) { + // Checks to see if it is an not an Array of messages, but just a single one + if (!TypedArrayEncoder.isTypedArray(data)) { + throw new WalletError(`${KeyType.Ed25519} does not support multiple singing of multiple messages`) + } + return await this.indy.cryptoVerify(key.publicKeyBase58, data as Buffer, signature) + } + + if (key.keyType === KeyType.Bls12381g2) { + return await BbsService.verify({ signature, publicKey: key.publicKey, messages: data }) + } + } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } + throw new WalletError(`Error verifying signature of data signed with verkey ${key.publicKeyBase58}`, { + cause: error, + }) + } + throw new WalletError(`Unsupported keyType: ${key.keyType}`) + } + public async pack( payload: Record, recipientKeys: string[], senderVerkey?: string - ): Promise { + ): Promise { try { const messageRaw = JsonEncoder.toBuffer(payload) const packedMessage = await this.indy.packMessage(this.handle, messageRaw, recipientKeys, senderVerkey ?? null) return JsonEncoder.fromBuffer(packedMessage) } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } throw new WalletError('Error packing message', { cause: error }) } } - public async unpack(messagePackage: WireMessage): Promise { + public async unpack(messagePackage: EncryptedMessage): Promise { try { const unpackedMessageBuffer = await this.indy.unpackMessage(this.handle, JsonEncoder.toBuffer(messagePackage)) const unpackedMessage = JsonEncoder.fromBuffer(unpackedMessageBuffer) return { - senderVerkey: unpackedMessage.sender_verkey, - recipientVerkey: unpackedMessage.recipient_verkey, - message: JsonEncoder.fromString(unpackedMessage.message), + senderKey: unpackedMessage.sender_verkey, + recipientKey: unpackedMessage.recipient_verkey, + plaintextMessage: JsonEncoder.fromString(unpackedMessage.message), } } catch (error) { + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } throw new WalletError('Error unpacking message', { cause: error }) } } - public async sign(data: Buffer, verkey: string): Promise { + public async generateNonce(): Promise { try { - return await this.indy.cryptoSign(this.handle, verkey, data) + return await this.indy.generateNonce() } catch (error) { - throw new WalletError(`Error signing data with verkey ${verkey}`, { cause: error }) + if (!isError(error)) { + throw new AriesFrameworkError('Attempted to throw error, but it was not of type Error') + } + throw new WalletError('Error generating nonce', { cause: error }) } } - public async verify(signerVerkey: string, data: Buffer, signature: Buffer): Promise { + private async retrieveKeyPair(publicKeyBase58: string): Promise { try { - // check signature - const isValid = await this.indy.cryptoVerify(signerVerkey, data, signature) - - return isValid + const { value } = await this.indy.getWalletRecord(this.handle, 'KeyPairRecord', `keypair-${publicKeyBase58}`, {}) + if (value) { + return JsonEncoder.fromString(value) as BlsKeyPair + } else { + throw new WalletError(`No content found for record with public key: ${publicKeyBase58}`) + } } catch (error) { - throw new WalletError(`Error verifying signature of data signed with verkey ${signerVerkey}`, { cause: error }) + if (isIndyError(error, 'WalletItemNotFound')) { + throw new RecordNotFoundError(`KeyPairRecord not found for public key: ${publicKeyBase58}.`, { + recordType: 'KeyPairRecord', + cause: error, + }) + } + throw isIndyError(error) ? new IndySdkError(error) : error } } - public async generateNonce() { + private async storeKeyPair(blsKeyPair: BlsKeyPair): Promise { try { - return await this.indy.generateNonce() + await this.indy.addWalletRecord( + this.handle, + 'KeyPairRecord', + `keypair-${blsKeyPair.publicKeyBase58}`, + JSON.stringify(blsKeyPair), + {} + ) } catch (error) { - throw new WalletError('Error generating nonce', { cause: error }) + if (isIndyError(error, 'WalletItemAlreadyExists')) { + throw new RecordDuplicateError(`Record already exists`, { recordType: 'KeyPairRecord' }) + } + throw isIndyError(error) ? new IndySdkError(error) : error } } } diff --git a/packages/core/src/wallet/Wallet.test.ts b/packages/core/src/wallet/Wallet.test.ts deleted file mode 100644 index 25ec52ba3f..0000000000 --- a/packages/core/src/wallet/Wallet.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getAgentConfig } from '../../tests/helpers' - -import { IndyWallet } from './IndyWallet' - -describe('Wallet', () => { - const config = getAgentConfig('WalletTest') - const wallet = new IndyWallet(config) - - test('initialize public did', async () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await wallet.initialize(config.walletConfig!) - - await wallet.initPublicDid({ seed: '00000000000000000000000Forward01' }) - - expect(wallet.publicDid).toEqual({ - did: 'DtWRdd6C5dN5vpcN6XRAvu', - verkey: '82RBSn3heLgXzZd74UsMC8Q8YRfEEhQoAM7LUqE6bevJ', - }) - }) - - afterEach(async () => { - await wallet.delete() - }) -}) diff --git a/packages/core/src/wallet/Wallet.ts b/packages/core/src/wallet/Wallet.ts index 1302e1a3f6..06a8899c4c 100644 --- a/packages/core/src/wallet/Wallet.ts +++ b/packages/core/src/wallet/Wallet.ts @@ -1,20 +1,35 @@ -import type { WireMessage, UnpackedMessageContext, WalletConfig } from '../types' +import type { Key, KeyType } from '../crypto' +import type { + EncryptedMessage, + WalletConfig, + WalletConfigRekey, + PlaintextMessage, + WalletExportImportConfig, +} from '../types' import type { Buffer } from '../utils/buffer' export interface Wallet { publicDid: DidInfo | undefined isInitialized: boolean + isProvisioned: boolean - initialize(walletConfig: WalletConfig): Promise + create(walletConfig: WalletConfig): Promise + createAndOpen(walletConfig: WalletConfig): Promise + open(walletConfig: WalletConfig): Promise + rotateKey(walletConfig: WalletConfigRekey): Promise close(): Promise delete(): Promise + export(exportConfig: WalletExportImportConfig): Promise + import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise + + createKey(options: CreateKeyOptions): Promise + sign(options: SignOptions): Promise + verify(options: VerifyOptions): Promise initPublicDid(didConfig: DidConfig): Promise createDid(didConfig?: DidConfig): Promise - pack(payload: Record, recipientKeys: string[], senderVerkey?: string): Promise - unpack(messagePackage: WireMessage): Promise - sign(data: Buffer, verkey: string): Promise - verify(signerVerkey: string, data: Buffer, signature: Buffer): Promise + pack(payload: Record, recipientKeys: string[], senderVerkey?: string): Promise + unpack(encryptedMessage: EncryptedMessage): Promise generateNonce(): Promise } @@ -23,6 +38,28 @@ export interface DidInfo { verkey: string } +export interface CreateKeyOptions { + keyType: KeyType + seed?: string +} + +export interface SignOptions { + data: Buffer | Buffer[] + key: Key +} + +export interface VerifyOptions { + data: Buffer | Buffer[] + key: Key + signature: Buffer +} + export interface DidConfig { seed?: string } + +export interface UnpackedMessageContext { + plaintextMessage: PlaintextMessage + senderKey?: string + recipientKey?: string +} diff --git a/packages/core/src/wallet/WalletModule.ts b/packages/core/src/wallet/WalletModule.ts new file mode 100644 index 0000000000..4ee09afb7c --- /dev/null +++ b/packages/core/src/wallet/WalletModule.ts @@ -0,0 +1,107 @@ +import type { Logger } from '../logger' +import type { WalletConfig, WalletConfigRekey, WalletExportImportConfig } from '../types' + +import { inject, Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../agent/AgentConfig' +import { InjectionSymbols } from '../constants' +import { StorageUpdateService } from '../storage' +import { CURRENT_FRAMEWORK_STORAGE_VERSION } from '../storage/migration/updates' + +import { Wallet } from './Wallet' +import { WalletError } from './error/WalletError' +import { WalletNotFoundError } from './error/WalletNotFoundError' + +@scoped(Lifecycle.ContainerScoped) +export class WalletModule { + private wallet: Wallet + private storageUpdateService: StorageUpdateService + private logger: Logger + private _walletConfig?: WalletConfig + + public constructor( + @inject(InjectionSymbols.Wallet) wallet: Wallet, + storageUpdateService: StorageUpdateService, + agentConfig: AgentConfig + ) { + this.wallet = wallet + this.storageUpdateService = storageUpdateService + this.logger = agentConfig.logger + } + + public get isInitialized() { + return this.wallet.isInitialized + } + + public get isProvisioned() { + return this.wallet.isProvisioned + } + + public get walletConfig() { + return this._walletConfig + } + + public async initialize(walletConfig: WalletConfig): Promise { + this.logger.info(`Initializing wallet '${walletConfig.id}'`, walletConfig) + + if (this.isInitialized) { + throw new WalletError( + 'Wallet instance already initialized. Close the currently opened wallet before re-initializing the wallet' + ) + } + + // Open wallet, creating if it doesn't exist yet + try { + await this.open(walletConfig) + } catch (error) { + // If the wallet does not exist yet, create it and try to open again + if (error instanceof WalletNotFoundError) { + // Keep the wallet open after creating it, this saves an extra round trip of closing/opening + // the wallet, which can save quite some time. + await this.createAndOpen(walletConfig) + } else { + throw error + } + } + } + + public async createAndOpen(walletConfig: WalletConfig): Promise { + // Always keep the wallet open, as we still need to store the storage version in the wallet. + await this.wallet.createAndOpen(walletConfig) + + this._walletConfig = walletConfig + + // Store the storage version in the wallet + await this.storageUpdateService.setCurrentStorageVersion(CURRENT_FRAMEWORK_STORAGE_VERSION) + } + + public async create(walletConfig: WalletConfig): Promise { + await this.createAndOpen(walletConfig) + await this.close() + } + + public async open(walletConfig: WalletConfig): Promise { + await this.wallet.open(walletConfig) + this._walletConfig = walletConfig + } + + public async close(): Promise { + await this.wallet.close() + } + + public async rotateKey(walletConfig: WalletConfigRekey): Promise { + await this.wallet.rotateKey(walletConfig) + } + + public async delete(): Promise { + await this.wallet.delete() + } + + public async export(exportConfig: WalletExportImportConfig): Promise { + await this.wallet.export(exportConfig) + } + + public async import(walletConfig: WalletConfig, importConfig: WalletExportImportConfig): Promise { + await this.wallet.import(walletConfig, importConfig) + } +} diff --git a/packages/core/src/wallet/index.ts b/packages/core/src/wallet/index.ts new file mode 100644 index 0000000000..c9f6729d0c --- /dev/null +++ b/packages/core/src/wallet/index.ts @@ -0,0 +1 @@ +export * from './Wallet' diff --git a/packages/core/tests/TestMessage.ts b/packages/core/tests/TestMessage.ts index 299f1f6147..040e4303f7 100644 --- a/packages/core/tests/TestMessage.ts +++ b/packages/core/tests/TestMessage.ts @@ -7,5 +7,5 @@ export class TestMessage extends AgentMessage { this.id = this.generateId() } - public readonly type = 'https://didcomm.org/connections/1.0/invitation' + public type = 'https://didcomm.org/connections/1.0/invitation' } diff --git a/packages/core/tests/agents.test.ts b/packages/core/tests/agents.test.ts index 5923eeab68..484d98aa4c 100644 --- a/packages/core/tests/agents.test.ts +++ b/packages/core/tests/agents.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' import type { ConnectionRecord } from '../src/modules/connections' @@ -6,6 +7,7 @@ import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' import { Agent } from '../src/agent/Agent' +import { HandshakeProtocol } from '../src/modules/connections' import { waitForBasicMessage, getBaseConfig } from './helpers' @@ -23,13 +25,10 @@ describe('agents', () => { let bobConnection: ConnectionRecord afterAll(async () => { - await bobAgent.shutdown({ - deleteWallet: true, - }) - - await aliceAgent.shutdown({ - deleteWallet: true, - }) + await bobAgent.shutdown() + await bobAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() }) test('make a connection between agents', async () => { @@ -43,19 +42,25 @@ describe('agents', () => { aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) - aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(aliceMessages, subjectMap)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() bobAgent = new Agent(bobConfig.config, bobConfig.agentDependencies) bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) - bobAgent.registerOutboundTransport(new SubjectOutboundTransport(bobMessages, subjectMap)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await bobAgent.initialize() - const aliceConnectionAtAliceBob = await aliceAgent.connections.createConnection() - const bobConnectionAtBobAlice = await bobAgent.connections.receiveInvitation(aliceConnectionAtAliceBob.invitation) + const aliceBobOutOfBandRecord = await aliceAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const { connectionRecord: bobConnectionAtBobAlice } = await bobAgent.oob.receiveInvitation( + aliceBobOutOfBandRecord.outOfBandInvitation + ) + bobConnection = await bobAgent.connections.returnWhenIsConnected(bobConnectionAtBobAlice!.id) - aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionAtAliceBob.connectionRecord.id) - bobConnection = await bobAgent.connections.returnWhenIsConnected(bobConnectionAtBobAlice.id) + const [aliceConnectionAtAliceBob] = await aliceAgent.connections.findAllByOutOfBandId(aliceBobOutOfBandRecord.id) + aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionAtAliceBob!.id) expect(aliceConnection).toBeConnectedWith(bobConnection) expect(bobConnection).toBeConnectedWith(aliceConnection) @@ -71,4 +76,12 @@ describe('agents', () => { expect(basicMessage.content).toBe(message) }) + + test('can shutdown and re-initialize the same agent', async () => { + expect(aliceAgent.isInitialized).toBe(true) + await aliceAgent.shutdown() + expect(aliceAgent.isInitialized).toBe(false) + await aliceAgent.initialize() + expect(aliceAgent.isInitialized).toBe(true) + }) }) diff --git a/packages/core/tests/connectionless-proofs.test.ts b/packages/core/tests/connectionless-proofs.test.ts index df59c5e816..f6bee43a02 100644 --- a/packages/core/tests/connectionless-proofs.test.ts +++ b/packages/core/tests/connectionless-proofs.test.ts @@ -1,3 +1,14 @@ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { ProofStateChangedEvent } from '../src/modules/proofs' + +import { Subject, ReplaySubject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { Agent } from '../src/agent/Agent' +import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachment' +import { HandshakeProtocol } from '../src/modules/connections' +import { V1CredentialPreview } from '../src/modules/credentials' import { PredicateType, ProofState, @@ -5,18 +16,40 @@ import { AttributeFilter, ProofPredicateInfo, AutoAcceptProof, + ProofEventTypes, } from '../src/modules/proofs' +import { MediatorPickupStrategy } from '../src/modules/routing' +import { LinkedAttachment } from '../src/utils/LinkedAttachment' +import { sleep } from '../src/utils/sleep' +import { uuid } from '../src/utils/uuid' -import { setupProofsTest, waitForProofRecordSubject } from './helpers' +import { + getBaseConfig, + issueCredential, + makeConnection, + prepareForIssuance, + setupProofsTest, + waitForProofRecordSubject, +} from './helpers' import testLogger from './logger' describe('Present Proof', () => { + let agents: Agent[] + + afterEach(async () => { + for (const agent of agents) { + await agent.shutdown() + await agent.wallet.delete() + } + }) + test('Faber starts with connection-less proof requests to Alice', async () => { const { aliceAgent, faberAgent, aliceReplay, credDefId, faberReplay } = await setupProofsTest( 'Faber connection-less Proofs', 'Alice connection-less Proofs', AutoAcceptProof.Never ) + agents = [aliceAgent, faberAgent] testLogger.test('Faber sends presentation request to Alice') const attributes = { @@ -93,6 +126,8 @@ describe('Present Proof', () => { AutoAcceptProof.Always ) + agents = [aliceAgent, faberAgent] + const attributes = { name: new ProofAttributeInfo({ name: 'name', @@ -141,4 +176,178 @@ describe('Present Proof', () => { state: ProofState.Done, }) }) + + test('Faber starts with connection-less proof requests to Alice with auto-accept enabled and both agents having a mediator', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + }) + + const unique = uuid().substring(0, 4) + + const mediatorConfig = getBaseConfig(`Connectionless proofs with mediator Mediator-${unique}`, { + autoAcceptMediationRequests: true, + endpoints: ['rxjs:mediator'], + }) + + const faberMessages = new Subject() + const aliceMessages = new Subject() + const mediatorMessages = new Subject() + + const subjectMap = { + 'rxjs:mediator': mediatorMessages, + } + + // Initialize mediator + const mediatorAgent = new Agent(mediatorConfig.config, mediatorConfig.agentDependencies) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + const faberMediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'faber invitation', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const aliceMediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'alice invitation', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const faberConfig = getBaseConfig(`Connectionless proofs with mediator Faber-${unique}`, { + autoAcceptProofs: AutoAcceptProof.Always, + mediatorConnectionsInvite: faberMediationOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }) + + const aliceConfig = getBaseConfig(`Connectionless proofs with mediator Alice-${unique}`, { + autoAcceptProofs: AutoAcceptProof.Always, + mediatorConnectionsInvite: aliceMediationOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }) + + const faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + await faberAgent.initialize() + + const aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + await aliceAgent.initialize() + + agents = [aliceAgent, faberAgent, mediatorAgent] + + const { definition } = await prepareForIssuance(faberAgent, ['name', 'age', 'image_0', 'image_1']) + + const [faberConnection, aliceConnection] = await makeConnection(faberAgent, aliceAgent) + expect(faberConnection.isReady).toBe(true) + expect(aliceConnection.isReady).toBe(true) + + await issueCredential({ + issuerAgent: faberAgent, + issuerConnectionId: faberConnection.id, + holderAgent: aliceAgent, + credentialTemplate: { + credentialDefinitionId: definition.id, + comment: 'some comment about credential', + preview: credentialPreview, + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + }) + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + faberAgent.events.observable(ProofEventTypes.ProofStateChanged).subscribe(faberReplay) + aliceAgent.events.observable(ProofEventTypes.ProofStateChanged).subscribe(aliceReplay) + + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: definition.id, + }), + ], + }), + } + + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: definition.id, + }), + ], + }), + } + + // eslint-disable-next-line prefer-const + let { proofRecord: faberProofRecord, requestMessage } = await faberAgent.proofs.createOutOfBandRequest( + { + name: 'test-proof-request', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + { + autoAcceptProof: AutoAcceptProof.ContentApproved, + } + ) + + const mediationRecord = await faberAgent.mediationRecipient.findDefaultMediator() + if (!mediationRecord) { + throw new Error('Faber agent has no default mediator') + } + + expect(requestMessage).toMatchObject({ + service: { + recipientKeys: [expect.any(String)], + routingKeys: mediationRecord.routingKeys, + serviceEndpoint: mediationRecord.endpoint, + }, + }) + + await aliceAgent.receiveMessage(requestMessage.toJSON()) + + await waitForProofRecordSubject(aliceReplay, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + + await waitForProofRecordSubject(faberReplay, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + + // We want to stop the mediator polling before the agent is shutdown. + // FIXME: add a way to stop mediator polling from the public api, and make sure this is + // being handled in the agent shutdown so we don't get any errors with wallets being closed. + faberAgent.config.stop$.next(true) + aliceAgent.config.stop$.next(true) + await sleep(2000) + }) }) diff --git a/packages/core/tests/connections.test.ts b/packages/core/tests/connections.test.ts index 842d27d497..e9cbe9906d 100644 --- a/packages/core/tests/connections.test.ts +++ b/packages/core/tests/connections.test.ts @@ -1,76 +1,142 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' import { Subject } from 'rxjs' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { ConnectionState } from '../src' +import { DidExchangeState, HandshakeProtocol } from '../src' import { Agent } from '../src/agent/Agent' +import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' import { getBaseConfig } from './helpers' -const faberConfig = getBaseConfig('Faber Agent Connections', { - endpoints: ['rxjs:faber'], -}) -const aliceConfig = getBaseConfig('Alice Agent Connections', { - endpoints: ['rxjs:alice'], -}) - describe('connections', () => { let faberAgent: Agent let aliceAgent: Agent + let acmeAgent: Agent + + afterEach(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + await acmeAgent.shutdown() + await acmeAgent.wallet.delete() + }) + + it('one should be able to make multiple connections using a multi use invite', async () => { + const faberConfig = getBaseConfig('Faber Agent Connections', { + endpoints: ['rxjs:faber'], + }) + const aliceConfig = getBaseConfig('Alice Agent Connections', { + endpoints: ['rxjs:alice'], + }) + const acmeConfig = getBaseConfig('Acme Agent Connections', { + endpoints: ['rxjs:acme'], + }) - beforeAll(async () => { const faberMessages = new Subject() const aliceMessages = new Subject() + const acmeMessages = new Subject() const subjectMap = { 'rxjs:faber': faberMessages, 'rxjs:alice': aliceMessages, + 'rxjs:acme': acmeMessages, } faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) - faberAgent.registerOutboundTransport(new SubjectOutboundTransport(aliceMessages, subjectMap)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await faberAgent.initialize() aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) - aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(faberMessages, subjectMap)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() - }) - afterAll(async () => { - await faberAgent.shutdown({ - deleteWallet: true, + acmeAgent = new Agent(acmeConfig.config, acmeConfig.agentDependencies) + acmeAgent.registerInboundTransport(new SubjectInboundTransport(acmeMessages)) + acmeAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await acmeAgent.initialize() + + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + multiUseInvitation: true, }) - await aliceAgent.shutdown({ - deleteWallet: true, + + const invitation = faberOutOfBandRecord.outOfBandInvitation + const invitationUrl = invitation.toUrl({ domain: 'https://example.com' }) + + // Receive invitation first time with alice agent + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + // Receive invitation second time with acme agent + let { connectionRecord: acmeFaberConnection } = await acmeAgent.oob.receiveInvitationFromUrl(invitationUrl, { + reuseConnection: false, }) + acmeFaberConnection = await acmeAgent.connections.returnWhenIsConnected(acmeFaberConnection!.id) + expect(acmeFaberConnection.state).toBe(DidExchangeState.Completed) + + let faberAliceConnection = await faberAgent.connections.getByThreadId(aliceFaberConnection.threadId!) + let faberAcmeConnection = await faberAgent.connections.getByThreadId(acmeFaberConnection.threadId!) + + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection.id) + faberAcmeConnection = await faberAgent.connections.returnWhenIsConnected(faberAcmeConnection.id) + + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(faberAcmeConnection).toBeConnectedWith(acmeFaberConnection) + + expect(faberAliceConnection.id).not.toBe(faberAcmeConnection.id) + + return expect(faberOutOfBandRecord.state).toBe(OutOfBandState.AwaitResponse) }) - it('should be able to make multiple connections using a multi use invite', async () => { - const { - invitation, - connectionRecord: { id: faberConnectionId }, - } = await faberAgent.connections.createConnection({ + xit('should be able to make multiple connections using a multi use invite', async () => { + const faberMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + } + + const faberConfig = getBaseConfig('Faber Agent Connections 2', { + endpoints: ['rxjs:faber'], + }) + const aliceConfig = getBaseConfig('Alice Agent Connections 2') + + // Faber defines both inbound and outbound transports + faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + // Alice only has outbound transport + aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], multiUseInvitation: true, }) + const invitation = faberOutOfBandRecord.outOfBandInvitation const invitationUrl = invitation.toUrl({ domain: 'https://example.com' }) // Create first connection - let aliceFaberConnection1 = await aliceAgent.connections.receiveInvitationFromUrl(invitationUrl) - aliceFaberConnection1 = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection1.id) - expect(aliceFaberConnection1.state).toBe(ConnectionState.Complete) + let { connectionRecord: aliceFaberConnection1 } = await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + aliceFaberConnection1 = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection1!.id) + expect(aliceFaberConnection1.state).toBe(DidExchangeState.Completed) // Create second connection - let aliceFaberConnection2 = await aliceAgent.connections.receiveInvitationFromUrl(invitationUrl) - aliceFaberConnection2 = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection2.id) - expect(aliceFaberConnection2.state).toBe(ConnectionState.Complete) + let { connectionRecord: aliceFaberConnection2 } = await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl, { + reuseConnection: false, + }) + aliceFaberConnection2 = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection2!.id) + expect(aliceFaberConnection2.state).toBe(DidExchangeState.Completed) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion let faberAliceConnection1 = await faberAgent.connections.getByThreadId(aliceFaberConnection1.threadId!) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion let faberAliceConnection2 = await faberAgent.connections.getByThreadId(aliceFaberConnection2.threadId!) faberAliceConnection1 = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection1.id) @@ -79,8 +145,8 @@ describe('connections', () => { expect(faberAliceConnection1).toBeConnectedWith(aliceFaberConnection1) expect(faberAliceConnection2).toBeConnectedWith(aliceFaberConnection2) - const faberConnection = await faberAgent.connections.getById(faberConnectionId) - // Expect initial connection to still be in state invited - return expect(faberConnection.state).toBe(ConnectionState.Invited) + expect(faberAliceConnection1.id).not.toBe(faberAliceConnection2.id) + + return expect(faberOutOfBandRecord.state).toBe(OutOfBandState.AwaitResponse) }) }) diff --git a/packages/core/tests/credentials-auto-accept.test.ts b/packages/core/tests/credentials-auto-accept.test.ts deleted file mode 100644 index 01e396d502..0000000000 --- a/packages/core/tests/credentials-auto-accept.test.ts +++ /dev/null @@ -1,489 +0,0 @@ -import type { Agent } from '../src/agent/Agent' -import type { ConnectionRecord } from '../src/modules/connections' - -import { AutoAcceptCredential, CredentialPreview, CredentialRecord, CredentialState } from '../src/modules/credentials' -import { JsonTransformer } from '../src/utils/JsonTransformer' -import { sleep } from '../src/utils/sleep' - -import { setupCredentialTests, waitForCredentialRecord } from './helpers' -import testLogger from './logger' - -const credentialPreview = CredentialPreview.fromRecord({ - name: 'John', - age: '99', -}) - -const newCredentialPreview = CredentialPreview.fromRecord({ - name: 'John', - age: '99', - lastname: 'Appleseed', -}) - -describe('auto accept credentials', () => { - let faberAgent: Agent - let aliceAgent: Agent - let credDefId: string - let schemaId: string - let faberConnection: ConnectionRecord - let aliceConnection: ConnectionRecord - let faberCredentialRecord: CredentialRecord - let aliceCredentialRecord: CredentialRecord - - describe('Auto accept on `always`', () => { - beforeAll(async () => { - ;({ faberAgent, aliceAgent, credDefId, schemaId, faberConnection, aliceConnection } = await setupCredentialTests( - 'faber agent: always', - 'alice agent: always', - AutoAcceptCredential.Always - )) - }) - - afterAll(async () => { - await aliceAgent.shutdown({ - deleteWallet: true, - }) - await faberAgent.shutdown({ - deleteWallet: true, - }) - }) - - test('Alice starts with credential proposal to Faber, both with autoAcceptCredential on `always`', async () => { - testLogger.test('Alice sends credential proposal to Faber') - let aliceCredentialRecord = await aliceAgent.credentials.proposeCredential(aliceConnection.id, { - credentialProposal: credentialPreview, - credentialDefinitionId: credDefId, - }) - - testLogger.test('Alice waits for credential from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.CredentialReceived, - }) - - testLogger.test('Faber waits for credential ack from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.Done, - }) - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - metadata: { - data: { - '_internal/indyRequest': expect.any(Object), - '_internal/indyCredential': { - schemaId, - credentialDefinitionId: credDefId, - }, - }, - }, - credentialId: expect.any(String), - state: CredentialState.Done, - }) - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - metadata: { - data: { - '_internal/indyCredential': { - schemaId, - credentialDefinitionId: credDefId, - }, - }, - }, - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - state: CredentialState.Done, - }) - }) - - test('Faber starts with credential offer to Alice, both with autoAcceptCredential on `always`', async () => { - testLogger.test('Faber sends credential offer to Alice') - faberCredentialRecord = await faberAgent.credentials.offerCredential(faberConnection.id, { - preview: credentialPreview, - credentialDefinitionId: credDefId, - comment: 'some comment about credential', - }) - - testLogger.test('Alice waits for credential from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.CredentialReceived, - }) - - testLogger.test('Faber waits for credential ack from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.Done, - }) - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - metadata: { - data: { - '_internal/indyRequest': expect.any(Object), - '_internal/indyCredential': { - schemaId, - credentialDefinitionId: credDefId, - }, - }, - }, - credentialId: expect.any(String), - state: CredentialState.Done, - }) - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - state: CredentialState.Done, - }) - }) - }) - - describe('Auto accept on `contentApproved`', () => { - beforeAll(async () => { - ;({ faberAgent, aliceAgent, credDefId, schemaId, faberConnection, aliceConnection } = await setupCredentialTests( - 'faber agent: contentApproved', - 'alice agent: contentApproved', - AutoAcceptCredential.ContentApproved - )) - }) - - afterAll(async () => { - await aliceAgent.shutdown({ - deleteWallet: true, - }) - await faberAgent.shutdown({ - deleteWallet: true, - }) - }) - - test('Alice starts with credential proposal to Faber, both with autoAcceptCredential on `contentApproved`', async () => { - testLogger.test('Alice sends credential proposal to Faber') - let aliceCredentialRecord = await aliceAgent.credentials.proposeCredential(aliceConnection.id, { - credentialProposal: credentialPreview, - credentialDefinitionId: credDefId, - }) - - testLogger.test('Faber waits for credential proposal from Alice') - let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.ProposalReceived, - }) - - testLogger.test('Faber sends credential offer to Alice') - faberCredentialRecord = await faberAgent.credentials.acceptProposal(faberCredentialRecord.id) - - testLogger.test('Alice waits for credential from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.CredentialReceived, - }) - - testLogger.test('Faber waits for credential ack from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.Done, - }) - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - metadata: { - data: { - '_internal/indyRequest': expect.any(Object), - '_internal/indyCredential': { - schemaId, - credentialDefinitionId: credDefId, - }, - }, - }, - credentialId: expect.any(String), - state: CredentialState.Done, - }) - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - metadata: { - data: { - '_internal/indyCredential': { - schemaId, - credentialDefinitionId: credDefId, - }, - }, - }, - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - state: CredentialState.Done, - }) - }) - - test('Faber starts with credential offer to Alice, both with autoAcceptCredential on `contentApproved`', async () => { - testLogger.test('Faber sends credential offer to Alice') - faberCredentialRecord = await faberAgent.credentials.offerCredential(faberConnection.id, { - preview: credentialPreview, - credentialDefinitionId: credDefId, - }) - - testLogger.test('Alice waits for credential offer from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.OfferReceived, - }) - - expect(JsonTransformer.toJSON(aliceCredentialRecord)).toMatchObject({ - createdAt: expect.any(Date), - offerMessage: { - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', - credential_preview: { - '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - value: 'John', - }, - { - name: 'age', - value: '99', - }, - ], - }, - 'offers~attach': expect.any(Array), - }, - state: CredentialState.OfferReceived, - }) - - // below values are not in json object - expect(aliceCredentialRecord.id).not.toBeNull() - expect(aliceCredentialRecord.getTags()).toEqual({ - threadId: aliceCredentialRecord.threadId, - state: aliceCredentialRecord.state, - connectionId: aliceConnection.id, - }) - expect(aliceCredentialRecord.type).toBe(CredentialRecord.name) - - testLogger.test('alice sends credential request to faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptOffer(aliceCredentialRecord.id) - - testLogger.test('Alice waits for credential from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.CredentialReceived, - }) - - testLogger.test('Faber waits for credential ack from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.Done, - }) - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - metadata: { - data: { - '_internal/indyRequest': expect.any(Object), - '_internal/indyCredential': { - schemaId, - credentialDefinitionId: credDefId, - }, - }, - }, - credentialId: expect.any(String), - state: CredentialState.Done, - }) - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - state: CredentialState.Done, - }) - }) - - test('Alice starts with credential proposal to Faber, both have autoAcceptCredential on `contentApproved` and attributes did change', async () => { - testLogger.test('Alice sends credential proposal to Faber') - let aliceCredentialRecord = await aliceAgent.credentials.proposeCredential(aliceConnection.id, { - credentialProposal: credentialPreview, - credentialDefinitionId: credDefId, - }) - - testLogger.test('Faber waits for credential proposal from Alice') - let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.ProposalReceived, - }) - - faberCredentialRecord = await faberAgent.credentials.negotiateProposal( - faberCredentialRecord.id, - newCredentialPreview - ) - - testLogger.test('Alice waits for credential offer from Faber') - - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.OfferReceived, - }) - - expect(JsonTransformer.toJSON(aliceCredentialRecord)).toMatchObject({ - createdAt: expect.any(Date), - offerMessage: { - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', - credential_preview: { - '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - value: 'John', - }, - { - name: 'age', - value: '99', - }, - { - name: 'lastname', - value: 'Appleseed', - }, - ], - }, - 'offers~attach': expect.any(Array), - }, - state: CredentialState.OfferReceived, - }) - - // below values are not in json object - expect(aliceCredentialRecord.id).not.toBeNull() - expect(aliceCredentialRecord.getTags()).toEqual({ - threadId: aliceCredentialRecord.threadId, - state: aliceCredentialRecord.state, - connectionId: aliceConnection.id, - }) - expect(aliceCredentialRecord.type).toBe(CredentialRecord.name) - - // Wait for ten seconds - await sleep(5000) - - // Check if the state of the credential records did not change - faberCredentialRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) - faberCredentialRecord.assertState(CredentialState.OfferSent) - - aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id) - aliceCredentialRecord.assertState(CredentialState.OfferReceived) - }) - - test('Faber starts with credential offer to Alice, both have autoAcceptCredential on `contentApproved` and attributes did change', async () => { - testLogger.test('Faber sends credential offer to Alice') - faberCredentialRecord = await faberAgent.credentials.offerCredential(faberConnection.id, { - preview: credentialPreview, - credentialDefinitionId: credDefId, - }) - - testLogger.test('Alice waits for credential offer from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.OfferReceived, - }) - - expect(JsonTransformer.toJSON(aliceCredentialRecord)).toMatchObject({ - createdAt: expect.any(Date), - offerMessage: { - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', - credential_preview: { - '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - value: 'John', - }, - { - name: 'age', - value: '99', - }, - ], - }, - 'offers~attach': expect.any(Array), - }, - state: CredentialState.OfferReceived, - }) - - // below values are not in json object - expect(aliceCredentialRecord.id).not.toBeNull() - expect(aliceCredentialRecord.getTags()).toEqual({ - threadId: aliceCredentialRecord.threadId, - state: aliceCredentialRecord.state, - connectionId: aliceConnection.id, - }) - expect(aliceCredentialRecord.type).toBe(CredentialRecord.name) - - testLogger.test('Alice sends credential request to Faber') - aliceCredentialRecord = await aliceAgent.credentials.negotiateOffer( - aliceCredentialRecord.id, - newCredentialPreview - ) - - expect(JsonTransformer.toJSON(aliceCredentialRecord)).toMatchObject({ - createdAt: expect.any(Date), - proposalMessage: { - '@type': 'https://didcomm.org/issue-credential/1.0/propose-credential', - '@id': expect.any(String), - credential_proposal: { - '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - value: 'John', - }, - { - name: 'age', - value: '99', - }, - { - name: 'lastname', - value: 'Appleseed', - }, - ], - }, - '~thread': { thid: expect.any(String) }, - }, - state: CredentialState.ProposalSent, - }) - - // Wait for ten seconds - await sleep(5000) - - // Check if the state of fabers credential record did not change - faberCredentialRecord = await faberAgent.credentials.getById(faberCredentialRecord.id) - faberCredentialRecord.assertState(CredentialState.ProposalReceived) - - aliceCredentialRecord = await aliceAgent.credentials.getById(aliceCredentialRecord.id) - aliceCredentialRecord.assertState(CredentialState.ProposalSent) - }) - }) -}) diff --git a/packages/core/tests/credentials.test.ts b/packages/core/tests/credentials.test.ts deleted file mode 100644 index 307e7446ac..0000000000 --- a/packages/core/tests/credentials.test.ts +++ /dev/null @@ -1,484 +0,0 @@ -import type { Agent } from '../src/agent/Agent' -import type { ConnectionRecord } from '../src/modules/connections' - -import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachment' -import { CredentialPreview, CredentialRecord, CredentialState } from '../src/modules/credentials' -import { JsonTransformer } from '../src/utils/JsonTransformer' -import { LinkedAttachment } from '../src/utils/LinkedAttachment' - -import { setupCredentialTests, waitForCredentialRecord } from './helpers' -import testLogger from './logger' - -const credentialPreview = CredentialPreview.fromRecord({ - name: 'John', - age: '99', -}) - -describe('credentials', () => { - let faberAgent: Agent - let aliceAgent: Agent - let credDefId: string - let faberConnection: ConnectionRecord - let aliceConnection: ConnectionRecord - let faberCredentialRecord: CredentialRecord - let aliceCredentialRecord: CredentialRecord - - beforeAll(async () => { - ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection } = await setupCredentialTests( - 'Faber Agent Credentials', - 'Alice Agent Credential' - )) - }) - - afterAll(async () => { - await aliceAgent.shutdown({ - deleteWallet: true, - }) - await faberAgent.shutdown({ - deleteWallet: true, - }) - }) - - test('Alice starts with credential proposal to Faber', async () => { - testLogger.test('Alice sends credential proposal to Faber') - let aliceCredentialRecord = await aliceAgent.credentials.proposeCredential(aliceConnection.id, { - credentialProposal: credentialPreview, - credentialDefinitionId: credDefId, - }) - - testLogger.test('Faber waits for credential proposal from Alice') - let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.ProposalReceived, - }) - - testLogger.test('Faber sends credential offer to Alice') - faberCredentialRecord = await faberAgent.credentials.acceptProposal(faberCredentialRecord.id, { - comment: 'some comment about credential', - }) - - testLogger.test('Alice waits for credential offer from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.OfferReceived, - }) - - expect(JsonTransformer.toJSON(aliceCredentialRecord)).toMatchObject({ - createdAt: expect.any(Date), - offerMessage: { - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', - comment: 'some comment about credential', - credential_preview: { - '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - 'mime-type': 'text/plain', - value: 'John', - }, - { - name: 'age', - 'mime-type': 'text/plain', - value: '99', - }, - ], - }, - 'offers~attach': expect.any(Array), - }, - state: CredentialState.OfferReceived, - }) - - // below values are not in json object - expect(aliceCredentialRecord.id).not.toBeNull() - expect(aliceCredentialRecord.getTags()).toEqual({ - threadId: faberCredentialRecord.threadId, - connectionId: aliceCredentialRecord.connectionId, - state: aliceCredentialRecord.state, - }) - expect(aliceCredentialRecord.type).toBe(CredentialRecord.name) - - testLogger.test('Alice sends credential request to Faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptOffer(aliceCredentialRecord.id) - - testLogger.test('Faber waits for credential request from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.RequestReceived, - }) - - testLogger.test('Faber sends credential to Alice') - faberCredentialRecord = await faberAgent.credentials.acceptRequest(faberCredentialRecord.id) - - testLogger.test('Alice waits for credential from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.CredentialReceived, - }) - - testLogger.test('Alice sends credential ack to Faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptCredential(aliceCredentialRecord.id) - - testLogger.test('Faber waits for credential ack from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.Done, - }) - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - threadId: expect.any(String), - connectionId: expect.any(String), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - credentialId: expect.any(String), - state: CredentialState.Done, - }) - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - threadId: expect.any(String), - connectionId: expect.any(String), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - state: CredentialState.Done, - }) - }) - - test('Faber starts with credential offer to Alice', async () => { - testLogger.test('Faber sends credential offer to Alice') - faberCredentialRecord = await faberAgent.credentials.offerCredential(faberConnection.id, { - preview: credentialPreview, - credentialDefinitionId: credDefId, - comment: 'some comment about credential', - }) - - testLogger.test('Alice waits for credential offer from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.OfferReceived, - }) - - expect(JsonTransformer.toJSON(aliceCredentialRecord)).toMatchObject({ - createdAt: expect.any(Date), - offerMessage: { - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', - comment: 'some comment about credential', - credential_preview: { - '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - 'mime-type': 'text/plain', - value: 'John', - }, - { - name: 'age', - 'mime-type': 'text/plain', - value: '99', - }, - ], - }, - 'offers~attach': expect.any(Array), - }, - state: CredentialState.OfferReceived, - }) - - // below values are not in json object - expect(aliceCredentialRecord.id).not.toBeNull() - expect(aliceCredentialRecord.getTags()).toEqual({ - threadId: faberCredentialRecord.threadId, - connectionId: aliceConnection.id, - state: aliceCredentialRecord.state, - }) - expect(aliceCredentialRecord.type).toBe(CredentialRecord.name) - - testLogger.test('Alice sends credential request to Faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptOffer(aliceCredentialRecord.id) - - testLogger.test('Faber waits for credential request from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.RequestReceived, - }) - - testLogger.test('Faber sends credential to Alice') - faberCredentialRecord = await faberAgent.credentials.acceptRequest(faberCredentialRecord.id) - - testLogger.test('Alice waits for credential from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.CredentialReceived, - }) - - testLogger.test('Alice sends credential ack to Faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptCredential(aliceCredentialRecord.id) - - testLogger.test('Faber waits for credential ack from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.Done, - }) - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - metadata: expect.any(Object), - credentialId: expect.any(String), - state: CredentialState.Done, - threadId: expect.any(String), - }) - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - offerMessage: expect.any(Object), - metadata: expect.any(Object), - requestMessage: expect.any(Object), - state: CredentialState.Done, - threadId: expect.any(String), - connectionId: expect.any(String), - }) - }) - - test('Alice starts with credential proposal, with attachments, to Faber', async () => { - testLogger.test('Alice sends credential proposal to Faber') - let aliceCredentialRecord = await aliceAgent.credentials.proposeCredential(aliceConnection.id, { - credentialProposal: credentialPreview, - credentialDefinitionId: credDefId, - linkedAttachments: [ - new LinkedAttachment({ - name: 'profile_picture', - attachment: new Attachment({ - mimeType: 'image/png', - data: new AttachmentData({ base64: 'base64encodedpic' }), - }), - }), - ], - }) - - testLogger.test('Faber waits for credential proposal from Alice') - let faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.ProposalReceived, - }) - - testLogger.test('Faber sends credential offer to Alice') - faberCredentialRecord = await faberAgent.credentials.acceptProposal(faberCredentialRecord.id, { - comment: 'some comment about credential', - }) - - testLogger.test('Alice waits for credential offer from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.OfferReceived, - }) - - expect(JsonTransformer.toJSON(aliceCredentialRecord)).toMatchObject({ - createdAt: expect.any(Date), - offerMessage: { - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', - comment: 'some comment about credential', - credential_preview: { - '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - 'mime-type': 'text/plain', - value: 'John', - }, - { - name: 'age', - 'mime-type': 'text/plain', - value: '99', - }, - { - name: 'profile_picture', - 'mime-type': 'image/png', - value: 'hl:zQmcKEWE6eZWpVqGKhbmhd8SxWBa9fgLX7aYW8RJzeHQMZg', - }, - ], - }, - '~attach': [{ '@id': 'zQmcKEWE6eZWpVqGKhbmhd8SxWBa9fgLX7aYW8RJzeHQMZg' }], - 'offers~attach': expect.any(Array), - }, - state: CredentialState.OfferReceived, - }) - - // below values are not in json object - expect(aliceCredentialRecord.id).not.toBeNull() - expect(aliceCredentialRecord.getTags()).toEqual({ - state: aliceCredentialRecord.state, - threadId: faberCredentialRecord.threadId, - connectionId: aliceCredentialRecord.connectionId, - }) - expect(aliceCredentialRecord.type).toBe(CredentialRecord.name) - - testLogger.test('Alice sends credential request to Faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptOffer(aliceCredentialRecord.id) - - testLogger.test('Faber waits for credential request from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.RequestReceived, - }) - - testLogger.test('Faber sends credential to Alice') - faberCredentialRecord = await faberAgent.credentials.acceptRequest(faberCredentialRecord.id) - - testLogger.test('Alice waits for credential from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.CredentialReceived, - }) - - testLogger.test('Alice sends credential ack to Faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptCredential(aliceCredentialRecord.id) - - testLogger.test('Faber waits for credential ack from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.Done, - }) - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - metadata: expect.any(Object), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - credentialId: expect.any(String), - state: CredentialState.Done, - }) - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - metadata: expect.any(Object), - offerMessage: expect.any(Object), - requestMessage: expect.any(Object), - state: CredentialState.Done, - }) - }) - - test('Faber starts with credential, with attachments, offer to Alice', async () => { - testLogger.test('Faber sends credential offer to Alice') - faberCredentialRecord = await faberAgent.credentials.offerCredential(faberConnection.id, { - preview: credentialPreview, - credentialDefinitionId: credDefId, - comment: 'some comment about credential', - linkedAttachments: [ - new LinkedAttachment({ - name: 'x-ray', - attachment: new Attachment({ - data: new AttachmentData({ - base64: 'c2Vjb25kYmFzZTY0ZW5jb2RlZHBpYw==', - }), - }), - }), - ], - }) - - testLogger.test('Alice waits for credential offer from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.OfferReceived, - }) - - expect(JsonTransformer.toJSON(aliceCredentialRecord)).toMatchObject({ - createdAt: expect.any(Date), - offerMessage: { - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', - comment: 'some comment about credential', - credential_preview: { - '@type': 'https://didcomm.org/issue-credential/1.0/credential-preview', - attributes: [ - { - name: 'name', - 'mime-type': 'text/plain', - value: 'John', - }, - { - name: 'age', - 'mime-type': 'text/plain', - value: '99', - }, - { - name: 'x-ray', - value: 'hl:zQmdsy1SSKztP7CGRiP2SuMV41Xxy9g69QswhUiSeo3d4pH', - }, - ], - }, - '~attach': [{ '@id': 'zQmdsy1SSKztP7CGRiP2SuMV41Xxy9g69QswhUiSeo3d4pH' }], - 'offers~attach': expect.any(Array), - }, - state: CredentialState.OfferReceived, - }) - - // below values are not in json object - expect(aliceCredentialRecord.id).not.toBeNull() - expect(aliceCredentialRecord.getTags()).toEqual({ - state: aliceCredentialRecord.state, - threadId: faberCredentialRecord.threadId, - connectionId: aliceCredentialRecord.connectionId, - }) - expect(aliceCredentialRecord.type).toBe(CredentialRecord.name) - - testLogger.test('Alice sends credential request to Faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptOffer(aliceCredentialRecord.id) - - testLogger.test('Faber waits for credential request from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: aliceCredentialRecord.threadId, - state: CredentialState.RequestReceived, - }) - - testLogger.test('Faber sends credential to Alice') - faberCredentialRecord = await faberAgent.credentials.acceptRequest(faberCredentialRecord.id) - - testLogger.test('Alice waits for credential from Faber') - aliceCredentialRecord = await waitForCredentialRecord(aliceAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.CredentialReceived, - }) - - testLogger.test('Alice sends credential ack to Faber') - aliceCredentialRecord = await aliceAgent.credentials.acceptCredential(aliceCredentialRecord.id) - - testLogger.test('Faber waits for credential ack from Alice') - faberCredentialRecord = await waitForCredentialRecord(faberAgent, { - threadId: faberCredentialRecord.threadId, - state: CredentialState.Done, - }) - - expect(aliceCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - requestMessage: expect.any(Object), - credentialId: expect.any(String), - state: CredentialState.Done, - }) - - expect(faberCredentialRecord).toMatchObject({ - type: CredentialRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - requestMessage: expect.any(Object), - state: CredentialState.Done, - }) - }) -}) diff --git a/packages/core/tests/dids.test.ts b/packages/core/tests/dids.test.ts new file mode 100644 index 0000000000..50c90c704d --- /dev/null +++ b/packages/core/tests/dids.test.ts @@ -0,0 +1,161 @@ +import { Agent } from '../src/agent/Agent' +import { JsonTransformer } from '../src/utils/JsonTransformer' + +import { getBaseConfig } from './helpers' + +const { config, agentDependencies } = getBaseConfig('Faber Dids', {}) + +describe('dids', () => { + let agent: Agent + + beforeAll(async () => { + agent = new Agent(config, agentDependencies) + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('should resolve a did:sov did', async () => { + const did = await agent.dids.resolve(`did:sov:TL1EaPFCZ8Si5aUrqScBDt`) + + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt', + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt', + id: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-1', + publicKeyBase58: 'FMGcFuU3QwAQLywxvmEnSorQT3NwU9wgDMMTaDFtvswm', + }, + { + controller: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt', + type: 'X25519KeyAgreementKey2019', + id: 'did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-agreement-1', + publicKeyBase58: '6oKfyWDYRpbutQWDUu8ots6GoqAZJ9HYRzPuuEiqfyM', + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: ['did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-1'], + assertionMethod: ['did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-1'], + keyAgreement: ['did:sov:TL1EaPFCZ8Si5aUrqScBDt#key-agreement-1'], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should resolve a did:key did', async () => { + const did = await agent.dids.resolve('did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + type: 'Ed25519VerificationKey2018', + controller: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + publicKeyBase58: '6fioC1zcDPyPEL19pXRS2E4iJ46zH7xP6uSgAaPdwDrx', + }, + ], + authentication: [ + 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + assertionMethod: [ + 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + capabilityInvocation: [ + 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + capabilityDelegation: [ + 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + keyAgreement: [ + { + id: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6LSrdqo4M24WRDJj1h2hXxgtDTyzjjKCiyapYVgrhwZAySn', + type: 'X25519KeyAgreementKey2019', + controller: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + publicKeyBase58: 'FxfdY3DCQxVZddKGAtSjZdFW9bCCW7oRwZn1NFJ2Tbg2', + }, + ], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should resolve a did:peer did', async () => { + const did = await agent.dids.resolve('did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL') + + expect(JsonTransformer.toJSON(did)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + id: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + type: 'Ed25519VerificationKey2018', + controller: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + publicKeyBase58: '6fioC1zcDPyPEL19pXRS2E4iJ46zH7xP6uSgAaPdwDrx', + }, + ], + authentication: [ + 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + assertionMethod: [ + 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + capabilityInvocation: [ + 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + capabilityDelegation: [ + 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + ], + keyAgreement: [ + { + id: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6LSrdqo4M24WRDJj1h2hXxgtDTyzjjKCiyapYVgrhwZAySn', + type: 'X25519KeyAgreementKey2019', + controller: 'did:peer:0z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', + publicKeyBase58: 'FxfdY3DCQxVZddKGAtSjZdFW9bCCW7oRwZn1NFJ2Tbg2', + }, + ], + service: undefined, + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) +}) diff --git a/packages/core/tests/generic-records.test.ts b/packages/core/tests/generic-records.test.ts new file mode 100644 index 0000000000..5d147d4f3b --- /dev/null +++ b/packages/core/tests/generic-records.test.ts @@ -0,0 +1,110 @@ +import type { GenericRecord } from '../src/modules/generic-records/repository/GenericRecord' + +import { Agent } from '../src/agent/Agent' + +import { getBaseConfig } from './helpers' + +const aliceConfig = getBaseConfig('Agents Alice', { + endpoints: ['rxjs:alice'], +}) + +describe('genericRecords', () => { + let aliceAgent: Agent + + const fooString = { foo: 'Some data saved' } + const fooNumber = { foo: 42 } + + const barString: Record = fooString + const barNumber: Record = fooNumber + + afterAll(async () => { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('store generic-record record', async () => { + aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + await aliceAgent.initialize() + + //Save genericRecord message (Minimal) + + const savedRecord1: GenericRecord = await aliceAgent.genericRecords.save({ content: barString }) + + //Save genericRecord message with tag + const tags1 = { myTag: 'foobar1' } + const tags2 = { myTag: 'foobar2' } + + const savedRecord2: GenericRecord = await aliceAgent.genericRecords.save({ content: barNumber, tags: tags1 }) + + expect(savedRecord1).toBeDefined() + expect(savedRecord2).toBeDefined() + + const savedRecord3: GenericRecord = await aliceAgent.genericRecords.save({ content: barString, tags: tags2 }) + expect(savedRecord3).toBeDefined() + }) + + test('get generic-record records', async () => { + //Create genericRecord message + const savedRecords = await aliceAgent.genericRecords.getAll() + expect(savedRecords.length).toBe(3) + }) + + test('get generic-record specific record', async () => { + //Create genericRecord message + const savedRecords1 = await aliceAgent.genericRecords.findAllByQuery({ myTag: 'foobar1' }) + expect(savedRecords1?.length == 1).toBe(true) + expect(savedRecords1[0].content).toEqual({ foo: 42 }) + + const savedRecords2 = await aliceAgent.genericRecords.findAllByQuery({ myTag: 'foobar2' }) + expect(savedRecords2?.length == 1).toBe(true) + expect(savedRecords2[0].content).toEqual({ foo: 'Some data saved' }) + }) + + test('find generic record using id', async () => { + const myId = '100' + const savedRecord1: GenericRecord = await aliceAgent.genericRecords.save({ content: barString, id: myId }) + expect(savedRecord1).toBeDefined() + + const retrievedRecord: GenericRecord | null = await aliceAgent.genericRecords.findById(savedRecord1.id) + + if (retrievedRecord) { + expect(retrievedRecord.content).toEqual({ foo: 'Some data saved' }) + } else { + throw Error('retrieved record not found') + } + }) + + test('delete generic record', async () => { + const myId = '100' + const savedRecord1: GenericRecord = await aliceAgent.genericRecords.save({ content: barString, id: myId }) + expect(savedRecord1).toBeDefined() + + await aliceAgent.genericRecords.delete(savedRecord1) + + const retrievedRecord: GenericRecord | null = await aliceAgent.genericRecords.findById(savedRecord1.id) + expect(retrievedRecord).toBeNull() + }) + + test('update generic record', async () => { + const myId = '100' + const savedRecord1: GenericRecord = await aliceAgent.genericRecords.save({ content: barString, id: myId }) + expect(savedRecord1).toBeDefined() + + let retrievedRecord: GenericRecord | null = await aliceAgent.genericRecords.findById(savedRecord1.id) + expect(retrievedRecord).toBeDefined() + + const amendedFooString = { foo: 'Some even more cool data saved' } + const barString2: Record = amendedFooString + + savedRecord1.content = barString2 + + await aliceAgent.genericRecords.update(savedRecord1) + + retrievedRecord = await aliceAgent.genericRecords.findById(savedRecord1.id) + if (retrievedRecord) { + expect(retrievedRecord.content).toEqual({ foo: 'Some even more cool data saved' }) + } else { + throw Error('retrieved record not found in update test') + } + }) +}) diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 5eda86c01c..30e278c556 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' import type { AutoAcceptProof, @@ -5,7 +6,6 @@ import type { BasicMessageStateChangedEvent, ConnectionRecordProps, CredentialDefinitionTemplate, - CredentialOfferTemplate, CredentialStateChangedEvent, InitConfig, ProofAttributeInfo, @@ -13,6 +13,8 @@ import type { ProofStateChangedEvent, SchemaTemplate, } from '../src' +import type { AcceptOfferOptions, OfferCredentialOptions } from '../src/modules/credentials/CredentialsModuleOptions' +import type { CredentialOfferTemplate } from '../src/modules/credentials/protocol' import type { Schema, CredDef } from 'indy-sdk' import type { Observable } from 'rxjs' @@ -22,31 +24,36 @@ import { catchError, filter, map, timeout } from 'rxjs/operators' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' -import { agentDependencies } from '../../node/src' +import { agentDependencies, WalletScheme } from '../../node/src' import { + PresentationPreview, + PresentationPreviewAttribute, + PresentationPreviewPredicate, + HandshakeProtocol, + DidExchangeState, + DidExchangeRole, LogLevel, AgentConfig, AriesFrameworkError, BasicMessageEventTypes, - ConnectionInvitationMessage, ConnectionRecord, - ConnectionRole, - ConnectionState, CredentialEventTypes, - CredentialPreview, CredentialState, - DidCommService, - DidDoc, PredicateType, - PresentationPreview, - PresentationPreviewAttribute, - PresentationPreviewPredicate, ProofEventTypes, ProofState, Agent, } from '../src' +import { Key, KeyType } from '../src/crypto' import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachment' import { AutoAcceptCredential } from '../src/modules/credentials/CredentialAutoAcceptType' +import { CredentialProtocolVersion } from '../src/modules/credentials/CredentialProtocolVersion' +import { V1CredentialPreview } from '../src/modules/credentials/protocol/v1/V1CredentialPreview' +import { DidCommV1Service, DidKey } from '../src/modules/dids' +import { OutOfBandRole } from '../src/modules/oob/domain/OutOfBandRole' +import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' +import { OutOfBandInvitation } from '../src/modules/oob/messages' +import { OutOfBandRecord } from '../src/modules/oob/repository' import { LinkedAttachment } from '../src/utils/LinkedAttachment' import { uuid } from '../src/utils/uuid' @@ -57,6 +64,7 @@ export const genesisPath = process.env.GENESIS_TXN_PATH : path.join(__dirname, '../../../network/genesis/local-genesis.txn') export const publicDidSeed = process.env.TEST_AGENT_PUBLIC_DID_SEED ?? '000000000000000000000000Trustee9' +export { agentDependencies } export function getBaseConfig(name: string, extraConfig: Partial = {}) { const config: InitConfig = { @@ -81,6 +89,42 @@ export function getBaseConfig(name: string, extraConfig: Partial = { return { config, agentDependencies } as const } +export function getBasePostgresConfig(name: string, extraConfig: Partial = {}) { + const config: InitConfig = { + label: `Agent: ${name}`, + walletConfig: { + id: `Wallet${name}`, + key: `Key${name}`, + storage: { + type: 'postgres_storage', + config: { + url: 'localhost:5432', + wallet_scheme: WalletScheme.DatabasePerWallet, + }, + credentials: { + account: 'postgres', + password: 'postgres', + admin_account: 'postgres', + admin_password: 'postgres', + }, + }, + }, + publicDidSeed, + autoAcceptConnections: true, + indyLedgers: [ + { + id: `pool-${name}`, + isProduction: false, + genesisPath, + }, + ], + logger: new TestLogger(LogLevel.error, name), + ...extraConfig, + } + + return { config, agentDependencies } as const +} + export function getAgentConfig(name: string, extraConfig: Partial = {}) { const { config, agentDependencies } = getBaseConfig(name, extraConfig) return new AgentConfig(config, agentDependencies) @@ -150,6 +194,7 @@ export function waitForCredentialRecordSubject( } ) { const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( observable.pipe( filter((e) => previousState === undefined || e.payload.previousState === previousState), @@ -178,7 +223,6 @@ export async function waitForCredentialRecord( } ) { const observable = agent.events.observable(CredentialEventTypes.CredentialStateChanged) - return waitForCredentialRecordSubject(observable, options) } @@ -199,78 +243,89 @@ export async function waitForBasicMessage(agent: Agent, { content }: { content?: } export function getMockConnection({ - state = ConnectionState.Invited, - role = ConnectionRole.Invitee, + state = DidExchangeState.InvitationReceived, + role = DidExchangeRole.Requester, id = 'test', did = 'test-did', threadId = 'threadId', - verkey = 'key-1', - didDoc = new DidDoc({ - id: did, - publicKey: [], - authentication: [], - service: [ - new DidCommService({ - id: `${did};indy`, - serviceEndpoint: 'https://endpoint.com', - recipientKeys: [verkey], - }), - ], - }), tags = {}, theirLabel, - invitation = new ConnectionInvitationMessage({ - label: 'test', - recipientKeys: [verkey], - serviceEndpoint: 'https:endpoint.com/msg', - }), theirDid = 'their-did', - theirDidDoc = new DidDoc({ - id: theirDid, - publicKey: [], - authentication: [], - service: [ - new DidCommService({ - id: `${did};indy`, - serviceEndpoint: 'https://endpoint.com', - recipientKeys: [verkey], - }), - ], - }), multiUseInvitation = false, }: Partial = {}) { return new ConnectionRecord({ did, - didDoc, threadId, theirDid, - theirDidDoc, id, role, state, tags, - verkey, - invitation, theirLabel, multiUseInvitation, }) } -export async function makeConnection( - agentA: Agent, - agentB: Agent, - config?: { - autoAcceptConnection?: boolean - alias?: string - mediatorId?: string +export function getMockOutOfBand({ + label, + serviceEndpoint, + recipientKeys, + did, + mediatorId, + role, + state, + reusable, + reuseConnectionId, +}: { + label?: string + serviceEndpoint?: string + did?: string + mediatorId?: string + recipientKeys?: string[] + role?: OutOfBandRole + state?: OutOfBandState + reusable?: boolean + reuseConnectionId?: string +} = {}) { + const options = { + label: label ?? 'label', + accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], + handshakeProtocols: [HandshakeProtocol.DidExchange], + services: [ + new DidCommV1Service({ + id: `#inline-0`, + priority: 0, + serviceEndpoint: serviceEndpoint ?? 'http://example.com', + recipientKeys: recipientKeys || [ + new DidKey(Key.fromPublicKeyBase58('ByHnpUCFb1vAfh9CFZ8ZkmUZguURW8nSw889hy6rD8L7', KeyType.Ed25519)).did, + ], + routingKeys: [], + }), + ], } -) { - // eslint-disable-next-line prefer-const - let { invitation, connectionRecord: agentAConnection } = await agentA.connections.createConnection(config) - let agentBConnection = await agentB.connections.receiveInvitation(invitation) + const outOfBandInvitation = new OutOfBandInvitation(options) + const outOfBandRecord = new OutOfBandRecord({ + did: did || 'test-did', + mediatorId, + role: role || OutOfBandRole.Receiver, + state: state || OutOfBandState.Initial, + outOfBandInvitation: outOfBandInvitation, + reusable, + reuseConnectionId, + }) + return outOfBandRecord +} + +export async function makeConnection(agentA: Agent, agentB: Agent) { + const agentAOutOfBand = await agentA.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + let { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation(agentAOutOfBand.outOfBandInvitation) - agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection.id) - agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection.id) + agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection!.id) + let [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) + agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection!.id) return [agentAConnection, agentBConnection] } @@ -323,7 +378,8 @@ export async function ensurePublicDidIsOnLedger(agent: Agent, publicDid: string) try { testLogger.test(`Ensure test DID ${publicDid} is written to ledger`) await agent.ledger.getPublicDid(publicDid) - } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { // Unfortunately, this won't prevent from the test suite running because of Jest runner runs all tests // regardless of thrown errors. We're more explicit about the problem with this error handling. throw new Error(`Test DID ${publicDid} is not written on ledger or ledger is not available: ${error.message}`) @@ -354,19 +410,32 @@ export async function issueCredential({ .observable(CredentialEventTypes.CredentialStateChanged) .subscribe(holderReplay) - let issuerCredentialRecord = await issuerAgent.credentials.offerCredential(issuerConnectionId, { - ...credentialTemplate, + const offerOptions: OfferCredentialOptions = { + comment: 'some comment about credential', + connectionId: issuerConnectionId, + protocolVersion: CredentialProtocolVersion.V1, + credentialFormats: { + indy: { + attributes: credentialTemplate.preview.attributes, + credentialDefinitionId: credentialTemplate.credentialDefinitionId, + linkedAttachments: credentialTemplate.linkedAttachments, + }, + }, autoAcceptCredential: AutoAcceptCredential.ContentApproved, - }) + } + let issuerCredentialRecord = await issuerAgent.credentials.offerCredential(offerOptions) let holderCredentialRecord = await waitForCredentialRecordSubject(holderReplay, { threadId: issuerCredentialRecord.threadId, state: CredentialState.OfferReceived, }) - await holderAgent.credentials.acceptOffer(holderCredentialRecord.id, { + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: holderCredentialRecord.id, autoAcceptCredential: AutoAcceptCredential.ContentApproved, - }) + } + + await holderAgent.credentials.acceptOffer(acceptOfferOptions) // Because we use auto-accept it can take a while to have the whole credential flow finished // Both parties need to interact with the ledger and sign/verify the credential @@ -374,7 +443,6 @@ export async function issueCredential({ threadId: issuerCredentialRecord.threadId, state: CredentialState.Done, }) - issuerCredentialRecord = await waitForCredentialRecordSubject(issuerReplay, { threadId: issuerCredentialRecord.threadId, state: CredentialState.Done, @@ -405,22 +473,35 @@ export async function issueConnectionLessCredential({ .observable(CredentialEventTypes.CredentialStateChanged) .subscribe(holderReplay) - // eslint-disable-next-line prefer-const - let { credentialRecord: issuerCredentialRecord, offerMessage } = await issuerAgent.credentials.createOutOfBandOffer({ - ...credentialTemplate, + const offerOptions: OfferCredentialOptions = { + comment: 'V1 Out of Band offer', + protocolVersion: CredentialProtocolVersion.V1, + credentialFormats: { + indy: { + attributes: credentialTemplate.preview.attributes, + credentialDefinitionId: credentialTemplate.credentialDefinitionId, + }, + }, autoAcceptCredential: AutoAcceptCredential.ContentApproved, - }) + connectionId: '', + } + // eslint-disable-next-line prefer-const + let { credentialRecord: issuerCredentialRecord, message } = await issuerAgent.credentials.createOutOfBandOffer( + offerOptions + ) - await holderAgent.receiveMessage(offerMessage.toJSON()) + await holderAgent.receiveMessage(message.toJSON()) let holderCredentialRecord = await waitForCredentialRecordSubject(holderReplay, { threadId: issuerCredentialRecord.threadId, state: CredentialState.OfferReceived, }) - - holderCredentialRecord = await holderAgent.credentials.acceptOffer(holderCredentialRecord.id, { + const acceptOfferOptions: AcceptOfferOptions = { + credentialRecordId: holderCredentialRecord.id, autoAcceptCredential: AutoAcceptCredential.ContentApproved, - }) + } + + await holderAgent.credentials.acceptOffer(acceptOfferOptions) holderCredentialRecord = await waitForCredentialRecordSubject(holderReplay, { threadId: issuerCredentialRecord.threadId, @@ -505,6 +586,14 @@ export function mockFunction any>(fn: T): jest.Moc return fn as jest.MockedFunction } +/** + * Set a property using a getter value on a mocked oject. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export function mockProperty(object: T, property: K, value: T[K]) { + Object.defineProperty(object, property, { get: () => value }) +} + export async function setupCredentialTests( faberName: string, aliceName: string, @@ -527,26 +616,26 @@ export async function setupCredentialTests( }) const faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) - faberAgent.registerOutboundTransport(new SubjectOutboundTransport(aliceMessages, subjectMap)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await faberAgent.initialize() const aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) - aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(faberMessages, subjectMap)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() const { - schema: { id: schemaId }, + schema, definition: { id: credDefId }, } = await prepareForIssuance(faberAgent, ['name', 'age', 'profile_picture', 'x-ray']) const [faberConnection, aliceConnection] = await makeConnection(faberAgent, aliceAgent) - return { faberAgent, aliceAgent, credDefId, schemaId, faberConnection, aliceConnection } + return { faberAgent, aliceAgent, credDefId, schema, faberConnection, aliceConnection } } export async function setupProofsTest(faberName: string, aliceName: string, autoAcceptProofs?: AutoAcceptProof) { - const credentialPreview = CredentialPreview.fromRecord({ + const credentialPreview = V1CredentialPreview.fromRecord({ name: 'John', age: '99', }) @@ -572,12 +661,12 @@ export async function setupProofsTest(faberName: string, aliceName: string, auto } const faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) - faberAgent.registerOutboundTransport(new SubjectOutboundTransport(aliceMessages, subjectMap)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await faberAgent.initialize() const aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) - aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(faberMessages, subjectMap)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() const { definition } = await prepareForIssuance(faberAgent, ['name', 'age', 'image_0', 'image_1']) @@ -638,7 +727,6 @@ export async function setupProofsTest(faberName: string, aliceName: string, auto ], }, }) - const faberReplay = new ReplaySubject() const aliceReplay = new ReplaySubject() diff --git a/packages/core/tests/ledger.test.ts b/packages/core/tests/ledger.test.ts index b819095eab..c36c44498f 100644 --- a/packages/core/tests/ledger.test.ts +++ b/packages/core/tests/ledger.test.ts @@ -21,9 +21,8 @@ describe('ledger', () => { }) afterAll(async () => { - await faberAgent.shutdown({ - deleteWallet: true, - }) + await faberAgent.shutdown() + await faberAgent.wallet.delete() }) test(`initialization of agent's public DID`, async () => { diff --git a/packages/core/tests/logger.ts b/packages/core/tests/logger.ts index 51c1126ccb..a8677ede59 100644 --- a/packages/core/tests/logger.ts +++ b/packages/core/tests/logger.ts @@ -7,6 +7,7 @@ import { Logger } from 'tslog' import { LogLevel } from '../src/logger' import { BaseLogger } from '../src/logger/BaseLogger' +import { replaceError } from '../src/logger/replaceError' function logToTransport(logObject: ILogObject) { appendFileSync('logs.txt', JSON.stringify(logObject) + '\n') @@ -55,7 +56,7 @@ export class TestLogger extends BaseLogger { const tsLogLevel = this.tsLogLevelMap[level] if (data) { - this.logger[tsLogLevel](message, data) + this.logger[tsLogLevel](message, JSON.parse(JSON.stringify(data, replaceError, 2))) } else { this.logger[tsLogLevel](message) } diff --git a/packages/core/tests/migration.test.ts b/packages/core/tests/migration.test.ts new file mode 100644 index 0000000000..ef1590e69e --- /dev/null +++ b/packages/core/tests/migration.test.ts @@ -0,0 +1,60 @@ +import type { VersionString } from '../src/utils/version' + +import { Agent } from '../src/agent/Agent' +import { UpdateAssistant } from '../src/storage/migration/UpdateAssistant' + +import { getBaseConfig } from './helpers' + +const { config, agentDependencies } = getBaseConfig('Migration', { publicDidSeed: undefined, indyLedgers: [] }) + +describe('migration', () => { + test('manually initiating the update assistant to perform an update', async () => { + const agent = new Agent(config, agentDependencies) + + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { mediationRoleUpdateStrategy: 'allMediator' }, + }) + await updateAssistant.initialize() + + if (!(await updateAssistant.isUpToDate())) { + await updateAssistant.update() + } + + await agent.initialize() + + await agent.shutdown() + await agent.wallet.delete() + }) + + test('manually initiating the update, but storing the current framework version outside of the agent storage', async () => { + // The storage version will normally be stored in e.g. persistent storage on a mobile device + let currentStorageVersion: VersionString = '0.1' + + const agent = new Agent(config, agentDependencies) + + if (currentStorageVersion !== UpdateAssistant.frameworkStorageVersion) { + const updateAssistant = new UpdateAssistant(agent, { + v0_1ToV0_2: { mediationRoleUpdateStrategy: 'recipientIfEndpoint' }, + }) + await updateAssistant.initialize() + await updateAssistant.update() + + // Store the version so we can leverage it during the next agent startup and don't have + // to initialize the update assistant again until a new version is released + currentStorageVersion = UpdateAssistant.frameworkStorageVersion + } + + await agent.initialize() + + await agent.shutdown() + await agent.wallet.delete() + }) + + test('Automatic update on agent startup', async () => { + const agent = new Agent({ ...config, autoUpdateStorageOnStartup: true }, agentDependencies) + + await agent.initialize() + await agent.shutdown() + await agent.wallet.delete() + }) +}) diff --git a/packages/core/tests/multi-protocol-version.test.ts b/packages/core/tests/multi-protocol-version.test.ts new file mode 100644 index 0000000000..138bcc89fd --- /dev/null +++ b/packages/core/tests/multi-protocol-version.test.ts @@ -0,0 +1,140 @@ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { AgentMessageProcessedEvent } from '../src/agent/Events' + +import { filter, firstValueFrom, Subject, timeout } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { parseMessageType, MessageSender, Dispatcher, AgentMessage, IsValidMessageType } from '../src' +import { Agent } from '../src/agent/Agent' +import { AgentEventTypes } from '../src/agent/Events' +import { createOutboundMessage } from '../src/agent/helpers' + +import { getBaseConfig } from './helpers' + +const aliceConfig = getBaseConfig('Multi Protocol Versions - Alice', { + endpoints: ['rxjs:alice'], +}) +const bobConfig = getBaseConfig('Multi Protocol Versions - Bob', { + endpoints: ['rxjs:bob'], +}) + +describe('multi version protocols', () => { + let aliceAgent: Agent + let bobAgent: Agent + + afterAll(async () => { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('should successfully handle a message with a lower minor version than the currently supported version', async () => { + const aliceMessages = new Subject() + const bobMessages = new Subject() + + const subjectMap = { + 'rxjs:alice': aliceMessages, + 'rxjs:bob': bobMessages, + } + + const mockHandle = jest.fn() + + aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + + // Register the test handler with the v1.3 version of the message + const dispatcher = aliceAgent.injectionContainer.resolve(Dispatcher) + dispatcher.registerHandler({ supportedMessages: [TestMessageV13], handle: mockHandle }) + + await aliceAgent.initialize() + + bobAgent = new Agent(bobConfig.config, bobConfig.agentDependencies) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await bobAgent.initialize() + + const { outOfBandInvitation, id } = await aliceAgent.oob.createInvitation() + let { connectionRecord: bobConnection } = await bobAgent.oob.receiveInvitation(outOfBandInvitation, { + autoAcceptConnection: true, + autoAcceptInvitation: true, + }) + + if (!bobConnection) { + throw new Error('No connection for bob') + } + + bobConnection = await bobAgent.connections.returnWhenIsConnected(bobConnection.id) + + let [aliceConnection] = await aliceAgent.connections.findAllByOutOfBandId(id) + aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnection.id) + + expect(aliceConnection).toBeConnectedWith(bobConnection) + expect(bobConnection).toBeConnectedWith(aliceConnection) + + const bobMessageSender = bobAgent.injectionContainer.resolve(MessageSender) + + // Start event listener for message processed + const agentMessageV11ProcessedPromise = firstValueFrom( + aliceAgent.events.observable(AgentEventTypes.AgentMessageProcessed).pipe( + filter((event) => event.payload.message.type === TestMessageV11.type.messageTypeUri), + timeout(8000) + ) + ) + + await bobMessageSender.sendMessage(createOutboundMessage(bobConnection, new TestMessageV11())) + + // Wait for the agent message processed event to be called + await agentMessageV11ProcessedPromise + + expect(mockHandle).toHaveBeenCalledTimes(1) + + // Start event listener for message processed + const agentMessageV15ProcessedPromise = firstValueFrom( + aliceAgent.events.observable(AgentEventTypes.AgentMessageProcessed).pipe( + filter((event) => event.payload.message.type === TestMessageV15.type.messageTypeUri), + timeout(8000) + ) + ) + + await bobMessageSender.sendMessage(createOutboundMessage(bobConnection, new TestMessageV15())) + await agentMessageV15ProcessedPromise + + expect(mockHandle).toHaveBeenCalledTimes(2) + }) +}) + +class TestMessageV11 extends AgentMessage { + public constructor() { + super() + this.id = this.generateId() + } + + @IsValidMessageType(TestMessageV11.type) + public readonly type = TestMessageV11.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/custom-protocol/1.1/test-message') +} + +class TestMessageV13 extends AgentMessage { + public constructor() { + super() + this.id = this.generateId() + } + + @IsValidMessageType(TestMessageV13.type) + public readonly type = TestMessageV13.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/custom-protocol/1.3/test-message') +} + +class TestMessageV15 extends AgentMessage { + public constructor() { + super() + this.id = this.generateId() + } + + @IsValidMessageType(TestMessageV15.type) + public readonly type = TestMessageV15.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/custom-protocol/1.5/test-message') +} diff --git a/packages/core/tests/oob-mediation-provision.test.ts b/packages/core/tests/oob-mediation-provision.test.ts new file mode 100644 index 0000000000..9c84886099 --- /dev/null +++ b/packages/core/tests/oob-mediation-provision.test.ts @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { OutOfBandInvitation } from '../src/modules/oob/messages' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { Agent } from '../src/agent/Agent' +import { DidExchangeState, HandshakeProtocol } from '../src/modules/connections' +import { MediationState, MediatorPickupStrategy } from '../src/modules/routing' + +import { getBaseConfig, waitForBasicMessage } from './helpers' + +const faberConfig = getBaseConfig('OOB mediation provision - Faber Agent', { + endpoints: ['rxjs:faber'], +}) +const aliceConfig = getBaseConfig('OOB mediation provision - Alice Recipient Agent', { + endpoints: ['rxjs:alice'], + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, +}) +const mediatorConfig = getBaseConfig('OOB mediation provision - Mediator Agent', { + endpoints: ['rxjs:mediator'], + autoAcceptMediationRequests: true, +}) + +describe('out of band with mediation set up with provision method', () => { + const makeConnectionConfig = { + goal: 'To make a connection', + goalCode: 'p2p-messaging', + label: 'Faber College', + handshake: true, + multiUseInvitation: false, + } + + let faberAgent: Agent + let aliceAgent: Agent + let mediatorAgent: Agent + + let mediatorOutOfBandInvitation: OutOfBandInvitation + + beforeAll(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + const mediatorMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + 'rxjs:mediator': mediatorMessages, + } + + mediatorAgent = new Agent(mediatorConfig.config, mediatorConfig.agentDependencies) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await mediatorAgent.initialize() + + faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + const mediatorRouting = await mediatorAgent.mediationRecipient.getRouting({}) + const mediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + ...makeConnectionConfig, + routing: mediatorRouting, + }) + mediatorOutOfBandInvitation = mediationOutOfBandRecord.outOfBandInvitation + + await aliceAgent.initialize() + let { connectionRecord } = await aliceAgent.oob.receiveInvitation(mediatorOutOfBandInvitation) + connectionRecord = await aliceAgent.connections.returnWhenIsConnected(connectionRecord!.id) + await aliceAgent.mediationRecipient.provision(connectionRecord!) + await aliceAgent.mediationRecipient.initialize() + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + }) + + test(`make a connection with ${HandshakeProtocol.DidExchange} on OOB invitation encoded in URL`, async () => { + // Check if mediation between Alice and Mediator has been set + const defaultMediator = await aliceAgent.mediationRecipient.findDefaultMediator() + expect(defaultMediator).not.toBeNull() + expect(defaultMediator?.state).toBe(MediationState.Granted) + + // Make a connection between Alice and Faber + const faberRouting = await faberAgent.mediationRecipient.getRouting({}) + const outOfBandRecord = await faberAgent.oob.createInvitation({ ...makeConnectionConfig, routing: faberRouting }) + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + + await aliceAgent.basicMessages.sendMessage(aliceFaberConnection.id, 'hello') + const basicMessage = await waitForBasicMessage(faberAgent, {}) + + expect(basicMessage.content).toBe('hello') + + // Test if we can call provision for the same out-of-band record, respectively connection + const reusedOutOfBandRecord = await aliceAgent.oob.findByInvitationId(mediatorOutOfBandInvitation.id) + const [reusedAliceMediatorConnection] = reusedOutOfBandRecord + ? await aliceAgent.connections.findAllByOutOfBandId(reusedOutOfBandRecord.id) + : [] + await aliceAgent.mediationRecipient.provision(reusedAliceMediatorConnection!) + const mediators = await aliceAgent.mediationRecipient.getMediators() + expect(mediators).toHaveLength(1) + }) +}) diff --git a/packages/core/tests/oob-mediation.test.ts b/packages/core/tests/oob-mediation.test.ts new file mode 100644 index 0000000000..e773740a43 --- /dev/null +++ b/packages/core/tests/oob-mediation.test.ts @@ -0,0 +1,129 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { Agent } from '../src/agent/Agent' +import { DidExchangeState, HandshakeProtocol } from '../src/modules/connections' +import { MediationState, MediatorPickupStrategy } from '../src/modules/routing' + +import { getBaseConfig, waitForBasicMessage } from './helpers' + +const faberConfig = getBaseConfig('OOB mediation - Faber Agent', { + endpoints: ['rxjs:faber'], +}) +const aliceConfig = getBaseConfig('OOB mediation - Alice Recipient Agent', { + endpoints: ['rxjs:alice'], + // FIXME: discover features returns that we support this protocol, but we don't support all roles + // we should return that we only support the mediator role so we don't have to explicitly declare this + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, +}) +const mediatorConfig = getBaseConfig('OOB mediation - Mediator Agent', { + endpoints: ['rxjs:mediator'], + autoAcceptMediationRequests: true, +}) + +describe('out of band with mediation', () => { + const makeConnectionConfig = { + goal: 'To make a connection', + goalCode: 'p2p-messaging', + label: 'Faber College', + handshake: true, + multiUseInvitation: false, + } + + let faberAgent: Agent + let aliceAgent: Agent + let mediatorAgent: Agent + + beforeAll(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + const mediatorMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + 'rxjs:mediator': mediatorMessages, + } + + faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + mediatorAgent = new Agent(mediatorConfig.config, mediatorConfig.agentDependencies) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await mediatorAgent.initialize() + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + }) + + test(`make a connection with ${HandshakeProtocol.DidExchange} on OOB invitation encoded in URL`, async () => { + // ========== Make a connection between Alice and Mediator agents ========== + const mediatorRouting = await mediatorAgent.mediationRecipient.getRouting({}) + const mediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + ...makeConnectionConfig, + routing: mediatorRouting, + }) + const { outOfBandInvitation: mediatorOutOfBandInvitation } = mediationOutOfBandRecord + const mediatorUrlMessage = mediatorOutOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceMediatorConnection } = await aliceAgent.oob.receiveInvitationFromUrl( + mediatorUrlMessage + ) + + aliceMediatorConnection = await aliceAgent.connections.returnWhenIsConnected(aliceMediatorConnection!.id) + expect(aliceMediatorConnection.state).toBe(DidExchangeState.Completed) + + let [mediatorAliceConnection] = await mediatorAgent.connections.findAllByOutOfBandId(mediationOutOfBandRecord.id) + mediatorAliceConnection = await mediatorAgent.connections.returnWhenIsConnected(mediatorAliceConnection!.id) + expect(mediatorAliceConnection.state).toBe(DidExchangeState.Completed) + + // ========== Set meadiation between Alice and Mediator agents ========== + const mediationRecord = await aliceAgent.mediationRecipient.requestAndAwaitGrant(aliceMediatorConnection) + expect(mediationRecord.state).toBe(MediationState.Granted) + + await aliceAgent.mediationRecipient.setDefaultMediator(mediationRecord) + await aliceAgent.mediationRecipient.initiateMessagePickup(mediationRecord) + const defaultMediator = await aliceAgent.mediationRecipient.findDefaultMediator() + expect(defaultMediator?.id).toBe(mediationRecord.id) + + // ========== Make a connection between Alice and Faber ========== + const faberRouting = await faberAgent.mediationRecipient.getRouting({}) + const outOfBandRecord = await faberAgent.oob.createInvitation({ ...makeConnectionConfig, routing: faberRouting }) + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + + await aliceAgent.basicMessages.sendMessage(aliceFaberConnection.id, 'hello') + const basicMessage = await waitForBasicMessage(faberAgent, {}) + + expect(basicMessage.content).toBe('hello') + }) +}) diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts new file mode 100644 index 0000000000..42b1bed678 --- /dev/null +++ b/packages/core/tests/oob.test.ts @@ -0,0 +1,660 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { OfferCredentialOptions } from '../src/modules/credentials/CredentialsModuleOptions' +import type { AgentMessage, AgentMessageReceivedEvent } from '@aries-framework/core' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { Agent } from '../src/agent/Agent' +import { DidExchangeState, HandshakeProtocol } from '../src/modules/connections' +import { OutOfBandDidCommService } from '../src/modules/oob/domain/OutOfBandDidCommService' +import { OutOfBandEventTypes } from '../src/modules/oob/domain/OutOfBandEvents' +import { OutOfBandRole } from '../src/modules/oob/domain/OutOfBandRole' +import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' +import { OutOfBandInvitation } from '../src/modules/oob/messages' + +import { TestMessage } from './TestMessage' +import { getBaseConfig, prepareForIssuance, waitForCredentialRecord } from './helpers' + +import { + AgentEventTypes, + AriesFrameworkError, + AutoAcceptCredential, + CredentialState, + V1CredentialPreview, + CredentialProtocolVersion, +} from '@aries-framework/core' // Maybe it's not bad to import from package? + +const faberConfig = getBaseConfig('Faber Agent OOB', { + endpoints: ['rxjs:faber'], +}) +const aliceConfig = getBaseConfig('Alice Agent OOB', { + endpoints: ['rxjs:alice'], +}) + +describe('out of band', () => { + const makeConnectionConfig = { + goal: 'To make a connection', + goalCode: 'p2p-messaging', + label: 'Faber College', + } + + const issueCredentialConfig = { + goal: 'To issue a credential', + goalCode: 'issue-vc', + label: 'Faber College', + handshake: false, + } + + const receiveInvitationConfig = { + autoAcceptConnection: false, + } + + let faberAgent: Agent + let aliceAgent: Agent + let credentialTemplate: OfferCredentialOptions + + beforeAll(async () => { + const faberMessages = new Subject() + const aliceMessages = new Subject() + const subjectMap = { + 'rxjs:faber': faberMessages, + 'rxjs:alice': aliceMessages, + } + + faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await faberAgent.initialize() + + aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + const { definition } = await prepareForIssuance(faberAgent, ['name', 'age', 'profile_picture', 'x-ray']) + + credentialTemplate = { + protocolVersion: CredentialProtocolVersion.V1, + credentialFormats: { + indy: { + attributes: V1CredentialPreview.fromRecord({ + name: 'name', + age: 'age', + profile_picture: 'profile_picture', + 'x-ray': 'x-ray', + }).attributes, + credentialDefinitionId: definition.id, + }, + }, + autoAcceptCredential: AutoAcceptCredential.Never, + } + }) + + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + afterEach(async () => { + const credentials = await aliceAgent.credentials.getAll() + for (const credential of credentials) { + await aliceAgent.credentials.deleteById(credential.id) + } + + const connections = await faberAgent.connections.getAll() + for (const connection of connections) { + await faberAgent.connections.deleteById(connection.id) + } + + jest.resetAllMocks() + }) + + describe('createInvitation', () => { + test('throw error when there is no handshake or message', async () => { + await expect(faberAgent.oob.createInvitation({ label: 'test-connection', handshake: false })).rejects.toEqual( + new AriesFrameworkError( + 'One or both of handshake_protocols and requests~attach MUST be included in the message.' + ) + ) + }) + + test('throw error when multiUseInvitation is true and messages are provided', async () => { + await expect( + faberAgent.oob.createInvitation({ + label: 'test-connection', + messages: [{} as AgentMessage], + multiUseInvitation: true, + }) + ).rejects.toEqual( + new AriesFrameworkError("Attribute 'multiUseInvitation' can not be 'true' when 'messages' is defined.") + ) + }) + + test('handles empty messages array as no messages being passed', async () => { + await expect( + faberAgent.oob.createInvitation({ + messages: [], + handshake: false, + }) + ).rejects.toEqual( + new AriesFrameworkError( + 'One or both of handshake_protocols and requests~attach MUST be included in the message.' + ) + ) + }) + + test('create OOB record', async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + // expect contains services + + expect(outOfBandRecord.autoAcceptConnection).toBe(true) + expect(outOfBandRecord.role).toBe(OutOfBandRole.Sender) + expect(outOfBandRecord.state).toBe(OutOfBandState.AwaitResponse) + expect(outOfBandRecord.reusable).toBe(false) + expect(outOfBandRecord.outOfBandInvitation.goal).toBe('To make a connection') + expect(outOfBandRecord.outOfBandInvitation.goalCode).toBe('p2p-messaging') + expect(outOfBandRecord.outOfBandInvitation.label).toBe('Faber College') + }) + + test('create OOB message only with handshake', async () => { + const { outOfBandInvitation } = await faberAgent.oob.createInvitation(makeConnectionConfig) + + // expect supported handshake protocols + expect(outOfBandInvitation.handshakeProtocols).toContain(HandshakeProtocol.DidExchange) + expect(outOfBandInvitation.getRequests()).toBeUndefined() + + // expect contains services + const [service] = outOfBandInvitation.services as OutOfBandDidCommService[] + expect(service).toMatchObject( + new OutOfBandDidCommService({ + id: expect.any(String), + serviceEndpoint: 'rxjs:faber', + recipientKeys: [expect.stringContaining('did:key:')], + routingKeys: [], + }) + ) + }) + + test('create OOB message only with requests', async () => { + const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + label: 'test-connection', + handshake: false, + messages: [message], + }) + + // expect supported handshake protocols + expect(outOfBandInvitation.handshakeProtocols).toBeUndefined() + expect(outOfBandInvitation.getRequests()).toHaveLength(1) + + // expect contains services + const [service] = outOfBandInvitation.services + expect(service).toMatchObject( + new OutOfBandDidCommService({ + id: expect.any(String), + serviceEndpoint: 'rxjs:faber', + recipientKeys: [expect.stringContaining('did:key:')], + routingKeys: [], + }) + ) + }) + + test('create OOB message with both handshake and requests', async () => { + const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + label: 'test-connection', + handshakeProtocols: [HandshakeProtocol.Connections], + messages: [message], + }) + + // expect supported handshake protocols + expect(outOfBandInvitation.handshakeProtocols).toContain(HandshakeProtocol.Connections) + expect(outOfBandInvitation.getRequests()).toHaveLength(1) + + // expect contains services + const [service] = outOfBandInvitation.services as OutOfBandDidCommService[] + expect(service).toMatchObject( + new OutOfBandDidCommService({ + id: expect.any(String), + serviceEndpoint: 'rxjs:faber', + recipientKeys: [expect.stringMatching('did:key:')], + routingKeys: [], + }) + ) + }) + + test('emits OutOfBandStateChanged event', async () => { + const eventListener = jest.fn() + + faberAgent.events.on(OutOfBandEventTypes.OutOfBandStateChanged, eventListener) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + label: 'test-connection', + handshake: true, + }) + + faberAgent.events.off(OutOfBandEventTypes.OutOfBandStateChanged, eventListener) + + expect(eventListener).toHaveBeenCalledWith({ + type: OutOfBandEventTypes.OutOfBandStateChanged, + payload: { + outOfBandRecord, + previousState: null, + }, + }) + }) + }) + + describe('receiveInvitation', () => { + test('receive OOB connection invitation', async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + const { outOfBandInvitation } = outOfBandRecord + + const { outOfBandRecord: receivedOutOfBandRecord, connectionRecord } = await aliceAgent.oob.receiveInvitation( + outOfBandInvitation, + { + autoAcceptInvitation: false, + autoAcceptConnection: false, + } + ) + + expect(connectionRecord).not.toBeDefined() + expect(receivedOutOfBandRecord.role).toBe(OutOfBandRole.Receiver) + expect(receivedOutOfBandRecord.state).toBe(OutOfBandState.Initial) + expect(receivedOutOfBandRecord.outOfBandInvitation).toEqual(outOfBandInvitation) + }) + + test(`make a connection with ${HandshakeProtocol.DidExchange} on OOB invitation encoded in URL`, async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + // eslint-disable-next-line prefer-const + let { outOfBandRecord: receivedOutOfBandRecord, connectionRecord: aliceFaberConnection } = + await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + expect(receivedOutOfBandRecord.state).toBe(OutOfBandState.PrepareResponse) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord!.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection?.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection!) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + }) + + test(`make a connection with ${HandshakeProtocol.Connections} based on OOB invitation encoded in URL`, async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation({ + ...makeConnectionConfig, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + const { outOfBandInvitation } = outOfBandRecord + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord!.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + }) + + test('make a connection based on old connection invitation encoded in URL', async () => { + const { outOfBandRecord, invitation } = await faberAgent.oob.createLegacyInvitation({ + ...makeConnectionConfig, + handshakeProtocols: [HandshakeProtocol.Connections], + }) + const urlMessage = invitation.toUrl({ domain: 'http://example.com' }) + + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitationFromUrl(urlMessage) + + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + + expect(aliceFaberConnection.state).toBe(DidExchangeState.Completed) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + }) + + test('process credential offer requests based on OOB message', async () => { + const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + ...issueCredentialConfig, + messages: [message], + }) + + const urlMessage = outOfBandInvitation.toUrl({ domain: 'http://example.com' }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + }) + await aliceAgent.oob.receiveInvitationFromUrl(urlMessage, receiveInvitationConfig) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + }) + + test('do not process requests when a connection is not ready', async () => { + const eventListener = jest.fn() + aliceAgent.events.on(AgentEventTypes.AgentMessageReceived, eventListener) + + const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + ...makeConnectionConfig, + messages: [message], + }) + + // First, we crate a connection but we won't accept it, therefore it won't be ready + await aliceAgent.oob.receiveInvitation(outOfBandInvitation, { autoAcceptConnection: false }) + + // Event should not be emitted because an agent must wait until the connection is ready + expect(eventListener).toHaveBeenCalledTimes(0) + + aliceAgent.events.off(AgentEventTypes.AgentMessageReceived, eventListener) + }) + + test('make a connection based on OOB invitation and process requests after the acceptation', async () => { + const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + ...makeConnectionConfig, + messages: [message], + }) + const { outOfBandInvitation } = outOfBandRecord + + // First, we crate a connection but we won't accept it, therefore it won't be ready + const { outOfBandRecord: aliceFaberOutOfBandRecord } = await aliceAgent.oob.receiveInvitation( + outOfBandInvitation, + { + autoAcceptInvitation: false, + autoAcceptConnection: false, + } + ) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + // We need to create the connection beforehand so it can take a while to complete + timeoutMs: 20000, + }) + + // Accept connection invitation + let { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.acceptInvitation( + aliceFaberOutOfBandRecord.id, + { + label: 'alice', + autoAcceptConnection: true, + } + ) + + // Wait until connection is ready + aliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord!.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + expect(faberAliceConnection).toBeConnectedWith(aliceFaberConnection) + expect(aliceFaberConnection).toBeConnectedWith(faberAliceConnection) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + }) + + test('do not create a new connection when no messages and handshake reuse succeeds', async () => { + const aliceReuseListener = jest.fn() + const faberReuseListener = jest.fn() + + // Create first connection + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + let { connectionRecord: firstAliceFaberConnection } = await aliceAgent.oob.receiveInvitation( + outOfBandRecord.outOfBandInvitation + ) + firstAliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(firstAliceFaberConnection!.id) + + const [firstFaberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + + // Create second connection + const outOfBandRecord2 = await faberAgent.oob.createInvitation(makeConnectionConfig) + + // Take over the recipientKeys from the first invitation so they match when encoded + const firstInvitationService = outOfBandRecord.outOfBandInvitation.services[0] as OutOfBandDidCommService + const secondInvitationService = outOfBandRecord2.outOfBandInvitation.services[0] as OutOfBandDidCommService + secondInvitationService.recipientKeys = firstInvitationService.recipientKeys + + aliceAgent.events.on(OutOfBandEventTypes.HandshakeReused, aliceReuseListener) + faberAgent.events.on(OutOfBandEventTypes.HandshakeReused, faberReuseListener) + + const { + connectionRecord: secondAliceFaberConnection, + outOfBandRecord: { id: secondOobRecordId }, + } = await aliceAgent.oob.receiveInvitation(outOfBandRecord2.outOfBandInvitation, { reuseConnection: true }) + + aliceAgent.events.off(OutOfBandEventTypes.HandshakeReused, aliceReuseListener) + faberAgent.events.off(OutOfBandEventTypes.HandshakeReused, faberReuseListener) + await aliceAgent.connections.returnWhenIsConnected(secondAliceFaberConnection!.id) + + // There shouldn't be any connection records for this oob id, as we reused an existing one + expect((await faberAgent.connections.findAllByOutOfBandId(secondOobRecordId)).length).toBe(0) + + expect(firstAliceFaberConnection.id).toEqual(secondAliceFaberConnection?.id) + + expect(faberReuseListener).toHaveBeenCalledTimes(1) + expect(aliceReuseListener).toHaveBeenCalledTimes(1) + const [[faberEvent]] = faberReuseListener.mock.calls + const [[aliceEvent]] = aliceReuseListener.mock.calls + + const reuseThreadId = faberEvent.payload.reuseThreadId + + expect(faberEvent).toMatchObject({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + connectionRecord: { + id: firstFaberAliceConnection.id, + }, + outOfBandRecord: { + id: outOfBandRecord2.id, + }, + reuseThreadId, + }, + }) + + expect(aliceEvent).toMatchObject({ + type: OutOfBandEventTypes.HandshakeReused, + payload: { + connectionRecord: { + id: firstAliceFaberConnection.id, + }, + outOfBandRecord: { + id: secondOobRecordId, + }, + reuseThreadId, + }, + }) + }) + + test('create a new connection when connection exists and reuse is false', async () => { + const reuseListener = jest.fn() + + // Create first connection + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + let { connectionRecord: firstAliceFaberConnection } = await aliceAgent.oob.receiveInvitation( + outOfBandRecord.outOfBandInvitation + ) + firstAliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(firstAliceFaberConnection!.id) + + // Create second connection + const outOfBandRecord2 = await faberAgent.oob.createInvitation(makeConnectionConfig) + + aliceAgent.events.on(OutOfBandEventTypes.HandshakeReused, reuseListener) + faberAgent.events.on(OutOfBandEventTypes.HandshakeReused, reuseListener) + + const { connectionRecord: secondAliceFaberConnection } = await aliceAgent.oob.receiveInvitation( + outOfBandRecord2.outOfBandInvitation, + { reuseConnection: false } + ) + + aliceAgent.events.off(OutOfBandEventTypes.HandshakeReused, reuseListener) + faberAgent.events.off(OutOfBandEventTypes.HandshakeReused, reuseListener) + await aliceAgent.connections.returnWhenIsConnected(secondAliceFaberConnection!.id) + + // If we're not reusing the connection, the reuse listener shouldn't be called + expect(reuseListener).not.toHaveBeenCalled() + expect(firstAliceFaberConnection.id).not.toEqual(secondAliceFaberConnection?.id) + + const faberConnections = await faberAgent.connections.getAll() + let [firstFaberAliceConnection, secondFaberAliceConnection] = faberConnections + firstFaberAliceConnection = await faberAgent.connections.returnWhenIsConnected(firstFaberAliceConnection.id) + secondFaberAliceConnection = await faberAgent.connections.returnWhenIsConnected(secondFaberAliceConnection.id) + + // expect the two connections contain the two out of band ids + expect(faberConnections.map((c) => c.outOfBandId)).toEqual( + expect.arrayContaining([outOfBandRecord.id, outOfBandRecord2.id]) + ) + + expect(faberConnections).toHaveLength(2) + expect(firstFaberAliceConnection.state).toBe(DidExchangeState.Completed) + expect(secondFaberAliceConnection.state).toBe(DidExchangeState.Completed) + }) + + test('throws an error when the invitation has already been received', async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation(makeConnectionConfig) + const { outOfBandInvitation } = outOfBandRecord + + const { connectionRecord: aliceFaberConnection } = await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + + // Wait until connection is ready + await aliceAgent.connections.returnWhenIsConnected(aliceFaberConnection!.id) + + const [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord.id) + await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + + // Try to receive the invitation again + await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation)).rejects.toThrow( + new AriesFrameworkError( + `An out of band record with invitation ${outOfBandInvitation.id} already exists. Invitations should have a unique id.` + ) + ) + }) + + test('emits OutOfBandStateChanged event', async () => { + const eventListener = jest.fn() + const { outOfBandInvitation, id } = await faberAgent.oob.createInvitation(makeConnectionConfig) + + aliceAgent.events.on(OutOfBandEventTypes.OutOfBandStateChanged, eventListener) + + const { outOfBandRecord, connectionRecord } = await aliceAgent.oob.receiveInvitation(outOfBandInvitation, { + autoAcceptConnection: true, + autoAcceptInvitation: true, + }) + + // Wait for the connection to complete so we don't get wallet closed errors + await aliceAgent.connections.returnWhenIsConnected(connectionRecord!.id) + aliceAgent.events.off(OutOfBandEventTypes.OutOfBandStateChanged, eventListener) + + const [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(id) + await faberAgent.connections.returnWhenIsConnected(faberAliceConnection.id) + + // Receiving the invitation + expect(eventListener).toHaveBeenNthCalledWith(1, { + type: OutOfBandEventTypes.OutOfBandStateChanged, + payload: { + outOfBandRecord: expect.objectContaining({ state: OutOfBandState.Initial }), + previousState: null, + }, + }) + + // Accepting the invitation + expect(eventListener).toHaveBeenNthCalledWith(2, { + type: OutOfBandEventTypes.OutOfBandStateChanged, + payload: { + outOfBandRecord, + previousState: OutOfBandState.Initial, + }, + }) + }) + + test.skip('do not create a new connection when connection exists and multiuse is false', async () => { + const outOfBandRecord = await faberAgent.oob.createInvitation({ + ...makeConnectionConfig, + multiUseInvitation: false, + }) + const { outOfBandInvitation } = outOfBandRecord + + let { connectionRecord: firstAliceFaberConnection } = await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + firstAliceFaberConnection = await aliceAgent.connections.returnWhenIsConnected(firstAliceFaberConnection!.id) + + await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + + // TODO Somehow check agents throws an error or sends problem report + + let [faberAliceConnection] = await faberAgent.connections.findAllByOutOfBandId(outOfBandRecord!.id) + faberAliceConnection = await faberAgent.connections.returnWhenIsConnected(faberAliceConnection!.id) + + const faberConnections = await faberAgent.connections.getAll() + expect(faberConnections).toHaveLength(1) + expect(faberAliceConnection.state).toBe(DidExchangeState.Completed) + expect(firstAliceFaberConnection.state).toBe(DidExchangeState.Completed) + }) + + test('throw an error when handshake protocols are not supported', async () => { + const outOfBandInvitation = new OutOfBandInvitation({ label: 'test-connection', services: [] }) + const unsupportedProtocol = 'https://didcomm.org/unsupported-connections-protocol/1.0' + outOfBandInvitation.handshakeProtocols = [unsupportedProtocol as HandshakeProtocol] + + await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( + new AriesFrameworkError( + `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.0,https://didcomm.org/connections/1.0]` + ) + ) + }) + + test('throw an error when the OOB message does not contain either handshake or requests', async () => { + const outOfBandInvitation = new OutOfBandInvitation({ label: 'test-connection', services: [] }) + + await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( + new AriesFrameworkError( + 'One or both of handshake_protocols and requests~attach MUST be included in the message.' + ) + ) + }) + + test('throw an error when the OOB message contains unsupported message request', async () => { + const testMessage = new TestMessage() + testMessage.type = 'https://didcomm.org/test-protocol/1.0/test-message' + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + ...issueCredentialConfig, + messages: [testMessage], + }) + + await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( + new AriesFrameworkError('There is no message in requests~attach supported by agent.') + ) + }) + + test('throw an error when a did is used in the out of band message', async () => { + const { message } = await faberAgent.credentials.createOutOfBandOffer(credentialTemplate) + const { outOfBandInvitation } = await faberAgent.oob.createInvitation({ + ...issueCredentialConfig, + messages: [message], + }) + outOfBandInvitation.services = ['somedid'] + + await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( + new AriesFrameworkError('Dids are not currently supported in out-of-band invitation services attribute.') + ) + }) + }) +}) diff --git a/packages/core/tests/postgres.test.ts b/packages/core/tests/postgres.test.ts new file mode 100644 index 0000000000..3a92c8ef46 --- /dev/null +++ b/packages/core/tests/postgres.test.ts @@ -0,0 +1,106 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { IndyPostgresStorageConfig } from '../../node/src' +import type { ConnectionRecord } from '../src/modules/connections' + +import { Subject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { loadPostgresPlugin, WalletScheme } from '../../node/src' +import { Agent } from '../src/agent/Agent' +import { HandshakeProtocol } from '../src/modules/connections' + +import { waitForBasicMessage, getBasePostgresConfig } from './helpers' + +const alicePostgresConfig = getBasePostgresConfig('AgentsAlice', { + endpoints: ['rxjs:alice'], +}) +const bobPostgresConfig = getBasePostgresConfig('AgentsBob', { + endpoints: ['rxjs:bob'], +}) + +describe('postgres agents', () => { + let aliceAgent: Agent + let bobAgent: Agent + let aliceConnection: ConnectionRecord + let bobConnection: ConnectionRecord + + afterAll(async () => { + await bobAgent.shutdown() + await bobAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('make a connection between postgres agents', async () => { + const aliceMessages = new Subject() + const bobMessages = new Subject() + + const subjectMap = { + 'rxjs:alice': aliceMessages, + 'rxjs:bob': bobMessages, + } + + const storageConfig: IndyPostgresStorageConfig = { + type: 'postgres_storage', + config: { + url: 'localhost:5432', + wallet_scheme: WalletScheme.DatabasePerWallet, + }, + credentials: { + account: 'postgres', + password: 'postgres', + admin_account: 'postgres', + admin_password: 'postgres', + }, + } + + // loading the postgres wallet plugin + loadPostgresPlugin(storageConfig.config, storageConfig.credentials) + + aliceAgent = new Agent(alicePostgresConfig.config, alicePostgresConfig.agentDependencies) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await aliceAgent.initialize() + + bobAgent = new Agent(bobPostgresConfig.config, bobPostgresConfig.agentDependencies) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await bobAgent.initialize() + + const aliceBobOutOfBandRecord = await aliceAgent.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const { connectionRecord: bobConnectionAtBobAlice } = await bobAgent.oob.receiveInvitation( + aliceBobOutOfBandRecord.outOfBandInvitation + ) + bobConnection = await bobAgent.connections.returnWhenIsConnected(bobConnectionAtBobAlice!.id) + + const [aliceConnectionAtAliceBob] = await aliceAgent.connections.findAllByOutOfBandId(aliceBobOutOfBandRecord.id) + aliceConnection = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionAtAliceBob!.id) + + expect(aliceConnection).toBeConnectedWith(bobConnection) + expect(bobConnection).toBeConnectedWith(aliceConnection) + }) + + test('send a message to connection', async () => { + const message = 'hello, world' + await aliceAgent.basicMessages.sendMessage(aliceConnection.id, message) + + const basicMessage = await waitForBasicMessage(bobAgent, { + content: message, + }) + + expect(basicMessage.content).toBe(message) + }) + + test('can shutdown and re-initialize the same postgres agent', async () => { + expect(aliceAgent.isInitialized).toBe(true) + await aliceAgent.shutdown() + expect(aliceAgent.isInitialized).toBe(false) + await aliceAgent.initialize() + expect(aliceAgent.isInitialized).toBe(true) + }) +}) diff --git a/packages/core/tests/proofs-auto-accept.test.ts b/packages/core/tests/proofs-auto-accept.test.ts index b50c401cc4..a990c3d070 100644 --- a/packages/core/tests/proofs-auto-accept.test.ts +++ b/packages/core/tests/proofs-auto-accept.test.ts @@ -31,12 +31,10 @@ describe('Auto accept present proof', () => { }) afterAll(async () => { - await aliceAgent.shutdown({ - deleteWallet: true, - }) - await faberAgent.shutdown({ - deleteWallet: true, - }) + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() }) test('Alice starts with proof proposal to Faber, both with autoAcceptProof on `always`', async () => { @@ -114,12 +112,10 @@ describe('Auto accept present proof', () => { }) afterAll(async () => { - await aliceAgent.shutdown({ - deleteWallet: true, - }) - await faberAgent.shutdown({ - deleteWallet: true, - }) + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() }) test('Alice starts with proof proposal to Faber, both with autoacceptproof on `contentApproved`', async () => { diff --git a/packages/core/tests/proofs.test.ts b/packages/core/tests/proofs.test.ts index d9066b8374..38c96c03ce 100644 --- a/packages/core/tests/proofs.test.ts +++ b/packages/core/tests/proofs.test.ts @@ -29,16 +29,15 @@ describe('Present Proof', () => { testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = await setupProofsTest('Faber agent', 'Alice agent')) + testLogger.test('Issuing second credential') }) afterAll(async () => { testLogger.test('Shutting down both agents') - await aliceAgent.shutdown({ - deleteWallet: true, - }) - await faberAgent.shutdown({ - deleteWallet: true, - }) + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() }) test('Alice starts with proof proposal to Faber', async () => { @@ -54,7 +53,7 @@ describe('Present Proof', () => { }) expect(JsonTransformer.toJSON(aliceProofRecord)).toMatchObject({ - createdAt: expect.any(Date), + createdAt: expect.any(String), id: expect.any(String), proposalMessage: { '@type': 'https://didcomm.org/present-proof/1.0/propose-presentation', @@ -107,9 +106,8 @@ describe('Present Proof', () => { threadId: aliceProofRecord.threadId, state: ProofState.PresentationReceived, }) - expect(JsonTransformer.toJSON(faberProofRecord)).toMatchObject({ - createdAt: expect.any(Date), + createdAt: expect.any(String), state: ProofState.PresentationReceived, isVerified: true, presentationMessage: { @@ -237,7 +235,7 @@ describe('Present Proof', () => { expect(JsonTransformer.toJSON(aliceProofRecord)).toMatchObject({ id: expect.any(String), - createdAt: expect.any(Date), + createdAt: expect.any(String), requestMessage: { '@id': expect.any(String), '@type': 'https://didcomm.org/present-proof/1.0/request-presentation', @@ -280,7 +278,7 @@ describe('Present Proof', () => { mimeType: 'application/json', }, ], - attachments: [ + appendedAttachments: [ { id: 'zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', filename: 'picture-of-a-cat.png', @@ -333,4 +331,40 @@ describe('Present Proof', () => { presentationMessage: expect.any(PresentationMessage), }) }) + + test('an attribute group name matches with a predicate group name so an error is thrown', async () => { + // Age attribute + const attributes = { + age: new ProofAttributeInfo({ + name: 'age', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + // Age predicate + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + await expect( + faberAgent.proofs.requestProof(faberConnection.id, { + name: 'test-proof-request', + requestedAttributes: attributes, + requestedPredicates: predicates, + }) + ).rejects.toThrowError(`The proof request contains duplicate items: age`) + }) }) diff --git a/packages/core/tests/setup.ts b/packages/core/tests/setup.ts index de254bf876..a2ba1429d2 100644 --- a/packages/core/tests/setup.ts +++ b/packages/core/tests/setup.ts @@ -6,21 +6,19 @@ jest.setTimeout(120000) expect.extend({ toBeConnectedWith }) // Custom matchers which can be used to extend Jest matchers via extend, e. g. `expect.extend({ toBeConnectedWith })`. -function toBeConnectedWith(received: ConnectionRecord, connection: ConnectionRecord) { - received.assertReady() - connection.assertReady() +function toBeConnectedWith(actual: ConnectionRecord, expected: ConnectionRecord) { + actual.assertReady() + expected.assertReady() - const pass = received.theirDid === connection.did && received.theirKey === connection.verkey + const pass = actual.theirDid === expected.did if (pass) { return { - message: () => - `expected connection ${received.did}, ${received.verkey} not to be connected to with ${connection.did}, ${connection.verkey}`, + message: () => `expected connection ${actual.theirDid} not to be connected to with ${expected.did}`, pass: true, } } else { return { - message: () => - `expected connection ${received.did}, ${received.verkey} to be connected to with ${connection.did}, ${connection.verkey}`, + message: () => `expected connection ${actual.theirDid} to be connected to with ${expected.did}`, pass: false, } } diff --git a/packages/core/tests/wallet.test.ts b/packages/core/tests/wallet.test.ts new file mode 100644 index 0000000000..aae1ea9660 --- /dev/null +++ b/packages/core/tests/wallet.test.ts @@ -0,0 +1,178 @@ +import { tmpdir } from 'os' +import path from 'path' + +import { Agent } from '../src/agent/Agent' +import { BasicMessageRepository, BasicMessageRecord, BasicMessageRole } from '../src/modules/basic-messages' +import { KeyDerivationMethod } from '../src/types' +import { uuid } from '../src/utils/uuid' +import { WalletInvalidKeyError } from '../src/wallet/error' +import { WalletDuplicateError } from '../src/wallet/error/WalletDuplicateError' +import { WalletNotFoundError } from '../src/wallet/error/WalletNotFoundError' + +import { getBaseConfig } from './helpers' + +const aliceConfig = getBaseConfig('wallet-tests-Alice') +const bobConfig = getBaseConfig('wallet-tests-Bob') + +describe('wallet', () => { + let aliceAgent: Agent + let bobAgent: Agent + + beforeEach(async () => { + aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + bobAgent = new Agent(bobConfig.config, bobConfig.agentDependencies) + }) + + afterEach(async () => { + await aliceAgent.shutdown() + await bobAgent.shutdown() + + if (aliceAgent.wallet.isProvisioned) { + await aliceAgent.wallet.delete() + } + + if (bobAgent.wallet.isProvisioned) { + await bobAgent.wallet.delete() + } + }) + + test('open, create and open wallet with different wallet key that it is in agent config', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey', + } + + try { + await aliceAgent.wallet.open(walletConfig) + } catch (error) { + if (error instanceof WalletNotFoundError) { + await aliceAgent.wallet.create(walletConfig) + await aliceAgent.wallet.open(walletConfig) + } + } + + await aliceAgent.initialize() + + expect(aliceAgent.isInitialized).toBe(true) + }) + + test('when creating already existing wallet throw WalletDuplicateError', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey', + } + + await aliceAgent.wallet.create(walletConfig) + + await expect(aliceAgent.wallet.create(walletConfig)).rejects.toThrowError(WalletDuplicateError) + }) + + test('when opening non-existing wallet throw WalletNotFoundError', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey', + } + + await expect(aliceAgent.wallet.open(walletConfig)).rejects.toThrowError(WalletNotFoundError) + }) + + test('when opening wallet with invalid key throw WalletInvalidKeyError', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey', + } + + await aliceAgent.wallet.create(walletConfig) + await expect(aliceAgent.wallet.open({ ...walletConfig, key: 'abcd' })).rejects.toThrowError(WalletInvalidKeyError) + }) + + test('when create wallet and shutdown, wallet is closed', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey', + } + + await aliceAgent.wallet.create(walletConfig) + + await aliceAgent.shutdown() + + await expect(aliceAgent.wallet.open(walletConfig)).resolves.toBeUndefined() + }) + + test('create wallet with custom key derivation method', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey', + keyDerivationMethod: KeyDerivationMethod.Argon2IInt, + } + + await aliceAgent.wallet.createAndOpen(walletConfig) + + expect(aliceAgent.wallet.isInitialized).toBe(true) + }) + + test('when exporting and importing a wallet, content is copied', async () => { + await bobAgent.initialize() + const bobBasicMessageRepository = bobAgent.injectionContainer.resolve(BasicMessageRepository) + + const basicMessageRecord = new BasicMessageRecord({ + id: 'some-id', + connectionId: 'connId', + content: 'hello', + role: BasicMessageRole.Receiver, + sentTime: 'sentIt', + }) + + // Save in wallet + await bobBasicMessageRepository.save(basicMessageRecord) + + if (!bobAgent.config.walletConfig) { + throw new Error('No wallet config on bobAgent') + } + + const backupKey = 'someBackupKey' + const backupWalletName = `backup-${uuid()}` + const backupPath = path.join(tmpdir(), backupWalletName) + + // Create backup and delete wallet + await bobAgent.wallet.export({ path: backupPath, key: backupKey }) + await bobAgent.wallet.delete() + + // Initialize the wallet again and assert record does not exist + // This should create a new wallet + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await bobAgent.wallet.initialize(bobConfig.config.walletConfig!) + expect(await bobBasicMessageRepository.findById(basicMessageRecord.id)).toBeNull() + await bobAgent.wallet.delete() + + // Import backup with different wallet id and initialize + await bobAgent.wallet.import({ id: backupWalletName, key: backupWalletName }, { path: backupPath, key: backupKey }) + await bobAgent.wallet.initialize({ id: backupWalletName, key: backupWalletName }) + + // Expect same basic message record to exist in new wallet + expect(await bobBasicMessageRepository.getById(basicMessageRecord.id)).toMatchObject(basicMessageRecord) + }) + + test('changing wallet key', async () => { + const walletConfig = { + id: 'mywallet', + key: 'mysecretwalletkey', + } + + await aliceAgent.wallet.createAndOpen(walletConfig) + await aliceAgent.initialize() + + //Close agent + const walletConfigRekey = { + id: 'mywallet', + key: 'mysecretwalletkey', + rekey: '123', + } + + await aliceAgent.shutdown() + await aliceAgent.wallet.rotateKey(walletConfigRekey) + await aliceAgent.initialize() + + expect(aliceAgent.isInitialized).toBe(true) + }) +}) diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 047f1ca335..93d9dd32b5 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "allowJs": false, "typeRoots": ["../../node_modules/@types", "src/types"], "types": ["jest"] } diff --git a/packages/core/types/jsonld-signatures.ts b/packages/core/types/jsonld-signatures.ts new file mode 100644 index 0000000000..c930e2bfe6 --- /dev/null +++ b/packages/core/types/jsonld-signatures.ts @@ -0,0 +1,23 @@ +import { + suites as JsonLdSuites, + purposes as JsonLdPurposes, + constants as JsonLdConstants, + //@ts-ignore +} from '@digitalcredentials/jsonld-signatures' + +interface Suites { + LinkedDataSignature: any + LinkedDataProof: any +} + +interface Purposes { + AssertionProofPurpose: any +} + +type Constants = any + +export const suites = JsonLdSuites as Suites + +export const purposes = JsonLdPurposes as Purposes + +export const constants = JsonLdConstants as Constants diff --git a/packages/core/types/jsonld.ts b/packages/core/types/jsonld.ts new file mode 100644 index 0000000000..3e54d97b10 --- /dev/null +++ b/packages/core/types/jsonld.ts @@ -0,0 +1,29 @@ +//@ts-ignore +import jsonld from '@digitalcredentials/jsonld' +//@ts-ignore +import nodeDocumentLoader from '@digitalcredentials/jsonld/lib/documentLoaders/node' +//@ts-ignore +import xhrDocumentLoader from '@digitalcredentials/jsonld/lib/documentLoaders/xhr' + +interface JsonLd { + compact(document: any, context: any, options?: any): any + fromRDF(document: any): any + frame(document: any, revealDocument: any, options?: any): any + canonize(document: any, options?: any): any + expand(document: any, options?: any): any + getValues(document: any, key: string): any + addValue(document: any, key: string, value: any): void +} + +export interface DocumentLoaderResult { + contextUrl?: string | null + documentUrl: string + document: Record +} + +export type DocumentLoader = (url: string) => Promise + +export const documentLoaderXhr = xhrDocumentLoader as () => DocumentLoader +export const documentLoaderNode = nodeDocumentLoader as () => DocumentLoader + +export default jsonld as unknown as JsonLd diff --git a/packages/core/types/vc.ts b/packages/core/types/vc.ts new file mode 100644 index 0000000000..ca6196528d --- /dev/null +++ b/packages/core/types/vc.ts @@ -0,0 +1,12 @@ +// @ts-ignore +import vc from '@digitalcredentials/vc' + +interface VC { + issue(options: any): Promise> + verifyCredential(options: any): Promise> + createPresentation(options: any): Promise> + signPresentation(options: any): Promise> + verify(options: any): Promise> +} + +export default vc as unknown as VC diff --git a/packages/node/CHANGELOG.md b/packages/node/CHANGELOG.md new file mode 100644 index 0000000000..10bdd50ca1 --- /dev/null +++ b/packages/node/CHANGELOG.md @@ -0,0 +1,19 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.1.0 (2021-12-23) + +### Bug Fixes + +- **node:** node v12 support for is-indy-installed ([#542](https://github.com/hyperledger/aries-framework-javascript/issues/542)) ([17e9157](https://github.com/hyperledger/aries-framework-javascript/commit/17e9157479d6bba90c2a94bce64697d7f65fac96)) + +### Features + +- add multiple inbound transports ([#433](https://github.com/hyperledger/aries-framework-javascript/issues/433)) ([56cb9f2](https://github.com/hyperledger/aries-framework-javascript/commit/56cb9f2202deb83b3c133905f21651bfefcb63f7)) +- break out indy wallet, better indy handling ([#396](https://github.com/hyperledger/aries-framework-javascript/issues/396)) ([9f1a4a7](https://github.com/hyperledger/aries-framework-javascript/commit/9f1a4a754a61573ce3fee78d52615363c7e25d58)) +- **core:** connection-less issuance and verification ([#359](https://github.com/hyperledger/aries-framework-javascript/issues/359)) ([fb46ade](https://github.com/hyperledger/aries-framework-javascript/commit/fb46ade4bc2dd4f3b63d4194bb170d2f329562b7)) +- **core:** support multiple indy ledgers ([#474](https://github.com/hyperledger/aries-framework-javascript/issues/474)) ([47149bc](https://github.com/hyperledger/aries-framework-javascript/commit/47149bc5742456f4f0b75e0944ce276972e645b8)) +- **node:** add http and ws inbound transport ([#392](https://github.com/hyperledger/aries-framework-javascript/issues/392)) ([34a6ff2](https://github.com/hyperledger/aries-framework-javascript/commit/34a6ff2699197b9d525422a0a405e241582a476c)) +- **node:** add is-indy-installed command ([#510](https://github.com/hyperledger/aries-framework-javascript/issues/510)) ([e50b821](https://github.com/hyperledger/aries-framework-javascript/commit/e50b821343970d299a4cacdcba3a051893524ed6)) diff --git a/packages/node/README.md b/packages/node/README.md new file mode 100644 index 0000000000..8a125322c4 --- /dev/null +++ b/packages/node/README.md @@ -0,0 +1,31 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript - Node

+

+ License + typescript + @aries-framework/node version + +

+
+ +Aries Framework JavaScript Node provides platform specific dependencies to run Aries Framework JavaScript in [Node.JS](https://nodejs.org). See the [Getting Started Guide](https://github.com/hyperledger/aries-framework-javascript#getting-started) for installation instructions. diff --git a/packages/node/package.json b/packages/node/package.json index 9e481ab6c4..ad137f2003 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -2,7 +2,7 @@ "name": "@aries-framework/node", "main": "build/index", "types": "build/index", - "version": "0.0.0", + "version": "0.1.0", "files": [ "build", "bin" @@ -28,16 +28,20 @@ "test": "jest" }, "dependencies": { - "@aries-framework/core": "*", + "@aries-framework/core": "0.1.0", "express": "^4.17.1", + "ffi-napi": "^4.0.3", "indy-sdk": "^1.16.0-dev-1636", "node-fetch": "^2.6.1", + "ref-napi": "^3.0.3", "ws": "^7.5.3" }, "devDependencies": { "@types/express": "^4.17.13", + "@types/ffi-napi": "^4.0.5", "@types/node": "^15.14.4", "@types/node-fetch": "^2.5.10", + "@types/ref-napi": "^3.0.4", "@types/ws": "^7.4.6", "rimraf": "~3.0.2", "typescript": "~4.3.0" diff --git a/packages/node/src/NodeFileSystem.ts b/packages/node/src/NodeFileSystem.ts index daafb1f28c..f739c40814 100644 --- a/packages/node/src/NodeFileSystem.ts +++ b/packages/node/src/NodeFileSystem.ts @@ -1,6 +1,8 @@ import type { FileSystem } from '@aries-framework/core' -import { promises } from 'fs' +import fs, { promises } from 'fs' +import http from 'http' +import https from 'https' import { tmpdir } from 'os' import { dirname } from 'path' @@ -37,4 +39,34 @@ export class NodeFileSystem implements FileSystem { public async read(path: string): Promise { return readFile(path, { encoding: 'utf-8' }) } + + public async downloadToFile(url: string, path: string) { + const httpMethod = url.startsWith('https') ? https : http + + // Make sure parent directories exist + await promises.mkdir(dirname(path), { recursive: true }) + + const file = fs.createWriteStream(path) + + return new Promise((resolve, reject) => { + httpMethod + .get(url, (response) => { + // check if response is success + if (response.statusCode !== 200) { + reject(`Unable to download file from url: ${url}. Response status was ${response.statusCode}`) + } + + response.pipe(file) + file.on('finish', () => { + file.close() + resolve() + }) + }) + .on('error', async (error) => { + // Handle errors + await fs.promises.unlink(path) // Delete the file async. (But we don't check the result) + reject(`Unable to download file from url: ${url}. ${error.message}`) + }) + }) + } } diff --git a/packages/node/src/PostgresPlugin.ts b/packages/node/src/PostgresPlugin.ts new file mode 100644 index 0000000000..4ad7dc5f37 --- /dev/null +++ b/packages/node/src/PostgresPlugin.ts @@ -0,0 +1,105 @@ +import { Library } from 'ffi-napi' +import fs from 'fs' +import os from 'os' +import path from 'path' +import { types } from 'ref-napi' + +const LIBNAME = 'indystrgpostgres' +const ENV_VAR = 'LIB_INDY_STRG_POSTGRES' + +type Platform = 'darwin' | 'linux' | 'win32' + +type ExtensionMap = Record + +const extensions: ExtensionMap = { + darwin: { prefix: 'lib', extension: '.dylib' }, + linux: { prefix: 'lib', extension: '.so' }, + win32: { extension: '.dll' }, +} + +const libPaths: Record> = { + darwin: ['/usr/local/lib/', '/usr/lib/', '/opt/homebrew/opt/'], + linux: ['/usr/lib/', '/usr/local/lib/'], + win32: ['c:\\windows\\system32\\'], +} + +// Alias for a simple function to check if the path exists +const doesPathExist = fs.existsSync + +const getLibrary = () => { + // Detect OS; darwin, linux and windows are only supported + const platform = os.platform() + + if (platform !== 'linux' && platform !== 'win32' && platform !== 'darwin') + throw new Error(`Unsupported platform: ${platform}. linux, win32 and darwin are supported.`) + + // Get a potential path from the environment variable + const pathFromEnvironment = process.env[ENV_VAR] + + // Get the paths specific to the users operating system + const platformPaths = libPaths[platform] + + // Check if the path from the environment variable is supplied and add it + // We use unshift here so that when we want to get a valid library path this will be the first to resolve + if (pathFromEnvironment) platformPaths.unshift(pathFromEnvironment) + + // Create the path + file + const libraries = platformPaths.map((p) => + path.join(p, `${extensions[platform].prefix ?? ''}${LIBNAME}${extensions[platform].extension}`) + ) + + // Gaurd so we quit if there is no valid path for the library + if (!libraries.some(doesPathExist)) + throw new Error(`Could not find ${LIBNAME} with these paths: ${libraries.join(' ')}`) + + // Get the first valid library + // Casting here as a string because there is a guard of none of the paths + // would be valid + const validLibraryPath = libraries.find((l) => doesPathExist(l)) as string + + return Library(validLibraryPath, { + postgresstorage_init: [types.int, []], + init_storagetype: [types.int, ['string', 'string']], + }) +} + +type NativeIndyPostgres = { + postgresstorage_init: () => number + init_storagetype: (arg0: string, arg1: string) => number +} + +let indyPostgresStorage: NativeIndyPostgres | undefined + +export interface WalletStorageConfig { + url: string + wallet_scheme: WalletScheme + path?: string +} + +export interface WalletStorageCredentials { + account: string + password: string + admin_account: string + admin_password: string +} + +export enum WalletScheme { + DatabasePerWallet = 'DatabasePerWallet', + MultiWalletSingleTable = 'MultiWalletSingleTable', + MultiWalletSingleTableSharedPool = 'MultiWalletSingleTableSharedPool', +} + +export interface IndyPostgresStorageConfig { + type: 'postgres_storage' + config: WalletStorageConfig + credentials: WalletStorageCredentials +} + +export function loadPostgresPlugin(config: WalletStorageConfig, credentials: WalletStorageCredentials) { + if (!indyPostgresStorage) { + indyPostgresStorage = getLibrary() + } + + indyPostgresStorage.postgresstorage_init() + indyPostgresStorage.init_storagetype(JSON.stringify(config), JSON.stringify(credentials)) +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 235c72f520..5e58035b32 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -6,6 +6,7 @@ import fetch from 'node-fetch' import WebSocket from 'ws' import { NodeFileSystem } from './NodeFileSystem' +import { IndyPostgresStorageConfig, loadPostgresPlugin, WalletScheme } from './PostgresPlugin' import { HttpInboundTransport } from './transport/HttpInboundTransport' import { WsInboundTransport } from './transport/WsInboundTransport' @@ -17,4 +18,11 @@ const agentDependencies: AgentDependencies = { indy, } -export { agentDependencies, HttpInboundTransport, WsInboundTransport } +export { + agentDependencies, + HttpInboundTransport, + WsInboundTransport, + loadPostgresPlugin, + IndyPostgresStorageConfig, + WalletScheme, +} diff --git a/packages/node/src/transport/HttpInboundTransport.ts b/packages/node/src/transport/HttpInboundTransport.ts index 3ebb90b589..d6ecd27910 100644 --- a/packages/node/src/transport/HttpInboundTransport.ts +++ b/packages/node/src/transport/HttpInboundTransport.ts @@ -1,4 +1,4 @@ -import type { InboundTransport, Agent, TransportSession, WireMessage } from '@aries-framework/core' +import type { InboundTransport, Agent, TransportSession, EncryptedMessage } from '@aries-framework/core' import type { Express, Request, Response } from 'express' import type { Server } from 'http' @@ -40,8 +40,8 @@ export class HttpInboundTransport implements InboundTransport { const session = new HttpTransportSession(utils.uuid(), req, res) try { const message = req.body - const packedMessage = JSON.parse(message) - await agent.receiveMessage(packedMessage, session) + const encryptedMessage = JSON.parse(message) + await agent.receiveMessage(encryptedMessage, session) // If agent did not use session when processing message we need to send response here. if (!res.headersSent) { @@ -75,13 +75,19 @@ export class HttpTransportSession implements TransportSession { this.res = res } - public async send(wireMessage: WireMessage): Promise { + public async close(): Promise { + if (!this.res.headersSent) { + this.res.status(200).end() + } + } + + public async send(encryptedMessage: EncryptedMessage): Promise { if (this.res.headersSent) { throw new AriesFrameworkError(`${this.type} transport session has been closed.`) } // FIXME: we should not use json(), but rather the correct // DIDComm content-type based on the req and agent config - this.res.status(200).json(wireMessage).end() + this.res.status(200).json(encryptedMessage).end() } } diff --git a/packages/node/src/transport/WsInboundTransport.ts b/packages/node/src/transport/WsInboundTransport.ts index 8eccad110b..58f21f1557 100644 --- a/packages/node/src/transport/WsInboundTransport.ts +++ b/packages/node/src/transport/WsInboundTransport.ts @@ -1,4 +1,4 @@ -import type { Agent, InboundTransport, Logger, TransportSession, WireMessage } from '@aries-framework/core' +import type { Agent, InboundTransport, Logger, TransportSession, EncryptedMessage } from '@aries-framework/core' import { AriesFrameworkError, AgentConfig, TransportService, utils } from '@aries-framework/core' import WebSocket, { Server } from 'ws' @@ -81,11 +81,17 @@ export class WebSocketTransportSession implements TransportSession { this.socket = socket } - public async send(wireMessage: WireMessage): Promise { + public async send(encryptedMessage: EncryptedMessage): Promise { if (this.socket.readyState !== WebSocket.OPEN) { throw new AriesFrameworkError(`${this.type} transport session has been closed.`) } - this.socket.send(JSON.stringify(wireMessage)) + this.socket.send(JSON.stringify(encryptedMessage)) + } + + public async close(): Promise { + if (this.socket.readyState === WebSocket.OPEN) { + this.socket.close() + } } } diff --git a/packages/react-native/CHANGELOG.md b/packages/react-native/CHANGELOG.md new file mode 100644 index 0000000000..0ec7c5d1f5 --- /dev/null +++ b/packages/react-native/CHANGELOG.md @@ -0,0 +1,16 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# 0.1.0 (2021-12-23) + +### Bug Fixes + +- **core:** using query-string to parse URLs ([#457](https://github.com/hyperledger/aries-framework-javascript/issues/457)) ([78e5057](https://github.com/hyperledger/aries-framework-javascript/commit/78e505750557f296cc72ef19c0edd8db8e1eaa7d)) +- monorepo release issues ([#386](https://github.com/hyperledger/aries-framework-javascript/issues/386)) ([89a628f](https://github.com/hyperledger/aries-framework-javascript/commit/89a628f7c3ea9e5730d2ba5720819ac6283ee404)) + +### Features + +- **core:** d_m invitation parameter and invitation image ([#456](https://github.com/hyperledger/aries-framework-javascript/issues/456)) ([f92c322](https://github.com/hyperledger/aries-framework-javascript/commit/f92c322b97be4a4867a82c3a35159d6068951f0b)) +- **core:** ledger module registerPublicDid implementation ([#398](https://github.com/hyperledger/aries-framework-javascript/issues/398)) ([5f2d512](https://github.com/hyperledger/aries-framework-javascript/commit/5f2d5126baed2ff58268c38755c2dbe75a654947)) diff --git a/packages/react-native/README.md b/packages/react-native/README.md new file mode 100644 index 0000000000..0f8722f585 --- /dev/null +++ b/packages/react-native/README.md @@ -0,0 +1,31 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript - React Native

+

+ License + typescript + @aries-framework/react-native version + +

+
+ +Aries Framework JavaScript React Native provides platform specific dependencies to run Aries Framework JavaScript in [React Native](https://reactnative.dev). See the [Getting Started Guide](https://github.com/hyperledger/aries-framework-javascript#getting-started) for installation instructions. diff --git a/packages/react-native/package.json b/packages/react-native/package.json index b1c8df1bf5..c86e50cbac 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -2,7 +2,7 @@ "name": "@aries-framework/react-native", "main": "build/index", "types": "build/index", - "version": "0.0.0", + "version": "0.1.0", "files": [ "build" ], @@ -24,14 +24,15 @@ "test": "jest" }, "dependencies": { - "@aries-framework/core": "*", + "@aries-framework/core": "0.1.0", "@azure/core-asynciterator-polyfill": "^1.0.0", "events": "^3.3.0" }, "devDependencies": { - "@types/indy-sdk-react-native": "npm:@types/indy-sdk@^1.16.6", + "@animo-id/react-native-bbs-signatures": "^0.1.0", + "@types/indy-sdk-react-native": "npm:@types/indy-sdk@^1.16.16", "@types/react-native": "^0.64.10", - "indy-sdk-react-native": "^0.1.13", + "indy-sdk-react-native": "^0.2.0", "react": "17.0.1", "react-native": "0.64.2", "react-native-fs": "^2.18.0", @@ -40,8 +41,9 @@ "typescript": "~4.3.0" }, "peerDependencies": { - "indy-sdk-react-native": "^0.1.13", + "indy-sdk-react-native": "^0.2.0", "react-native-fs": "^2.18.0", - "react-native-get-random-values": "^1.7.0" + "react-native-get-random-values": "^1.7.0", + "react-native-bbs-signatures": "0.1.0" } } diff --git a/packages/react-native/src/ReactNativeFileSystem.ts b/packages/react-native/src/ReactNativeFileSystem.ts index eacba6f5b0..331fa11a54 100644 --- a/packages/react-native/src/ReactNativeFileSystem.ts +++ b/packages/react-native/src/ReactNativeFileSystem.ts @@ -31,4 +31,16 @@ export class ReactNativeFileSystem implements FileSystem { public async read(path: string): Promise { return RNFS.readFile(path, 'utf8') } + + public async downloadToFile(url: string, path: string) { + // Make sure parent directories exist + await RNFS.mkdir(getDirFromFilePath(path)) + + const { promise } = RNFS.downloadFile({ + fromUrl: url, + toFile: path, + }) + + await promise + } } diff --git a/samples/extension-module/README.md b/samples/extension-module/README.md new file mode 100644 index 0000000000..dcb70074a3 --- /dev/null +++ b/samples/extension-module/README.md @@ -0,0 +1,80 @@ +

Extension module example

+ +This example shows how can an extension module be written and injected to an Aries Framework Javascript `Agent` instance. Its structure is similar to the one of regular modules, although is not strictly needed to follow it to achieve this goal. + +An extension module could be used for different purposes, such as storing data in an Identity Wallet, supporting custom protocols over Didcomm or implementing new [Aries RFCs](https://github.com/hyperledger/aries-rfcs/tree/main/features) without the need of embed them right into AFJ's Core package. Injected modules can access to other core modules and services and trigger events, so in practice they work much in the same way as if they were included statically. + +## Dummy module + +This example consists of a module that implements a very simple request-response protocol called Dummy. In order to do so and be able to be injected into an AFJ instance, some steps were followed: + +- Define Dummy protocol message classes (inherited from `AgentMessage`) +- Create handlers for those messages (inherited from `Handler`) +- Define records (inherited from `BaseRecord`) and a container-scoped repository (inherited from `Repository`) for state persistance +- Define events (inherited from `BaseEvent`) +- Create a container-scoped service class that manages records and repository, and also trigger events using Agent's `EventEmitter` +- Create a container-scoped module class that registers handlers in Agent's `Dispatcher` and provides a simple API to do requests and responses, with the aid of service classes and Agent's `MessageSender` + +## Usage + +In order to use this module, it must be injected into an AFJ instance. This can be done by resolving DummyModule right after agent is instantiated: + +```ts +import { DummyModule } from './dummy' + +const agent = new Agent(/** agent config... */) + +const dummyModule = agent.injectionContainer.resolve(DummyModule) + +await agent.initialize() +``` + +Then, Dummy module API methods can be called, and events listeners can be created: + +```ts +agent.events.on(DummyEventTypes.StateChanged, async (event: DummyStateChangedEvent) => { + if (event.payload.dummyRecord.state === DummyState.RequestReceived) { + await dummyModule.respond(event.payload.dummyRecord) + } +}) + +const record = await dummyModule.request(connection) +``` + +## Run demo + +This repository includes a demonstration of a requester and a responder controller using this module to exchange Dummy protocol messages. For environment set up, make sure you followed instructions for [NodeJS](/docs/setup-nodejs.md). + +These are the steps for running it: + +Clone the AFJ git repository: + +```sh +git clone https://github.com/hyperledger/aries-framework-javascript.git +``` + +Open two different terminals and go to the extension-module directory: + +```sh +cd aries-framework-javascript/samples/extension-module +``` + +Install the project in one of the terminals: + +```sh +yarn install +``` + +In that terminal run the responder: + +```sh +yarn responder +``` + +Wait for it to finish the startup process (i.e. logger showing 'Responder listening to port ...') and run requester in another terminal: + +```sh +yarn requester +``` + +If everything goes right, requester will connect to responder and, as soon as connection protocol is finished, it will send a Dummy request. Responder will answer with a Dummy response and requester will happily exit. diff --git a/samples/extension-module/dummy/DummyModule.ts b/samples/extension-module/dummy/DummyModule.ts new file mode 100644 index 0000000000..f569af05ec --- /dev/null +++ b/samples/extension-module/dummy/DummyModule.ts @@ -0,0 +1,80 @@ +import type { DummyRecord } from './repository/DummyRecord' +import type { ConnectionRecord } from '@aries-framework/core' + +import { ConnectionService, Dispatcher, MessageSender } from '@aries-framework/core' +import { Lifecycle, scoped } from 'tsyringe' + +import { DummyRequestHandler, DummyResponseHandler } from './handlers' +import { DummyState } from './repository' +import { DummyService } from './services' + +@scoped(Lifecycle.ContainerScoped) +export class DummyModule { + private messageSender: MessageSender + private dummyService: DummyService + private connectionService: ConnectionService + + public constructor( + dispatcher: Dispatcher, + messageSender: MessageSender, + dummyService: DummyService, + connectionService: ConnectionService + ) { + this.messageSender = messageSender + this.dummyService = dummyService + this.connectionService = connectionService + this.registerHandlers(dispatcher) + } + + /** + * Send a Dummy Request + * + * @param connection record of the target responder (must be active) + * @returns created Dummy Record + */ + public async request(connection: ConnectionRecord) { + const { record, message: payload } = await this.dummyService.createRequest(connection) + + await this.messageSender.sendMessage({ connection, payload }) + + await this.dummyService.updateState(record, DummyState.RequestSent) + + return record + } + + /** + * Respond a Dummy Request + * + * @param record Dummy record + * @returns Updated dummy record + */ + public async respond(record: DummyRecord) { + if (!record.connectionId) { + throw new Error('Connection not found!') + } + + const connection = await this.connectionService.getById(record.connectionId) + + const payload = await this.dummyService.createResponse(record) + + await this.messageSender.sendMessage({ connection, payload }) + + await this.dummyService.updateState(record, DummyState.ResponseSent) + + return record + } + + /** + * Retrieve all dummy records + * + * @returns List containing all records + */ + public getAll(): Promise { + return this.dummyService.getAll() + } + + private registerHandlers(dispatcher: Dispatcher) { + dispatcher.registerHandler(new DummyRequestHandler(this.dummyService)) + dispatcher.registerHandler(new DummyResponseHandler(this.dummyService)) + } +} diff --git a/samples/extension-module/dummy/handlers/DummyRequestHandler.ts b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts new file mode 100644 index 0000000000..c5b1e047e6 --- /dev/null +++ b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts @@ -0,0 +1,19 @@ +import type { DummyService } from '../services' +import type { Handler, HandlerInboundMessage } from '@aries-framework/core' + +import { DummyRequestMessage } from '../messages' + +export class DummyRequestHandler implements Handler { + public supportedMessages = [DummyRequestMessage] + private dummyService: DummyService + + public constructor(dummyService: DummyService) { + this.dummyService = dummyService + } + + public async handle(inboundMessage: HandlerInboundMessage) { + inboundMessage.assertReadyConnection() + + await this.dummyService.processRequest(inboundMessage) + } +} diff --git a/samples/extension-module/dummy/handlers/DummyResponseHandler.ts b/samples/extension-module/dummy/handlers/DummyResponseHandler.ts new file mode 100644 index 0000000000..faca594166 --- /dev/null +++ b/samples/extension-module/dummy/handlers/DummyResponseHandler.ts @@ -0,0 +1,19 @@ +import type { DummyService } from '../services' +import type { Handler, HandlerInboundMessage } from '@aries-framework/core' + +import { DummyResponseMessage } from '../messages' + +export class DummyResponseHandler implements Handler { + public supportedMessages = [DummyResponseMessage] + private dummyService: DummyService + + public constructor(dummyService: DummyService) { + this.dummyService = dummyService + } + + public async handle(inboundMessage: HandlerInboundMessage) { + inboundMessage.assertReadyConnection() + + await this.dummyService.processResponse(inboundMessage) + } +} diff --git a/samples/extension-module/dummy/handlers/index.ts b/samples/extension-module/dummy/handlers/index.ts new file mode 100644 index 0000000000..1aacc16089 --- /dev/null +++ b/samples/extension-module/dummy/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './DummyRequestHandler' +export * from './DummyResponseHandler' diff --git a/samples/extension-module/dummy/index.ts b/samples/extension-module/dummy/index.ts new file mode 100644 index 0000000000..2ca47f690a --- /dev/null +++ b/samples/extension-module/dummy/index.ts @@ -0,0 +1,5 @@ +export * from './DummyModule' +export * from './handlers' +export * from './messages' +export * from './services' +export * from './repository' diff --git a/samples/extension-module/dummy/messages/DummyRequestMessage.ts b/samples/extension-module/dummy/messages/DummyRequestMessage.ts new file mode 100644 index 0000000000..4b93734810 --- /dev/null +++ b/samples/extension-module/dummy/messages/DummyRequestMessage.ts @@ -0,0 +1,19 @@ +import { AgentMessage, IsValidMessageType, parseMessageType } from '@aries-framework/core' + +export interface DummyRequestMessageOptions { + id?: string +} + +export class DummyRequestMessage extends AgentMessage { + public constructor(options: DummyRequestMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + } + } + + @IsValidMessageType(DummyRequestMessage.type) + public readonly type = DummyRequestMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/dummy/1.0/request') +} diff --git a/samples/extension-module/dummy/messages/DummyResponseMessage.ts b/samples/extension-module/dummy/messages/DummyResponseMessage.ts new file mode 100644 index 0000000000..421243d516 --- /dev/null +++ b/samples/extension-module/dummy/messages/DummyResponseMessage.ts @@ -0,0 +1,23 @@ +import { AgentMessage, IsValidMessageType, parseMessageType } from '@aries-framework/core' + +export interface DummyResponseMessageOptions { + id?: string + threadId: string +} + +export class DummyResponseMessage extends AgentMessage { + public constructor(options: DummyResponseMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.setThread({ + threadId: options.threadId, + }) + } + } + + @IsValidMessageType(DummyResponseMessage.type) + public readonly type = DummyResponseMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://2060.io/didcomm/dummy/1.0/response') +} diff --git a/samples/extension-module/dummy/messages/index.ts b/samples/extension-module/dummy/messages/index.ts new file mode 100644 index 0000000000..7b11bafe4f --- /dev/null +++ b/samples/extension-module/dummy/messages/index.ts @@ -0,0 +1,2 @@ +export * from './DummyRequestMessage' +export * from './DummyResponseMessage' diff --git a/samples/extension-module/dummy/repository/DummyRecord.ts b/samples/extension-module/dummy/repository/DummyRecord.ts new file mode 100644 index 0000000000..c321e5941a --- /dev/null +++ b/samples/extension-module/dummy/repository/DummyRecord.ts @@ -0,0 +1,51 @@ +import type { DummyState } from './DummyState' + +import { BaseRecord } from '@aries-framework/core' +import { v4 as uuid } from 'uuid' + +export interface DummyStorageProps { + id?: string + createdAt?: Date + connectionId?: string + threadId: string + state: DummyState +} + +export class DummyRecord extends BaseRecord implements DummyStorageProps { + public connectionId?: string + public threadId!: string + public state!: DummyState + + public static readonly type = 'DummyRecord' + public readonly type = DummyRecord.type + + public constructor(props: DummyStorageProps) { + super() + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.state = props.state + this.connectionId = props.connectionId + this.threadId = props.threadId + } + } + + public getTags() { + return { + ...this._tags, + threadId: this.threadId, + connectionId: this.connectionId, + state: this.state, + } + } + + public assertState(expectedStates: DummyState | DummyState[]) { + if (!Array.isArray(expectedStates)) { + expectedStates = [expectedStates] + } + + if (!expectedStates.includes(this.state)) { + throw new Error(`Dummy record is in invalid state ${this.state}. Valid states are: ${expectedStates.join(', ')}.`) + } + } +} diff --git a/samples/extension-module/dummy/repository/DummyRepository.ts b/samples/extension-module/dummy/repository/DummyRepository.ts new file mode 100644 index 0000000000..f9384e0cfc --- /dev/null +++ b/samples/extension-module/dummy/repository/DummyRepository.ts @@ -0,0 +1,11 @@ +import { Repository, StorageService, InjectionSymbols } from '@aries-framework/core' +import { inject, scoped, Lifecycle } from 'tsyringe' + +import { DummyRecord } from './DummyRecord' + +@scoped(Lifecycle.ContainerScoped) +export class DummyRepository extends Repository { + public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService) { + super(DummyRecord, storageService) + } +} diff --git a/samples/extension-module/dummy/repository/DummyState.ts b/samples/extension-module/dummy/repository/DummyState.ts new file mode 100644 index 0000000000..c5f8f411b1 --- /dev/null +++ b/samples/extension-module/dummy/repository/DummyState.ts @@ -0,0 +1,7 @@ +export enum DummyState { + Init = 'init', + RequestSent = 'request-sent', + RequestReceived = 'request-received', + ResponseSent = 'response-sent', + ResponseReceived = 'response-received', +} diff --git a/samples/extension-module/dummy/repository/index.ts b/samples/extension-module/dummy/repository/index.ts new file mode 100644 index 0000000000..38d0353bd5 --- /dev/null +++ b/samples/extension-module/dummy/repository/index.ts @@ -0,0 +1,3 @@ +export * from './DummyRecord' +export * from './DummyRepository' +export * from './DummyState' diff --git a/samples/extension-module/dummy/services/DummyEvents.ts b/samples/extension-module/dummy/services/DummyEvents.ts new file mode 100644 index 0000000000..981630e0df --- /dev/null +++ b/samples/extension-module/dummy/services/DummyEvents.ts @@ -0,0 +1,15 @@ +import type { DummyRecord } from '../repository/DummyRecord' +import type { DummyState } from '../repository/DummyState' +import type { BaseEvent } from '@aries-framework/core' + +export enum DummyEventTypes { + StateChanged = 'DummyStateChanged', +} + +export interface DummyStateChangedEvent extends BaseEvent { + type: DummyEventTypes.StateChanged + payload: { + dummyRecord: DummyRecord + previousState: DummyState | null + } +} diff --git a/samples/extension-module/dummy/services/DummyService.ts b/samples/extension-module/dummy/services/DummyService.ts new file mode 100644 index 0000000000..d0c3635d33 --- /dev/null +++ b/samples/extension-module/dummy/services/DummyService.ts @@ -0,0 +1,176 @@ +import type { DummyStateChangedEvent } from './DummyEvents' +import type { ConnectionRecord, InboundMessageContext } from '@aries-framework/core' + +import { EventEmitter } from '@aries-framework/core' +import { Lifecycle, scoped } from 'tsyringe' + +import { DummyRequestMessage, DummyResponseMessage } from '../messages' +import { DummyRecord } from '../repository/DummyRecord' +import { DummyRepository } from '../repository/DummyRepository' +import { DummyState } from '../repository/DummyState' + +import { DummyEventTypes } from './DummyEvents' + +@scoped(Lifecycle.ContainerScoped) +export class DummyService { + private dummyRepository: DummyRepository + private eventEmitter: EventEmitter + + public constructor(dummyRepository: DummyRepository, eventEmitter: EventEmitter) { + this.dummyRepository = dummyRepository + this.eventEmitter = eventEmitter + } + + /** + * Create a {@link DummyRequestMessage}. + * + * @param connectionRecord The connection for which to create the dummy request + * @returns Object containing dummy request message and associated dummy record + * + */ + public async createRequest(connectionRecord: ConnectionRecord) { + // Create message + const message = new DummyRequestMessage({}) + + // Create record + const record = new DummyRecord({ + connectionId: connectionRecord.id, + threadId: message.id, + state: DummyState.Init, + }) + + await this.dummyRepository.save(record) + + this.eventEmitter.emit({ + type: DummyEventTypes.StateChanged, + payload: { + dummyRecord: record, + previousState: null, + }, + }) + + return { record, message } + } + + /** + * Create a dummy response message for the specified dummy record. + * + * @param record the dummy record for which to create a dummy response + * @returns outbound message containing dummy response + */ + public async createResponse(record: DummyRecord) { + const responseMessage = new DummyResponseMessage({ + threadId: record.threadId, + }) + + return responseMessage + } + + /** + * Process a received {@link DummyRequestMessage}. + * + * @param messageContext The message context containing a dummy request message + * @returns dummy record associated with the dummy request message + * + */ + public async processRequest(messageContext: InboundMessageContext) { + const connectionRecord = messageContext.connection + + // Create record + const record = new DummyRecord({ + connectionId: connectionRecord?.id, + threadId: messageContext.message.id, + state: DummyState.RequestReceived, + }) + + await this.dummyRepository.save(record) + + this.eventEmitter.emit({ + type: DummyEventTypes.StateChanged, + payload: { + dummyRecord: record, + previousState: null, + }, + }) + + return record + } + + /** + * Process a received {@link DummyResponseMessage}. + * + * @param messageContext The message context containing a dummy response message + * @returns dummy record associated with the dummy response message + * + */ + public async processResponse(messageContext: InboundMessageContext) { + const { connection, message } = messageContext + + // Dummy record already exists + const record = await this.findByThreadAndConnectionId(message.threadId, connection?.id) + + if (record) { + // Check current state + record.assertState(DummyState.RequestSent) + + await this.updateState(record, DummyState.ResponseReceived) + } else { + throw new Error(`Dummy record not found with threadId ${message.threadId}`) + } + + return record + } + + /** + * Retrieve all dummy records + * + * @returns List containing all dummy records + */ + public getAll(): Promise { + return this.dummyRepository.getAll() + } + + /** + * Retrieve a dummy record by id + * + * @param dummyRecordId The dummy record id + * @throws {RecordNotFoundError} If no record is found + * @return The dummy record + * + */ + public getById(dummyRecordId: string): Promise { + return this.dummyRepository.getById(dummyRecordId) + } + + /** + * Retrieve a dummy record by connection id and thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The dummy record + */ + public async findByThreadAndConnectionId(threadId: string, connectionId?: string): Promise { + return this.dummyRepository.findSingleByQuery({ threadId, connectionId }) + } + + /** + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. + * + * @param dummyRecord The record to update the state for + * @param newState The state to update to + * + */ + public async updateState(dummyRecord: DummyRecord, newState: DummyState) { + const previousState = dummyRecord.state + dummyRecord.state = newState + await this.dummyRepository.update(dummyRecord) + + this.eventEmitter.emit({ + type: DummyEventTypes.StateChanged, + payload: { dummyRecord, previousState: previousState }, + }) + } +} diff --git a/samples/extension-module/dummy/services/index.ts b/samples/extension-module/dummy/services/index.ts new file mode 100644 index 0000000000..05bcbc5d0a --- /dev/null +++ b/samples/extension-module/dummy/services/index.ts @@ -0,0 +1,2 @@ +export * from './DummyService' +export * from './DummyEvents' diff --git a/samples/extension-module/package.json b/samples/extension-module/package.json new file mode 100644 index 0000000000..ae446d5838 --- /dev/null +++ b/samples/extension-module/package.json @@ -0,0 +1,29 @@ +{ + "name": "afj-extension-module-sample", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "samples/extension-module/" + }, + "license": "Apache-2.0", + "scripts": { + "requester": "ts-node requester.ts", + "responder": "ts-node responder.ts" + }, + "devDependencies": { + "@aries-framework/core": "^0.1.0", + "@aries-framework/node": "^0.1.0", + "ts-node": "^10.4.0" + }, + "dependencies": { + "@types/express": "^4.17.13", + "@types/uuid": "^8.3.1", + "@types/ws": "^7.4.6", + "class-validator": "0.13.1", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.2.0", + "tsyringe": "^4.5.0" + } +} diff --git a/samples/extension-module/requester.ts b/samples/extension-module/requester.ts new file mode 100644 index 0000000000..7079c01375 --- /dev/null +++ b/samples/extension-module/requester.ts @@ -0,0 +1,70 @@ +import type { DummyRecord, DummyStateChangedEvent } from './dummy' + +import { Agent, AriesFrameworkError, ConsoleLogger, LogLevel, WsOutboundTransport } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' +import { filter, first, firstValueFrom, map, ReplaySubject, timeout } from 'rxjs' + +import { DummyEventTypes, DummyModule, DummyState } from './dummy' + +const run = async () => { + // Create transports + const port = process.env.RESPONDER_PORT ? Number(process.env.RESPONDER_PORT) : 3002 + const wsOutboundTransport = new WsOutboundTransport() + + // Setup the agent + const agent = new Agent( + { + label: 'Dummy-powered agent - requester', + walletConfig: { + id: 'requester', + key: 'requester', + }, + logger: new ConsoleLogger(LogLevel.test), + autoAcceptConnections: true, + }, + agentDependencies + ) + + // Register transports + agent.registerOutboundTransport(wsOutboundTransport) + + // Inject DummyModule + const dummyModule = agent.injectionContainer.resolve(DummyModule) + + // Now agent will handle messages and events from Dummy protocol + + //Initialize the agent + await agent.initialize() + + // Connect to responder using its invitation endpoint + const invitationUrl = await (await agentDependencies.fetch(`http://localhost:${port}/invitation`)).text() + const { connectionRecord: connection } = await agent.oob.receiveInvitationFromUrl(invitationUrl) + if (!connection) { + throw new AriesFrameworkError('Connection record for out-of-band invitation was not created.') + } + await agent.connections.returnWhenIsConnected(connection.id) + + // Create observable for Response Received event + const observable = agent.events.observable(DummyEventTypes.StateChanged) + const subject = new ReplaySubject(1) + + observable + .pipe( + filter((event: DummyStateChangedEvent) => event.payload.dummyRecord.state === DummyState.ResponseReceived), + map((e) => e.payload.dummyRecord), + first(), + timeout(5000) + ) + .subscribe(subject) + + // Send a dummy request and wait for response + const record = await dummyModule.request(connection) + agent.config.logger.info(`Request received for Dummy Record: ${record.id}`) + + const dummyRecord = await firstValueFrom(subject) + agent.config.logger.info(`Response received for Dummy Record: ${dummyRecord.id}`) + + await agent.shutdown() +} + +void run() diff --git a/samples/extension-module/responder.ts b/samples/extension-module/responder.ts new file mode 100644 index 0000000000..140f3c2a3f --- /dev/null +++ b/samples/extension-module/responder.ts @@ -0,0 +1,71 @@ +import type { DummyStateChangedEvent } from './dummy' +import type { Socket } from 'net' + +import { Agent, ConsoleLogger, LogLevel, WsOutboundTransport } from '@aries-framework/core' +import { agentDependencies, HttpInboundTransport, WsInboundTransport } from '@aries-framework/node' +import express from 'express' +import { Server } from 'ws' + +import { DummyEventTypes, DummyModule, DummyState } from './dummy' + +const run = async () => { + // Create transports + const port = process.env.RESPONDER_PORT ? Number(process.env.RESPONDER_PORT) : 3002 + const app = express() + const socketServer = new Server({ noServer: true }) + + const httpInboundTransport = new HttpInboundTransport({ app, port }) + const wsInboundTransport = new WsInboundTransport({ server: socketServer }) + const wsOutboundTransport = new WsOutboundTransport() + + // Setup the agent + const agent = new Agent( + { + label: 'Dummy-powered agent - responder', + endpoints: [`ws://localhost:${port}`], + walletConfig: { + id: 'responder', + key: 'responder', + }, + logger: new ConsoleLogger(LogLevel.test), + autoAcceptConnections: true, + }, + agentDependencies + ) + + // Register transports + agent.registerInboundTransport(httpInboundTransport) + agent.registerInboundTransport(wsInboundTransport) + agent.registerInboundTransport(wsOutboundTransport) + + // Allow to create invitation, no other way to ask for invitation yet + app.get('/invitation', async (req, res) => { + const { outOfBandInvitation } = await agent.oob.createInvitation() + res.send(outOfBandInvitation.toUrl({ domain: `http://localhost:${port}/invitation` })) + }) + + // Inject DummyModule + const dummyModule = agent.injectionContainer.resolve(DummyModule) + + // Now agent will handle messages and events from Dummy protocol + + //Initialize the agent + await agent.initialize() + + httpInboundTransport.server?.on('upgrade', (request, socket, head) => { + socketServer.handleUpgrade(request, socket as Socket, head, (socket) => { + socketServer.emit('connection', socket, request) + }) + }) + + // Subscribe to dummy record events + agent.events.on(DummyEventTypes.StateChanged, async (event: DummyStateChangedEvent) => { + if (event.payload.dummyRecord.state === DummyState.RequestReceived) { + await dummyModule.respond(event.payload.dummyRecord) + } + }) + + agent.config.logger.info(`Responder listening to port ${port}`) +} + +void run() diff --git a/samples/extension-module/tsconfig.json b/samples/extension-module/tsconfig.json new file mode 100644 index 0000000000..2e05131598 --- /dev/null +++ b/samples/extension-module/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node"] + } +} diff --git a/samples/mediator.ts b/samples/mediator.ts index 83f6db65ec..7be3cdce9a 100644 --- a/samples/mediator.ts +++ b/samples/mediator.ts @@ -13,6 +13,7 @@ */ import type { InitConfig } from '@aries-framework/core' +import type { Socket } from 'net' import express from 'express' import { Server } from 'ws' @@ -74,10 +75,9 @@ httpInboundTransport.app.get('/invitation', async (req, res) => { const invitation = await ConnectionInvitationMessage.fromUrl(req.url) res.send(invitation.toJSON()) } else { - const { invitation } = await agent.connections.createConnection() - + const { outOfBandInvitation } = await agent.oob.createInvitation() const httpEndpoint = config.endpoints.find((e) => e.startsWith('http')) - res.send(invitation.toUrl({ domain: httpEndpoint + '/invitation' })) + res.send(outOfBandInvitation.toUrl({ domain: httpEndpoint + '/invitation' })) } }) @@ -87,10 +87,10 @@ const run = async () => { // When an 'upgrade' to WS is made on our http server, we forward the // request to the WS server httpInboundTransport.server?.on('upgrade', (request, socket, head) => { - socketServer.handleUpgrade(request, socket, head, (socket) => { + socketServer.handleUpgrade(request, socket as Socket, head, (socket) => { socketServer.emit('connection', socket, request) }) }) } -run() +void run() diff --git a/tests/InMemoryStorageService.ts b/tests/InMemoryStorageService.ts new file mode 100644 index 0000000000..c7a4b777ff --- /dev/null +++ b/tests/InMemoryStorageService.ts @@ -0,0 +1,125 @@ +import type { BaseRecord, TagsBase } from '../packages/core/src/storage/BaseRecord' +import type { StorageService, BaseRecordConstructor } from '../packages/core/src/storage/StorageService' + +import { scoped, Lifecycle } from 'tsyringe' + +import { RecordNotFoundError, RecordDuplicateError, JsonTransformer } from '@aries-framework/core' + +interface StorageRecord { + value: Record + tags: Record + type: string + id: string +} + +@scoped(Lifecycle.ContainerScoped) +export class InMemoryStorageService implements StorageService { + public records: { [id: string]: StorageRecord } + + public constructor(records: { [id: string]: StorageRecord } = {}) { + this.records = records + } + + private recordToInstance(record: StorageRecord, recordClass: BaseRecordConstructor): T { + const instance = JsonTransformer.fromJSON(record.value, recordClass) + instance.id = record.id + instance.replaceTags(record.tags as TagsBase) + + return instance + } + + /** @inheritDoc */ + public async save(record: T) { + const value = JsonTransformer.toJSON(record) + + if (this.records[record.id]) { + throw new RecordDuplicateError(`Record with id ${record.id} already exists`, { recordType: record.type }) + } + + this.records[record.id] = { + value, + id: record.id, + type: record.type, + tags: record.getTags(), + } + } + + /** @inheritDoc */ + public async update(record: T): Promise { + const value = JsonTransformer.toJSON(record) + delete value._tags + + if (!this.records[record.id]) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + }) + } + + this.records[record.id] = { + value, + id: record.id, + type: record.type, + tags: record.getTags(), + } + } + + /** @inheritDoc */ + public async delete(record: T) { + if (!this.records[record.id]) { + throw new RecordNotFoundError(`record with id ${record.id} not found.`, { + recordType: record.type, + }) + } + + delete this.records[record.id] + } + + /** @inheritDoc */ + public async getById(recordClass: BaseRecordConstructor, id: string): Promise { + const record = this.records[id] + + if (!record) { + throw new RecordNotFoundError(`record with id ${id} not found.`, { + recordType: recordClass.type, + }) + } + + return this.recordToInstance(record, recordClass) + } + + /** @inheritDoc */ + public async getAll(recordClass: BaseRecordConstructor): Promise { + const records = Object.values(this.records) + .filter((record) => record.type === recordClass.type) + .map((record) => this.recordToInstance(record, recordClass)) + + return records + } + + /** @inheritDoc */ + public async findByQuery( + recordClass: BaseRecordConstructor, + query: Partial> + ): Promise { + const records = Object.values(this.records) + .filter((record) => { + const tags = record.tags as TagsBase + + for (const [key, value] of Object.entries(query)) { + if (Array.isArray(value)) { + const tagValue = tags[key] + if (!Array.isArray(tagValue) || !value.every((v) => tagValue.includes(v))) { + return false + } + } else if (tags[key] !== value) { + return false + } + } + + return true + }) + .map((record) => this.recordToInstance(record, recordClass)) + + return records + } +} diff --git a/tests/e2e-http.test.ts b/tests/e2e-http.test.ts index 167bf37553..fa140f4220 100644 --- a/tests/e2e-http.test.ts +++ b/tests/e2e-http.test.ts @@ -2,11 +2,12 @@ import { getBaseConfig } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' -import { HttpOutboundTransport, Agent, AutoAcceptCredential } from '@aries-framework/core' +import { HttpOutboundTransport, Agent, AutoAcceptCredential, MediatorPickupStrategy } from '@aries-framework/core' import { HttpInboundTransport } from '@aries-framework/node' const recipientConfig = getBaseConfig('E2E HTTP Recipient', { autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }) const mediatorPort = 3000 @@ -20,6 +21,7 @@ const senderConfig = getBaseConfig('E2E HTTP Sender', { endpoints: [`http://localhost:${senderPort}`], mediatorPollingInterval: 1000, autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }) describe('E2E HTTP tests', () => { @@ -34,9 +36,12 @@ describe('E2E HTTP tests', () => { }) afterEach(async () => { - await recipientAgent.shutdown({ deleteWallet: true }) - await mediatorAgent.shutdown({ deleteWallet: true }) - await senderAgent.shutdown({ deleteWallet: true }) + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + await senderAgent.shutdown() + await senderAgent.wallet.delete() }) test('Full HTTP flow (connect, request mediation, issue, verify)', async () => { diff --git a/tests/e2e-subject.test.ts b/tests/e2e-subject.test.ts index 14cc20c0b3..51945cf1ed 100644 --- a/tests/e2e-subject.test.ts +++ b/tests/e2e-subject.test.ts @@ -8,10 +8,11 @@ import { e2eTest } from './e2e-test' import { SubjectInboundTransport } from './transport/SubjectInboundTransport' import { SubjectOutboundTransport } from './transport/SubjectOutboundTransport' -import { Agent, AutoAcceptCredential } from '@aries-framework/core' +import { Agent, AutoAcceptCredential, MediatorPickupStrategy } from '@aries-framework/core' const recipientConfig = getBaseConfig('E2E Subject Recipient', { autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }) const mediatorConfig = getBaseConfig('E2E Subject Mediator', { endpoints: ['rxjs:mediator'], @@ -21,6 +22,7 @@ const senderConfig = getBaseConfig('E2E Subject Sender', { endpoints: ['rxjs:sender'], mediatorPollingInterval: 1000, autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }) describe('E2E Subject tests', () => { @@ -35,14 +37,16 @@ describe('E2E Subject tests', () => { }) afterEach(async () => { - await recipientAgent.shutdown({ deleteWallet: true }) - await mediatorAgent.shutdown({ deleteWallet: true }) - await senderAgent.shutdown({ deleteWallet: true }) + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + await senderAgent.shutdown() + await senderAgent.wallet.delete() }) test('Full Subject flow (connect, request mediation, issue, verify)', async () => { const mediatorMessages = new Subject() - const recipientMessages = new Subject() const senderMessages = new Subject() const subjectMap = { @@ -51,17 +55,16 @@ describe('E2E Subject tests', () => { } // Recipient Setup - recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(recipientMessages, subjectMap)) - recipientAgent.registerInboundTransport(new SubjectInboundTransport(recipientMessages)) + recipientAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await recipientAgent.initialize() // Mediator Setup - mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(mediatorMessages, subjectMap)) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) await mediatorAgent.initialize() // Sender Setup - senderAgent.registerOutboundTransport(new SubjectOutboundTransport(senderMessages, subjectMap)) + senderAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) senderAgent.registerInboundTransport(new SubjectInboundTransport(senderMessages)) await senderAgent.initialize() diff --git a/tests/e2e-test.ts b/tests/e2e-test.ts index c6e5172743..512a578067 100644 --- a/tests/e2e-test.ts +++ b/tests/e2e-test.ts @@ -1,9 +1,10 @@ import type { Agent } from '@aries-framework/core' +import { sleep } from '../packages/core/src/utils/sleep' import { issueCredential, makeConnection, prepareForIssuance, presentProof } from '../packages/core/tests/helpers' import { - CredentialPreview, + V1CredentialPreview, AttributeFilter, CredentialState, MediationState, @@ -48,7 +49,7 @@ export async function e2eTest({ issuerConnectionId: senderRecipientConnection.id, credentialTemplate: { credentialDefinitionId: definition.id, - preview: CredentialPreview.fromRecord({ + preview: V1CredentialPreview.fromRecord({ name: 'John', age: '25', // year month day @@ -90,4 +91,10 @@ export async function e2eTest({ expect(holderProof.state).toBe(ProofState.Done) expect(verifierProof.state).toBe(ProofState.Done) + + // We want to stop the mediator polling before the agent is shutdown. + // FIXME: add a way to stop mediator polling from the public api, and make sure this is + // being handled in the agent shutdown so we don't get any errors with wallets being closed. + recipientAgent.config.stop$.next(true) + await sleep(2000) } diff --git a/tests/e2e-ws.test.ts b/tests/e2e-ws.test.ts index e28c6d94fd..f8452eb484 100644 --- a/tests/e2e-ws.test.ts +++ b/tests/e2e-ws.test.ts @@ -2,11 +2,12 @@ import { getBaseConfig } from '../packages/core/tests/helpers' import { e2eTest } from './e2e-test' -import { Agent, WsOutboundTransport, AutoAcceptCredential } from '@aries-framework/core' +import { Agent, WsOutboundTransport, AutoAcceptCredential, MediatorPickupStrategy } from '@aries-framework/core' import { WsInboundTransport } from '@aries-framework/node' const recipientConfig = getBaseConfig('E2E WS Recipient ', { autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }) const mediatorPort = 4000 @@ -20,6 +21,7 @@ const senderConfig = getBaseConfig('E2E WS Sender', { endpoints: [`ws://localhost:${senderPort}`], mediatorPollingInterval: 1000, autoAcceptCredentials: AutoAcceptCredential.ContentApproved, + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, }) describe('E2E WS tests', () => { @@ -34,9 +36,12 @@ describe('E2E WS tests', () => { }) afterEach(async () => { - await recipientAgent.shutdown({ deleteWallet: true }) - await mediatorAgent.shutdown({ deleteWallet: true }) - await senderAgent.shutdown({ deleteWallet: true }) + await recipientAgent.shutdown() + await recipientAgent.wallet.delete() + await mediatorAgent.shutdown() + await mediatorAgent.wallet.delete() + await senderAgent.shutdown() + await senderAgent.wallet.delete() }) test('Full WS flow (connect, request mediation, issue, verify)', async () => { diff --git a/tests/transport/SubjectInboundTransport.ts b/tests/transport/SubjectInboundTransport.ts index 7d325a3eef..10c978654d 100644 --- a/tests/transport/SubjectInboundTransport.ts +++ b/tests/transport/SubjectInboundTransport.ts @@ -1,19 +1,20 @@ import type { InboundTransport, Agent } from '../../packages/core/src' import type { TransportSession } from '../../packages/core/src/agent/TransportService' -import type { WireMessage } from '../../packages/core/src/types' +import type { EncryptedMessage } from '../../packages/core/src/types' import type { Subject, Subscription } from 'rxjs' import { AgentConfig } from '../../packages/core/src/agent/AgentConfig' +import { TransportService } from '../../packages/core/src/agent/TransportService' import { uuid } from '../../packages/core/src/utils/uuid' -export type SubjectMessage = { message: WireMessage; replySubject?: Subject } +export type SubjectMessage = { message: EncryptedMessage; replySubject?: Subject } export class SubjectInboundTransport implements InboundTransport { - private subject: Subject + private ourSubject: Subject private subscription?: Subscription - public constructor(subject: Subject) { - this.subject = subject + public constructor(ourSubject: Subject) { + this.ourSubject = ourSubject } public async start(agent: Agent) { @@ -26,14 +27,22 @@ export class SubjectInboundTransport implements InboundTransport { private subscribe(agent: Agent) { const logger = agent.injectionContainer.resolve(AgentConfig).logger + const transportService = agent.injectionContainer.resolve(TransportService) - this.subscription = this.subject.subscribe({ + this.subscription = this.ourSubject.subscribe({ next: async ({ message, replySubject }: SubjectMessage) => { logger.test('Received message') - let session + let session: SubjectTransportSession | undefined if (replySubject) { session = new SubjectTransportSession(`subject-session-${uuid()}`, replySubject) + + // When the subject is completed (e.g. when the session is closed), we need to + // remove the session from the transport service so it won't be used for sending messages + // in the future. + replySubject.subscribe({ + complete: () => session && transportService.removeSession(session), + }) } await agent.receiveMessage(message, session) @@ -52,7 +61,11 @@ export class SubjectTransportSession implements TransportSession { this.replySubject = replySubject } - public async send(wireMessage: WireMessage): Promise { - this.replySubject.next({ message: wireMessage }) + public async send(encryptedMessage: EncryptedMessage): Promise { + this.replySubject.next({ message: encryptedMessage }) + } + + public async close(): Promise { + this.replySubject.complete() } } diff --git a/tests/transport/SubjectOutboundTransport.ts b/tests/transport/SubjectOutboundTransport.ts index 79249aebf1..385fcdc08c 100644 --- a/tests/transport/SubjectOutboundTransport.ts +++ b/tests/transport/SubjectOutboundTransport.ts @@ -1,32 +1,29 @@ -import type { Agent, Logger } from '../../packages/core/src' -import type { OutboundTransport } from '../../packages/core/src/transport/OutboundTransport' -import type { OutboundPackage } from '../../packages/core/src/types' import type { SubjectMessage } from './SubjectInboundTransport' -import type { Subject } from 'rxjs' +import type { OutboundPackage, OutboundTransport, Agent, Logger } from '@aries-framework/core' -import { InjectionSymbols, AriesFrameworkError } from '../../packages/core/src' +import { takeUntil, Subject, take } from 'rxjs' + +import { InjectionSymbols, AriesFrameworkError } from '@aries-framework/core' export class SubjectOutboundTransport implements OutboundTransport { private logger!: Logger - private ourSubject: Subject private subjectMap: { [key: string]: Subject | undefined } + private agent!: Agent public supportedSchemes = ['rxjs'] - public constructor( - ourSubject: Subject, - subjectMap: { [key: string]: Subject | undefined } - ) { - this.ourSubject = ourSubject + public constructor(subjectMap: { [key: string]: Subject | undefined }) { this.subjectMap = subjectMap } public async start(agent: Agent): Promise { + this.agent = agent + this.logger = agent.injectionContainer.resolve(InjectionSymbols.Logger) } public async stop(): Promise { - // Nothing required to stop + // No logic needed } public async sendMessage(outboundPackage: OutboundPackage) { @@ -45,6 +42,19 @@ export class SubjectOutboundTransport implements OutboundTransport { throw new AriesFrameworkError(`No subject found for endpoint ${endpoint}`) } - subject.next({ message: payload, replySubject: this.ourSubject }) + // Create a replySubject just for this session. Both ends will be able to close it, + // mimicking a transport like http or websocket. Close session automatically when agent stops + const replySubject = new Subject() + this.agent.config.stop$.pipe(take(1)).subscribe(() => !replySubject.closed && replySubject.complete()) + + replySubject.pipe(takeUntil(this.agent.config.stop$)).subscribe({ + next: async ({ message }: SubjectMessage) => { + this.logger.test('Received message') + + await this.agent.receiveMessage(message) + }, + }) + + subject.next({ message: payload, replySubject }) } } diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 274c5d5063..6daf88f2b0 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -7,6 +7,15 @@ "@aries-framework/*": ["packages/*/src"] } }, - "include": ["packages", "./.eslintrc.js", "./jest.config.ts", "./jest.config.base.ts", "types", "tests", "samples"], + "include": [ + "packages", + "./.eslintrc.js", + "./jest.config.ts", + "./jest.config.base.ts", + "types", + "tests", + "samples", + "demo" + ], "exclude": ["node_modules", "build"] } diff --git a/tsconfig.test.json b/tsconfig.test.json index 7ce2827adb..096b728637 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -10,6 +10,6 @@ }, "types": ["jest", "node"] }, - "include": ["tests", "samples", "packages/core/types/jest.d.ts"], + "include": ["tests", "samples", "demo", "packages/core/types/jest.d.ts"], "exclude": ["node_modules", "build", "**/build/**"] } diff --git a/yarn.lock b/yarn.lock index 2fa13757c5..86098d8c00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,23 @@ # yarn lockfile v1 +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@animo-id/react-native-bbs-signatures@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@animo-id/react-native-bbs-signatures/-/react-native-bbs-signatures-0.1.0.tgz#f62bc16b867c9f690977982d66f0a03566b21ad2" + integrity sha512-7qvsiWhGfUev8ngE8YzF6ON9PtCID5LiYVYM4EC5eyj80gCdhx3R46CI7K1qbqIlGsoTYQ/Xx5Ubo5Ji9eaUEA== + "@azure/core-asynciterator-polyfill@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz#dcccebb88406e5c76e0e1d52e8cc4c43a68b3ee7" - integrity sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg== + version "1.0.2" + resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz#0dd3849fb8d97f062a39db0e5cadc9ffaf861fec" + integrity sha512-3rkP4LnnlWawl0LZptJOdXNrT/fHp2eQMadoasa6afspXdpGrtPZuAQc2PD0cpgyuoXtUWyC3tv7xfntjGS5Dw== "@babel/code-frame@7.12.11": version "7.12.11" @@ -14,97 +27,98 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" - integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== - dependencies: - "@babel/highlight" "^7.14.5" - -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.5", "@babel/compat-data@^7.14.7": - version "7.14.7" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.7.tgz#7b047d7a3a89a67d2258dc61f604f098f1bc7e08" - integrity sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw== - -"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.1.6", "@babel/core@^7.7.2", "@babel/core@^7.7.5": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.8.tgz#20cdf7c84b5d86d83fac8710a8bc605a7ba3f010" - integrity sha512-/AtaeEhT6ErpDhInbXmjHcUQXH0L0TEgscfcxk1qbOvLuKCa5aZT0SOOtDKFY96/CLROwbLSKyFor6idgNaU4Q== - dependencies: - "@babel/code-frame" "^7.14.5" - "@babel/generator" "^7.14.8" - "@babel/helper-compilation-targets" "^7.14.5" - "@babel/helper-module-transforms" "^7.14.8" - "@babel/helpers" "^7.14.8" - "@babel/parser" "^7.14.8" - "@babel/template" "^7.14.5" - "@babel/traverse" "^7.14.8" - "@babel/types" "^7.14.8" +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" + integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw== + +"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.1.6", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.12.tgz#b4eb2d7ebc3449b062381644c93050db545b70ee" + integrity sha512-44ODe6O1IVz9s2oJE3rZ4trNNKTX9O7KpQpfAP4t8QII/zwrVRHL7i2pxhqtcY7tqMLrrKfMlBKnm1QlrRFs5w== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.12" + "@babel/helper-compilation-targets" "^7.17.10" + "@babel/helper-module-transforms" "^7.17.12" + "@babel/helpers" "^7.17.9" + "@babel/parser" "^7.17.12" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.12" + "@babel/types" "^7.17.12" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.1.2" + json5 "^2.2.1" semver "^6.3.0" - source-map "^0.5.0" -"@babel/generator@^7.14.8", "@babel/generator@^7.5.0", "@babel/generator@^7.7.2": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.8.tgz#bf86fd6af96cf3b74395a8ca409515f89423e070" - integrity sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg== +"@babel/generator@^7.17.12", "@babel/generator@^7.5.0", "@babel/generator@^7.7.2": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.12.tgz#5970e6160e9be0428e02f4aba62d8551ec366cc8" + integrity sha512-V49KtZiiiLjH/CnIW6OjJdrenrGoyh6AmKQ3k2AZFKozC1h846Q4NYlZ5nqAigPDUXfGzC88+LOUuG8yKd2kCw== dependencies: - "@babel/types" "^7.14.8" + "@babel/types" "^7.17.12" + "@jridgewell/gen-mapping" "^0.3.0" jsesc "^2.5.1" - source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61" - integrity sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA== +"@babel/helper-annotate-as-pure@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" + integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== dependencies: - "@babel/types" "^7.14.5" + "@babel/types" "^7.16.7" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz#b939b43f8c37765443a19ae74ad8b15978e0a191" - integrity sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" + integrity sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA== dependencies: - "@babel/helper-explode-assignable-expression" "^7.14.5" - "@babel/types" "^7.14.5" + "@babel/helper-explode-assignable-expression" "^7.16.7" + "@babel/types" "^7.16.7" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz#7a99c5d0967911e972fe2c3411f7d5b498498ecf" - integrity sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw== +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz#09c63106d47af93cf31803db6bc49fef354e2ebe" + integrity sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ== dependencies: - "@babel/compat-data" "^7.14.5" - "@babel/helper-validator-option" "^7.14.5" - browserslist "^4.16.6" + "@babel/compat-data" "^7.17.10" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.20.2" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.14.5", "@babel/helper-create-class-features-plugin@^7.14.6": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.8.tgz#a6f8c3de208b1e5629424a9a63567f56501955fc" - integrity sha512-bpYvH8zJBWzeqi1o+co8qOrw+EXzQ/0c74gVmY205AWXy9nifHrOg77y+1zwxX5lXE7Icq4sPlSQ4O2kWBrteQ== +"@babel/helper-create-class-features-plugin@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.12.tgz#d4f8393fc4838cbff6b7c199af5229aee16d07cf" + integrity sha512-sZoOeUTkFJMyhqCei2+Z+wtH/BehW8NVKQt7IRUQlRiOARuXymJYfN/FCcI8CvVbR0XVyDM6eLFOlR7YtiXnew== dependencies: - "@babel/helper-annotate-as-pure" "^7.14.5" - "@babel/helper-function-name" "^7.14.5" - "@babel/helper-member-expression-to-functions" "^7.14.7" - "@babel/helper-optimise-call-expression" "^7.14.5" - "@babel/helper-replace-supers" "^7.14.5" - "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-member-expression-to-functions" "^7.17.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" -"@babel/helper-create-regexp-features-plugin@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz#c7d5ac5e9cf621c26057722fb7a8a4c5889358c4" - integrity sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A== +"@babel/helper-create-regexp-features-plugin@^7.16.7": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.12.tgz#bb37ca467f9694bbe55b884ae7a5cc1e0084e4fd" + integrity sha512-b2aZrV4zvutr9AIa6/gA3wsZKRwTKYoDxYiFKcESS3Ug2GTXzwBEvMuuFLhCQpEnRXs1zng4ISAXSUxxKBIcxw== dependencies: - "@babel/helper-annotate-as-pure" "^7.14.5" - regexpu-core "^4.7.1" + "@babel/helper-annotate-as-pure" "^7.16.7" + regexpu-core "^5.0.1" -"@babel/helper-define-polyfill-provider@^0.2.2": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz#0525edec5094653a282688d34d846e4c75e9c0b6" - integrity sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew== +"@babel/helper-define-polyfill-provider@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665" + integrity sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA== dependencies: "@babel/helper-compilation-targets" "^7.13.0" "@babel/helper-module-imports" "^7.12.13" @@ -115,190 +129,190 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-explode-assignable-expression@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz#8aa72e708205c7bb643e45c73b4386cdf2a1f645" - integrity sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ== +"@babel/helper-environment-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" + integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== dependencies: - "@babel/types" "^7.14.5" + "@babel/types" "^7.16.7" -"@babel/helper-function-name@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz#89e2c474972f15d8e233b52ee8c480e2cfcd50c4" - integrity sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ== +"@babel/helper-explode-assignable-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" + integrity sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ== dependencies: - "@babel/helper-get-function-arity" "^7.14.5" - "@babel/template" "^7.14.5" - "@babel/types" "^7.14.5" + "@babel/types" "^7.16.7" -"@babel/helper-get-function-arity@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz#25fbfa579b0937eee1f3b805ece4ce398c431815" - integrity sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg== - dependencies: - "@babel/types" "^7.14.5" +"@babel/helper-function-name@^7.16.7", "@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" -"@babel/helper-hoist-variables@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz#e0dd27c33a78e577d7c8884916a3e7ef1f7c7f8d" - integrity sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ== +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== dependencies: - "@babel/types" "^7.14.5" + "@babel/types" "^7.16.7" -"@babel/helper-member-expression-to-functions@^7.14.5", "@babel/helper-member-expression-to-functions@^7.14.7": - version "7.14.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz#97e56244beb94211fe277bd818e3a329c66f7970" - integrity sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA== +"@babel/helper-member-expression-to-functions@^7.16.7", "@babel/helper-member-expression-to-functions@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4" + integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw== dependencies: - "@babel/types" "^7.14.5" + "@babel/types" "^7.17.0" -"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3" - integrity sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ== - dependencies: - "@babel/types" "^7.14.5" - -"@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.14.8": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.8.tgz#d4279f7e3fd5f4d5d342d833af36d4dd87d7dc49" - integrity sha512-RyE+NFOjXn5A9YU1dkpeBaduagTlZ0+fccnIcAGbv1KGUlReBj7utF7oEth8IdIBQPcux0DDgW5MFBH2xu9KcA== - dependencies: - "@babel/helper-module-imports" "^7.14.5" - "@babel/helper-replace-supers" "^7.14.5" - "@babel/helper-simple-access" "^7.14.8" - "@babel/helper-split-export-declaration" "^7.14.5" - "@babel/helper-validator-identifier" "^7.14.8" - "@babel/template" "^7.14.5" - "@babel/traverse" "^7.14.8" - "@babel/types" "^7.14.8" - -"@babel/helper-optimise-call-expression@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c" - integrity sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA== +"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== dependencies: - "@babel/types" "^7.14.5" + "@babel/types" "^7.16.7" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== - -"@babel/helper-replace-supers@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz#0ecc0b03c41cd567b4024ea016134c28414abb94" - integrity sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow== +"@babel/helper-module-transforms@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.12.tgz#bec00139520cb3feb078ef7a4578562480efb77e" + integrity sha512-t5s2BeSWIghhFRPh9XMn6EIGmvn8Lmw5RVASJzkIx1mSemubQQBNIZiQD7WzaFmaHIrjAec4x8z9Yx8SjJ1/LA== dependencies: - "@babel/helper-member-expression-to-functions" "^7.14.5" - "@babel/helper-optimise-call-expression" "^7.14.5" - "@babel/traverse" "^7.14.5" - "@babel/types" "^7.14.5" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.12" + "@babel/types" "^7.17.12" -"@babel/helper-simple-access@^7.14.5", "@babel/helper-simple-access@^7.14.8": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924" - integrity sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg== +"@babel/helper-optimise-call-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2" + integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w== dependencies: - "@babel/types" "^7.14.8" + "@babel/types" "^7.16.7" -"@babel/helper-skip-transparent-expression-wrappers@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz#96f486ac050ca9f44b009fbe5b7d394cab3a0ee4" - integrity sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ== - dependencies: - "@babel/types" "^7.14.5" +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.17.12", "@babel/helper-plugin-utils@^7.8.0": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96" + integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA== -"@babel/helper-split-export-declaration@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a" - integrity sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA== +"@babel/helper-replace-supers@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz#e9f5f5f32ac90429c1a4bdec0f231ef0c2838ab1" + integrity sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw== dependencies: - "@babel/types" "^7.14.5" - -"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.8": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz#32be33a756f29e278a0d644fa08a2c9e0f88a34c" - integrity sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow== + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" -"@babel/helper-validator-option@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" - integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== +"@babel/helper-simple-access@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" + integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== + dependencies: + "@babel/types" "^7.17.0" -"@babel/helpers@^7.14.8": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.8.tgz#839f88f463025886cff7f85a35297007e2da1b77" - integrity sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw== +"@babel/helper-skip-transparent-expression-wrappers@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" + integrity sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw== dependencies: - "@babel/template" "^7.14.5" - "@babel/traverse" "^7.14.8" - "@babel/types" "^7.14.8" + "@babel/types" "^7.16.0" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" - integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== dependencies: - "@babel/helper-validator-identifier" "^7.14.5" + "@babel/types" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helpers@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.9.tgz#b2af120821bfbe44f9907b1826e168e819375a1a" + integrity sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.9" + "@babel/types" "^7.17.0" + +"@babel/highlight@^7.10.4", "@babel/highlight@^7.16.7": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.12.tgz#257de56ee5afbd20451ac0a75686b6b404257351" + integrity sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.1.6", "@babel/parser@^7.14.5", "@babel/parser@^7.14.8", "@babel/parser@^7.7.2": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.8.tgz#66fd41666b2d7b840bd5ace7f7416d5ac60208d4" - integrity sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.1.6", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.12.tgz#36c2ed06944e3691ba82735fc4cf62d12d491a23" + integrity sha512-FLzHmN9V3AJIrWfOpvRlZCeVg/WLdicSnTMsLur6uDj9TT8ymUlG9XxURdW/XvuygK+2CW0poOJABdA4m/YKxA== "@babel/plugin-proposal-class-properties@^7.0.0", "@babel/plugin-proposal-class-properties@^7.1.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz#40d1ee140c5b1e31a350f4f5eed945096559b42e" - integrity sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.17.12.tgz#84f65c0cc247d46f40a6da99aadd6438315d80a4" + integrity sha512-U0mI9q8pW5Q9EaTHFPwSVusPMV/DV9Mm8p7csqROFLtIE9rBF5piLqyrBGigftALrBcsBGu4m38JneAe7ZDLXw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.17.12" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-proposal-export-default-from@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.14.5.tgz#8931a6560632c650f92a8e5948f6e73019d6d321" - integrity sha512-T8KZ5abXvKMjF6JcoXjgac3ElmXf0AWzJwi2O/42Jk+HmCky3D9+i1B7NPP1FblyceqTevKeV/9szeikFoaMDg== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.17.12.tgz#df785e638618d8ffa14e08c78c44d9695d083b73" + integrity sha512-LpsTRw725eBAXXKUOnJJct+SEaOzwR78zahcLuripD2+dKc2Sj+8Q2DzA+GC/jOpOu/KlDXuxrzG214o1zTauQ== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-export-default-from" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/plugin-syntax-export-default-from" "^7.16.7" "@babel/plugin-proposal-nullish-coalescing-operator@^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.1.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz#ee38589ce00e2cc59b299ec3ea406fcd3a0fdaf6" - integrity sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.17.12.tgz#1e93079bbc2cbc756f6db6a1925157c4a92b94be" + integrity sha512-ws/g3FSGVzv+VH86+QvgtuJL/kR67xaEIF2x0iPqdDfYW6ra6JF3lKVBkWynRLcNtIC1oCTfDRVxmm2mKzy+ag== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" "@babel/plugin-proposal-object-rest-spread@^7.0.0": - version "7.14.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz#5920a2b3df7f7901df0205974c0641b13fd9d363" - integrity sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.12.tgz#f94a91715a7f2f8cfb3c06af820c776440bc0148" + integrity sha512-6l9cO3YXXRh4yPCPRA776ZyJ3RobG4ZKJZhp7NDRbKIOeV3dBPG8FXCF7ZtiO2RTCIOkQOph1xDDcc01iWVNjQ== dependencies: - "@babel/compat-data" "^7.14.7" - "@babel/helper-compilation-targets" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/compat-data" "^7.17.10" + "@babel/helper-compilation-targets" "^7.17.10" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.14.5" + "@babel/plugin-transform-parameters" "^7.17.12" "@babel/plugin-proposal-optional-catch-binding@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz#939dd6eddeff3a67fdf7b3f044b5347262598c3c" - integrity sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz#c623a430674ffc4ab732fd0a0ae7722b67cb74cf" + integrity sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-proposal-optional-chaining@^7.0.0", "@babel/plugin-proposal-optional-chaining@^7.1.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz#fa83651e60a360e3f13797eef00b8d519695b603" - integrity sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.17.12.tgz#f96949e9bacace3a9066323a5cf90cfb9de67174" + integrity sha512-7wigcOs/Z4YWlK7xxjkvaIw84vGhDv/P1dFGQap0nHkc8gFKY/r+hXc8Qzf5k1gY7CvGIcHqAnOagVKJJ1wVOQ== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-async-generators@^7.8.4": @@ -329,19 +343,19 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-export-default-from@^7.0.0", "@babel/plugin-syntax-export-default-from@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.14.5.tgz#cdfa9d43d2b2c89b6f1af3e83518e8c8b9ed0dbc" - integrity sha512-snWDxjuaPEobRBnhpqEfZ8RMxDbHt8+87fiEioGuE+Uc0xAKgSD8QiuL3lF93hPVQfZFAcYwrrf+H5qUhike3Q== +"@babel/plugin-syntax-export-default-from@^7.0.0", "@babel/plugin-syntax-export-default-from@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.16.7.tgz#fa89cf13b60de2c3f79acdc2b52a21174c6de060" + integrity sha512-4C3E4NsrLOgftKaTYTULhHsuQrGv3FHrBzOMDiS7UYKIpgGBkAdawg4h+EI8zPeK9M0fiIIh72hIwsI24K7MbA== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.14.5", "@babel/plugin-syntax-flow@^7.2.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.14.5.tgz#2ff654999497d7d7d142493260005263731da180" - integrity sha512-9WK5ZwKCdWHxVuU13XNT6X73FGmutAXeor5lGFq6qhOFtMFUF4jkbijuyUdZZlpYq6E2hZeZf/u3959X9wsv0Q== +"@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.17.12", "@babel/plugin-syntax-flow@^7.2.0": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.17.12.tgz#23d852902acd19f42923fca9d0f196984d124e73" + integrity sha512-B8QIgBvkIG6G2jgsOHQUist7Sm0EBLDCx8sen072IwqNuzMegZNXrYnSv77cYzA8mLDZAfQYqsLIhimiP1s2HQ== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" @@ -357,12 +371,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz#000e2e25d8673cce49300517a3eda44c263e4201" - integrity sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw== +"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.17.12.tgz#834035b45061983a491f60096f61a2e7c5674a47" + integrity sha512-spyY3E3AURfxh/RHtjx5j6hs8am5NbUBGfcZ2vB3uShSpZdQyXSf5rR5Mk76vbtlAZOelyVQ71Fg0x9SG4fsog== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" @@ -413,308 +427,311 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.14.5", "@babel/plugin-syntax-typescript@^7.7.2": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz#b82c6ce471b165b5ce420cf92914d6fb46225716" - integrity sha512-u6OXzDaIXjEstBRRoBCQ/uKQKlbuaeE5in0RvWdA4pN6AhqxTIwUsnHPU1CFZA/amYObMsuWhYfRl3Ch90HD0Q== +"@babel/plugin-syntax-typescript@^7.17.12", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.12.tgz#b54fc3be6de734a56b87508f99d6428b5b605a7b" + integrity sha512-TYY0SXFiO31YXtNg3HtFwNJHjLsAyIIhAhNWkQ5whPPS7HWUFlg9z0Ta4qAQNjQbP1wsSt/oKkmZ/4/WWdMUpw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-transform-arrow-functions@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz#f7187d9588a768dd080bf4c9ffe117ea62f7862a" - integrity sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.17.12.tgz#dddd783b473b1b1537ef46423e3944ff24898c45" + integrity sha512-PHln3CNi/49V+mza4xMwrg+WGYevSF1oaiXaC2EQfdp4HWlSjRsrDXWJiQBKpP7749u6vQ9mcry2uuFOv5CXvA== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-transform-block-scoped-functions@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz#e48641d999d4bc157a67ef336aeb54bc44fd3ad4" - integrity sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz#4d0d57d9632ef6062cdf354bb717102ee042a620" + integrity sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-block-scoping@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.5.tgz#8cc63e61e50f42e078e6f09be775a75f23ef9939" - integrity sha512-LBYm4ZocNgoCqyxMLoOnwpsmQ18HWTQvql64t3GvMUzLQrNoV1BDG0lNftC8QKYERkZgCCT/7J5xWGObGAyHDw== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.17.12.tgz#68fc3c4b3bb7dfd809d97b7ed19a584052a2725c" + integrity sha512-jw8XW/B1i7Lqwqj2CbrViPcZijSxfguBWZP2aN59NHgxUyO/OcO1mfdCxH13QhN5LbWhPkX+f+brKGhZTiqtZQ== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-transform-classes@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.5.tgz#0e98e82097b38550b03b483f9b51a78de0acb2cf" - integrity sha512-J4VxKAMykM06K/64z9rwiL6xnBHgB1+FVspqvlgCdwD1KUbQNfszeKVVOMh59w3sztHYIZDgnhOC4WbdEfHFDA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.14.5" - "@babel/helper-function-name" "^7.14.5" - "@babel/helper-optimise-call-expression" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-replace-supers" "^7.14.5" - "@babel/helper-split-export-declaration" "^7.14.5" + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.17.12.tgz#da889e89a4d38375eeb24985218edeab93af4f29" + integrity sha512-cvO7lc7pZat6BsvH6l/EGaI8zpl8paICaoGk+7x7guvtfak/TbIf66nYmJOH13EuG0H+Xx3M+9LQDtSvZFKXKw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" globals "^11.1.0" "@babel/plugin-transform-computed-properties@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz#1b9d78987420d11223d41195461cc43b974b204f" - integrity sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.17.12.tgz#bca616a83679698f3258e892ed422546e531387f" + integrity sha512-a7XINeplB5cQUWMg1E/GI1tFz3LfK021IjV1rj1ypE+R7jHm+pIHmHl25VNkZxtx9uuYp7ThGk8fur1HHG7PgQ== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-transform-destructuring@^7.0.0": - version "7.14.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz#0ad58ed37e23e22084d109f185260835e5557576" - integrity sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.12.tgz#0861d61e75e2401aca30f2570d46dfc85caacf35" + integrity sha512-P8pt0YiKtX5UMUL5Xzsc9Oyij+pJE6JuC+F1k0/brq/OOGs5jDa1If3OY0LRWGvJsJhI+8tsiecL3nJLc0WTlg== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-transform-exponentiation-operator@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz#5154b8dd6a3dfe6d90923d61724bd3deeb90b493" - integrity sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz#efa9862ef97e9e9e5f653f6ddc7b665e8536fe9b" + integrity sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.14.5.tgz#0dc9c1d11dcdc873417903d6df4bed019ef0f85e" - integrity sha512-KhcolBKfXbvjwI3TV7r7TkYm8oNXHNBqGOy6JDVwtecFaRoKYsUUqJdS10q0YDKW1c6aZQgO+Ys3LfGkox8pXA== +"@babel/plugin-transform-flow-strip-types@^7.0.0", "@babel/plugin-transform-flow-strip-types@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.17.12.tgz#5e070f99a4152194bd9275de140e83a92966cab3" + integrity sha512-g8cSNt+cHCpG/uunPQELdq/TeV3eg1OLJYwxypwHtAWo9+nErH3lQx9CSO2uI9lF74A0mR0t4KoMjs1snSgnTw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-flow" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/plugin-syntax-flow" "^7.17.12" "@babel/plugin-transform-for-of@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz#dae384613de8f77c196a8869cbf602a44f7fc0eb" - integrity sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.17.12.tgz#5397c22554ec737a27918e7e7e0e7b679b05f5ec" + integrity sha512-76lTwYaCxw8ldT7tNmye4LLwSoKDbRCBzu6n/DcK/P3FOR29+38CIIaVIZfwol9By8W/QHORYEnYSLuvcQKrsg== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-transform-function-name@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz#e81c65ecb900746d7f31802f6bed1f52d915d6f2" - integrity sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz#5ab34375c64d61d083d7d2f05c38d90b97ec65cf" + integrity sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA== dependencies: - "@babel/helper-function-name" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-literals@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz#41d06c7ff5d4d09e3cf4587bd3ecf3930c730f78" - integrity sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.17.12.tgz#97131fbc6bbb261487105b4b3edbf9ebf9c830ae" + integrity sha512-8iRkvaTjJciWycPIZ9k9duu663FT7VrBdNqNgxnVXEFwOIp55JWcZd23VBRySYbnS3PwQ3rGiabJBBBGj5APmQ== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-transform-member-expression-literals@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz#b39cd5212a2bf235a617d320ec2b48bcc091b8a7" - integrity sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz#6e5dcf906ef8a098e630149d14c867dd28f92384" + integrity sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.1.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.5.tgz#7aaee0ea98283de94da98b28f8c35701429dad97" - integrity sha512-en8GfBtgnydoao2PS+87mKyw62k02k7kJ9ltbKe0fXTHrQmG6QZZflYuGI1VVG7sVpx4E1n7KBpNlPb8m78J+A== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.12.tgz#37691c7404320d007288edd5a2d8600bcef61c34" + integrity sha512-tVPs6MImAJz+DiX8Y1xXEMdTk5Lwxu9jiPjlS+nv5M2A59R7+/d1+9A8C/sbuY0b3QjIxqClkj6KAplEtRvzaA== dependencies: - "@babel/helper-module-transforms" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-simple-access" "^7.14.5" + "@babel/helper-module-transforms" "^7.17.12" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/helper-simple-access" "^7.17.7" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-object-assign@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.14.5.tgz#62537d54b6d85de04f4df48bfdba2eebff17b760" - integrity sha512-lvhjk4UN9xJJYB1mI5KC0/o1D5EcJXdbhVe+4fSk08D6ZN+iuAIs7LJC+71h8av9Ew4+uRq9452v9R93SFmQlQ== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.16.7.tgz#5fe08d63dccfeb6a33aa2638faf98e5c584100f8" + integrity sha512-R8mawvm3x0COTJtveuoqZIjNypn2FjfvXZr4pSQ8VhEFBuQGBz4XhHasZtHXjgXU4XptZ4HtGof3NoYc93ZH9Q== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-object-super@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz#d0b5faeac9e98597a161a9cf78c527ed934cdc45" - integrity sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz#ac359cf8d32cf4354d27a46867999490b6c32a94" + integrity sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-replace-supers" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" -"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz#49662e86a1f3ddccac6363a7dfb1ff0a158afeb3" - integrity sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA== +"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.17.12": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.17.12.tgz#eb467cd9586ff5ff115a9880d6fdbd4a846b7766" + integrity sha512-6qW4rWo1cyCdq1FkYri7AHpauchbGLXpdwnYsfxFb+KtddHENfsY5JZb35xUwkK5opOLcJ3BNd2l7PhRYGlwIA== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-transform-property-literals@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz#0ddbaa1f83db3606f1cdf4846fa1dfb473458b34" - integrity sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz#2dadac85155436f22c696c4827730e0fe1057a55" + integrity sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-react-display-name@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.14.5.tgz#baa92d15c4570411301a85a74c13534873885b65" - integrity sha512-07aqY1ChoPgIxsuDviptRpVkWCSbXWmzQqcgy65C6YSFOfPFvb/DX3bBRHh7pCd/PMEEYHYWUTSVkCbkVainYQ== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz#7b6d40d232f4c0f550ea348593db3b21e2404340" + integrity sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-react-jsx-self@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.14.5.tgz#703b5d1edccd342179c2a99ee8c7065c2b4403cc" - integrity sha512-M/fmDX6n0cfHK/NLTcPmrfVAORKDhK8tyjDhyxlUjYyPYYO8FRWwuxBA3WBx8kWN/uBUuwGa3s/0+hQ9JIN3Tg== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.17.12.tgz#7f2e9b8c08d6a4204733138d8c29d4dba4bb66c2" + integrity sha512-7S9G2B44EnYOx74mue02t1uD8ckWZ/ee6Uz/qfdzc35uWHX5NgRy9i+iJSb2LFRgMd+QV9zNcStQaazzzZ3n3Q== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-transform-react-jsx-source@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.14.5.tgz#79f728e60e6dbd31a2b860b0bf6c9765918acf1d" - integrity sha512-1TpSDnD9XR/rQ2tzunBVPThF5poaYT9GqP+of8fAtguYuI/dm2RkrMBDemsxtY0XBzvW7nXjYM0hRyKX9QYj7Q== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.16.7.tgz#1879c3f23629d287cc6186a6c683154509ec70c0" + integrity sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-react-jsx@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.5.tgz#39749f0ee1efd8a1bd729152cf5f78f1d247a44a" - integrity sha512-7RylxNeDnxc1OleDm0F5Q/BSL+whYRbOAR+bwgCxIr0L32v7UFh/pz1DLMZideAUxKT6eMoS2zQH6fyODLEi8Q== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.12.tgz#2aa20022709cd6a3f40b45d60603d5f269586dba" + integrity sha512-Lcaw8bxd1DKht3thfD4A12dqo1X16he1Lm8rIv8sTwjAYNInRS1qHa9aJoqvzpscItXvftKDCfaEQzwoVyXpEQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.14.5" - "@babel/helper-module-imports" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-jsx" "^7.14.5" - "@babel/types" "^7.14.5" + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/plugin-syntax-jsx" "^7.17.12" + "@babel/types" "^7.17.12" "@babel/plugin-transform-regenerator@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz#9676fd5707ed28f522727c5b3c0aa8544440b04f" - integrity sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.17.9.tgz#0a33c3a61cf47f45ed3232903683a0afd2d3460c" + integrity sha512-Lc2TfbxR1HOyn/c6b4Y/b6NHoTb67n/IoWLxTu4kC7h4KQnWlhCq2S8Tx0t2SVvv5Uu87Hs+6JEJ5kt2tYGylQ== dependencies: - regenerator-transform "^0.14.2" + regenerator-transform "^0.15.0" "@babel/plugin-transform-runtime@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.5.tgz#30491dad49c6059f8f8fa5ee8896a0089e987523" - integrity sha512-fPMBhh1AV8ZyneiCIA+wYYUH1arzlXR1UMcApjvchDhfKxhy2r2lReJv8uHEyihi4IFIGlr1Pdx7S5fkESDQsg== - dependencies: - "@babel/helper-module-imports" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" - babel-plugin-polyfill-corejs2 "^0.2.2" - babel-plugin-polyfill-corejs3 "^0.2.2" - babel-plugin-polyfill-regenerator "^0.2.2" + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.12.tgz#5dc79735c4038c6f4fc0490f68f2798ce608cadd" + integrity sha512-xsl5MeGjWnmV6Ui9PfILM2+YRpa3GqLOrczPpXV3N2KCgQGU+sU8OfzuMbjkIdfvZEZIm+3y0V7w58sk0SGzlw== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.17.12" + babel-plugin-polyfill-corejs2 "^0.3.0" + babel-plugin-polyfill-corejs3 "^0.5.0" + babel-plugin-polyfill-regenerator "^0.3.0" semver "^6.3.0" "@babel/plugin-transform-shorthand-properties@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz#97f13855f1409338d8cadcbaca670ad79e091a58" - integrity sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a" + integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-spread@^7.0.0": - version "7.14.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz#6bd40e57fe7de94aa904851963b5616652f73144" - integrity sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.17.12.tgz#c112cad3064299f03ea32afed1d659223935d1f5" + integrity sha512-9pgmuQAtFi3lpNUstvG9nGfk9DkrdmWNp9KeKPFmuZCpEnxRzYlS8JgwPjYj+1AWDOSvoGN0H30p1cBOmT/Svg== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" "@babel/plugin-transform-sticky-regex@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz#5b617542675e8b7761294381f3c28c633f40aeb9" - integrity sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz#c84741d4f4a38072b9a1e2e3fd56d359552e8660" + integrity sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-transform-template-literals@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz#a5f2bc233937d8453885dc736bdd8d9ffabf3d93" - integrity sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.17.12.tgz#4aec0a18f39dd86c442e1d077746df003e362c6e" + integrity sha512-kAKJ7DX1dSRa2s7WN1xUAuaQmkTpN+uig4wCKWivVXIObqGbVTUlSavHyfI2iZvz89GFAMGm9p2DBJ4Y1Tp0hw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" -"@babel/plugin-transform-typescript@^7.14.5", "@babel/plugin-transform-typescript@^7.5.0": - version "7.14.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.6.tgz#6e9c2d98da2507ebe0a883b100cde3c7279df36c" - integrity sha512-XlTdBq7Awr4FYIzqhmYY80WN0V0azF74DMPyFqVHBvf81ZUgc4X7ZOpx6O8eLDK6iM5cCQzeyJw0ynTaefixRA== +"@babel/plugin-transform-typescript@^7.17.12", "@babel/plugin-transform-typescript@^7.5.0": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.17.12.tgz#9654587131bc776ff713218d929fa9a2e98ca16d" + integrity sha512-ICbXZqg6hgenjmwciVI/UfqZtExBrZOrS8sLB5mTHGO/j08Io3MmooULBiijWk9JBknjM3CbbtTc/0ZsqLrjXQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.14.6" - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/plugin-syntax-typescript" "^7.14.5" + "@babel/helper-create-class-features-plugin" "^7.17.12" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/plugin-syntax-typescript" "^7.17.12" "@babel/plugin-transform-unicode-regex@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz#4cd09b6c8425dd81255c7ceb3fb1836e7414382e" - integrity sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw== + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz#0f7aa4a501198976e25e82702574c34cfebe9ef2" + integrity sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.14.5" - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" "@babel/preset-flow@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.14.5.tgz#a1810b0780c8b48ab0bece8e7ab8d0d37712751c" - integrity sha512-pP5QEb4qRUSVGzzKx9xqRuHUrM/jEzMqdrZpdMA+oUCRgd5zM1qGr5y5+ZgAL/1tVv1H0dyk5t4SKJntqyiVtg== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.17.12.tgz#664a5df59190260939eee862800a255bef3bd66f" + integrity sha512-7QDz7k4uiaBdu7N89VKjUn807pJRXmdirQu0KyR9LXnQrr5Jt41eIMKTS7ljej+H29erwmMrwq9Io9mJHLI3Lw== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-option" "^7.14.5" - "@babel/plugin-transform-flow-strip-types" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-transform-flow-strip-types" "^7.17.12" "@babel/preset-typescript@^7.1.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.14.5.tgz#aa98de119cf9852b79511f19e7f44a2d379bcce0" - integrity sha512-u4zO6CdbRKbS9TypMqrlGH7sd2TAJppZwn3c/ZRLeO/wGsbddxgbPDUZVNrie3JWYLQ9vpineKlsrWFvO6Pwkw== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.17.12.tgz#40269e0a0084d56fc5731b6c40febe1c9a4a3e8c" + integrity sha512-S1ViF8W2QwAKUGJXxP9NAfNaqGDdEBJKpYkxHf5Yy2C4NPPzXGeR3Lhk7G8xJaaLcFTRfNjVbtbVtm8Gb0mqvg== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - "@babel/helper-validator-option" "^7.14.5" - "@babel/plugin-transform-typescript" "^7.14.5" + "@babel/helper-plugin-utils" "^7.17.12" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-transform-typescript" "^7.17.12" "@babel/register@^7.0.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.14.5.tgz#d0eac615065d9c2f1995842f85d6e56c345f3233" - integrity sha512-TjJpGz/aDjFGWsItRBQMOFTrmTI9tr79CHOK+KIvLeCkbxuOAk2M5QHjvruIMGoo9OuccMh5euplPzc5FjAKGg== + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.17.7.tgz#5eef3e0f4afc07e25e847720e7b987ae33f08d0b" + integrity sha512-fg56SwvXRifootQEDQAu1mKdjh5uthPzdO0N6t358FktfL4XjAVXuH58ULoiW8mesxiOgNIrxiImqEwv0+hRRA== dependencies: clone-deep "^4.0.1" find-cache-dir "^2.0.0" make-dir "^2.1.0" - pirates "^4.0.0" + pirates "^4.0.5" source-map-support "^0.5.16" "@babel/runtime@^7.8.4": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" - integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.0.0", "@babel/template@^7.14.5", "@babel/template@^7.3.3": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" - integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g== - dependencies: - "@babel/code-frame" "^7.14.5" - "@babel/parser" "^7.14.5" - "@babel/types" "^7.14.5" - -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.14.8", "@babel/traverse@^7.7.2": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.8.tgz#c0253f02677c5de1a8ff9df6b0aacbec7da1a8ce" - integrity sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg== - dependencies: - "@babel/code-frame" "^7.14.5" - "@babel/generator" "^7.14.8" - "@babel/helper-function-name" "^7.14.5" - "@babel/helper-hoist-variables" "^7.14.5" - "@babel/helper-split-export-declaration" "^7.14.5" - "@babel/parser" "^7.14.8" - "@babel/types" "^7.14.8" +"@babel/template@^7.0.0", "@babel/template@^7.16.7", "@babel/template@^7.3.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@^7.0.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.17.12", "@babel/traverse@^7.17.9", "@babel/traverse@^7.7.2": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.12.tgz#011874d2abbca0ccf1adbe38f6f7a4ff1747599c" + integrity sha512-zULPs+TbCvOkIFd4FrG53xrpxvCBwLIgo6tO0tJorY7YV2IWFxUfS/lXDJbGgfyYt9ery/Gxj2niwttNnB0gIw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.12" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.12" + "@babel/types" "^7.17.12" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.8.tgz#38109de8fcadc06415fbd9b74df0065d4d41c728" - integrity sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q== +"@babel/types@^7.0.0", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.17.12", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.12.tgz#1210690a516489c0200f355d87619157fbbd69a0" + integrity sha512-rH8i29wcZ6x9xjzI5ILHL/yZkbQnCERdHlogKuIb4PUr7do4iT8DPekrTbBLWTnRQm6U0GYABbTMSzijmEqlAg== dependencies: - "@babel/helper-validator-identifier" "^7.14.8" + "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -730,6 +747,78 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + +"@digitalbazaar/http-client@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-1.2.0.tgz#1ea3661e77000a15bd892a294f20dc6cc5d1c93b" + integrity sha512-W9KQQ5pUJcaR0I4c2HPJC0a7kRbZApIorZgPnEDwMBgj16iQzutGLrCXYaZOmxqVLVNqqlQ4aUJh+HBQZy4W6Q== + dependencies: + esm "^3.2.22" + ky "^0.25.1" + ky-universal "^0.8.2" + +"@digitalbazaar/security-context@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/security-context/-/security-context-1.0.0.tgz#23624692cfadc6d97e1eb787ad38a19635d89297" + integrity sha512-mlj+UmodxTAdMCHGxnGVTRLHcSLyiEOVRiz3J6yiRliJWyrgeXs34wlWjBorDIEMDIjK2JwZrDuFEKO9bS5nKQ== + +"@digitalcredentials/http-client@^1.0.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/http-client/-/http-client-1.2.2.tgz#8b09ab6f1e3aa8878d91d3ca51946ca8265cc92e" + integrity sha512-YOwaE+vUDSwiDhZT0BbXSWVg+bvp1HA1eg/gEc8OCwCOj9Bn9FRQdu8P9Y/fnYqyFCioDwwTRzGxgJLl50baEg== + dependencies: + ky "^0.25.1" + ky-universal "^0.8.2" + +"@digitalcredentials/jsonld-signatures@^9.3.1": + version "9.3.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld-signatures/-/jsonld-signatures-9.3.1.tgz#e00175ab4199c580c9b308effade021da805c695" + integrity sha512-YMh1e1GpTeHDqq2a2Kd+pLcHsMiPeKyE2Zs17NSwqckij7UMRVDQ54S5VQhHvoXZ1mlkpVaI2xtj5M5N6rzylw== + dependencies: + "@digitalbazaar/security-context" "^1.0.0" + "@digitalcredentials/jsonld" "^5.2.1" + fast-text-encoding "^1.0.3" + isomorphic-webcrypto "^2.3.8" + serialize-error "^8.0.1" + +"@digitalcredentials/jsonld@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld/-/jsonld-5.2.1.tgz#60acf587bec8331e86324819fd19692939118775" + integrity sha512-pDiO1liw8xs+J/43qnMZsxyz0VOWOb7Q2yUlBt/tyjq6SlT9xPo+3716tJPbjGPnou2lQRw3H5/I++z+6oQ07w== + dependencies: + "@digitalcredentials/http-client" "^1.0.0" + "@digitalcredentials/rdf-canonize" "^1.0.0" + canonicalize "^1.0.1" + lru-cache "^6.0.0" + +"@digitalcredentials/rdf-canonize@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/rdf-canonize/-/rdf-canonize-1.0.0.tgz#6297d512072004c2be7f280246383a9c4b0877ff" + integrity sha512-z8St0Ex2doecsExCFK1uI4gJC+a5EqYYu1xpRH1pKmqSS9l/nxfuVxexNFyaeEum4dUdg1EetIC2rTwLIFhPRA== + dependencies: + fast-text-encoding "^1.0.3" + isomorphic-webcrypto "^2.3.8" + +"@digitalcredentials/vc@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc/-/vc-1.1.2.tgz#868a56962f5137c29eb51eea1ba60251ebf69ad1" + integrity sha512-TSgny9XUh+W7uFjdcpvZzN7I35F9YMTv6jVINXr7UaLNgrinIjy6A5RMGQH9ecpcaoLMemKB5XjtLOOOQ3vknQ== + dependencies: + "@digitalcredentials/jsonld" "^5.2.1" + "@digitalcredentials/jsonld-signatures" "^9.3.1" + credentials-context "^2.0.0" + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -745,10 +834,15 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + "@hapi/hoek@^9.0.0": - version "9.2.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" - integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== "@hapi/topo@^5.0.0": version "5.1.0" @@ -767,9 +861,9 @@ minimatch "^3.0.4" "@humanwhocodes/object-schema@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" - integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@hutson/parse-repository-url@^3.0.0": version "3.0.2" @@ -792,49 +886,48 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.0.6.tgz#3eb72ea80897495c3d73dd97aab7f26770e2260f" - integrity sha512-fMlIBocSHPZ3JxgWiDNW/KPj6s+YRd0hicb33IrmelCcjXo/pXPwvuiKFmZz+XuqI/1u7nbUK10zSsWL/1aegg== +"@jest/console@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" + integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== dependencies: - "@jest/types" "^27.0.6" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^27.0.6" - jest-util "^27.0.6" + jest-message-util "^27.5.1" + jest-util "^27.5.1" slash "^3.0.0" -"@jest/core@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.0.6.tgz#c5f642727a0b3bf0f37c4b46c675372d0978d4a1" - integrity sha512-SsYBm3yhqOn5ZLJCtccaBcvD/ccTLCeuDv8U41WJH/V1MW5eKUkeMHT9U+Pw/v1m1AIWlnIW/eM2XzQr0rEmow== +"@jest/core@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" + integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== dependencies: - "@jest/console" "^27.0.6" - "@jest/reporters" "^27.0.6" - "@jest/test-result" "^27.0.6" - "@jest/transform" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/console" "^27.5.1" + "@jest/reporters" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.8.1" exit "^0.1.2" - graceful-fs "^4.2.4" - jest-changed-files "^27.0.6" - jest-config "^27.0.6" - jest-haste-map "^27.0.6" - jest-message-util "^27.0.6" - jest-regex-util "^27.0.6" - jest-resolve "^27.0.6" - jest-resolve-dependencies "^27.0.6" - jest-runner "^27.0.6" - jest-runtime "^27.0.6" - jest-snapshot "^27.0.6" - jest-util "^27.0.6" - jest-validate "^27.0.6" - jest-watcher "^27.0.6" + graceful-fs "^4.2.9" + jest-changed-files "^27.5.1" + jest-config "^27.5.1" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-resolve-dependencies "^27.5.1" + jest-runner "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + jest-watcher "^27.5.1" micromatch "^4.0.4" - p-each-series "^2.1.0" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" @@ -846,113 +939,114 @@ dependencies: "@jest/types" "^26.6.2" -"@jest/environment@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.0.6.tgz#ee293fe996db01d7d663b8108fa0e1ff436219d2" - integrity sha512-4XywtdhwZwCpPJ/qfAkqExRsERW+UaoSRStSHCCiQTUpoYdLukj+YJbQSFrZjhlUDRZeNiU9SFH0u7iNimdiIg== +"@jest/environment@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" + integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== dependencies: - "@jest/fake-timers" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^27.0.6" + jest-mock "^27.5.1" -"@jest/fake-timers@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.0.6.tgz#cbad52f3fe6abe30e7acb8cd5fa3466b9588e3df" - integrity sha512-sqd+xTWtZ94l3yWDKnRTdvTeZ+A/V7SSKrxsrOKSqdyddb9CeNRF8fbhAU0D7ZJBpTTW2nbp6MftmKJDZfW2LQ== +"@jest/fake-timers@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" + integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== dependencies: - "@jest/types" "^27.0.6" - "@sinonjs/fake-timers" "^7.0.2" + "@jest/types" "^27.5.1" + "@sinonjs/fake-timers" "^8.0.1" "@types/node" "*" - jest-message-util "^27.0.6" - jest-mock "^27.0.6" - jest-util "^27.0.6" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" -"@jest/globals@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.0.6.tgz#48e3903f99a4650673d8657334d13c9caf0e8f82" - integrity sha512-DdTGCP606rh9bjkdQ7VvChV18iS7q0IMJVP1piwTWyWskol4iqcVwthZmoJEf7obE1nc34OpIyoVGPeqLC+ryw== +"@jest/globals@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" + integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== dependencies: - "@jest/environment" "^27.0.6" - "@jest/types" "^27.0.6" - expect "^27.0.6" + "@jest/environment" "^27.5.1" + "@jest/types" "^27.5.1" + expect "^27.5.1" -"@jest/reporters@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.0.6.tgz#91e7f2d98c002ad5df94d5b5167c1eb0b9fd5b00" - integrity sha512-TIkBt09Cb2gptji3yJXb3EE+eVltW6BjO7frO7NEfjI9vSIYoISi5R3aI3KpEDXlB1xwB+97NXIqz84qYeYsfA== +"@jest/reporters@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" + integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.0.6" - "@jest/test-result" "^27.0.6" - "@jest/transform" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/console" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.2" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.3" + istanbul-lib-instrument "^5.1.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - jest-haste-map "^27.0.6" - jest-resolve "^27.0.6" - jest-util "^27.0.6" - jest-worker "^27.0.6" + istanbul-reports "^3.1.3" + jest-haste-map "^27.5.1" + jest-resolve "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^8.0.0" + v8-to-istanbul "^8.1.0" -"@jest/source-map@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.0.6.tgz#be9e9b93565d49b0548b86e232092491fb60551f" - integrity sha512-Fek4mi5KQrqmlY07T23JRi0e7Z9bXTOOD86V/uS0EIW4PClvPDqZOyFlLpNJheS6QI0FNX1CgmPjtJ4EA/2M+g== +"@jest/source-map@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" + integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== dependencies: callsites "^3.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" source-map "^0.6.0" -"@jest/test-result@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.0.6.tgz#3fa42015a14e4fdede6acd042ce98c7f36627051" - integrity sha512-ja/pBOMTufjX4JLEauLxE3LQBPaI2YjGFtXexRAjt1I/MbfNlMx0sytSX3tn5hSLzQsR3Qy2rd0hc1BWojtj9w== +"@jest/test-result@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" + integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== dependencies: - "@jest/console" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/console" "^27.5.1" + "@jest/types" "^27.5.1" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.0.6.tgz#80a913ed7a1130545b1cd777ff2735dd3af5d34b" - integrity sha512-bISzNIApazYOlTHDum9PwW22NOyDa6VI31n6JucpjTVM0jD6JDgqEZ9+yn575nDdPF0+4csYDxNNW13NvFQGZA== +"@jest/test-sequencer@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" + integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== dependencies: - "@jest/test-result" "^27.0.6" - graceful-fs "^4.2.4" - jest-haste-map "^27.0.6" - jest-runtime "^27.0.6" + "@jest/test-result" "^27.5.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-runtime "^27.5.1" -"@jest/transform@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.0.6.tgz#189ad7107413208f7600f4719f81dd2f7278cc95" - integrity sha512-rj5Dw+mtIcntAUnMlW/Vju5mr73u8yg+irnHwzgtgoeI6cCPOvUwQ0D1uQtc/APmWgvRweEb1g05pkUpxH3iCA== +"@jest/transform@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^27.0.6" - babel-plugin-istanbul "^6.0.0" + "@jest/types" "^27.5.1" + babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^27.0.6" - jest-regex-util "^27.0.6" - jest-util "^27.0.6" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-regex-util "^27.5.1" + jest-util "^27.5.1" micromatch "^4.0.4" - pirates "^4.0.1" + pirates "^4.0.4" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" @@ -968,10 +1062,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@jest/types@^27.0.6": - version "27.0.6" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.0.6.tgz#9a992bc517e0c49f035938b8549719c2de40706b" - integrity sha512-aSquT1qa9Pik26JK5/3rvnYb4bGtm1VFNesHKmNTwmPIgOrixvhL2ghIvFRNEpzy3gU+rUgjIF/KodbkFAl++g== +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -979,6 +1073,46 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" + integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" + integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== + +"@jridgewell/set-array@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" + integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.13" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" + integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" + integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@lerna/add@4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f" @@ -1650,6 +1784,32 @@ npmlog "^4.1.2" write-file-atomic "^3.0.3" +"@mattrglobal/bbs-signatures@1.0.0", "@mattrglobal/bbs-signatures@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@mattrglobal/bbs-signatures/-/bbs-signatures-1.0.0.tgz#8ff272c6d201aadab7e08bd84dbfd6e0d48ba12d" + integrity sha512-FFzybdKqSCrS/e7pl5s6Tl/m/x8ZD5EMBbcTBQaqSOms/lebm91lFukYOIe2qc0a5o+gLhtRKye8OfKwD1Ex/g== + dependencies: + "@stablelib/random" "1.0.0" + optionalDependencies: + "@mattrglobal/node-bbs-signatures" "0.13.0" + +"@mattrglobal/bls12381-key-pair@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@mattrglobal/bls12381-key-pair/-/bls12381-key-pair-1.0.0.tgz#2959f8663595de0209bebe88517235ae34f1e2b1" + integrity sha512-FbvSkoy1n3t5FHtAPj8cyQJL7Bz+hvvmquCBZW2+bOBBBT26JhGtr//s6EmXE9e4EZk7bAA1yMHI6i1Ky2us0Q== + dependencies: + "@mattrglobal/bbs-signatures" "1.0.0" + bs58 "4.0.1" + rfc4648 "1.4.0" + +"@mattrglobal/node-bbs-signatures@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@mattrglobal/node-bbs-signatures/-/node-bbs-signatures-0.13.0.tgz#3e431b915325d4b139706f8b26fd84b27c192a29" + integrity sha512-S2wOwDCQYxdjSEjVfcbP3bTq4ZMKeRw/wvBhWRff8CEwuH5u3Qiul+azwDGSesvve1DDceaEhXWiGkXeZTojfQ== + dependencies: + neon-cli "0.8.2" + node-pre-gyp "0.17.0" + "@multiformats/base-x@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" @@ -1677,9 +1837,17 @@ fastq "^1.6.0" "@npmcli/ci-detect@^1.0.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a" - integrity sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q== + version "1.4.0" + resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.4.0.tgz#18478bbaa900c37bfbd8a2006a6262c62e8b0fe1" + integrity sha512-3BGrt6FLjqM6br5AhWRKTr3u5GIVkjRYeAFrMp3HjnfICrg4xOrVRwFavKT6tsp++bq5dluL5t8ME/Nha/6c1Q== + +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" "@npmcli/git@^2.1.0": version "2.1.0" @@ -1712,9 +1880,9 @@ rimraf "^3.0.2" "@npmcli/node-gyp@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-1.0.2.tgz#3cdc1f30e9736dbc417373ed803b42b1a0a29ede" - integrity sha512-yrJUe6reVMpktcvagumoqD9r08fH1iRo01gn1u0zoCApa9lnZGEigVKUd2hzsCId4gdtkZZIVscLhNxMECKgRg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz#a912e637418ffc5f2db375e93b85837691a43a33" + integrity sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA== "@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": version "1.3.2" @@ -1724,31 +1892,30 @@ infer-owner "^1.0.4" "@npmcli/run-script@^1.8.2": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.5.tgz#f250a0c5e1a08a792d775a315d0ff42fc3a51e1d" - integrity sha512-NQspusBCpTjNwNRFMtz2C5MxoxyzlbuJ4YEhxAKrIonTiirKDtatsZictx9RgamQIx6+QuHMNmPl0wQdoESs9A== + version "1.8.6" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.6.tgz#18314802a6660b0d4baa4c3afe7f1ad39d8c28b7" + integrity sha512-e42bVZnC6VluBZBAFEr3YrdqSspG3bgilyg4nSLBJ7TRGNCzxHa92XAHxQBLYg0BmgwO4b2mf3h/l5EkEWRn3g== dependencies: "@npmcli/node-gyp" "^1.0.2" "@npmcli/promise-spawn" "^1.3.2" - infer-owner "^1.0.4" node-gyp "^7.1.0" read-package-json-fast "^2.0.1" "@octokit/auth-token@^2.4.4": - version "2.4.5" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" - integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA== + version "2.5.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" + integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g== dependencies: "@octokit/types" "^6.0.3" -"@octokit/core@^3.5.0": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" - integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== +"@octokit/core@^3.5.1": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" + integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q== dependencies: "@octokit/auth-token" "^2.4.4" "@octokit/graphql" "^4.5.8" - "@octokit/request" "^5.6.0" + "@octokit/request" "^5.6.3" "@octokit/request-error" "^2.0.5" "@octokit/types" "^6.0.3" before-after-hook "^2.2.0" @@ -1764,42 +1931,42 @@ universal-user-agent "^6.0.0" "@octokit/graphql@^4.5.8": - version "4.6.4" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.6.4.tgz#0c3f5bed440822182e972317122acb65d311a5ed" - integrity sha512-SWTdXsVheRmlotWNjKzPOb6Js6tjSqA2a8z9+glDJng0Aqjzti8MEWOtuT8ZSu6wHnci7LZNuarE87+WJBG4vg== + version "4.8.0" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" + integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg== dependencies: "@octokit/request" "^5.6.0" "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/openapi-types@^9.1.1": - version "9.1.1" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.1.1.tgz#fb87f2e2f44b95a5720d61dee409a9f1fbc59217" - integrity sha512-xmyPP9tVb4T4A6Lk6SL6ScnIqAHpPV4jfMZI8VtY286212ri9J/6IFGuLsZ26daADUmriuLejake4k+azEfnaw== +"@octokit/openapi-types@^11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6" + integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA== "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== -"@octokit/plugin-paginate-rest@^2.6.2": - version "2.14.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.14.0.tgz#f469cb4a908792fb44679c5973d8bba820c88b0f" - integrity sha512-S2uEu2uHeI7Vf+Lvj8tv3O5/5TCAa8GHS0dUQN7gdM7vKA6ZHAbR6HkAVm5yMb1mbedLEbxOuQ+Fa0SQ7tCDLA== +"@octokit/plugin-paginate-rest@^2.16.8": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7" + integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw== dependencies: - "@octokit/types" "^6.18.0" + "@octokit/types" "^6.34.0" -"@octokit/plugin-request-log@^1.0.2": +"@octokit/plugin-request-log@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== -"@octokit/plugin-rest-endpoint-methods@5.5.1": - version "5.5.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.5.1.tgz#31cce8fc3eda4d186bd90828cb7a2203ad95e3d1" - integrity sha512-Al57+OZmO65JpiPk4JS6u6kQ2y9qjoZtY1IWiSshc4N+F7EcrK8Rgy/cUJBB4WIcSFUQyF66EJQK1oKgXWeRNw== +"@octokit/plugin-rest-endpoint-methods@^5.12.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz#8c46109021a3412233f6f50d28786f8e552427ba" + integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA== dependencies: - "@octokit/types" "^6.21.1" + "@octokit/types" "^6.34.0" deprecation "^2.3.1" "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": @@ -1811,34 +1978,61 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.6.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.0.tgz#6084861b6e4fa21dc40c8e2a739ec5eff597e672" - integrity sha512-4cPp/N+NqmaGQwbh3vUsYqokQIzt7VjsgTYVXiwpUP2pxd5YiZB2XuTedbb0SPtv9XS7nzAKjAuQxmY8/aZkiA== +"@octokit/request@^5.6.0", "@octokit/request@^5.6.3": + version "5.6.3" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" + integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A== dependencies: "@octokit/endpoint" "^6.0.1" "@octokit/request-error" "^2.1.0" "@octokit/types" "^6.16.1" is-plain-object "^5.0.0" - node-fetch "^2.6.1" + node-fetch "^2.6.7" universal-user-agent "^6.0.0" "@octokit/rest@^18.1.0": - version "18.7.1" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.7.1.tgz#575ecf8b881b79540daa28b0fa3a2b3ae8ef2649" - integrity sha512-790Yv8Xpbqs3BtnMAO5hlOftVICHPdgZ/3qlTmeOoqrQGzT25BIpHkg/KKMeKG9Fg8d598PLxGhf80RswElv9g== + version "18.12.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.12.0.tgz#f06bc4952fc87130308d810ca9d00e79f6988881" + integrity sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q== dependencies: - "@octokit/core" "^3.5.0" - "@octokit/plugin-paginate-rest" "^2.6.2" - "@octokit/plugin-request-log" "^1.0.2" - "@octokit/plugin-rest-endpoint-methods" "5.5.1" + "@octokit/core" "^3.5.1" + "@octokit/plugin-paginate-rest" "^2.16.8" + "@octokit/plugin-request-log" "^1.0.4" + "@octokit/plugin-rest-endpoint-methods" "^5.12.0" -"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.18.0", "@octokit/types@^6.21.1": - version "6.21.1" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.21.1.tgz#d0f2b7598c88e13d0bd87e330d975e3fb2a90180" - integrity sha512-PP+m3T5EWZKawru4zi/FvX8KL2vkO5f1fLthx78/7743p7RtJUevt3z7698k+7oAYRA7YuVqfXthSEHqkDvZ8g== +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.34.0": + version "6.34.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218" + integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw== dependencies: - "@octokit/openapi-types" "^9.1.1" + "@octokit/openapi-types" "^11.2.0" + +"@peculiar/asn1-schema@^2.1.6": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.1.8.tgz#552300a1ed7991b22c9abf789a3920a3cb94c26b" + integrity sha512-u34H/bpqCdDuqrCVZvH0vpwFBT/dNEdNY+eE8u4IuC26yYnhDkXF4+Hliqca88Avbb7hyN2EF/eokyDdyS7G/A== + dependencies: + asn1js "^3.0.4" + pvtsutils "^1.3.2" + tslib "^2.4.0" + +"@peculiar/json-schema@^1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" + integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== + dependencies: + tslib "^2.0.0" + +"@peculiar/webcrypto@^1.0.22": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.4.0.tgz#f941bd95285a0f8a3d2af39ccda5197b80cd32bf" + integrity sha512-U58N44b2m3OuTgpmKgf0LPDOmP3bhwNz01vAnj1mBwxBASRhptWYK+M3zG+HBkDqGQM+bFsoIihTW8MdmPXEqg== + dependencies: + "@peculiar/asn1-schema" "^2.1.6" + "@peculiar/json-schema" "^1.1.12" + pvtsutils "^1.3.2" + tslib "^2.4.0" + webcrypto-core "^1.7.4" "@react-native-community/cli-debugger-ui@^5.0.1": version "5.0.1" @@ -1875,9 +2069,9 @@ xmldoc "^1.1.2" "@react-native-community/cli-platform-ios@^5.0.1-alpha.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-5.0.1.tgz#efa9c9b3bba0978d0a26d6442eefeffb5006a196" - integrity sha512-Nr/edBEYJfElgBNvjDevs2BuDicsvQaM8nYkTGgp33pyuCZRBxsYxQqfsNmnLalTzcYaebjWj6AnjUSxzQBWqg== + version "5.0.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-5.0.2.tgz#62485534053c0dad28a67de188248de177f4b0fb" + integrity sha512-IAJ2B3j2BTsQUJZ4R6cVvnTbPq0Vza7+dOgP81ISz2BKRtQ0VqNFv+VOALH2jLaDzf4t7NFlskzIXFqWqy2BLg== dependencies: "@react-native-community/cli-tools" "^5.0.1" chalk "^3.0.0" @@ -1979,10 +2173,10 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-1.0.0.tgz#05bb0031533598f9458cf65a502b8df0eecae780" integrity sha512-0jbp4RxjYopTsIdLl+/Fy2TiwVYHy4mgeu07DG4b/LyM0OS/+lPP5c9sbnt/AMlnF6qz2JRZpPpGw1eMNS6A4w== -"@sideway/address@^4.1.0": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.2.tgz#811b84333a335739d3969cfc434736268170cad1" - integrity sha512-idTz8ibqWFrPU8kMirL0CoPH/A29XOzzAzpyN3zQ4kAWnzmNfFmRaoMNN6VI8ske5M73HZyhIaW4OuSFIdM4oA== +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== dependencies: "@hapi/hoek" "^9.0.0" @@ -2003,10 +2197,10 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^7.0.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5" - integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg== +"@sinonjs/fake-timers@^8.0.1": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== dependencies: "@sinonjs/commons" "^1.7.0" @@ -2015,6 +2209,71 @@ resolved "https://registry.yarnpkg.com/@sovpro/delimited-stream/-/delimited-stream-1.1.0.tgz#4334bba7ee241036e580fdd99c019377630d26b4" integrity sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw== +"@stablelib/binary@^1.0.0", "@stablelib/binary@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/binary/-/binary-1.0.1.tgz#c5900b94368baf00f811da5bdb1610963dfddf7f" + integrity sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q== + dependencies: + "@stablelib/int" "^1.0.1" + +"@stablelib/ed25519@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@stablelib/ed25519/-/ed25519-1.0.2.tgz#937a88a2f73a71d9bdc3ea276efe8954776ae0f4" + integrity sha512-FtnvUwvKbp6l1dNcg4CswMAVFVu/nzLK3oC7/PRtjYyHbWsIkD8j+5cjXHmwcCpdCpRCaTGACkEhhMQ1RcdSOQ== + dependencies: + "@stablelib/random" "^1.0.1" + "@stablelib/sha512" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/hash@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/hash/-/hash-1.0.1.tgz#3c944403ff2239fad8ebb9015e33e98444058bc5" + integrity sha512-eTPJc/stDkdtOcrNMZ6mcMK1e6yBbqRBaNW55XA1jU8w/7QdnCF0CmMmOD1m7VSkBR44PWrMHU2l6r8YEQHMgg== + +"@stablelib/int@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/int/-/int-1.0.1.tgz#75928cc25d59d73d75ae361f02128588c15fd008" + integrity sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w== + +"@stablelib/random@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.0.tgz#f441495075cdeaa45de16d7ddcc269c0b8edb16b" + integrity sha512-G9vwwKrNCGMI/uHL6XeWe2Nk4BuxkYyWZagGaDU9wrsuV+9hUwNI1lok2WVo8uJDa2zx7ahNwN7Ij983hOUFEw== + dependencies: + "@stablelib/binary" "^1.0.0" + "@stablelib/wipe" "^1.0.0" + +"@stablelib/random@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.1.tgz#4357a00cb1249d484a9a71e6054bc7b8324a7009" + integrity sha512-zOh+JHX3XG9MSfIB0LZl/YwPP9w3o6WBiJkZvjPoKKu5LKFW4OLV71vMxWp9qG5T43NaWyn0QQTWgqCdO+yOBQ== + dependencies: + "@stablelib/binary" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/sha256@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/sha256/-/sha256-1.0.1.tgz#77b6675b67f9b0ea081d2e31bda4866297a3ae4f" + integrity sha512-GIIH3e6KH+91FqGV42Kcj71Uefd/QEe7Dy42sBTeqppXV95ggCcxLTk39bEr+lZfJmp+ghsR07J++ORkRELsBQ== + dependencies: + "@stablelib/binary" "^1.0.1" + "@stablelib/hash" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/sha512@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/sha512/-/sha512-1.0.1.tgz#6da700c901c2c0ceacbd3ae122a38ac57c72145f" + integrity sha512-13gl/iawHV9zvDKciLo1fQ8Bgn2Pvf7OV6amaRVKiq3pjQ3UmEpXxWiAfV8tYjUpeZroBxtyrwtdooQT/i3hzw== + dependencies: + "@stablelib/binary" "^1.0.1" + "@stablelib/hash" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + +"@stablelib/wipe@^1.0.0", "@stablelib/wipe@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/wipe/-/wipe-1.0.1.tgz#d21401f1d59ade56a62e139462a97f104ed19a36" + integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg== + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -2035,15 +2294,15 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== -"@tsconfig/node16@^1.0.1": +"@tsconfig/node16@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": - version "7.1.15" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024" - integrity sha512-bxlMKPDbY8x5h6HBwVzEOk2C8fb6SLfYQ5Jw3uBYuYF1lfWk/kbLd81la82vrIkBb0l+JdmrZaDikPrNxpS/Ew== + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -2052,9 +2311,9 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.3" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.3.tgz#f456b4b2ce79137f768aa130d2423d2f0ccfaba5" - integrity sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA== + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== dependencies: "@babel/types" "^7.0.0" @@ -2067,9 +2326,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" - integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== + version "7.17.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.17.1.tgz#1a0e73e8c28c7e832656db372b779bfd2ef37314" + integrity sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA== dependencies: "@babel/types" "^7.3.0" @@ -2081,9 +2340,9 @@ "@types/node" "*" "@types/body-parser@*": - version "1.19.1" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c" - integrity sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg== + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== dependencies: "@types/connect" "*" "@types/node" "*" @@ -2101,17 +2360,17 @@ integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== "@types/eslint@^7.2.13": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.0.tgz#7e41f2481d301c68e14f483fe10b017753ce8d5a" - integrity sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A== + version "7.29.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" + integrity sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng== dependencies: "@types/estree" "*" "@types/json-schema" "*" "@types/estree@*": - version "0.0.50" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" - integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== "@types/events@^3.0.0": version "3.0.0" @@ -2119,9 +2378,9 @@ integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== "@types/express-serve-static-core@^4.17.18": - version "4.17.24" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07" - integrity sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA== + version "4.17.28" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" + integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== dependencies: "@types/node" "*" "@types/qs" "*" @@ -2137,6 +2396,20 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/ffi-napi@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/ffi-napi/-/ffi-napi-4.0.5.tgz#0b2dc2d549361947a117d55156ff34fd9632c3df" + integrity sha512-WDPpCcHaPhHmP1FIw3ds/+OLt8bYQ/h3SO7o+8kH771PL21kHVzTwii7+WyMBXMQrBsR6xVU2y7w+h+9ggpaQw== + dependencies: + "@types/node" "*" + "@types/ref-napi" "*" + "@types/ref-struct-di" "*" + +"@types/figlet@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/figlet/-/figlet-1.5.4.tgz#54a426d63e921a9bca44102c5b1b1f206fa56d93" + integrity sha512-cskPTju7glYgzvkJy/hftqw7Fen3fsd0yrPOqcbBLJu+YdDQuA438akS1g+2XVKGzsQOnXGV2I9ePv6xUBnKMQ== + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -2144,17 +2417,25 @@ dependencies: "@types/node" "*" -"@types/indy-sdk-react-native@npm:@types/indy-sdk@^1.16.6", "@types/indy-sdk@^1.16.6": - version "1.16.6" - resolved "https://registry.yarnpkg.com/@types/indy-sdk/-/indy-sdk-1.16.6.tgz#ec8df3dd70cb939c85caa6ebcc32d851e2f3c454" - integrity sha512-BhmqsM2z65aOrg6Hum7YICX02dQA2OS05BjEypdoScmPO3ySsZ5QXngeh6pAi+se5yGYp+cL5msoTqldAhlOGA== +"@types/indy-sdk-react-native@npm:@types/indy-sdk@^1.16.16", "@types/indy-sdk@^1.16.16": + version "1.16.17" + resolved "https://registry.yarnpkg.com/@types/indy-sdk/-/indy-sdk-1.16.17.tgz#cb090033951d078809f493036746804a1a594497" + integrity sha512-FI5urEpXiu/NHOoL1TciJDU38QusUBtPZv9FDMUOWPczl87fVb08CYHWYtAZoLnsKfi5zeGD+WEBpYC14aF9Uw== dependencies: buffer "^6.0.0" +"@types/inquirer@^8.1.3": + version "8.2.1" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.1.tgz#28a139be3105a1175e205537e8ac10830e38dbf4" + integrity sha512-wKW3SKIUMmltbykg4I5JzCVzUhkuD9trD6efAmYgN2MrSntY0SMRQzEnD3mkyJ/rv9NLbTC7g3hKKE86YwEDLw== + dependencies: + "@types/through" "*" + rxjs "^7.2.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" - integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== "@types/istanbul-lib-report@*": version "3.0.0" @@ -2179,9 +2460,14 @@ pretty-format "^26.0.0" "@types/json-schema@*", "@types/json-schema@^7.0.7": - version "7.0.8" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818" - integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/luxon@^1.27.0": version "1.27.1" @@ -2204,17 +2490,17 @@ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/node-fetch@^2.5.10": - version "2.5.12" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66" - integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw== + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" + integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA== dependencies: "@types/node" "*" form-data "^3.0.0" "@types/node@*", "@types/node@^15.14.4": - version "15.14.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.4.tgz#aaf18436ef67f24676d92b8bbe0f5f41b08db3e8" - integrity sha512-yblJrsfCxdxYDUa2fM5sP93ZLk5xL3/+3MJei+YtsNbIdY75ePy2AiCfpq+onepzax+8/Yv+OD/fLNleWpCzVg== + version "15.14.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" + integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -2232,14 +2518,14 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prettier@^2.1.5": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.3.2.tgz#fc8c2825e4ed2142473b4a81064e6e081463d1b3" - integrity sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog== + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.1.tgz#76e72d8a775eef7ce649c63c8acae1a0824bbaed" + integrity sha512-XFjFHmaLVifrAKaZ+EKghFHtHSUonyw8P2Qmy2/+osBnrKbH9UYtlK10zg8/kCt47MFilll/DEDKy3DHfJ0URw== "@types/prop-types@*": - version "15.7.4" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" - integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== "@types/qs@*": version "6.9.7" @@ -2252,21 +2538,35 @@ integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== "@types/react-native@^0.64.10": - version "0.64.12" - resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.64.12.tgz#1c6a3226c26d7a5949cdf8878e6cfe95fe0951d6" - integrity sha512-sw6WGSaL219zqrgdb4kQUtFB9iGXC/LmecLZ+UUWEgwYvD0YH81FqWYmONa2HuTkOFAsxu2bK4DspkWRUHIABQ== + version "0.64.24" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.64.24.tgz#54d8aaadc12d429004b0a573dc3a65ad27430cc6" + integrity sha512-qgqOJub7BYsAkcg3VSL3w63cgJdLoMmAX6TSTAPL53heCzUkIdtpWqjyNRH0n7jPjxPGG1Qmsv6GSUh7IfyqRg== dependencies: "@types/react" "*" "@types/react@*": - version "17.0.15" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.15.tgz#c7533dc38025677e312606502df7656a6ea626d0" - integrity sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw== + version "18.0.9" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" + integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" csstype "^3.0.2" +"@types/ref-napi@*", "@types/ref-napi@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/ref-napi/-/ref-napi-3.0.4.tgz#d7edc063b244c85767867ce1167ec2d7051728a1" + integrity sha512-ng8SCmdZbz1GHaW3qgGoX9IaHoIvgMqgBHLe3sv18NbAkHVgnjRW8fJq51VTUm4lnJyLu60q9/002o7qjOg13g== + dependencies: + "@types/node" "*" + +"@types/ref-struct-di@*": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/ref-struct-di/-/ref-struct-di-1.1.6.tgz#9775753b24ba5bf248dd66d79d4fdb7cebef6e95" + integrity sha512-+Sa2H3ynDYo2ungR3d5kmNetlkAYNqQVjJvs1k7i6zvo7Zu/qb+OsrXU54RuiOYJCwY9piN+hOd4YRRaiEOqgw== + dependencies: + "@types/ref-napi" "*" + "@types/scheduler@*": version "0.16.2" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" @@ -2285,17 +2585,31 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/through@*": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@types/uuid@^8.3.0", "@types/uuid@^8.3.1": - version "8.3.1" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" - integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== "@types/validator@^13.1.3": - version "13.7.0" - resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.0.tgz#fa25263656d234473025c2d48249a900053c355a" - integrity sha512-+jBxVvXVuggZOrm04NR8z+5+bgoW4VZyLzUO+hmPPW1mVFL/HaitLAkizfv4yg9TbG8lkfHWVMQ11yDqrVVCzA== + version "13.7.2" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.2.tgz#a2114225d9be743fb154b06c29b8257aaca42922" + integrity sha512-KFcchQ3h0OPQgFirBRPZr5F/sVjxZsOrQHedj3zi8AH3Zv/hOLx2OLR4hxR5HcfoU+33n69ZuOfzthKVdMoTiw== -"@types/ws@^7.4.4", "@types/ws@^7.4.6": +"@types/varint@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/varint/-/varint-6.0.0.tgz#4ad73c23cbc9b7e44379a7729ace7ed9c8bc9854" + integrity sha512-2jBazyxGl4644tvu3VAez8UA/AtrcEetT9HOeAbqZ/vAcRVL/ZDFQjSS7rkWusU5cyONQVUz+nwwrNZdMva4ow== + dependencies: + "@types/node" "*" + +"@types/ws@^7.4.6": version "7.4.7" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== @@ -2303,9 +2617,9 @@ "@types/node" "*" "@types/yargs-parser@*": - version "20.2.1" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" - integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== "@types/yargs@^15.0.0": version "15.0.14" @@ -2322,74 +2636,90 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^4.26.1": - version "4.28.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz#8197f1473e7da8218c6a37ff308d695707835684" - integrity sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q== + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" + integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg== dependencies: - "@typescript-eslint/experimental-utils" "4.28.5" - "@typescript-eslint/scope-manager" "4.28.5" + "@typescript-eslint/experimental-utils" "4.33.0" + "@typescript-eslint/scope-manager" "4.33.0" debug "^4.3.1" functional-red-black-tree "^1.0.1" + ignore "^5.1.8" regexpp "^3.1.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.28.5": - version "4.28.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz#66c28bef115b417cf9d80812a713e0e46bb42a64" - integrity sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA== +"@typescript-eslint/experimental-utils@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" + integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== dependencies: "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.28.5" - "@typescript-eslint/types" "4.28.5" - "@typescript-eslint/typescript-estree" "4.28.5" + "@typescript-eslint/scope-manager" "4.33.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/typescript-estree" "4.33.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" "@typescript-eslint/parser@^4.26.1": - version "4.28.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.5.tgz#9c971668f86d1b5c552266c47788a87488a47d1c" - integrity sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw== + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899" + integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA== dependencies: - "@typescript-eslint/scope-manager" "4.28.5" - "@typescript-eslint/types" "4.28.5" - "@typescript-eslint/typescript-estree" "4.28.5" + "@typescript-eslint/scope-manager" "4.33.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/typescript-estree" "4.33.0" debug "^4.3.1" -"@typescript-eslint/scope-manager@4.28.5": - version "4.28.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz#3a1b70c50c1535ac33322786ea99ebe403d3b923" - integrity sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ== +"@typescript-eslint/scope-manager@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" + integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ== dependencies: - "@typescript-eslint/types" "4.28.5" - "@typescript-eslint/visitor-keys" "4.28.5" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" -"@typescript-eslint/types@4.28.5": - version "4.28.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.5.tgz#d33edf8e429f0c0930a7c3d44e9b010354c422e9" - integrity sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA== +"@typescript-eslint/types@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" + integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== -"@typescript-eslint/typescript-estree@4.28.5": - version "4.28.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz#4906d343de693cf3d8dcc301383ed638e0441cd1" - integrity sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw== +"@typescript-eslint/typescript-estree@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" + integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== dependencies: - "@typescript-eslint/types" "4.28.5" - "@typescript-eslint/visitor-keys" "4.28.5" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" debug "^4.3.1" globby "^11.0.3" is-glob "^4.0.1" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@4.28.5": - version "4.28.5" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz#ffee2c602762ed6893405ee7c1144d9cc0a29675" - integrity sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg== +"@typescript-eslint/visitor-keys@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" + integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg== dependencies: - "@typescript-eslint/types" "4.28.5" + "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" +"@unimodules/core@*": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@unimodules/core/-/core-7.1.2.tgz#5181b99586476a5d87afd0958f26a04714c47fa1" + integrity sha512-lY+e2TAFuebD3vshHMIRqru3X4+k7Xkba4Wa7QsDBd+ex4c4N2dHAO61E2SrGD9+TRBD8w/o7mzK6ljbqRnbyg== + dependencies: + compare-versions "^3.4.0" + +"@unimodules/react-native-adapter@*": + version "6.3.9" + resolved "https://registry.yarnpkg.com/@unimodules/react-native-adapter/-/react-native-adapter-6.3.9.tgz#2f4bef6b7532dce5bf9f236e69f96403d0243c30" + integrity sha512-i9/9Si4AQ8awls+YGAKkByFbeAsOPgUNeLoYeh2SQ3ddjxJ5ZJDtq/I74clDnpDcn8zS9pYlcDJ9fgVJa39Glw== + dependencies: + expo-modules-autolinking "^0.0.3" + invariant "^2.2.4" + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -2399,9 +2729,9 @@ JSONStream@^1.0.4: through ">=2.2.7 <3" abab@^2.0.3, abab@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" - integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== abbrev@1: version "1.1.1" @@ -2418,15 +2748,15 @@ abort-controller@^3.0.0: absolute-path@^0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/absolute-path/-/absolute-path-0.0.0.tgz#a78762fbdadfb5297be99b15d35a785b2f095bf7" - integrity sha1-p4di+9rftSl76ZsV01p4Wy8JW/c= + integrity sha512-HQiug4c+/s3WOvEnDRxXVmNtSG5s2gJM9r19BTcqjp7BWcE48PB+Y2G6jE65kqI0LpsQeMZygt/b60Gi4KxGyA== -accepts@^1.3.7, accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== +accepts@^1.3.7, accepts@~1.3.5, accepts@~1.3.7, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" + mime-types "~2.1.34" + negotiator "0.6.3" acorn-globals@^6.0.0: version "6.0.0" @@ -2446,20 +2776,25 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4: - version "8.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" - integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== +acorn@^8.2.4, acorn@^8.4.1: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" - integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= + integrity sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ== agent-base@6, agent-base@^6.0.2: version "6.0.2" @@ -2469,9 +2804,9 @@ agent-base@6, agent-base@^6.0.2: debug "4" agentkeepalive@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" - integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ== + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== dependencies: debug "^4.1.0" depd "^1.1.2" @@ -2496,9 +2831,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: uri-js "^4.2.2" ajv@^8.0.1: - version "8.6.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" - integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -2511,9 +2846,9 @@ anser@^1.4.9: integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww== ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-escapes@^4.2.1: version "4.3.2" @@ -2534,22 +2869,17 @@ ansi-fragments@^0.2.1: ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.0, ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" @@ -2587,24 +2917,32 @@ anymatch@^3.0.3: picomatch "^2.0.4" appdirsjs@^1.2.4: - version "1.2.5" - resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.5.tgz#c9888c8a0a908014533d5176ec56f1d5a8fd3700" - integrity sha512-UyaAyzj+7XLoKhbXJi4zoAw8IDXCiLNCKfQEiuCsCCTkDmiG1vpCliQn/MoUvO3DZqCN1i6gOahokcFtNSIrVA== + version "1.2.6" + resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.6.tgz#fccf9ee543315492867cacfcfd4a2b32257d30ac" + integrity sha512-D8wJNkqMCeQs3kLasatELsddox/Xqkhp+J07iXGyL54fVN7oc+nmNfYzGuCs1IEP6uBw+TfpuO3JKwc+lECy4w== aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -aproba@^2.0.0: +"aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== +are-we-there-yet@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz#ba20bd6b553e31d62fc8c31bd23d22b95734390d" + integrity sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== dependencies: delegates "^1.0.0" readable-stream "^2.0.6" @@ -2624,7 +2962,7 @@ argparse@^1.0.7: arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== arr-flatten@^1.1.0: version "1.1.0" @@ -2634,7 +2972,17 @@ arr-flatten@^1.1.0: arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== + +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1, array-back@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== array-differ@^3.0.0: version "3.0.0" @@ -2644,38 +2992,38 @@ array-differ@^3.0.0: array-filter@~0.0.0: version "0.0.1" resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" - integrity sha1-fajPLiZijtcygDWB/SH2fKzS7uw= + integrity sha512-VW0FpCIhjZdarWjIz8Vpva7U95fl2Jn+b+mmFFMLn8PIVscOQcAgEznwUzTEuUHuqZqIxwzRlcaN/urTFFQoiw== array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" - integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= + integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== -array-includes@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" - integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== +array-includes@^3.1.4: + version "3.1.5" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb" + integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" get-intrinsic "^1.1.1" - is-string "^1.0.5" + is-string "^1.0.7" array-map@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" - integrity sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI= + integrity sha512-123XMszMB01QKVptpDQ7x1m1pP5NmJIG1kbl0JSPPRezvwQChxAN0Gvzo7rvR1IZ2tOL2tmiy7kY/KKgnpVVpg== array-reduce@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" - integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys= + integrity sha512-8jR+StqaC636u7h3ye1co3lQRefgVVUQUhuAmRbDqIMeR2yuXzRvkCNQiQ5J/wbREmoBLNtp13dhaaVpZQDRUw== array-union@^2.1.0: version "2.1.0" @@ -2685,21 +3033,22 @@ array-union@^2.1.0: array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== -array.prototype.flat@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" - integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== +array.prototype.flat@^1.2.5: + version "1.3.0" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b" + integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" + es-abstract "^1.19.2" + es-shim-unscopables "^1.0.0" arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== arrify@^2.0.1: version "2.0.1" @@ -2709,24 +3058,38 @@ arrify@^2.0.1: asap@^2.0.0, asap@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +asmcrypto.js@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/asmcrypto.js/-/asmcrypto.js-0.22.0.tgz#38fc1440884d802c7bd37d1d23c2b26a5cd5d2d2" + integrity sha512-usgMoyXjMbx/ZPdzTSXExhMPur2FTdz/Vo5PVx2gIaBcdAAJNOFlsdgqveM8Cff7W0v+xrf9BwjOV26JSAF9qA== asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== dependencies: safer-buffer "~2.1.0" +asn1js@^3.0.1, asn1js@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.4.tgz#65fece61bd30d0ef1e31b39fcd383810f44c9fb5" + integrity sha512-ZibuNYyfODvHiVyRFs80xLAUjCwBSkLbE+r1TasjlRKwdodENGT4AlLdaN12Pl/EcK3lFMDYXU6lE2g7Sq9VVQ== + dependencies: + pvtsutils "^1.3.2" + pvutils "^1.1.3" + tslib "^2.4.0" + assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== ast-types@0.14.2: version "0.14.2" @@ -2751,16 +3114,16 @@ async-limiter@~1.0.0: integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== async@^2.4.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== at-least-node@^1.0.0: version "1.0.0" @@ -2775,30 +3138,44 @@ atob@^2.1.2: aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: version "1.11.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +b64-lite@^1.3.1, b64-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/b64-lite/-/b64-lite-1.4.0.tgz#e62442de11f1f21c60e38b74f111ac0242283d3d" + integrity sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w== + dependencies: + base-64 "^0.1.0" + +b64u-lite@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/b64u-lite/-/b64u-lite-1.1.0.tgz#a581b7df94cbd4bed7cbb19feae816654f0b1bf0" + integrity sha512-929qWGDVCRph7gQVTC6koHqQIpF4vtVaSbwLltFQo44B1bYUquALswZdBKFfrJCPEnsCOvWkJsPdQYZ/Ukhw8A== + dependencies: + b64-lite "^1.4.0" + babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== -babel-jest@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.0.6.tgz#e99c6e0577da2655118e3608b68761a5a69bd0d8" - integrity sha512-iTJyYLNc4wRofASmofpOc5NK9QunwMk+TLFgGXsTFS8uEqmd8wdI7sga0FPe2oVH3b5Agt/EAK1QjPEuKL8VfA== +babel-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== dependencies: - "@jest/transform" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^27.0.6" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^27.5.1" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" slash "^3.0.0" babel-plugin-dynamic-import-node@^2.3.3: @@ -2808,50 +3185,50 @@ babel-plugin-dynamic-import-node@^2.3.3: dependencies: object.assign "^4.1.0" -babel-plugin-istanbul@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765" - integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ== +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@istanbuljs/load-nyc-config" "^1.0.0" "@istanbuljs/schema" "^0.1.2" - istanbul-lib-instrument "^4.0.0" + istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.0.6.tgz#f7c6b3d764af21cb4a2a1ab6870117dbde15b456" - integrity sha512-CewFeM9Vv2gM7Yr9n5eyyLVPRSiBnk6lKZRjgwYnGKSl9M14TMn2vkN02wTF04OGuSDLEzlWiMzvjXuW9mB6Gw== +babel-plugin-jest-hoist@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-plugin-polyfill-corejs2@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz#e9124785e6fd94f94b618a7954e5693053bf5327" - integrity sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ== +babel-plugin-polyfill-corejs2@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz#440f1b70ccfaabc6b676d196239b138f8a2cfba5" + integrity sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w== dependencies: "@babel/compat-data" "^7.13.11" - "@babel/helper-define-polyfill-provider" "^0.2.2" + "@babel/helper-define-polyfill-provider" "^0.3.1" semver "^6.1.1" -babel-plugin-polyfill-corejs3@^0.2.2: - version "0.2.4" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.4.tgz#68cb81316b0e8d9d721a92e0009ec6ecd4cd2ca9" - integrity sha512-z3HnJE5TY/j4EFEa/qpQMSbcUJZ5JQi+3UFjXzn6pQCmIKc5Ug5j98SuYyH+m4xQnvKlMDIW4plLfgyVnd0IcQ== +babel-plugin-polyfill-corejs3@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72" + integrity sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ== dependencies: - "@babel/helper-define-polyfill-provider" "^0.2.2" - core-js-compat "^3.14.0" + "@babel/helper-define-polyfill-provider" "^0.3.1" + core-js-compat "^3.21.0" -babel-plugin-polyfill-regenerator@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz#b310c8d642acada348c1fa3b3e6ce0e851bee077" - integrity sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg== +babel-plugin-polyfill-regenerator@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz#2c0678ea47c75c8cc2fbb1852278d8fb68233990" + integrity sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A== dependencies: - "@babel/helper-define-polyfill-provider" "^0.2.2" + "@babel/helper-define-polyfill-provider" "^0.3.1" babel-plugin-syntax-trailing-function-commas@^7.0.0-beta.0: version "7.0.0-beta.0" @@ -2909,12 +3286,12 @@ babel-preset-fbjs@^3.3.0: "@babel/plugin-transform-template-literals" "^7.0.0" babel-plugin-syntax-trailing-function-commas "^7.0.0-beta.0" -babel-preset-jest@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.0.6.tgz#909ef08e9f24a4679768be2f60a3df0856843f9d" - integrity sha512-WObA0/Biw2LrVVwZkF/2GqbOdzhKD6Fkdwhoy9ASIrOWr/zodcSpQh72JOkEn6NWyjmnPDjNSqaGN4KnpKzhXw== +babel-preset-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== dependencies: - babel-plugin-jest-hoist "^27.0.6" + babel-plugin-jest-hoist "^27.5.1" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: @@ -2925,9 +3302,16 @@ balanced-match@^1.0.0: base-64@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" - integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs= + integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== + +base-x@^3.0.2: + version "3.0.9" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" + integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== + dependencies: + safe-buffer "^5.0.1" -base64-js@^1.1.2, base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@*, base64-js@^1.1.2, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2948,7 +3332,7 @@ base@^0.11.1: bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== dependencies: tweetnacl "^0.14.3" @@ -2957,15 +3341,15 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== -big-integer@^1.6.44: - version "1.6.48" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" - integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== +big-integer@1.6.x: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== bignumber.js@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" - integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== + version "9.0.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673" + integrity sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw== bindings@^1.3.1: version "1.5.0" @@ -2979,21 +3363,23 @@ bn.js@^5.2.0: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== +body-parser@1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== dependencies: - bytes "3.1.0" + bytes "3.1.2" content-type "~1.0.4" debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" borc@^3.0.0: version "3.0.0" @@ -3008,19 +3394,19 @@ borc@^3.0.0: json-text-sequence "~0.3.0" readable-stream "^3.6.0" -bplist-creator@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.0.8.tgz#56b2a6e79e9aec3fc33bf831d09347d73794e79c" - integrity sha512-Za9JKzD6fjLC16oX2wsXfc+qBEhJBJB1YPInoAQpMLhDuj5aVOv1baGeIQSq1Fr3OCqzvsoQcSBSwGId/Ja2PA== +bplist-creator@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.1.0.tgz#018a2d1b587f769e379ef5519103730f8963ba1e" + integrity sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg== dependencies: - stream-buffers "~2.2.0" + stream-buffers "2.2.x" -bplist-parser@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.2.0.tgz#43a9d183e5bf9d545200ceac3e712f79ebbe8d0e" - integrity sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw== +bplist-parser@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.3.1.tgz#e1c90b2ca2a9f9474cc72f6862bbf3fee8341fd1" + integrity sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA== dependencies: - big-integer "^1.6.44" + big-integer "1.6.x" brace-expansion@^1.1.7: version "1.1.11" @@ -3046,7 +3432,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -3058,16 +3444,16 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.16.6: - version "4.16.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" - integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== +browserslist@^4.20.2, browserslist@^4.20.3: + version "4.20.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" + integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== dependencies: - caniuse-lite "^1.0.30001219" - colorette "^1.2.2" - electron-to-chromium "^1.3.723" + caniuse-lite "^1.0.30001332" + electron-to-chromium "^1.4.118" escalade "^3.1.1" - node-releases "^1.1.71" + node-releases "^2.0.3" + picocolors "^1.0.0" bs-logger@0.x: version "0.2.6" @@ -3076,6 +3462,13 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "2.x" +bs58@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -3083,10 +3476,10 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -buffer-from@1.x, buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer@^6.0.0, buffer@^6.0.2, buffer@^6.0.3: version "6.0.3" @@ -3099,12 +3492,12 @@ buffer@^6.0.0, buffer@^6.0.2, buffer@^6.0.3: builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" - integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= + integrity sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ== byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" - integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + integrity sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q== byte-size@^7.0.0: version "7.0.1" @@ -3114,18 +3507,19 @@ byte-size@^7.0.0: bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== cacache@^15.0.5, cacache@^15.2.0: - version "15.2.0" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.2.0.tgz#73af75f77c58e72d8c630a7a2858cb18ef523389" - integrity sha512-uKoJSHmnrqXgthDFx/IU6ED/5xd+NNGe+Bb+kLZy7Ku4P+BaiWEUflAKPZ7eAzsYGcsAGASJZsybXp+quEcHTw== + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== dependencies: + "@npmcli/fs" "^1.0.0" "@npmcli/move-file" "^1.0.1" chownr "^2.0.0" fs-minipass "^2.0.0" @@ -3170,21 +3564,21 @@ call-bind@^1.0.0, call-bind@^1.0.2: caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== dependencies: callsites "^2.0.0" caller-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== dependencies: caller-callsite "^2.0.0" callsites@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== callsites@^3.0.0: version "3.1.0" @@ -3206,14 +3600,19 @@ camelcase@^5.0.0, camelcase@^5.3.1: integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== camelcase@^6.0.0, camelcase@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001219: - version "1.0.30001248" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001248.tgz#26ab45e340f155ea5da2920dadb76a533cb8ebce" - integrity sha512-NwlQbJkxUFJ8nMErnGtT0QTM2TJ33xgz4KXJSMIrjXIbDVdaYueGyjOrLKRtJC+rTiWfi6j5cnZN1NBiSBJGNw== +caniuse-lite@^1.0.30001332: + version "1.0.30001341" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz#59590c8ffa8b5939cf4161f00827b8873ad72498" + integrity sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA== + +canonicalize@^1.0.1: + version "1.0.8" + resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.8.tgz#24d1f1a00ed202faafd9bf8e63352cd4450c6df1" + integrity sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A== capture-exit@^2.0.0: version "2.0.0" @@ -3225,7 +3624,7 @@ capture-exit@^2.0.0: caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2: version "2.4.2" @@ -3245,9 +3644,9 @@ chalk@^3.0.0: supports-color "^7.1.0" chalk@^4.0.0, chalk@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" - integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -3277,10 +3676,10 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -ci-info@^3.1.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6" - integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A== +ci-info@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.1.tgz#58331f6f472a25fe3a50a351ae3052936c2c7f32" + integrity sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg== cjs-module-lexer@^1.0.0: version "1.2.2" @@ -3316,10 +3715,15 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +clear@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/clear/-/clear-0.1.0.tgz#b81b1e03437a716984fd7ac97c87d73bdfe7048a" + integrity sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw== + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + integrity sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw== dependencies: restore-cursor "^2.0.0" @@ -3331,9 +3735,9 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-spinners@^2.0.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" - integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== cli-width@^3.0.0: version "3.0.0" @@ -3426,10 +3830,15 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^1.0.7, colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +colorette@^1.0.7: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== colors@^1.1.2: version "1.4.0" @@ -3437,11 +3846,11 @@ colors@^1.1.2: integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== columnify@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" - integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= + version "1.6.0" + resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.6.0.tgz#6989531713c9008bb29735e61e37acf5bd553cf3" + integrity sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q== dependencies: - strip-ansi "^3.0.0" + strip-ansi "^6.0.1" wcwidth "^1.0.0" combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: @@ -3456,11 +3865,48 @@ command-exists@^1.2.8: resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== +command-line-args@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-commands@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/command-line-commands/-/command-line-commands-3.0.2.tgz#53872a1181db837f21906b1228e260a4eeb42ee4" + integrity sha512-ac6PdCtdR6q7S3HN+JiVLIWGHY30PRYIEl2qPo+FuEuzwAUk0UYyimrngrg7FvF/mCr4Jgoqv5ZnHZgads50rw== + dependencies: + array-back "^4.0.1" + +command-line-usage@^6.1.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" + integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== + dependencies: + array-back "^4.0.2" + chalk "^2.4.2" + table-layout "^1.0.2" + typical "^5.2.0" + commander@^2.15.0, commander@^2.19.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" @@ -3479,6 +3925,11 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" +compare-versions@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" + integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -3537,17 +3988,17 @@ connect@^3.6.5: parseurl "~1.3.3" utils-merge "1.0.1" -console-control-strings@^1.0.0, console-control-strings@~1.1.0: +console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: - safe-buffer "5.1.2" + safe-buffer "5.2.1" content-type@~1.0.4: version "1.0.4" @@ -3555,17 +4006,17 @@ content-type@~1.0.4: integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== conventional-changelog-angular@^5.0.12: - version "5.0.12" - resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz#c979b8b921cbfe26402eb3da5bbfda02d865a2b9" - integrity sha512-5GLsbnkR/7A89RyHLvvoExbiGbd9xKdKqDTrArnPbOqBqG/2wIosu0fHwpeIRI8Tl94MhVNBXcLJZl92ZQ5USw== + version "5.0.13" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" + integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA== dependencies: compare-func "^2.0.0" q "^1.5.1" conventional-changelog-core@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.3.tgz#ce44d4bbba4032e3dc14c00fcd5b53fc00b66433" - integrity sha512-MwnZjIoMRL3jtPH5GywVNqetGILC7g6RQFvdb8LRU/fA/338JbeWAku3PZ8yQ+mtVRViiISqJlb0sOz0htBZig== + version "4.2.4" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz#e50d047e8ebacf63fac3dc67bf918177001e1e9f" + integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg== dependencies: add-stream "^1.0.0" conventional-changelog-writer "^5.0.0" @@ -3588,13 +4039,13 @@ conventional-changelog-preset-loader@^2.3.4: integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== conventional-changelog-writer@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz#c4042f3f1542f2f41d7d2e0d6cad23aba8df8eec" - integrity sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g== + version "5.0.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz#e0757072f045fe03d91da6343c843029e702f359" + integrity sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ== dependencies: conventional-commits-filter "^2.0.7" dateformat "^3.0.0" - handlebars "^4.7.6" + handlebars "^4.7.7" json-stringify-safe "^5.0.1" lodash "^4.17.15" meow "^8.0.0" @@ -3611,9 +4062,9 @@ conventional-commits-filter@^2.0.7: modify-values "^1.0.0" conventional-commits-parser@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.1.tgz#ba44f0b3b6588da2ee9fd8da508ebff50d116ce2" - integrity sha512-OG9kQtmMZBJD/32NEw5IhN5+HnBqVjy03eC+I71I0oQRFA5rOgA4OtPOYG7mz1GkCfCNxn3gKIX8EiHJYuf1cA== + version "3.2.4" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz#a7d3b77758a202a9b2293d2112a8d8052c740972" + integrity sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q== dependencies: JSONStream "^1.0.4" is-text-path "^1.0.1" @@ -3621,7 +4072,6 @@ conventional-commits-parser@^3.2.0: meow "^8.0.0" split2 "^3.0.0" through2 "^4.0.0" - trim-off-newlines "^1.0.0" conventional-recommended-bump@^6.1.0: version "6.1.0" @@ -3649,29 +4099,34 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js-compat@^3.14.0: - version "3.15.2" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.15.2.tgz#47272fbb479880de14b4e6081f71f3492f5bd3cb" - integrity sha512-Wp+BJVvwopjI+A1EFqm2dwUmWYXrvucmtIB2LgXn/Rb+gWPKYxtmb4GKHGKG/KGF1eK9jfjzT38DITbTOCX/SQ== +core-js-compat@^3.21.0: + version "3.22.5" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.5.tgz#7fffa1d20cb18405bd22756ca1353c6f1a0e8614" + integrity sha512-rEF75n3QtInrYICvJjrAgV03HwKiYvtKHdPtaba1KucG+cNZ4NJnH9isqt979e67KZlhpbCOTwnsvnIr+CVeOg== dependencies: - browserslist "^4.16.6" + browserslist "^4.20.3" semver "7.0.0" -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cors@^2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -3691,9 +4146,9 @@ cosmiconfig@^5.0.5, cosmiconfig@^5.1.0: parse-json "^4.0.0" cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== dependencies: "@types/parse-json" "^4.0.0" import-fresh "^3.2.1" @@ -3706,6 +4161,18 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +credentials-context@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/credentials-context/-/credentials-context-2.0.0.tgz#68a9a1a88850c398d3bba4976c8490530af093e8" + integrity sha512-/mFKax6FK26KjgV2KW2D4YqKgoJ5DVJpNt87X2Jc9IxT2HBMy7nEIlc+n7pEi+YFFe721XqrvZPd+jbyyBjsvQ== + +cross-fetch@^3.1.2: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -3744,9 +4211,9 @@ cssstyle@^2.3.0: cssom "~0.3.6" csstype@^3.0.2: - version "3.0.8" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" - integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2" + integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA== dargs@^7.0.0: version "7.0.0" @@ -3760,6 +4227,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" + integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -3775,9 +4247,9 @@ dateformat@^3.0.0: integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== dayjs@^1.8.15: - version "1.10.6" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.6.tgz#288b2aa82f2d8418a6c9d4df5898c0737ad02a63" - integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw== + version "1.11.2" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5" + integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw== debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" @@ -3786,14 +4258,14 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@^3.2.7: +debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -3833,10 +4305,15 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-extend@^0.6.0, deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3, deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== deepmerge@^3.2.0: version "3.3.0" @@ -3855,12 +4332,13 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== +define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== dependencies: - object-keys "^1.0.12" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" define-property@^0.2.5: version "0.2.5" @@ -3899,7 +4377,12 @@ denodeify@^1.2.1: resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" integrity sha1-OjYof1A05pnnV3kBBSwubJQlFjE= -depd@^1.1.2, depd@~1.1.2: +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -3909,10 +4392,10 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== detect-indent@^5.0.0: version "5.0.0" @@ -3924,28 +4407,38 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== dezalgo@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" - integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== dependencies: asap "^2.0.0" wrappy "1" +did-resolver@^3.1.3, did-resolver@^3.1.5: + version "3.2.0" + resolved "https://registry.yarnpkg.com/did-resolver/-/did-resolver-3.2.0.tgz#b89edd0dd70ad6f1c65ca1285472e021c2239707" + integrity sha512-8YiTRitfGt9hJYDIzjc254gXgJptO4zq6Q2BMZMNqkbCf9EFkV6BD4QIh5BUF4YjBglBgJY+duQRzO3UZAlZsw== + diff-sequences@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== -diff-sequences@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723" - integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ== +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== diff@^4.0.1: version "4.0.2" @@ -4017,10 +4510,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.723: - version "1.3.790" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.790.tgz#5c569290929d92c8094fa08c79bc9393ca9e94e7" - integrity sha512-epMH/S2MkhBv+Y0+nHK8dC7bzmOaPwcmiYqt+VwxSUJLgPzkqZnGUEQ8eVhy5zGmgWm9tDDdXkHDzOEsVU979A== +electron-to-chromium@^1.4.118: + version "1.4.137" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz#186180a45617283f1c012284458510cd99d6787f" + integrity sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA== emittery@^0.8.1: version "0.8.1" @@ -4081,9 +4574,9 @@ error-ex@^1.3.1: is-arrayish "^0.2.1" error-stack-parser@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.6.tgz#5a99a707bd7a4c58a797902d48d82803ede6aad8" - integrity sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ== + version "2.0.7" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.7.tgz#b0c6e2ce27d0495cf78ad98715e0cad1219abb57" + integrity sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA== dependencies: stackframe "^1.1.1" @@ -4095,27 +4588,41 @@ errorhandler@^1.5.0: accepts "~1.3.7" escape-html "~1.0.3" -es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: - version "1.18.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0" - integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw== +es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5: + version "1.20.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" + integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" function-bind "^1.1.1" + function.prototype.name "^1.1.5" get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" has "^1.0.3" - has-symbols "^1.0.2" - is-callable "^1.2.3" - is-negative-zero "^2.0.1" - is-regex "^1.1.3" - is-string "^1.0.6" - object-inspect "^1.10.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.0" object-keys "^1.1.1" object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" + regexp.prototype.flags "^1.4.3" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" es-to-primitive@^1.2.1: version "1.2.1" @@ -4164,62 +4671,60 @@ escodegen@^2.0.0: source-map "~0.6.1" eslint-config-prettier@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== + version "8.5.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== -eslint-import-resolver-node@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" - integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== +eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== dependencies: - debug "^2.6.9" - resolve "^1.13.1" + debug "^3.2.7" + resolve "^1.20.0" eslint-import-resolver-typescript@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.4.0.tgz#ec1e7063ebe807f0362a7320543aaed6fe1100e1" - integrity sha512-useJKURidCcldRLCNKWemr1fFQL1SzB3G4a0li6lFGvlc5xGe1hY343bvG07cbpCzPuM/lK19FIJB3XGFSkplA== + version "2.7.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz#a90a4a1c80da8d632df25994c4c5fdcdd02b8751" + integrity sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ== dependencies: - debug "^4.1.1" - glob "^7.1.6" - is-glob "^4.0.1" - resolve "^1.17.0" - tsconfig-paths "^3.9.0" + debug "^4.3.4" + glob "^7.2.0" + is-glob "^4.0.3" + resolve "^1.22.0" + tsconfig-paths "^3.14.1" -eslint-module-utils@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz#b51be1e473dd0de1c5ea638e22429c2490ea8233" - integrity sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A== +eslint-module-utils@^2.7.3: + version "2.7.3" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" + integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== dependencies: debug "^3.2.7" - pkg-dir "^2.0.0" + find-up "^2.1.0" eslint-plugin-import@^2.23.4: - version "2.23.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.23.4.tgz#8dceb1ed6b73e46e50ec9a5bb2411b645e7d3d97" - integrity sha512-6/wP8zZRsnQFiR3iaPFgh5ImVRM1WN5NUWfTIRqwOdeiGJlBcSk82o1FEVq8yXmy4lkIzTo7YhHCIxlU/2HyEQ== + version "2.26.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" + integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== dependencies: - array-includes "^3.1.3" - array.prototype.flat "^1.2.4" + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" debug "^2.6.9" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.4" - eslint-module-utils "^2.6.1" - find-up "^2.0.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.3" has "^1.0.3" - is-core-module "^2.4.0" - minimatch "^3.0.4" - object.values "^1.1.3" - pkg-up "^2.0.0" - read-pkg-up "^3.0.0" - resolve "^1.20.0" - tsconfig-paths "^3.9.0" + is-core-module "^2.8.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.values "^1.1.5" + resolve "^1.22.0" + tsconfig-paths "^3.14.1" eslint-plugin-prettier@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz#cdbad3bf1dbd2b177e9825737fe63b476a08f0c7" - integrity sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw== + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5" + integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g== dependencies: prettier-linter-helpers "^1.0.0" @@ -4256,9 +4761,9 @@ eslint-visitor-keys@^2.0.0: integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== eslint@^7.28.0: - version "7.31.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.31.0.tgz#f972b539424bf2604907a970860732c5d99d3aca" - integrity sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA== + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== dependencies: "@babel/code-frame" "7.12.11" "@eslint/eslintrc" "^0.4.3" @@ -4301,6 +4806,11 @@ eslint@^7.28.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +esm@^3.2.22: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + espree@^7.3.0, espree@^7.3.1: version "7.3.1" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" @@ -4335,9 +4845,9 @@ estraverse@^4.1.1: integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" @@ -4415,50 +4925,67 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expect@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.0.6.tgz#a4d74fbe27222c718fff68ef49d78e26a8fd4c05" - integrity sha512-psNLt8j2kwg42jGBDSfAlU49CEZxejN1f1PlANWDZqIhBOVU/c2Pm888FcjWJzFewhIsNWfZJeLjUjtKGiPuSw== +expect@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" + integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== + dependencies: + "@jest/types" "^27.5.1" + jest-get-type "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + +expo-modules-autolinking@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-0.0.3.tgz#45ba8cb1798f9339347ae35e96e9cc70eafb3727" + integrity sha512-azkCRYj/DxbK4udDuDxA9beYzQTwpJ5a9QA0bBgha2jHtWdFGF4ZZWSY+zNA5mtU3KqzYt8jWHfoqgSvKyu1Aw== + dependencies: + chalk "^4.1.0" + commander "^7.2.0" + fast-glob "^3.2.5" + find-up "~5.0.0" + fs-extra "^9.1.0" + +expo-random@*: + version "12.2.0" + resolved "https://registry.yarnpkg.com/expo-random/-/expo-random-12.2.0.tgz#a3c8a9ce84ef2c85900131d96eea6c7123285482" + integrity sha512-SihCGLmDyDOALzBN8XXpz2hCw0RSx9c4/rvjcS4Bfqhw6luHjL2rHNTLrFYrPrPRmG1jHM6dXXJe/Zm8jdu+2g== dependencies: - "@jest/types" "^27.0.6" - ansi-styles "^5.0.0" - jest-get-type "^27.0.6" - jest-matcher-utils "^27.0.6" - jest-message-util "^27.0.6" - jest-regex-util "^27.0.6" + base64-js "^1.3.0" express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + version "4.18.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" + integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== dependencies: - accepts "~1.3.7" + accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" + body-parser "1.20.0" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.4.0" + cookie "0.5.0" cookie-signature "1.0.6" debug "2.6.9" - depd "~1.1.2" + depd "2.0.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "~1.1.2" + finalhandler "1.2.0" fresh "0.5.2" + http-errors "2.0.0" merge-descriptors "1.0.1" methods "~1.1.2" - on-finished "~2.3.0" + on-finished "2.4.1" parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" + proxy-addr "~2.0.7" + qs "6.10.3" range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" @@ -4512,9 +5039,9 @@ extsprintf@1.3.0: integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== fast-base64-decode@^1.0.0: version "1.0.0" @@ -4531,10 +5058,10 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== -fast-glob@^3.1.1: - version "3.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== +fast-glob@^3.2.5, fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -4552,10 +5079,15 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-text-encoding@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" + integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== + fastq@^1.6.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.1.tgz#5d8175aae17db61947f8b162cfc7f63264d22807" - integrity sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw== + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== dependencies: reusify "^1.0.4" @@ -4566,6 +5098,28 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-blob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c" + integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow== + +ffi-napi@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/ffi-napi/-/ffi-napi-4.0.3.tgz#27a8d42a8ea938457154895c59761fbf1a10f441" + integrity sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg== + dependencies: + debug "^4.1.1" + get-uv-event-loop-napi-h "^1.0.5" + node-addon-api "^3.0.0" + node-gyp-build "^4.2.1" + ref-napi "^2.0.1 || ^3.0.2" + ref-struct-di "^1.1.0" + +figlet@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.5.2.tgz#dda34ff233c9a48e36fcff6741aeb5bafe49b634" + integrity sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ== + figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -4607,7 +5161,7 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= -finalhandler@1.1.2, finalhandler@~1.1.2: +finalhandler@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== @@ -4620,6 +5174,19 @@ finalhandler@1.1.2, finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + find-cache-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -4629,6 +5196,13 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -4651,6 +5225,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@~5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -4660,14 +5242,14 @@ flat-cache@^3.0.4: rimraf "^3.0.2" flatted@^3.1.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" - integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== + version "3.2.5" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" + integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== flow-parser@0.*: - version "0.156.0" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.156.0.tgz#5b463ea4923fe8ca34e38eb367497060d9707d0b" - integrity sha512-OCE3oIixhOttaV4ahIGtxf9XfaDdxujiTnXuHu+0dvDVVDiSDJlQpgCWdDKqP0OHfFnxQKrjMamArDAXtrBtZw== + version "0.178.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.178.0.tgz#85d300e29b146b54cb79e277e092ffd401b05f0c" + integrity sha512-OviMR2Y/sMSyUzR1xLLAmQvmHXTsD1Sq69OTmP5AckVulld7sVNsCfwsw7t3uK00dO9A7k4fD+wodbzzaaEn5g== flow-parser@^0.121.0: version "0.121.0" @@ -4776,11 +5358,40 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -4805,7 +5416,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -4820,14 +5431,14 @@ get-package-type@^0.1.0: integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== get-pkg-repo@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.1.2.tgz#c4ffd60015cf091be666a0212753fc158f01a4c0" - integrity sha512-/FjamZL9cBYllEbReZkxF2IMh80d8TJoC4e3bmLNif8ibHw95aj0N/tzqK0kZz9eU/3w3dL6lF4fnnX/sDdW3A== + version "4.2.1" + resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" + integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA== dependencies: "@hutson/parse-repository-url" "^3.0.0" hosted-git-info "^4.0.0" - meow "^7.0.0" through2 "^2.0.0" + yargs "^16.2.0" get-port@^5.1.1: version "5.1.1" @@ -4846,6 +5457,26 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-symbol-from-current-process-h@^1.0.1, get-symbol-from-current-process-h@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz#510af52eaef873f7028854c3377f47f7bb200265" + integrity sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw== + +get-uv-event-loop-napi-h@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz#42b0b06b74c3ed21fbac8e7c72845fdb7a200208" + integrity sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg== + dependencies: + get-symbol-from-current-process-h "^1.0.1" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -4858,10 +5489,17 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +git-config@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/git-config/-/git-config-0.0.7.tgz#a9c8a3ef07a776c3d72261356d8b727b62202b28" + integrity sha1-qcij7wendsPXImE1bYtye2IgKyg= + dependencies: + iniparser "~1.0.5" + git-raw-commits@^2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" - integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== + version "2.0.11" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723" + integrity sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A== dependencies: dargs "^7.0.0" lodash "^4.17.15" @@ -4894,9 +5532,9 @@ git-up@^4.0.0: parse-url "^6.0.0" git-url-parse@^11.4.4: - version "11.5.0" - resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.5.0.tgz#acaaf65239cb1536185b19165a24bbc754b3f764" - integrity sha512-TZYSMDeM37r71Lqg1mbnMlOqlHd7BSij9qN7XwTkRqSAYFMihGLGhfHwgqQob3GUhEneKnV4nskN9rbQw2KGxA== + version "11.6.0" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.6.0.tgz#c634b8de7faa66498a2b88932df31702c67df605" + integrity sha512-WWUxvJs5HsyHL6L08wOusa/IXYtMuCAhrMmnTjQPpBU0TTHyDhnOATNH3xNQz7YOQUsqIIPTGr4xiVti1Hsk5g== dependencies: git-up "^4.0.0" @@ -4914,15 +5552,15 @@ glob-parent@^5.1.1, glob-parent@^5.1.2: dependencies: is-glob "^4.0.1" -glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== +glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" @@ -4932,30 +5570,30 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.6.0, globals@^13.9.0: - version "13.10.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676" - integrity sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g== + version "13.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" + integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== dependencies: type-fest "^0.20.2" globby@^11.0.2, globby@^11.0.3: - version "11.0.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" slash "^3.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -handlebars@^4.7.6: +handlebars@^4.7.6, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== @@ -4985,10 +5623,10 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== has-flag@^3.0.0: version "3.0.0" @@ -5000,10 +5638,24 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" @@ -5066,9 +5718,9 @@ hosted-git-info@^2.1.4: integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" - integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== dependencies: lru-cache "^6.0.0" @@ -5089,27 +5741,16 @@ http-cache-semantics@^4.1.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== dependencies: - depd "~1.1.2" + depd "2.0.0" inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" http-proxy-agent@^4.0.1: version "4.0.1" @@ -5130,9 +5771,9 @@ http-signature@~1.2.0: sshpk "^1.7.0" https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" @@ -5150,11 +5791,11 @@ humanize-ms@^1.2.1: ms "^2.0.0" husky@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.1.tgz#579f4180b5da4520263e8713cc832942b48e1f1c" - integrity sha512-gceRaITVZ+cJH9sNHqx5tFwbzlLCVxtVZcusME8JYQ8Edy5mpGDOqD8QBCdMhpyo9a+JXddnujQ4rpY2Ff9SJA== + version "7.0.4" + resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" + integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== -iconv-lite@0.4.24, iconv-lite@^0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -5173,7 +5814,7 @@ ieee754@^1.1.13, ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore-walk@^3.0.3: +ignore-walk@^3.0.1, ignore-walk@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== @@ -5185,10 +5826,10 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +ignore@^5.1.8, ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== image-size@^0.6.0: version "0.6.3" @@ -5212,9 +5853,9 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: resolve-from "^4.0.0" import-local@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" - integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== dependencies: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" @@ -5229,17 +5870,17 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -indy-sdk-react-native@^0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/indy-sdk-react-native/-/indy-sdk-react-native-0.1.13.tgz#7a131541f21d4fa5091d214ed75f2235977512e9" - integrity sha512-V0vfR6y8rOy9efc0An5FqSxeiynYFXrOd2tcXYVAA2iX5Y6OuOmbzAGHTTMfiagZaLyE+/1j7DSSaYlXo76VYA== +indy-sdk-react-native@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/indy-sdk-react-native/-/indy-sdk-react-native-0.2.0.tgz#9f02dc29726b22b5f90aa602b6a0d15cbd26b48b" + integrity sha512-eipyH5GzQFjTf89sMCSMy5axbl3uVDln79LOH2rpqN2cb+80Pzb3tMFYWb9TaU4jMKYzlaEE0RsuQoS151g2jQ== dependencies: buffer "^6.0.2" indy-sdk@^1.16.0-dev-1636: - version "1.16.0-dev-1636" - resolved "https://registry.yarnpkg.com/indy-sdk/-/indy-sdk-1.16.0-dev-1636.tgz#e53b719a6ce536459b356dbf32b9a7cb18ca59e8" - integrity sha512-1SYJWdf0xCr+Yd7zTLzYxS7i/j/H2dmBj9C5muPPSdh5XPkL133L0QxZ0NmVdciUY4J5TAyyCjdDgvji1ZSwAw== + version "1.16.0-dev-1655" + resolved "https://registry.yarnpkg.com/indy-sdk/-/indy-sdk-1.16.0-dev-1655.tgz#098c38df4a6eb4e13f89c0b86ebe9636944b71e0" + integrity sha512-MSWRY8rdnGAegs4v4AnzE6CT9O/3JBMUiE45I0Ihj2DMuH+XS1EJZUQEJsyis6aOQzRavv/xVtaBC8o+6azKuw== dependencies: bindings "^1.3.1" nan "^2.11.1" @@ -5263,26 +5904,25 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@^1.3.2, ini@^1.3.4: +ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +iniparser@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/iniparser/-/iniparser-1.0.5.tgz#836d6befe6dfbfcee0bccf1cf9f2acc7027f783d" + integrity sha1-g21r7+bfv87gvM8c+fKsxwJ/eD0= + init-package-json@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-2.0.3.tgz#c8ae4f2a4ad353bcbc089e5ffe98a8f1a314e8fd" - integrity sha512-tk/gAgbMMxR6fn1MgMaM1HpU1ryAmBWWitnxG5OhuNXeX0cbpbgV5jA4AIpQJVNoyOfOevTtO6WX+rPs+EFqaQ== + version "2.0.5" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-2.0.5.tgz#78b85f3c36014db42d8f32117252504f68022646" + integrity sha512-u1uGAtEFu3VA6HNl/yUWw57jmKEMx8SKOxHhxjGnOFUiIlFnohKDFg4ZrPpv9wWqk44nDxGJAtqjdQFm+9XXQA== dependencies: - glob "^7.1.1" - npm-package-arg "^8.1.2" + npm-package-arg "^8.1.5" promzard "^0.3.0" read "~1.0.1" - read-package-json "^3.0.1" + read-package-json "^4.1.1" semver "^7.3.5" validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" @@ -5306,6 +5946,15 @@ inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -5319,9 +5968,9 @@ invariant@^2.2.4: loose-envify "^1.0.0" ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + version "1.1.8" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" + integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== ipaddr.js@1.9.1: version "1.9.1" @@ -5348,26 +5997,29 @@ is-arrayish@^0.2.1: integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= is-bigint@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a" - integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" is-boolean-object@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8" - integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng== + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== dependencies: call-bind "^1.0.2" + has-tostringtag "^1.0.0" is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" - integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== is-ci@^2.0.0: version "2.0.0" @@ -5376,17 +6028,10 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-ci@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994" - integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ== - dependencies: - ci-info "^3.1.1" - -is-core-module@^2.2.0, is-core-module@^2.4.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.5.0.tgz#f754843617c70bfd29b7bd87327400cda5c18491" - integrity sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg== +is-core-module@^2.5.0, is-core-module@^2.8.1: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== dependencies: has "^1.0.3" @@ -5405,9 +6050,11 @@ is-data-descriptor@^1.0.0: kind-of "^6.0.0" is-date-object@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5" - integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A== + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" is-descriptor@^0.1.0: version "0.1.6" @@ -5471,10 +6118,10 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.0, is-glob@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" @@ -5483,15 +6130,17 @@ is-lambda@^1.0.1: resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== is-number-object@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb" - integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" is-number@^3.0.0: version "3.0.0" @@ -5537,13 +6186,20 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-regex@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f" - integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ== +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== dependencies: call-bind "^1.0.2" - has-symbols "^1.0.2" is-ssh@^1.3.0: version "1.3.3" @@ -5562,10 +6218,12 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-string@^1.0.5, is-string@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" - integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" @@ -5586,6 +6244,13 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -5607,9 +6272,9 @@ isexe@^2.0.0: integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= iso-url@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-1.1.5.tgz#875a0f2bf33fa1fc200f8d89e3f49eee57a8f0d9" - integrity sha512-+3JqoKdBTGmyv9vOkS6b9iHhvK34UajfTibrH/1HOK8TI7K2VsM0qOCd+aJdWKtSOA8g3PqZfcwDmnR0p3klqQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-1.2.1.tgz#db96a49d8d9a64a1c889fc07cc525d093afb1811" + integrity sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng== isobject@^2.0.0: version "2.1.0" @@ -5623,24 +6288,43 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +isomorphic-webcrypto@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/isomorphic-webcrypto/-/isomorphic-webcrypto-2.3.8.tgz#4a7493b486ef072b9f11b6f8fd66adde856e3eec" + integrity sha512-XddQSI0WYlSCjxtm1AI8kWQOulf7hAN3k3DclF1sxDJZqOe0pcsOt675zvWW91cZH9hYs3nlA3Ev8QK5i80SxQ== + dependencies: + "@peculiar/webcrypto" "^1.0.22" + asmcrypto.js "^0.22.0" + b64-lite "^1.3.1" + b64u-lite "^1.0.1" + msrcrypto "^1.5.6" + str2buf "^1.3.0" + webcrypto-shim "^0.1.4" + optionalDependencies: + "@unimodules/core" "*" + "@unimodules/react-native-adapter" "*" + expo-random "*" + react-native-securerandom "^0.1.1" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-lib-coverage@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" - integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f" + integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== dependencies: - "@babel/core" "^7.7.5" + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" + istanbul-lib-coverage "^3.2.0" semver "^6.3.0" istanbul-lib-report@^3.0.0: @@ -5653,100 +6337,103 @@ istanbul-lib-report@^3.0.0: supports-color "^7.1.0" istanbul-lib-source-maps@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" - integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" - integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== +istanbul-reports@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.0.6.tgz#bed6183fcdea8a285482e3b50a9a7712d49a7a8b" - integrity sha512-BuL/ZDauaq5dumYh5y20sn4IISnf1P9A0TDswTxUi84ORGtVa86ApuBHqICL0vepqAnZiY6a7xeSPWv2/yy4eA== +jest-changed-files@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" + integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== dependencies: - "@jest/types" "^27.0.6" + "@jest/types" "^27.5.1" execa "^5.0.0" throat "^6.0.1" -jest-circus@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.0.6.tgz#dd4df17c4697db6a2c232aaad4e9cec666926668" - integrity sha512-OJlsz6BBeX9qR+7O9lXefWoc2m9ZqcZ5Ohlzz0pTEAG4xMiZUJoacY8f4YDHxgk0oKYxj277AfOk9w6hZYvi1Q== +jest-circus@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" + integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== dependencies: - "@jest/environment" "^27.0.6" - "@jest/test-result" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" - expect "^27.0.6" + expect "^27.5.1" is-generator-fn "^2.0.0" - jest-each "^27.0.6" - jest-matcher-utils "^27.0.6" - jest-message-util "^27.0.6" - jest-runtime "^27.0.6" - jest-snapshot "^27.0.6" - jest-util "^27.0.6" - pretty-format "^27.0.6" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" slash "^3.0.0" stack-utils "^2.0.3" throat "^6.0.1" -jest-cli@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.0.6.tgz#d021e5f4d86d6a212450d4c7b86cb219f1e6864f" - integrity sha512-qUUVlGb9fdKir3RDE+B10ULI+LQrz+MCflEH2UJyoUjoHHCbxDrMxSzjQAPUMsic4SncI62ofYCcAvW6+6rhhg== +jest-cli@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" + integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== dependencies: - "@jest/core" "^27.0.6" - "@jest/test-result" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/core" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" chalk "^4.0.0" exit "^0.1.2" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^27.0.6" - jest-util "^27.0.6" - jest-validate "^27.0.6" + jest-config "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" prompts "^2.0.1" - yargs "^16.0.3" + yargs "^16.2.0" -jest-config@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.0.6.tgz#119fb10f149ba63d9c50621baa4f1f179500277f" - integrity sha512-JZRR3I1Plr2YxPBhgqRspDE2S5zprbga3swYNrvY3HfQGu7p/GjyLOqwrYad97tX3U3mzT53TPHVmozacfP/3w== +jest-config@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" + integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^27.0.6" - "@jest/types" "^27.0.6" - babel-jest "^27.0.6" + "@babel/core" "^7.8.0" + "@jest/test-sequencer" "^27.5.1" + "@jest/types" "^27.5.1" + babel-jest "^27.5.1" chalk "^4.0.0" + ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.1" - graceful-fs "^4.2.4" - is-ci "^3.0.0" - jest-circus "^27.0.6" - jest-environment-jsdom "^27.0.6" - jest-environment-node "^27.0.6" - jest-get-type "^27.0.6" - jest-jasmine2 "^27.0.6" - jest-regex-util "^27.0.6" - jest-resolve "^27.0.6" - jest-runner "^27.0.6" - jest-util "^27.0.6" - jest-validate "^27.0.6" + graceful-fs "^4.2.9" + jest-circus "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-get-type "^27.5.1" + jest-jasmine2 "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runner "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" micromatch "^4.0.4" - pretty-format "^27.0.6" + parse-json "^5.2.0" + pretty-format "^27.5.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" jest-diff@^26.0.0: version "26.6.2" @@ -5758,68 +6445,68 @@ jest-diff@^26.0.0: jest-get-type "^26.3.0" pretty-format "^26.6.2" -jest-diff@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.0.6.tgz#4a7a19ee6f04ad70e0e3388f35829394a44c7b5e" - integrity sha512-Z1mqgkTCSYaFgwTlP/NUiRzdqgxmmhzHY1Tq17zL94morOHfHu3K4bgSgl+CR4GLhpV8VxkuOYuIWnQ9LnFqmg== +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== dependencies: chalk "^4.0.0" - diff-sequences "^27.0.6" - jest-get-type "^27.0.6" - pretty-format "^27.0.6" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-docblock@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.0.6.tgz#cc78266acf7fe693ca462cbbda0ea4e639e4e5f3" - integrity sha512-Fid6dPcjwepTFraz0YxIMCi7dejjJ/KL9FBjPYhBp4Sv1Y9PdhImlKZqYU555BlN4TQKaTc+F2Av1z+anVyGkA== +jest-docblock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" + integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== dependencies: detect-newline "^3.0.0" -jest-each@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.0.6.tgz#cee117071b04060158dc8d9a66dc50ad40ef453b" - integrity sha512-m6yKcV3bkSWrUIjxkE9OC0mhBZZdhovIW5ergBYirqnkLXkyEn3oUUF/QZgyecA1cF1QFyTE8bRRl8Tfg1pfLA== +jest-each@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" + integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== dependencies: - "@jest/types" "^27.0.6" + "@jest/types" "^27.5.1" chalk "^4.0.0" - jest-get-type "^27.0.6" - jest-util "^27.0.6" - pretty-format "^27.0.6" - -jest-environment-jsdom@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.0.6.tgz#f66426c4c9950807d0a9f209c590ce544f73291f" - integrity sha512-FvetXg7lnXL9+78H+xUAsra3IeZRTiegA3An01cWeXBspKXUhAwMM9ycIJ4yBaR0L7HkoMPaZsozCLHh4T8fuw== - dependencies: - "@jest/environment" "^27.0.6" - "@jest/fake-timers" "^27.0.6" - "@jest/types" "^27.0.6" + jest-get-type "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + +jest-environment-jsdom@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" + integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^27.0.6" - jest-util "^27.0.6" + jest-mock "^27.5.1" + jest-util "^27.5.1" jsdom "^16.6.0" -jest-environment-node@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.0.6.tgz#a6699b7ceb52e8d68138b9808b0c404e505f3e07" - integrity sha512-+Vi6yLrPg/qC81jfXx3IBlVnDTI6kmRr08iVa2hFCWmJt4zha0XW7ucQltCAPhSR0FEKEoJ3i+W4E6T0s9is0w== +jest-environment-node@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" + integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== dependencies: - "@jest/environment" "^27.0.6" - "@jest/fake-timers" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^27.0.6" - jest-util "^27.0.6" + jest-mock "^27.5.1" + jest-util "^27.5.1" jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== -jest-get-type@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe" - integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg== +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== jest-haste-map@^26.5.2: version "26.6.2" @@ -5842,89 +6529,88 @@ jest-haste-map@^26.5.2: optionalDependencies: fsevents "^2.1.2" -jest-haste-map@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.0.6.tgz#4683a4e68f6ecaa74231679dca237279562c8dc7" - integrity sha512-4ldjPXX9h8doB2JlRzg9oAZ2p6/GpQUNAeiYXqcpmrKbP0Qev0wdZlxSMOmz8mPOEnt4h6qIzXFLDi8RScX/1w== +jest-haste-map@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== dependencies: - "@jest/types" "^27.0.6" + "@jest/types" "^27.5.1" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^27.0.6" - jest-serializer "^27.0.6" - jest-util "^27.0.6" - jest-worker "^27.0.6" + graceful-fs "^4.2.9" + jest-regex-util "^27.5.1" + jest-serializer "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" micromatch "^4.0.4" walker "^1.0.7" optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.0.6.tgz#fd509a9ed3d92bd6edb68a779f4738b100655b37" - integrity sha512-cjpH2sBy+t6dvCeKBsHpW41mjHzXgsavaFMp+VWRf0eR4EW8xASk1acqmljFtK2DgyIECMv2yCdY41r2l1+4iA== +jest-jasmine2@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" + integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^27.0.6" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/environment" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^27.0.6" + expect "^27.5.1" is-generator-fn "^2.0.0" - jest-each "^27.0.6" - jest-matcher-utils "^27.0.6" - jest-message-util "^27.0.6" - jest-runtime "^27.0.6" - jest-snapshot "^27.0.6" - jest-util "^27.0.6" - pretty-format "^27.0.6" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" throat "^6.0.1" -jest-leak-detector@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.0.6.tgz#545854275f85450d4ef4b8fe305ca2a26450450f" - integrity sha512-2/d6n2wlH5zEcdctX4zdbgX8oM61tb67PQt4Xh8JFAIy6LRKUnX528HulkaG6nD5qDl5vRV1NXejCe1XRCH5gQ== +jest-leak-detector@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" + integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== dependencies: - jest-get-type "^27.0.6" - pretty-format "^27.0.6" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-matcher-utils@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.0.6.tgz#2a8da1e86c620b39459f4352eaa255f0d43e39a9" - integrity sha512-OFgF2VCQx9vdPSYTHWJ9MzFCehs20TsyFi6bIHbk5V1u52zJOnvF0Y/65z3GLZHKRuTgVPY4Z6LVePNahaQ+tA== +jest-matcher-utils@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== dependencies: chalk "^4.0.0" - jest-diff "^27.0.6" - jest-get-type "^27.0.6" - pretty-format "^27.0.6" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-message-util@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.0.6.tgz#158bcdf4785706492d164a39abca6a14da5ab8b5" - integrity sha512-rBxIs2XK7rGy+zGxgi+UJKP6WqQ+KrBbD1YMj517HYN3v2BG66t3Xan3FWqYHKZwjdB700KiAJ+iES9a0M+ixw== +jest-message-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" + integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== dependencies: "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.0.6" + "@jest/types" "^27.5.1" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" micromatch "^4.0.4" - pretty-format "^27.0.6" + pretty-format "^27.5.1" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.0.6.tgz#0efdd40851398307ba16778728f6d34d583e3467" - integrity sha512-lzBETUoK8cSxts2NYXSBWT+EJNzmUVtVVwS1sU9GwE1DLCfGsngg+ZVSIe0yd0ZSm+y791esiuo+WSwpXJQ5Bw== +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== dependencies: - "@jest/types" "^27.0.6" + "@jest/types" "^27.5.1" "@types/node" "*" jest-pnp-resolver@^1.2.2: @@ -5937,94 +6623,90 @@ jest-regex-util@^26.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-regex-util@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.0.6.tgz#02e112082935ae949ce5d13b2675db3d8c87d9c5" - integrity sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ== +jest-regex-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== -jest-resolve-dependencies@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.0.6.tgz#3e619e0ef391c3ecfcf6ef4056207a3d2be3269f" - integrity sha512-mg9x9DS3BPAREWKCAoyg3QucCr0n6S8HEEsqRCKSPjPcu9HzRILzhdzY3imsLoZWeosEbJZz6TKasveczzpJZA== +jest-resolve-dependencies@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" + integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== dependencies: - "@jest/types" "^27.0.6" - jest-regex-util "^27.0.6" - jest-snapshot "^27.0.6" + "@jest/types" "^27.5.1" + jest-regex-util "^27.5.1" + jest-snapshot "^27.5.1" -jest-resolve@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.0.6.tgz#e90f436dd4f8fbf53f58a91c42344864f8e55bff" - integrity sha512-yKmIgw2LgTh7uAJtzv8UFHGF7Dm7XfvOe/LQ3Txv101fLM8cx2h1QVwtSJ51Q/SCxpIiKfVn6G2jYYMDNHZteA== +jest-resolve@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" + integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== dependencies: - "@jest/types" "^27.0.6" + "@jest/types" "^27.5.1" chalk "^4.0.0" - escalade "^3.1.1" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" jest-pnp-resolver "^1.2.2" - jest-util "^27.0.6" - jest-validate "^27.0.6" + jest-util "^27.5.1" + jest-validate "^27.5.1" resolve "^1.20.0" + resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.0.6.tgz#1325f45055539222bbc7256a6976e993ad2f9520" - integrity sha512-W3Bz5qAgaSChuivLn+nKOgjqNxM7O/9JOJoKDCqThPIg2sH/d4A/lzyiaFgnb9V1/w29Le11NpzTJSzga1vyYQ== +jest-runner@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" + integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== dependencies: - "@jest/console" "^27.0.6" - "@jest/environment" "^27.0.6" - "@jest/test-result" "^27.0.6" - "@jest/transform" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/console" "^27.5.1" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" emittery "^0.8.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-docblock "^27.0.6" - jest-environment-jsdom "^27.0.6" - jest-environment-node "^27.0.6" - jest-haste-map "^27.0.6" - jest-leak-detector "^27.0.6" - jest-message-util "^27.0.6" - jest-resolve "^27.0.6" - jest-runtime "^27.0.6" - jest-util "^27.0.6" - jest-worker "^27.0.6" + graceful-fs "^4.2.9" + jest-docblock "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-haste-map "^27.5.1" + jest-leak-detector "^27.5.1" + jest-message-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runtime "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" source-map-support "^0.5.6" throat "^6.0.1" -jest-runtime@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.0.6.tgz#45877cfcd386afdd4f317def551fc369794c27c9" - integrity sha512-BhvHLRVfKibYyqqEFkybsznKwhrsu7AWx2F3y9G9L95VSIN3/ZZ9vBpm/XCS2bS+BWz3sSeNGLzI3TVQ0uL85Q== - dependencies: - "@jest/console" "^27.0.6" - "@jest/environment" "^27.0.6" - "@jest/fake-timers" "^27.0.6" - "@jest/globals" "^27.0.6" - "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.0.6" - "@jest/transform" "^27.0.6" - "@jest/types" "^27.0.6" - "@types/yargs" "^16.0.0" +jest-runtime@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" + integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/globals" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" - exit "^0.1.2" + execa "^5.0.0" glob "^7.1.3" - graceful-fs "^4.2.4" - jest-haste-map "^27.0.6" - jest-message-util "^27.0.6" - jest-mock "^27.0.6" - jest-regex-util "^27.0.6" - jest-resolve "^27.0.6" - jest-snapshot "^27.0.6" - jest-util "^27.0.6" - jest-validate "^27.0.6" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" slash "^3.0.0" strip-bom "^4.0.0" - yargs "^16.0.3" jest-serializer@^26.6.2: version "26.6.2" @@ -6034,42 +6716,40 @@ jest-serializer@^26.6.2: "@types/node" "*" graceful-fs "^4.2.4" -jest-serializer@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.0.6.tgz#93a6c74e0132b81a2d54623251c46c498bb5bec1" - integrity sha512-PtGdVK9EGC7dsaziskfqaAPib6wTViY3G8E5wz9tLVPhHyiDNTZn/xjZ4khAw+09QkoOVpn7vF5nPSN6dtBexA== +jest-serializer@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== dependencies: "@types/node" "*" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" -jest-snapshot@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.0.6.tgz#f4e6b208bd2e92e888344d78f0f650bcff05a4bf" - integrity sha512-NTHaz8He+ATUagUgE7C/UtFcRoHqR2Gc+KDfhQIyx+VFgwbeEMjeP+ILpUTLosZn/ZtbNdCF5LkVnN/l+V751A== +jest-snapshot@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" + integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== dependencies: "@babel/core" "^7.7.2" "@babel/generator" "^7.7.2" - "@babel/parser" "^7.7.2" "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.0.0" - "@jest/transform" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/babel__traverse" "^7.0.4" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^27.0.6" - graceful-fs "^4.2.4" - jest-diff "^27.0.6" - jest-get-type "^27.0.6" - jest-haste-map "^27.0.6" - jest-matcher-utils "^27.0.6" - jest-message-util "^27.0.6" - jest-resolve "^27.0.6" - jest-util "^27.0.6" + expect "^27.5.1" + graceful-fs "^4.2.9" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + jest-haste-map "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-util "^27.5.1" natural-compare "^1.4.0" - pretty-format "^27.0.6" + pretty-format "^27.5.1" semver "^7.3.2" jest-util@^26.6.2: @@ -6084,16 +6764,16 @@ jest-util@^26.6.2: is-ci "^2.0.0" micromatch "^4.0.2" -jest-util@^27.0.0, jest-util@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.0.6.tgz#e8e04eec159de2f4d5f57f795df9cdc091e50297" - integrity sha512-1JjlaIh+C65H/F7D11GNkGDDZtDfMEM8EBXsvd+l/cxtgQ6QhxuloOaiayt89DxUvDarbVhqI98HhgrM1yliFQ== +jest-util@^27.0.0, jest-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== dependencies: - "@jest/types" "^27.0.6" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" - graceful-fs "^4.2.4" - is-ci "^3.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" picomatch "^2.2.3" jest-validate@^26.5.2: @@ -6108,29 +6788,29 @@ jest-validate@^26.5.2: leven "^3.1.0" pretty-format "^26.6.2" -jest-validate@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.0.6.tgz#930a527c7a951927df269f43b2dc23262457e2a6" - integrity sha512-yhZZOaMH3Zg6DC83n60pLmdU1DQE46DW+KLozPiPbSbPhlXXaiUTDlhHQhHFpaqIFRrInko1FHXjTRpjWRuWfA== +jest-validate@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" + integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== dependencies: - "@jest/types" "^27.0.6" + "@jest/types" "^27.5.1" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^27.0.6" + jest-get-type "^27.5.1" leven "^3.1.0" - pretty-format "^27.0.6" + pretty-format "^27.5.1" -jest-watcher@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.0.6.tgz#89526f7f9edf1eac4e4be989bcb6dec6b8878d9c" - integrity sha512-/jIoKBhAP00/iMGnTwUBLgvxkn7vsOweDrOTSPzc7X9uOyUtJIDthQBTI1EXz90bdkrxorUZVhJwiB69gcHtYQ== +jest-watcher@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" + integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== dependencies: - "@jest/test-result" "^27.0.6" - "@jest/types" "^27.0.6" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^27.0.6" + jest-util "^27.5.1" string-length "^4.0.1" jest-worker@^26.0.0, jest-worker@^26.6.2: @@ -6142,23 +6822,23 @@ jest-worker@^26.0.0, jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed" - integrity sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA== +jest-worker@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^8.0.0" jest@^27.0.4: - version "27.0.6" - resolved "https://registry.yarnpkg.com/jest/-/jest-27.0.6.tgz#10517b2a628f0409087fbf473db44777d7a04505" - integrity sha512-EjV8aETrsD0wHl7CKMibKwQNQc3gIRBXlTikBmmHUeVMKaPFxdcUIBfoDqTSXDoGJIivAYGqCWVlzCSaVjPQsA== + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" + integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== dependencies: - "@jest/core" "^27.0.6" + "@jest/core" "^27.5.1" import-local "^3.0.2" - jest-cli "^27.0.6" + jest-cli "^27.5.1" jetifier@^1.6.2: version "1.6.8" @@ -6166,21 +6846,16 @@ jetifier@^1.6.2: integrity sha512-3Zi16h6L5tXDRQJTb221cnRoVG9/9OvreLdLU2/ZjRv/GILL+2Cemt0IKvkowwkDpvouAU1DQPOJ7qaiHeIdrw== joi@^17.2.1: - version "17.4.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.1.tgz#15d2f23c8cbe4d1baded2dd190c58f8dbe11cca0" - integrity sha512-gDPOwQ5sr+BUxXuPDGrC1pSNcVR/yGGcTI0aCnjYxZEa3za60K/iCQ+OFIkEHWZGVCUcUlXlFKvMmrlmxrG6UQ== + version "17.6.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2" + integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw== dependencies: "@hapi/hoek" "^9.0.0" "@hapi/topo" "^5.0.0" - "@sideway/address" "^4.1.0" + "@sideway/address" "^4.1.3" "@sideway/formula" "^3.0.0" "@sideway/pinpoint" "^2.0.0" -js-sha256@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" - integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6230,9 +6905,9 @@ jscodeshift@^0.11.0: write-file-atomic "^2.3.0" jsdom@^16.6.0: - version "16.6.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.6.0.tgz#f79b3786682065492a3da6a60a4695da983805ac" - integrity sha512-Ty1vmF4NHJkolaEmdjtxTfSfkdb8Ywarwf63f+F8/mDD1uLSSWDxDuMiZxiPhwunLrn9LOSVItWj4bLYsLN3Dg== + version "16.7.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== dependencies: abab "^2.0.5" acorn "^8.2.4" @@ -6259,7 +6934,7 @@ jsdom@^16.6.0: whatwg-encoding "^1.0.5" whatwg-mimetype "^2.3.0" whatwg-url "^8.5.0" - ws "^7.4.5" + ws "^7.4.6" xml-name-validator "^3.0.0" jsesc@^2.5.1: @@ -6292,10 +6967,10 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" @@ -6314,12 +6989,17 @@ json-text-sequence@~0.3.0: dependencies: "@sovpro/delimited-stream" "^1.1.0" -json5@2.x, json5@^2.1.2, json5@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== +json5@2.x, json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== dependencies: - minimist "^1.2.5" + minimist "^1.2.0" jsonfile@^2.1.0: version "2.4.0" @@ -6349,19 +7029,29 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonld@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-5.2.0.tgz#d1e8af38a334cb95edf0f2ae4e2b58baf8d2b5a9" + integrity sha512-JymgT6Xzk5CHEmHuEyvoTNviEPxv6ihLWSPu1gFdtjSAyM6cFqNrv02yS/SIur3BBIkCf0HjizRc24d8/FfQKw== + dependencies: + "@digitalbazaar/http-client" "^1.1.0" + canonicalize "^1.0.1" + lru-cache "^6.0.0" + rdf-canonize "^3.0.0" + jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== dependencies: assert-plus "1.0.0" extsprintf "1.3.0" - json-schema "0.2.3" + json-schema "0.4.0" verror "1.10.0" kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: @@ -6400,6 +7090,19 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +ky-universal@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.8.2.tgz#edc398d54cf495d7d6830aa1ab69559a3cc7f824" + integrity sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ== + dependencies: + abort-controller "^3.0.0" + node-fetch "3.0.0-beta.9" + +ky@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc" + integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA== + lerna@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" @@ -6467,14 +7170,14 @@ libnpmpublish@^4.0.0: ssri "^8.0.1" libphonenumber-js@^1.9.7: - version "1.9.43" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.43.tgz#2371e4383e6780990381d5b900b8c22666221cbb" - integrity sha512-tNB87ZutAiAkl3DE/Bo0Mxqn/XZbNxhPg4v9bYBwQQW4dlhBGqXl1vtmPxeDWbrijzwOA9vRjOOFm5V9SK/W3w== + version "1.10.4" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.4.tgz#90397f0ed620262570a32244c9fbc389cc417ce4" + integrity sha512-9QWxEk4GW5RDnFzt8UtyRENfFpAN8u7Sbf9wf32tcXY9tdtnz1dKHIBwW2Wnfx8ypXJb9zUnTpK9aQJ/B8AlnA== lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== load-json-file@^4.0.0: version "4.0.0" @@ -6519,15 +7222,22 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= lodash.debounce@^4.0.8: version "4.0.8" @@ -6539,6 +7249,11 @@ lodash.ismatch@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -6569,7 +7284,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@4.x, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.7.0: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6634,7 +7349,7 @@ make-error@1.x, make-error@^1.1.1, make-error@^1.3.6: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -make-fetch-happen@^8.0.14, make-fetch-happen@^8.0.9: +make-fetch-happen@^8.0.9: version "8.0.14" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222" integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== @@ -6655,10 +7370,10 @@ make-fetch-happen@^8.0.14, make-fetch-happen@^8.0.9: socks-proxy-agent "^5.0.0" ssri "^8.0.0" -make-fetch-happen@^9.0.1: - version "9.0.4" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.0.4.tgz#ceaa100e60e0ef9e8d1ede94614bb2ba83c8bb24" - integrity sha512-sQWNKMYqSmbAGXqJg2jZ+PmHh5JAybvwu0xM8mZR/bsTjGiTASj3ldXJV7KFHy1k/IJIBkjxQFoWIVsv9+PQMg== +make-fetch-happen@^9.0.1, make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== dependencies: agentkeepalive "^4.1.3" cacache "^15.2.0" @@ -6674,15 +7389,20 @@ make-fetch-happen@^9.0.1: minipass-pipeline "^1.2.4" negotiator "^0.6.2" promise-retry "^2.0.1" - socks-proxy-agent "^5.0.0" + socks-proxy-agent "^6.0.0" ssri "^8.0.0" -makeerror@1.0.x: - version "1.0.11" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" - integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= +make-promises-safe@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/make-promises-safe/-/make-promises-safe-5.1.0.tgz#dd9d311f555bcaa144f12e225b3d37785f0aa8f2" + integrity sha512-AfdZ49rtyhQR/6cqVKGoH7y4ql7XkS5HJI1lZm0/5N6CQosy1eYbBJ/qbhkKHzo17UH7M918Bysf6XB9f3kS1g== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: - tmpl "1.0.x" + tmpl "1.0.5" map-cache@^0.2.2: version "0.2.2" @@ -6695,9 +7415,9 @@ map-obj@^1.0.0: integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= map-obj@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.1.tgz#e4ea399dbc979ae735c83c863dd31bdf364277b7" - integrity sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ== + version "4.3.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" + integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== map-visit@^1.0.0: version "1.0.0" @@ -6711,23 +7431,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -meow@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" - integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^2.5.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.13.1" - yargs-parser "^18.1.3" - meow@^8.0.0: version "8.1.2" resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" @@ -6755,7 +7458,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -7047,24 +7750,24 @@ micromatch@^3.1.10, micromatch@^3.1.4: to-regex "^3.0.2" micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + braces "^3.0.2" + picomatch "^2.3.1" -mime-db@1.49.0, "mime-db@>= 1.43.0 < 2": - version "1.49.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" - integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.32" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" - integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.49.0" + mime-db "1.52.0" mime@1.6.0: version "1.6.0" @@ -7072,9 +7775,9 @@ mime@1.6.0: integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mime@^2.4.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" - integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== mimic-fn@^1.0.0: version "1.2.0" @@ -7091,10 +7794,10 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@^3.0.2, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -7107,10 +7810,10 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== minipass-collect@^1.0.2: version "1.0.2" @@ -7120,9 +7823,9 @@ minipass-collect@^1.0.2: minipass "^3.0.0" minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: - version "1.3.4" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.4.tgz#63f5af868a38746ca7b33b03393ddf8c291244fe" - integrity sha512-TielGogIzbUEtd1LsjZFs47RWuHHfhl6TiCx1InVxApBAmQ8bL0dL5ilkLGcRvuyW/A9nE+Lvn855Ewz8S0PnQ== + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== dependencies: minipass "^3.1.0" minipass-sized "^1.0.3" @@ -7168,9 +7871,9 @@ minipass@^2.6.0, minipass@^2.9.0: yallist "^3.0.0" minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" - integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + version "3.1.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== dependencies: yallist "^4.0.0" @@ -7206,18 +7909,18 @@ mkdirp-infer-owner@^2.0.0: infer-owner "^1.0.4" mkdirp "^1.0.3" -mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: +mkdirp@^0.5.1, mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^0.5.1, mkdirp@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -7228,41 +7931,20 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0, ms@^2.1.1: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -multibase@^4.0.1, multibase@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/multibase/-/multibase-4.0.4.tgz#55ef53e6acce223c5a09341a8a3a3d973871a577" - integrity sha512-8/JmrdSGzlw6KTgAJCOqUBSGd1V6186i/X8dDCGy/lbCKrQ+1QB6f3HE+wPr7Tpdj4U3gutaj9jG2rNX6UpiJg== - dependencies: - "@multiformats/base-x" "^4.0.1" - -multiformats@^9.4.2: - version "9.4.3" - resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.4.3.tgz#9da626a633ed43a4444b911eaf3344060326be5d" - integrity sha512-sCNjBP/NPCeQu83Mst8IQZq9+HuR7Catvk/m7CeH0r/nupsU6gM7GINf5E1HCDRxDeU+Cgda/WPmcwQhYs3dyA== - -multihashes@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-4.0.2.tgz#d76aeac3a302a1bed9fe1ec964fb7a22fa662283" - integrity sha512-xpx++1iZr4ZQHjN1mcrXS6904R36LWLxX/CBifczjtmrtCXEX623DMWOF1eiNSg+pFpiZDFVBgou/4v6ayCHSQ== - dependencies: - multibase "^4.0.1" - uint8arrays "^2.1.3" - varint "^5.0.2" +msrcrypto@^1.5.6: + version "1.5.8" + resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c" + integrity sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q== multimatch@^5.0.0: version "5.0.0" @@ -7281,9 +7963,9 @@ mute-stream@0.0.8, mute-stream@~0.0.4: integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== nan@^2.11.1: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== nanomatch@^1.2.9: version "1.2.13" @@ -7307,16 +7989,45 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -negotiator@0.6.2, negotiator@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +needle@^2.5.2: + version "2.9.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.9.1.tgz#22d1dffbe3490c2b83e301f7709b6736cd8f2684" + integrity sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + +negotiator@0.6.3, negotiator@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== neo-async@^2.5.0, neo-async@^2.6.0: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +neon-cli@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/neon-cli/-/neon-cli-0.8.2.tgz#5111b0e9d5d90273bdf85a9aa40a1a47a32df2ef" + integrity sha512-vYRBmiLiwPVeBvR9huCFXRAtdLYfsoSG3hgsXrcuyMSXk7yqpnZlgvOGGuxfhrRb/iNfcd0M0cEs0j22mDgZ+A== + dependencies: + chalk "^4.1.0" + command-line-args "^5.1.1" + command-line-commands "^3.0.1" + command-line-usage "^6.1.0" + git-config "0.0.7" + handlebars "^4.7.6" + inquirer "^7.3.3" + make-promises-safe "^5.1.0" + rimraf "^3.0.2" + semver "^7.3.2" + toml "^3.0.0" + ts-typed-json "^0.3.2" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -7327,6 +8038,11 @@ nocache@^2.1.0: resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f" integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q== +node-addon-api@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + node-dir@^0.1.17: version "0.1.17" resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" @@ -7334,10 +8050,25 @@ node-dir@^0.1.17: dependencies: minimatch "^3.0.2" -node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@2.6.7, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@3.0.0-beta.9: + version "3.0.0-beta.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.0.0-beta.9.tgz#0a7554cfb824380dd6812864389923c783c80d9b" + integrity sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg== + dependencies: + data-uri-to-buffer "^3.0.1" + fetch-blob "^2.1.1" + +node-gyp-build@^4.2.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4" + integrity sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ== node-gyp@^5.0.2: version "5.1.1" @@ -7373,19 +8104,19 @@ node-gyp@^7.1.0: which "^2.0.2" node-gyp@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.1.0.tgz#81f43283e922d285c886fb0e0f520a7fd431d8c2" - integrity sha512-o2elh1qt7YUp3lkMwY3/l4KF3j/A3fI/Qt4NH+CQQgPJdqGE9y7qnP84cjIWN27Q0jJkrSAhCVDg+wBVNBYdBg== + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== dependencies: env-paths "^2.2.0" glob "^7.1.4" graceful-fs "^4.2.6" - make-fetch-happen "^8.0.14" + make-fetch-happen "^9.1.0" nopt "^5.0.0" - npmlog "^4.1.2" + npmlog "^6.0.0" rimraf "^3.0.2" semver "^7.3.5" - tar "^6.1.0" + tar "^6.1.2" which "^2.0.2" node-int64@^0.4.0: @@ -7393,22 +8124,33 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-modules-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" - integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= +node-pre-gyp@0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.17.0.tgz#5af3f7b4c3848b5ed00edc3d298ff836daae5f1d" + integrity sha512-abzZt1hmOjkZez29ppg+5gGqdPLUuJeAEwVPtHYEJgx0qzttCbcKFpxrCQn2HYbwCv2c+7JwH4BgEzFkUGpn4A== + dependencies: + detect-libc "^1.0.3" + mkdirp "^0.5.5" + needle "^2.5.2" + nopt "^4.0.3" + npm-packlist "^1.4.8" + npmlog "^4.1.2" + rc "^1.2.8" + rimraf "^2.7.1" + semver "^5.7.1" + tar "^4.4.13" -node-releases@^1.1.71: - version "1.1.73" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" - integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg== +node-releases@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" + integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== node-stream-zip@^1.9.1: - version "1.14.0" - resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.14.0.tgz#fdf9b86d10d55c1e50aa1be4fea88bae256c4eba" - integrity sha512-SKXyiBy9DBemsPHf/piHT00Y+iPK+zwru1G6+8UdOBzITnmmPMHYBMV6M1znyzyhDhUFQW0HEmbGiPqtp51M6Q== + version "1.15.0" + resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" + integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw== -nopt@^4.0.1: +nopt@^4.0.1, nopt@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== @@ -7434,12 +8176,12 @@ normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package- validate-npm-package-license "^3.0.1" normalize-package-data@^3.0.0, normalize-package-data@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.2.tgz#cae5c410ae2434f9a6c1baa65d5bc3b9366c8699" - integrity sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg== + version "3.0.3" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== dependencies: hosted-git-info "^4.0.1" - resolve "^1.20.0" + is-core-module "^2.5.0" semver "^7.3.4" validate-npm-package-license "^3.0.1" @@ -7460,7 +8202,7 @@ normalize-url@^6.1.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== -npm-bundled@^1.1.1: +npm-bundled@^1.0.1, npm-bundled@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== @@ -7493,7 +8235,7 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== -npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.2: +npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.2, npm-package-arg@^8.1.5: version "8.1.5" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.5.tgz#3369b2d5fe8fdc674baa7f1786514ddc15466e44" integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== @@ -7502,6 +8244,15 @@ npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-pack semver "^7.3.4" validate-npm-package-name "^3.0.0" +npm-packlist@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" + integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + npm-normalize-package-bin "^1.0.1" + npm-packlist@^2.1.4: version "2.2.2" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-2.2.2.tgz#076b97293fa620f632833186a7a8f65aaa6148c8" @@ -7572,6 +8323,16 @@ npmlog@^4.1.2: gauge "~2.7.3" set-blocking "~2.0.0" +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + nullthrows@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" @@ -7611,12 +8372,12 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.10.3, object-inspect@^1.9.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" - integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== +object-inspect@^1.10.3, object-inspect@^1.12.0, object-inspect@^1.9.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== -object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -7639,13 +8400,13 @@ object.assign@^4.1.0, object.assign@^4.1.2: object-keys "^1.1.1" object.getownpropertydescriptors@^2.0.3: - version "2.1.2" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" - integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e" + integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" + es-abstract "^1.19.1" object.pick@^1.3.0: version "1.3.0" @@ -7654,14 +8415,21 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" - integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== +object.values@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.2" + es-abstract "^1.19.1" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" on-finished@~2.3.0: version "2.3.0" @@ -7762,11 +8530,6 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -p-each-series@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" - integrity sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA== - p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -7786,6 +8549,13 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -7807,6 +8577,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map-series@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" @@ -7901,7 +8678,7 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -7971,7 +8748,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -7998,10 +8775,15 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== pify@^2.3.0: version "2.3.0" @@ -8023,19 +8805,10 @@ pify@^5.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== -pirates@^4.0.0, pirates@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" - integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== - dependencies: - node-modules-regexp "^1.0.0" - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" +pirates@^4.0.4, pirates@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== pkg-dir@^3.0.0: version "3.0.0" @@ -8051,21 +8824,13 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" - integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= - dependencies: - find-up "^2.1.0" - -plist@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.2.tgz#74bbf011124b90421c22d15779cee60060ba95bc" - integrity sha512-MSrkwZBdQ6YapHy87/8hDU8MnIcyxBKjeF+McXnr5A9MtffPewTs7G3hlpodT5TacyfIyFTaJEhh3GGcmasTgQ== +plist@^3.0.1, plist@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.5.tgz#2cbeb52d10e3cdccccf0c11a63a85d830970a987" + integrity sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA== dependencies: base64-js "^1.5.1" xmlbuilder "^9.0.7" - xmldom "^0.5.0" posix-character-classes@^0.1.0: version "0.1.1" @@ -8090,9 +8855,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" - integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== + version "2.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" + integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== pretty-format@^26.0.0, pretty-format@^26.5.2, pretty-format@^26.6.2: version "26.6.2" @@ -8104,13 +8869,12 @@ pretty-format@^26.0.0, pretty-format@^26.5.2, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" -pretty-format@^27.0.6: - version "27.0.6" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.0.6.tgz#ab770c47b2c6f893a21aefc57b75da63ef49a11f" - integrity sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ== +pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== dependencies: - "@jest/types" "^27.0.6" - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" ansi-styles "^5.0.0" react-is "^17.0.1" @@ -8145,9 +8909,9 @@ promise@^8.0.3: asap "~2.0.6" prompts@^2.0.1, prompts@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" - integrity sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ== + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3" sisteransi "^1.0.5" @@ -8160,13 +8924,13 @@ promzard@^0.3.0: read "1" prop-types@^15.7.2: - version "15.7.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: loose-envify "^1.4.0" object-assign "^4.1.1" - react-is "^16.8.1" + react-is "^16.13.1" proto-list@~1.2.1: version "1.2.4" @@ -8178,7 +8942,7 @@ protocols@^1.1.0, protocols@^1.4.0: resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== -proxy-addr@~2.0.5: +proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -8204,27 +8968,34 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +pvtsutils@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de" + integrity sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ== + dependencies: + tslib "^2.4.0" + +pvutils@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" + integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== + q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -qs@^6.9.4: - version "6.10.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" - integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== +qs@6.10.3, qs@^6.9.4: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== dependencies: side-channel "^1.0.4" qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== query-string@^6.13.8: version "6.14.1" @@ -8237,9 +9008,9 @@ query-string@^6.13.8: strict-uri-encode "^2.0.0" query-string@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.0.1.tgz#45bd149cf586aaa582dffc7ec7a8ad97dd02f75d" - integrity sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA== + version "7.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1" + integrity sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w== dependencies: decode-uri-component "^0.2.0" filter-obj "^1.1.0" @@ -8261,25 +9032,42 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== dependencies: - bytes "3.1.0" - http-errors "1.7.2" + bytes "3.1.2" + http-errors "2.0.0" iconv-lite "0.4.24" unpipe "1.0.0" +rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +rdf-canonize@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rdf-canonize/-/rdf-canonize-3.0.0.tgz#f5bade563e5e58f5cc5881afcba3c43839e8c747" + integrity sha512-LXRkhab1QaPJnhUIt1gtXXKswQCZ9zpflsSZFczG7mCLAkMvVjdqCGk9VXCUss0aOUeEyV2jtFxGcdX8DSkj9w== + dependencies: + setimmediate "^1.0.5" + react-devtools-core@^4.6.0: - version "4.14.0" - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.14.0.tgz#4b9dc50937ed4cf4c04fa293430cac62d829fa8b" - integrity sha512-cE7tkSUkGCDxTA79pntDGJCBgzNN/XxA3kgPdXujdfSfEfVhzrItQIEsN0kCN/hJJACDvH2Q8p5+tJb/K4B3qA== + version "4.24.6" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.24.6.tgz#3262114f483465179c97a49b7ada845048f4f97e" + integrity sha512-+6y6JAtAo1NUUxaCwCYTb13ViBpc7RjNTlj1HZRlDJmi7UYToj5+BNn8Duzz2YizzAzmRUWZkRM7OtqxnN6TnA== dependencies: shell-quote "^1.6.1" ws "^7" -react-is@^16.8.1: +react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -8299,20 +9087,27 @@ react-native-codegen@^0.0.6: nullthrows "^1.1.1" react-native-fs@^2.18.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.18.0.tgz#987b99cc90518ef26663a8d60e62104694b41c21" - integrity sha512-9iQhkUNnN2JNED0in06JwZy88YEVyIGKWz4KLlQYxa5Y2U0U2AZh9FUHtA04oWj+xt2LlHh0LFPCzhmNsAsUDg== + version "2.20.0" + resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" + integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== dependencies: base-64 "^0.1.0" utf8 "^3.0.0" react-native-get-random-values@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.7.0.tgz#86d9d1960828b606392dba4540bf760605448530" - integrity sha512-zDhmpWUekGRFb9I+MQkxllHcqXN9HBSsgPwBQfrZ1KZYpzDspWLZ6/yLMMZrtq4pVqNR7C7N96L3SuLpXv1nhQ== + version "1.8.0" + resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.8.0.tgz#1cb4bd4bd3966a356e59697b8f372999fe97cb16" + integrity sha512-H/zghhun0T+UIJLmig3+ZuBCvF66rdbiWUfRSNS6kv5oDSpa1ZiVyvRWtuPesQpT8dXj+Bv7WJRQOUP+5TB1sA== dependencies: fast-base64-decode "^1.0.0" +react-native-securerandom@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/react-native-securerandom/-/react-native-securerandom-0.1.1.tgz#f130623a412c338b0afadedbc204c5cbb8bf2070" + integrity sha1-8TBiOkEsM4sK+t7bwgTFy7i/IHA= + dependencies: + base64-js "*" + react-native@0.64.2: version "0.64.2" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.64.2.tgz#233b6ed84ac4749c8bc2a2d6cf63577a1c437d18" @@ -8387,7 +9182,7 @@ read-package-json@^2.0.0: normalize-package-data "^2.0.0" npm-normalize-package-bin "^1.0.0" -read-package-json@^3.0.0, read-package-json@^3.0.1: +read-package-json@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-3.0.1.tgz#c7108f0b9390257b08c21e3004d2404c806744b9" integrity sha512-aLcPqxovhJTVJcsnROuuzQvv6oziQx4zd3JvG0vGCL5MjTONUc4uJ90zCBC6R7W7oUKBNoR/F8pkyfVwlbxqng== @@ -8397,6 +9192,16 @@ read-package-json@^3.0.0, read-package-json@^3.0.1: normalize-package-data "^3.0.0" npm-normalize-package-bin "^1.0.0" +read-package-json@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-4.1.2.tgz#b444d047de7c75d4a160cb056d00c0693c1df703" + integrity sha512-Dqer4pqzamDE2O4M55xp1qZMuLPqi4ldk2ya648FOMHRjwMzFhuxVrG04wd0c38IsvkVdr3vgHI6z+QTPdAjrQ== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + read-package-tree@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" @@ -8506,19 +9311,41 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + +"ref-napi@^2.0.1 || ^3.0.2", ref-napi@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/ref-napi/-/ref-napi-3.0.3.tgz#e259bfc2bbafb3e169e8cd9ba49037dd00396b22" + integrity sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA== + dependencies: + debug "^4.1.1" + get-symbol-from-current-process-h "^1.0.2" + node-addon-api "^3.0.0" + node-gyp-build "^4.2.1" + +ref-struct-di@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ref-struct-di/-/ref-struct-di-1.1.1.tgz#5827b1d3b32372058f177547093db1fe1602dc10" + integrity sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g== + dependencies: + debug "^3.1.0" + reflect-metadata@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== -regenerate-unicode-properties@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" - integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== +regenerate-unicode-properties@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" + integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== dependencies: - regenerate "^1.4.0" + regenerate "^1.4.2" -regenerate@^1.4.0: +regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== @@ -8528,10 +9355,10 @@ regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== +regenerator-transform@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" + integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== dependencies: "@babel/runtime" "^7.8.4" @@ -8543,32 +9370,41 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + regexpp@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regexpu-core@^4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" - integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== +regexpu-core@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3" + integrity sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw== dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.0.1" + regjsgen "^0.6.0" + regjsparser "^0.8.2" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" -regjsgen@^0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" - integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== +regjsgen@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" + integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== -regjsparser@^0.6.4: - version "0.6.9" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.9.tgz#b489eef7c9a2ce43727627011429cf833a7183e6" - integrity sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ== +regjsparser@^0.8.2: + version "0.8.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" + integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== dependencies: jsesc "~0.5.0" @@ -8655,13 +9491,19 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== +resolve.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" restore-cursor@^2.0.0: version "2.0.0" @@ -8694,7 +9536,12 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^2.5.4, rimraf@^2.6.3: +rfc4648@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd" + integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg== + +rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -8744,19 +9591,19 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" -rxjs@^7.1.0, rxjs@^7.2.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.3.0.tgz#39fe4f3461dc1e50be1475b2b85a0a88c1e938c6" - integrity sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw== +rxjs@^7.2.0: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== dependencies: - tslib "~2.1.0" + tslib "^2.1.0" safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -8788,7 +9635,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sax@^1.2.1: +sax@^1.2.1, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -8819,9 +9666,9 @@ semver@7.0.0: integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== semver@7.x, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" @@ -8830,39 +9677,46 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== dependencies: debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" + depd "2.0.0" + destroy "1.2.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.7.2" + http-errors "2.0.0" mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" + ms "2.1.3" + on-finished "2.4.1" range-parser "~1.2.1" - statuses "~1.5.0" + statuses "2.0.1" serialize-error@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" integrity sha1-ULZ51WNc34Rme9yOWa9OW4HV9go= -serve-static@1.14.1, serve-static@^1.13.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== +serialize-error@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" + integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== + dependencies: + type-fest "^0.20.2" + +serve-static@1.15.0, serve-static@^1.13.1: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.17.1" + send "0.18.0" set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" @@ -8879,10 +9733,15 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shallow-clone@^3.0.0: version "3.0.1" @@ -8926,14 +9785,14 @@ shell-quote@1.6.1: jsonify "~0.0.0" shell-quote@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== shelljs@^0.8.4: - version "0.8.4" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" - integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -8948,19 +9807,19 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== simple-plist@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.1.1.tgz#54367ca28bc5996a982c325c1c4a4c1a05f4047c" - integrity sha512-pKMCVKvZbZTsqYR6RKgLfBHkh2cV89GXcA/0CVPje3sOiNOnXA8+rp/ciAMZ7JRaUdLzlEM6JFfUn+fS6Nt3hg== + version "1.3.1" + resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.3.1.tgz#16e1d8f62c6c9b691b8383127663d834112fb017" + integrity sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw== dependencies: - bplist-creator "0.0.8" - bplist-parser "0.2.0" - plist "^3.0.1" + bplist-creator "0.1.0" + bplist-parser "0.3.1" + plist "^3.0.5" sisteransi@^1.0.5: version "1.0.5" @@ -8995,10 +9854,10 @@ slide@^1.1.6: resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= -smart-buffer@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" - integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== snapdragon-node@^2.0.1: version "2.1.1" @@ -9039,13 +9898,22 @@ socks-proxy-agent@^5.0.0: debug "4" socks "^2.3.3" -socks@^2.3.3: - version "2.6.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" - integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== +socks-proxy-agent@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.0.tgz#f6b5229cc0cbd6f2f202d9695f09d871e951c85e" + integrity sha512-wWqJhjb32Q6GsrUqzuFkukxb/zzide5quXYcMVpIjxalDBBYy2nqKCFQ/9+Ie4dvOYSQdOk3hUlZSdzZOd3zMQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.3.3, socks@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.2.tgz#ec042d7960073d40d94268ff3bb727dc685f111a" + integrity sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA== dependencies: ip "^1.1.5" - smart-buffer "^4.1.0" + smart-buffer "^4.2.0" sort-keys@^2.0.0: version "2.0.0" @@ -9072,10 +9940,10 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.16, source-map-support@^0.5.17, source-map-support@^0.5.19, source-map-support@^0.5.6: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== +source-map-support@^0.5.16, source-map-support@^0.5.21, source-map-support@^0.5.6: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -9085,7 +9953,7 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== -source-map@^0.5.0, source-map@^0.5.6: +source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -9122,9 +9990,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz#8a595135def9592bda69709474f1cbeea7c2467f" - integrity sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ== + version "3.0.11" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" + integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== split-on-first@^1.0.0: version "1.1.0" @@ -9158,9 +10026,9 @@ sprintf-js@~1.0.2: integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -9180,16 +10048,16 @@ ssri@^8.0.0, ssri@^8.0.1: minipass "^3.1.1" stack-utils@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" - integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw== + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== dependencies: escape-string-regexp "^2.0.0" stackframe@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303" - integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA== + version "1.2.1" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.1.tgz#1033a3473ee67f08e2f2fc8eba6aef4f845124e1" + integrity sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg== stacktrace-parser@^0.1.3: version "0.1.10" @@ -9206,12 +10074,22 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.5.0 < 2", statuses@~1.5.0: +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -stream-buffers@~2.2.0: +str2buf@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/str2buf/-/str2buf-1.3.0.tgz#a4172afff4310e67235178e738a2dbb573abead0" + integrity sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA== + +stream-buffers@2.2.x: version "2.2.0" resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" integrity sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ= @@ -9238,38 +10116,32 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" + define-properties "^1.1.4" + es-abstract "^1.19.5" -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" + define-properties "^1.1.4" + es-abstract "^1.19.5" string_decoder@^1.1.1: version "1.3.0" @@ -9292,13 +10164,6 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - strip-ansi@^5.0.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -9306,12 +10171,12 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" strip-bom@^3.0.0: version "3.0.0" @@ -9345,6 +10210,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + strong-log-transformer@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" @@ -9388,24 +10258,38 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +table-layout@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + table@^6.0.9: - version "6.7.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" - integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== + version "6.8.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" + integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== dependencies: ajv "^8.0.1" - lodash.clonedeep "^4.5.0" lodash.truncate "^4.4.2" slice-ansi "^4.0.0" - string-width "^4.2.0" - strip-ansi "^6.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" -tar@^4.4.12: +tar@^4.4.12, tar@^4.4.13: version "4.4.19" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== @@ -9418,10 +10302,10 @@ tar@^4.4.12: safe-buffer "^5.2.1" yallist "^3.1.1" -tar@^6.0.2, tar@^6.1.0: - version "6.1.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.2.tgz#1f045a90a6eb23557a603595f41a16c57d47adc6" - integrity sha512-EwKEgqJ7nJoS+s8QfLYVGMDmAsj+StbI2AM/RTHeUSsOw6Z8bwNBRv5z3CY0m7laC5qUAqruLX5AhMuc5deY3Q== +tar@^6.0.2, tar@^6.1.0, tar@^6.1.2: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -9525,7 +10409,7 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" -tmpl@1.0.x: +tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== @@ -9567,10 +10451,15 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== tough-cookie@^4.0.0: version "4.0.0" @@ -9596,55 +10485,62 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== -trim-off-newlines@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" - integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM= - ts-jest@^27.0.3: - version "27.0.4" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.0.4.tgz#df49683535831560ccb58f94c023d831b1b80df0" - integrity sha512-c4E1ECy9Xz2WGfTMyHbSaArlIva7Wi2p43QOMmCqjSSjHP06KXv+aT+eSY+yZMuqsMi3k7pyGsGj2q5oSl5WfQ== + version "27.1.5" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.5.tgz#0ddf1b163fbaae3d5b7504a1e65c914a95cff297" + integrity sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA== dependencies: bs-logger "0.x" - buffer-from "1.x" fast-json-stable-stringify "2.x" jest-util "^27.0.0" json5 "2.x" - lodash "4.x" + lodash.memoize "4.x" make-error "1.x" - mkdirp "1.x" semver "7.x" yargs-parser "20.x" -ts-node@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.1.0.tgz#e656d8ad3b61106938a867f69c39a8ba6efc966e" - integrity sha512-6szn3+J9WyG2hE+5W8e0ruZrzyk1uFLYye6IGMBadnOzDh8aP7t8CbFpsfCiEx2+wMixAhjFt7lOZC4+l+WbEA== +ts-node@^10.0.0, ts-node@^10.4.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" + integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== dependencies: + "@cspotcode/source-map-support" "0.7.0" "@tsconfig/node10" "^1.0.7" "@tsconfig/node12" "^1.0.7" "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.1" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" arg "^4.1.0" create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.17" + v8-compile-cache-lib "^3.0.0" yn "3.1.1" -tsconfig-paths@^3.9.0: - version "3.10.1" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz#79ae67a68c15289fdf5c51cb74f397522d795ed7" - integrity sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q== +ts-typed-json@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ts-typed-json/-/ts-typed-json-0.3.2.tgz#f4f20f45950bae0a383857f7b0a94187eca1b56a" + integrity sha512-Tdu3BWzaer7R5RvBIJcg9r8HrTZgpJmsX+1meXMJzYypbkj8NK2oJN0yvm4Dp/Iv6tzFa/L5jKRmEVTga6K3nA== + +tsconfig-paths@^3.14.1, tsconfig-paths@^3.9.0: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== dependencies: - json5 "^2.2.0" - minimist "^1.2.0" + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" strip-bom "^3.0.0" tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: @@ -9652,22 +10548,17 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" - integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== - -tslib@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== tslog@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.2.0.tgz#4982c289a8948670d6a1c49c29977ae9f861adfa" - integrity sha512-xOCghepl5w+wcI4qXI7vJy6c53loF8OoC/EuKz1ktAPMtltEDz00yo1poKuyBYIQaq4ZDYKYFPD9PfqVrFXh0A== + version "3.3.3" + resolved "https://registry.yarnpkg.com/tslog/-/tslog-3.3.3.tgz#751a469e0d36841bd7e03676c27e53e7ffe9bc3d" + integrity sha512-lGrkndwpAohZ9ntQpT+xtUw5k9YFV1DjsksiWQlBSf82TTqsSAWBARPRD9juI730r8o3Awpkjp2aXy9k+6vr+g== dependencies: - source-map-support "^0.5.19" + source-map-support "^0.5.21" tsutils@^3.21.0: version "3.21.0" @@ -9676,7 +10567,7 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tsyringe@^4.5.0: +tsyringe@^4.5.0, tsyringe@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.6.0.tgz#14915d3d7f0db35e1cf7269bdbf7c440713c8d07" integrity sha512-BMQAZamSfEmIQzH8WJeRu1yZGQbPSDuI9g+yEiKZFIcO46GPZuMOC2d0b52cVBdw1d++06JnDSIIZvEnogMdAw== @@ -9714,11 +10605,6 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" - integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== - type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -9754,7 +10640,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.17, type-is@~1.6.18: +type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -9774,11 +10660,21 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^4.3.0, typescript@~4.3.0: +typescript@~4.3.0: version "4.3.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + uglify-es@^3.1.9: version "3.3.9" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" @@ -9788,22 +10684,15 @@ uglify-es@^3.1.9: source-map "~0.6.1" uglify-js@^3.1.4: - version "3.14.1" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.1.tgz#e2cb9fe34db9cb4cf7e35d1d26dfea28e09a7d06" - integrity sha512-JhS3hmcVaXlp/xSo3PKY5R0JqKs5M3IV+exdLHW99qKvKivPO4Z8qbej6mte17SOPqAOVMjt/XGgWacnFSzM3g== + version "3.15.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.5.tgz#2b10f9e0bfb3f5c15a8e8404393b6361eaeb33b3" + integrity sha512-hNM5q5GbBRB5xB+PMqVRcgYe4c8jbyZ1pzZhS6jbq54/4F2gFK869ZheiE5A8/t+W5jtTNpWef/5Q9zk639FNQ== uid-number@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= -uint8arrays@^2.1.3: - version "2.1.8" - resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.8.tgz#79394390ba93c7d858ce5703705dcf9012f0c9d4" - integrity sha512-qpZ/B88mSea11W3LvoimtnGWIC2i3gGuXby5wBkn8jY+OFulbaQwyjpOYSyrASqgcNEvKdAkLiOwiUt5cPSdcQ== - dependencies: - multiformats "^9.4.2" - ultron@1.0.x: version "1.0.2" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" @@ -9814,38 +10703,38 @@ umask@^1.1.0: resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" - integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== +unicode-match-property-value-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" + integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== -unicode-property-aliases-ecmascript@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" - integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== +unicode-property-aliases-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" + integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== union-value@^1.0.0: version "1.0.1" @@ -9917,11 +10806,16 @@ urix@^0.1.0: integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= use-subscription@^1.0.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" - integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== + version "1.7.0" + resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.7.0.tgz#c7505263315deac9fd2581cdf4ab1e3ff2585d0f" + integrity sha512-87x6MjiIVE/BWqtxfiRvM6jfvGudN+UeVOnWi7qKYp2c0YJn5+Z5Jt0kZw6Tt+8hs7kw/BWo2WBhizJSAZsQJA== dependencies: - object-assign "^4.1.1" + use-sync-external-store "^1.1.0" + +use-sync-external-store@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82" + integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ== use@^3.1.0: version "3.1.1" @@ -9960,15 +10854,20 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -v8-to-istanbul@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.0.0.tgz#4229f2a99e367f3f018fa1d5c2b8ec684667c69c" - integrity sha512-LkmXi8UUNxnCC+JlH7/fsfsKr5AU110l+SYGJimWNkWhxbN5EyeOtm1MJ0hhvqMMOhGwBj1Fp70Yv9i+hX0QAg== +v8-to-istanbul@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" + integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" @@ -9994,10 +10893,10 @@ validator@^13.5.2: resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== -varint@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4" - integrity sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow== +varint@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" + integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== vary@^1, vary@~1.1.2: version "1.1.2" @@ -10033,11 +10932,11 @@ w3c-xmlserializer@^2.0.0: xml-name-validator "^3.0.0" walker@^1.0.7, walker@~1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" - integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: - makeerror "1.0.x" + makeerror "1.0.12" wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" @@ -10046,6 +10945,35 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-did-resolver@^2.0.8: + version "2.0.16" + resolved "https://registry.yarnpkg.com/web-did-resolver/-/web-did-resolver-2.0.16.tgz#23e6607a6a068218ff8403d967b8a70af2e0cc25" + integrity sha512-PNGO9nP8H1mTxBRzg/AdzB40HXHhQ99BMCMEQYLK1fatohdmEDetJglgTFwavKQEbBexDG3xknCIzryWD7iS0A== + dependencies: + cross-fetch "^3.1.2" + did-resolver "^3.1.5" + +webcrypto-core@^1.7.4: + version "1.7.5" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.5.tgz#c02104c953ca7107557f9c165d194c6316587ca4" + integrity sha512-gaExY2/3EHQlRNNNVSrbG2Cg94Rutl7fAaKILS1w8ZDhGxdFOaw6EbCfHIxPy9vt/xwp5o0VQAx9aySPF6hU1A== + dependencies: + "@peculiar/asn1-schema" "^2.1.6" + "@peculiar/json-schema" "^1.1.12" + asn1js "^3.0.1" + pvtsutils "^1.3.2" + tslib "^2.4.0" + +webcrypto-shim@^0.1.4: + version "0.1.7" + resolved "https://registry.yarnpkg.com/webcrypto-shim/-/webcrypto-shim-0.1.7.tgz#da8be23061a0451cf23b424d4a9b61c10f091c12" + integrity sha512-JAvAQR5mRNRxZW2jKigWMjCMkjSdmP5cColRP1U/pTg69VgHXEi1orv5vVpJ55Zc5MIaPc1aaurzd9pjv2bveg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -10073,6 +11001,14 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" @@ -10112,12 +11048,12 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== +wide-align@^1.1.0, wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== dependencies: - string-width "^1.0.2 || 2" + string-width "^1.0.2 || 2 || 3 || 4" word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" @@ -10129,6 +11065,14 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -10219,10 +11163,10 @@ ws@^6.1.4: dependencies: async-limiter "~1.0.0" -ws@^7, ws@^7.4.5, ws@^7.4.6, ws@^7.5.3: - version "7.5.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74" - integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg== +ws@^7, ws@^7.4.6, ws@^7.5.3: + version "7.5.7" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" + integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== xcode@^2.0.0: version "2.1.0" @@ -10254,11 +11198,6 @@ xmldoc@^1.1.2: dependencies: sax "^1.2.1" -xmldom@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e" - integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA== - xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -10299,7 +11238,7 @@ yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^18.1.2, yargs-parser@^18.1.3: +yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -10324,7 +11263,7 @@ yargs@^15.1.0, yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.0.3, yargs@^16.2.0: +yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== @@ -10341,3 +11280,8 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==