From 13f074b5fdfbaf02ede3ed1fc58f99a892ca85f9 Mon Sep 17 00:00:00 2001 From: Ry Jones Date: Fri, 21 Jul 2023 22:08:01 +0900 Subject: [PATCH 01/10] build: Update Github workflows (#1500) Co-authored-by: Ankur Banerjee Signed-off-by: Ry Jones Signed-off-by: Ankur Banerjee --- .dockerignore | 2 + .github/actions/setup-node/action.yml | 34 ---------- .github/dependabot.yml | 32 +++++++++ .github/workflows/cleanup-cache.yml | 16 +++++ .github/workflows/codeql-analysis.yml | 35 ---------- .github/workflows/continuous-deployment.yml | 18 +++-- .github/workflows/continuous-integration.yml | 32 +++++---- .github/workflows/lint-pr.yml | 2 +- .github/workflows/repolinter.yml | 3 +- Dockerfile | 70 ++++++++++++-------- 10 files changed, 124 insertions(+), 120 deletions(-) delete mode 100644 .github/actions/setup-node/action.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/cleanup-cache.yml delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.dockerignore b/.dockerignore index dd87e2d73f..2399fcb20b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ +# Skip unncecessary folders node_modules build +.github diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml deleted file mode 100644 index 27fe8d108d..0000000000 --- a/.github/actions/setup-node/action.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Setup NodeJS -description: Setup NodeJS with caching -author: 'timo@animo.id' - -inputs: - node-version: - description: Node version to use - required: true - -runs: - using: composite - steps: - - name: Get yarn cache directory path - id: yarn-cache-dir-path - shell: bash - run: echo "::set-output name=dir::$(yarn cache dir)" - - - uses: actions/cache@v2 - id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Setup node v${{ inputs.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ inputs.node-version }} - registry-url: 'https://registry.npmjs.org/' - -branding: - icon: scissors - color: purple diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..eb9c53f64c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +################################# +# GitHub Dependabot Config info # +################################# + +version: 2 +updates: + # Maintain dependencies for NPM + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'monthly' + allow: + # Focus on main dependencies, not devDependencies + - dependency-type: 'production' + + # Maintain dependencies for GitHub Actions + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'monthly' + + # Maintain dependencies for Docker + - package-ecosystem: 'docker' + directory: '/' + schedule: + interval: 'monthly' + + # Maintain dependencies for Cargo + - package-ecosystem: 'cargo' + directory: '/' + schedule: + interval: 'monthly' diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml new file mode 100644 index 0000000000..4b4ecd5bd5 --- /dev/null +++ b/.github/workflows/cleanup-cache.yml @@ -0,0 +1,16 @@ +# Repositories have 10 GB of cache storage per repository +# Documentation: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy +name: 'Cleanup - Cache' +on: + schedule: + - cron: '0 0 * * 0/3' + workflow_dispatch: + +jobs: + delete-caches: + name: 'Delete Actions caches' + runs-on: ubuntu-latest + + steps: + - name: 'Wipe Github Actions cache' + uses: easimon/wipe-cache@v2 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 470451a5c8..0000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: 'CodeQL' - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - - cron: '45 0 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ['javascript'] - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 82d66fb8f7..7186585fa4 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -5,6 +5,9 @@ on: branches: - main +env: + NODE_OPTIONS: --max_old_space_size=6144 + jobs: release-canary: runs-on: aries-ubuntu-2004 @@ -12,19 +15,21 @@ jobs: if: "!startsWith(github.event.head_commit.message, 'chore(release): v')" steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # pulls all commits (needed for lerna to correctly version) fetch-depth: 0 + persist-credentials: false # setup dependencies - name: Setup Libindy uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: 16 + cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile @@ -43,7 +48,7 @@ jobs: run: | LAST_RELEASED_VERSION=$(npm view @aries-framework/core@alpha version) - echo "::set-output name=version::$LAST_RELEASED_VERSION" + echo version="${LAST_RELEASED_VERSION}" >> "$GITHUB_OUTPUT" - name: Setup git user run: | @@ -62,16 +67,17 @@ jobs: if: "startsWith(github.event.head_commit.message, 'chore(release): v')" steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 # setup dependencies - name: Setup Libindy uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: 16 + cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile @@ -82,7 +88,7 @@ jobs: NEW_VERSION=$(node -p "require('./lerna.json').version") echo $NEW_VERSION - echo "::set-output name=version::$NEW_VERSION" + echo version="${NEW_VERSION}" >> "$GITHUB_OUTPUT" - name: Create Tag uses: mathieudutour/github-tag-action@v6.0 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 19ea4923ba..4eabc4d03e 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -13,13 +13,14 @@ env: ENDORSER_AGENT_PUBLIC_DID_SEED: 00000000000000000000000Endorser9 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 - NODE_OPTIONS: --max_old_space_size=4096 + NODE_OPTIONS: --max_old_space_size=6144 # 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: # "When concurrency is specified at the job level, order is not guaranteed for jobs or runs that queue within 5 minutes of each other." concurrency: - group: aries-framework-${{ github.ref }}-${{ github.repository }}-${{ github.event_name }} + # Cancel previous runs that are not completed yet + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -43,26 +44,27 @@ jobs: fi echo "SHOULD_RUN: ${SHOULD_RUN}" - echo "::set-output name=triggered::${SHOULD_RUN}" + echo triggered="${SHOULD_RUN}" >> "$GITHUB_OUTPUT" validate: runs-on: aries-ubuntu-2004 name: Validate steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 # setup dependencies - name: Setup Libindy uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: 16 + cache: 'yarn' - name: Install dependencies - run: yarn install + run: yarn install --frozen-lockfile - name: Linting run: yarn lint @@ -86,7 +88,7 @@ jobs: steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 # setup dependencies @@ -109,16 +111,17 @@ jobs: uses: ./.github/actions/setup-postgres-wallet-plugin - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + cache: 'yarn' - name: Add ref-napi resolution in Node18 - run: node ./scripts/add-ref-napi-resolution.js if: matrix.node-version == '18.x' + run: node ./scripts/add-ref-napi-resolution.js - name: Install dependencies - run: yarn install + run: yarn install --frozen-lockfile - name: Run tests run: TEST_AGENT_PUBLIC_DID_SEED=${TEST_AGENT_PUBLIC_DID_SEED} ENDORSER_AGENT_PUBLIC_DID_SEED=${ENDORSER_AGENT_PUBLIC_DID_SEED} GENESIS_TXN_PATH=${GENESIS_TXN_PATH} yarn test --coverage --forceExit --bail @@ -133,19 +136,21 @@ jobs: if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' steps: - name: Checkout aries-framework-javascript - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # pulls all commits (needed for lerna to correctly version) fetch-depth: 0 + persist-credentials: false # setup dependencies - name: Setup Libindy uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: ./.github/actions/setup-node + uses: actions/setup-node@v3 with: node-version: 16 + cache: 'yarn' - name: Install dependencies run: yarn install --frozen-lockfile @@ -174,8 +179,7 @@ jobs: run: | NEW_VERSION=$(node -p "require('./lerna.json').version") echo $NEW_VERSION - - echo "::set-output name=version::$NEW_VERSION" + echo version="${NEW_VERSION}" >> "$GITHUB_OUTPUT" - name: Create Pull Request uses: peter-evans/create-pull-request@v3 diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index d5af138f96..a14c516acb 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -14,7 +14,7 @@ jobs: 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 + - uses: amannn/action-semantic-pull-request@v5.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/repolinter.yml b/.github/workflows/repolinter.yml index f33a30eb1d..4a4501a8a8 100644 --- a/.github/workflows/repolinter.yml +++ b/.github/workflows/repolinter.yml @@ -12,6 +12,7 @@ jobs: container: ghcr.io/todogroup/repolinter:v0.10.1 steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + - name: Lint Repo run: bundle exec /app/bin/repolinter.js --rulesetUrl https://raw.githubusercontent.com/hyperledger-labs/hyperledger-community-management-tools/master/repo_structure/repolint.json diff --git a/Dockerfile b/Dockerfile index cd68166f9e..9514936098 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,41 @@ -FROM ubuntu:20.04 as base +## Stage 1: Build indy-sdk and postgres plugin -ENV DEBIAN_FRONTEND noninteractive +FROM ubuntu:22.04 as base -RUN apt-get update -y && apt-get install -y \ - software-properties-common \ - apt-transport-https \ - curl \ - # Only needed to build indy-sdk - build-essential \ - git \ - libzmq3-dev libsodium-dev pkg-config libssl-dev +# Set this value only during build +ARG DEBIAN_FRONTEND noninteractive -# libindy -RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 -RUN add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" +# Define packages to install +ENV PACKAGES software-properties-common ca-certificates \ + curl build-essential git \ + libzmq3-dev libsodium-dev pkg-config -# nodejs 16x LTS Debian -RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - +# Combined update and install to ensure Docker caching works correctly +RUN apt-get update -y \ + && apt-get install -y $PACKAGES -# yarn -RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list +RUN curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1-1ubuntu2.1~18.04.21_amd64.deb -o libssl1.1.deb \ + # libssl1.1 (required by libindy) + && dpkg -i libssl1.1.deb \ + && curl http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl-dev_1.1.1-1ubuntu2.1~18.04.21_amd64.deb -o libssl-dev1.1.deb \ + # libssl-dev1.1 (required to compile libindy with posgres plugin) + && dpkg -i libssl-dev1.1.deb -# install depdencies -RUN apt-get update -y && apt-get install -y --allow-unauthenticated \ - libindy \ - nodejs +# Add APT sources +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 \ + && add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" \ + && curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list -# Install yarn seperately due to `no-install-recommends` to skip nodejs install -RUN apt-get install -y --no-install-recommends yarn +# Install libindy, NodeJS and yarn +RUN apt-get update -y \ + # Install libindy + && apt-get install -y --allow-unauthenticated libindy \ + && apt-get install -y nodejs \ + && apt-get install -y --no-install-recommends yarn \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean -y # postgres plugin setup # install rust and set up rustup @@ -46,14 +53,19 @@ RUN cargo build --release # set up library path for postgres plugin ENV LIB_INDY_STRG_POSTGRES="/indy-sdk/experimental/plugins/postgres_storage/target/release" +## Stage 2: Build Aries Framework JavaScript + FROM base as final -# AFJ specifc setup -WORKDIR /www +# Set environment variables ENV RUN_MODE="docker" -# Copy dependencies +# Set working directory +WORKDIR /www + +# Copy repository files COPY . . -RUN yarn install -RUN yarn build \ No newline at end of file +# Run yarn install and build +RUN yarn install --frozen-lockfile \ + && yarn build From cd720988a2edf6d2fcdc2e32d45bd9742cf5cee2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jul 2023 16:48:25 -0300 Subject: [PATCH 02/10] build(deps): bump fast-xml-parser from 4.2.4 to 4.2.6 (#1513) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ca25ee7e7c..b04a1ef7e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5496,9 +5496,9 @@ fast-text-encoding@^1.0.3: integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== fast-xml-parser@^4.0.12: - version "4.2.4" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.4.tgz#6e846ede1e56ad9e5ef07d8720809edf0ed07e9b" - integrity sha512-fbfMDvgBNIdDJLdLOwacjFAPYt67tr31H9ZhWSm45CDAxvd0I6WTlSOUo7K2P/K5sA5JgMKG64PI3DMcaFdWpQ== + version "4.2.6" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.6.tgz#30ad37b014c16e31eec0e01fbf90a85cedb4eacf" + integrity sha512-Xo1qV++h/Y3Ng8dphjahnYe+rGHaaNdsYOBWL9Y9GCPKpNKilJtilvWkLcI9f9X2DoKTLsZsGYAls5+JL5jfLA== dependencies: strnum "^1.0.5" From 08cdf1b18cc6406f8e505baaec404dcefc5b05d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:26:31 +0000 Subject: [PATCH 03/10] build(deps): bump codecov/codecov-action from 1 to 3 (#1516) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 4eabc4d03e..53732502a6 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -126,7 +126,7 @@ jobs: - name: Run tests run: TEST_AGENT_PUBLIC_DID_SEED=${TEST_AGENT_PUBLIC_DID_SEED} ENDORSER_AGENT_PUBLIC_DID_SEED=${ENDORSER_AGENT_PUBLIC_DID_SEED} GENESIS_TXN_PATH=${GENESIS_TXN_PATH} yarn test --coverage --forceExit --bail - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 if: always() version-stable: From 8a9ed2d28b5b8cf0224b38444cb068d818607cc1 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Mon, 24 Jul 2023 03:47:51 -0300 Subject: [PATCH 04/10] ci: set npm registry for releases (#1519) Signed-off-by: Ariel Gentile --- .github/workflows/continuous-deployment.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 7186585fa4..9014743d69 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -30,6 +30,7 @@ jobs: with: node-version: 16 cache: 'yarn' + registry-url: 'https://registry.npmjs.org/' - name: Install dependencies run: yarn install --frozen-lockfile @@ -78,6 +79,7 @@ jobs: with: node-version: 16 cache: 'yarn' + registry-url: 'https://registry.npmjs.org/' - name: Install dependencies run: yarn install --frozen-lockfile From fe3bf8b65fbb8b37dfc9ca8079d68f295f8d1640 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 12:36:55 +0000 Subject: [PATCH 05/10] build(deps): bump mathieudutour/github-tag-action from 6.0 to 6.1 (#1514) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous-deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 9014743d69..7b23381d32 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -93,7 +93,7 @@ jobs: echo version="${NEW_VERSION}" >> "$GITHUB_OUTPUT" - name: Create Tag - uses: mathieudutour/github-tag-action@v6.0 + uses: mathieudutour/github-tag-action@v6.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} custom_tag: ${{ steps.new-version.outputs.version }} From e6a0829377e162cfac2b04732ae0c06b4f773661 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 12:54:33 +0000 Subject: [PATCH 06/10] build(deps): bump peter-evans/create-pull-request from 3 to 5 (#1515) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 53732502a6..3d9afd40f7 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -182,7 +182,7 @@ jobs: echo version="${NEW_VERSION}" >> "$GITHUB_OUTPUT" - name: Create Pull Request - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v5 with: commit-message: | chore(release): v${{ steps.new-version.outputs.version }} From f26d5fd2a7813e15354a442daa6bf6f8b4d56b99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:38:08 +0000 Subject: [PATCH 07/10] build(deps): bump word-wrap from 1.2.3 to 1.2.4 (#1509) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b04a1ef7e6..692c86ec6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12051,9 +12051,9 @@ wide-align@^1.1.0, wide-align@^1.1.2, wide-align@^1.1.5: string-width "^1.0.2 || 2 || 3 || 4" word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== wordwrap@^1.0.0: version "1.0.0" From c6f03e49d79a33b1c4b459cef11add93dee051d0 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Tue, 25 Jul 2023 06:14:18 -0300 Subject: [PATCH 08/10] feat(anoncreds): auto create link secret (#1521) Signed-off-by: Ariel Gentile --- demo/src/Alice.ts | 5 ---- .../src/AnonCredsRsModuleConfig.ts | 11 +++++++ .../src/services/AnonCredsRsHolderService.ts | 17 +++++++---- packages/anoncreds/src/AnonCredsApi.ts | 27 ++++++----------- .../legacy-indy-format-services.test.ts | 2 ++ packages/anoncreds/src/index.ts | 1 + .../v1-connectionless-proofs.e2e.test.ts | 5 ---- packages/anoncreds/src/utils/index.ts | 1 + packages/anoncreds/src/utils/linkSecret.ts | 30 +++++++++++++++++++ packages/anoncreds/tests/anoncreds.test.ts | 1 + .../anoncreds/tests/legacyAnonCredsSetup.ts | 6 ---- .../v2-connectionless-credentials.e2e.test.ts | 6 ---- ...f.credentials.propose-offerED25519.test.ts | 6 ---- .../v2-indy-connectionless-proofs.e2e.test.ts | 5 ---- packages/indy-sdk/src/IndySdkModuleConfig.ts | 11 +++++++ .../services/IndySdkHolderService.ts | 17 +++++++---- tests/e2e-test.ts | 3 -- 17 files changed, 90 insertions(+), 64 deletions(-) create mode 100644 packages/anoncreds/src/utils/linkSecret.ts diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts index 8374e27238..2de378d8c1 100644 --- a/demo/src/Alice.ts +++ b/demo/src/Alice.ts @@ -46,11 +46,6 @@ export class Alice extends BaseAgent { } public async acceptCredentialOffer(credentialRecord: CredentialExchangeRecord) { - const linkSecretIds = await this.agent.modules.anoncreds.getLinkSecretIds() - if (linkSecretIds.length === 0) { - await this.agent.modules.anoncreds.createLinkSecret() - } - await this.agent.credentials.acceptOffer({ credentialRecordId: credentialRecord.id, }) diff --git a/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts b/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts index 2d676b4d52..d47fd3b905 100644 --- a/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts +++ b/packages/anoncreds-rs/src/AnonCredsRsModuleConfig.ts @@ -40,6 +40,12 @@ export interface AnonCredsRsModuleConfigOptions { * ``` */ anoncreds: Anoncreds + + /** + * Create a default link secret if there are no created link secrets. + * @defaultValue true + */ + autoCreateLinkSecret?: boolean } /** @@ -55,4 +61,9 @@ export class AnonCredsRsModuleConfig { public get anoncreds() { return this.options.anoncreds } + + /** See {@link AnonCredsModuleConfigOptions.autoCreateLinkSecret} */ + public get autoCreateLinkSecret() { + return this.options.autoCreateLinkSecret ?? true + } } diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts index 20b9c5a74d..7249da2662 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts @@ -34,6 +34,7 @@ import { AnonCredsRestrictionWrapper, unqualifiedCredentialDefinitionIdRegex, AnonCredsRegistryService, + storeLinkSecret, } from '@aries-framework/anoncreds' import { AriesFrameworkError, JsonTransformer, TypedArrayEncoder, injectable, utils } from '@aries-framework/core' import { @@ -47,6 +48,7 @@ import { anoncreds, } from '@hyperledger/anoncreds-shared' +import { AnonCredsRsModuleConfig } from '../AnonCredsRsModuleConfig' import { AnonCredsRsError } from '../errors/AnonCredsRsError' @injectable() @@ -198,15 +200,20 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository) // If a link secret is specified, use it. Otherwise, attempt to use default link secret - const linkSecretRecord = options.linkSecretId + let linkSecretRecord = options.linkSecretId ? await linkSecretRepository.getByLinkSecretId(agentContext, options.linkSecretId) : await linkSecretRepository.findDefault(agentContext) + // No default link secret. Automatically create one if set on module config if (!linkSecretRecord) { - // No default link secret - throw new AnonCredsRsError( - 'No link secret provided to createCredentialRequest and no default link secret has been found' - ) + const moduleConfig = agentContext.dependencyManager.resolve(AnonCredsRsModuleConfig) + if (!moduleConfig.autoCreateLinkSecret) { + throw new AnonCredsRsError( + 'No link secret provided to createCredentialRequest and no default link secret has been found' + ) + } + const { linkSecretId, linkSecretValue } = await this.createLinkSecret(agentContext, {}) + linkSecretRecord = await storeLinkSecret(agentContext, { linkSecretId, linkSecretValue, setAsDefault: true }) } if (!linkSecretRecord.value) { diff --git a/packages/anoncreds/src/AnonCredsApi.ts b/packages/anoncreds/src/AnonCredsApi.ts index a4beea9b20..5659f5a813 100644 --- a/packages/anoncreds/src/AnonCredsApi.ts +++ b/packages/anoncreds/src/AnonCredsApi.ts @@ -25,7 +25,6 @@ import { AnonCredsCredentialDefinitionPrivateRepository, AnonCredsKeyCorrectnessProofRecord, AnonCredsKeyCorrectnessProofRepository, - AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository, } from './repository' import { AnonCredsCredentialDefinitionRecord } from './repository/AnonCredsCredentialDefinitionRecord' @@ -40,6 +39,7 @@ import { AnonCredsIssuerServiceSymbol, } from './services' import { AnonCredsRegistryService } from './services/registry/AnonCredsRegistryService' +import { storeLinkSecret } from './utils' @injectable() export class AnonCredsApi { @@ -81,30 +81,21 @@ export class AnonCredsApi { /** * Create a Link Secret, optionally indicating its ID and if it will be the default one - * If there is no default Link Secret, this will be set as default (even if setAsDefault is true). + * If there is no default Link Secret, this will be set as default (even if setAsDefault is false). * */ - public async createLinkSecret(options?: AnonCredsCreateLinkSecretOptions) { + public async createLinkSecret(options?: AnonCredsCreateLinkSecretOptions): Promise { const { linkSecretId, linkSecretValue } = await this.anonCredsHolderService.createLinkSecret(this.agentContext, { linkSecretId: options?.linkSecretId, }) - // In some cases we don't have the linkSecretValue. However we still want a record so we know which link secret ids are valid - const linkSecretRecord = new AnonCredsLinkSecretRecord({ linkSecretId, value: linkSecretValue }) - - // If it is the first link secret registered, set as default - const defaultLinkSecretRecord = await this.anonCredsLinkSecretRepository.findDefault(this.agentContext) - if (!defaultLinkSecretRecord || options?.setAsDefault) { - linkSecretRecord.setTag('isDefault', true) - } - - // Set the current default link secret as not default - if (defaultLinkSecretRecord && options?.setAsDefault) { - defaultLinkSecretRecord.setTag('isDefault', false) - await this.anonCredsLinkSecretRepository.update(this.agentContext, defaultLinkSecretRecord) - } + await storeLinkSecret(this.agentContext, { + linkSecretId, + linkSecretValue, + setAsDefault: options?.setAsDefault, + }) - await this.anonCredsLinkSecretRepository.save(this.agentContext, linkSecretRecord) + return linkSecretId } /** diff --git a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts index e3e6b25275..7ca8053a38 100644 --- a/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts +++ b/packages/anoncreds/src/formats/__tests__/legacy-indy-format-services.test.ts @@ -17,6 +17,7 @@ import { agentDependencies, getAgentConfig, getAgentContext } from '../../../../ import { IndySdkHolderService, IndySdkIssuerService, + IndySdkModuleConfig, IndySdkStorageService, IndySdkVerifierService, IndySdkWallet, @@ -63,6 +64,7 @@ const agentContext = getAgentContext({ [AnonCredsRegistryService, new AnonCredsRegistryService()], [AnonCredsModuleConfig, anonCredsModuleConfig], [AnonCredsLinkSecretRepository, anonCredsLinkSecretRepository], + [IndySdkModuleConfig, new IndySdkModuleConfig({ indySdk, autoCreateLinkSecret: false })], ], agentConfig, wallet, diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index edc9883578..fad5355d54 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -14,3 +14,4 @@ export * from './AnonCredsApiOptions' export { generateLegacyProverDidLikeString } from './utils/proverDid' export * from './utils/indyIdentifiers' export { assertBestPracticeRevocationInterval } from './utils/revocationInterval' +export { storeLinkSecret } from './utils/linkSecret' diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts index 47bb04e2d4..4a54a41bbf 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts @@ -448,11 +448,6 @@ describe('V1 Proofs - Connectionless - Indy', () => { expect(faberConnection.isReady).toBe(true) expect(aliceConnection.isReady).toBe(true) - await aliceAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - await issueLegacyAnonCredsCredential({ issuerAgent: faberAgent as AnonCredsTestsAgent, issuerReplay: faberReplay, diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts index 7f2d7763fe..d995623bb0 100644 --- a/packages/anoncreds/src/utils/index.ts +++ b/packages/anoncreds/src/utils/index.ts @@ -9,6 +9,7 @@ export { encodeCredentialValue, checkValidCredentialValueEncoding } from './cred export { IsMap } from './isMap' export { composeCredentialAutoAccept, composeProofAutoAccept } from './composeAutoAccept' export { areCredentialPreviewAttributesEqual } from './credentialPreviewAttributes' +export { storeLinkSecret } from './linkSecret' export { unqualifiedCredentialDefinitionIdRegex, unqualifiedIndyDidRegex, diff --git a/packages/anoncreds/src/utils/linkSecret.ts b/packages/anoncreds/src/utils/linkSecret.ts new file mode 100644 index 0000000000..b301c9717d --- /dev/null +++ b/packages/anoncreds/src/utils/linkSecret.ts @@ -0,0 +1,30 @@ +import type { AgentContext } from '@aries-framework/core' + +import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository } from '../repository' + +export async function storeLinkSecret( + agentContext: AgentContext, + options: { linkSecretId: string; linkSecretValue?: string; setAsDefault?: boolean } +) { + const { linkSecretId, linkSecretValue, setAsDefault } = options + const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository) + + // In some cases we don't have the linkSecretValue. However we still want a record so we know which link secret ids are valid + const linkSecretRecord = new AnonCredsLinkSecretRecord({ linkSecretId, value: linkSecretValue }) + + // If it is the first link secret registered, set as default + const defaultLinkSecretRecord = await linkSecretRepository.findDefault(agentContext) + if (!defaultLinkSecretRecord || setAsDefault) { + linkSecretRecord.setTag('isDefault', true) + } + + // Set the current default link secret as not default + if (defaultLinkSecretRecord && setAsDefault) { + defaultLinkSecretRecord.setTag('isDefault', false) + await linkSecretRepository.update(agentContext, defaultLinkSecretRecord) + } + + await linkSecretRepository.save(agentContext, linkSecretRecord) + + return linkSecretRecord +} diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index d5590ca4ba..d56dc4b630 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -83,6 +83,7 @@ const agent = new Agent({ modules: { indySdk: new IndySdkModule({ indySdk, + autoCreateLinkSecret: false, }), anoncreds: new AnonCredsModule({ registries: [ diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index 5517779900..39e9b53c47 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -402,12 +402,6 @@ export async function setupAnonCredsTests< await holderAgent.initialize() if (verifierAgent) await verifierAgent.initialize() - // Create default link secret for holder - await holderAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - const { credentialDefinition, schema } = await prepareForAnonCredsIssuance(issuerAgent, { attributeNames, }) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts index 1fcbc44ed8..7fdb14f74a 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2-connectionless-credentials.e2e.test.ts @@ -66,12 +66,6 @@ describe('V2 Connectionless Credentials', () => { aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() - // Create link secret for alice - await aliceAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { attributeNames: ['name', 'age'], }) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts index de3aee8612..2e3d773f97 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts @@ -169,12 +169,6 @@ describe('V2 Credentials - JSON-LD - Ed25519', () => { await aliceAgent.initialize() ;[, { id: aliceConnectionId }] = await makeConnection(faberAgent, aliceAgent) - // Create link secret for alice - await aliceAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { attributeNames: ['name', 'age', 'profile_picture', 'x-ray'], }) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts index 55f1e8bbbb..e83dc5809a 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts @@ -345,11 +345,6 @@ describe('V2 Connectionless Proofs - Indy', () => { ) agents = [aliceAgent, faberAgent, mediatorAgent] - await aliceAgent.modules.anoncreds.createLinkSecret({ - linkSecretId: 'default', - setAsDefault: true, - }) - const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { attributeNames: ['name', 'age', 'image_0', 'image_1'], }) diff --git a/packages/indy-sdk/src/IndySdkModuleConfig.ts b/packages/indy-sdk/src/IndySdkModuleConfig.ts index 5bb066bebb..65478f507c 100644 --- a/packages/indy-sdk/src/IndySdkModuleConfig.ts +++ b/packages/indy-sdk/src/IndySdkModuleConfig.ts @@ -50,6 +50,12 @@ export interface IndySdkModuleConfigOptions { * ``` */ networks?: IndySdkPoolConfig[] + + /** + * Create a default link secret if there are no created link secrets. + * @defaultValue true + */ + autoCreateLinkSecret?: boolean } export class IndySdkModuleConfig { @@ -67,4 +73,9 @@ export class IndySdkModuleConfig { public get networks() { return this.options.networks ?? [] } + + /** See {@link AnonCredsModuleConfigOptions.autoCreateLinkSecret} */ + public get autoCreateLinkSecret() { + return this.options.autoCreateLinkSecret ?? true + } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts index ef44edb2e2..9f47e305ac 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts @@ -28,9 +28,11 @@ import { parseIndyCredentialDefinitionId, AnonCredsLinkSecretRepository, generateLegacyProverDidLikeString, + storeLinkSecret, } from '@aries-framework/anoncreds' import { AriesFrameworkError, injectable, inject, utils } from '@aries-framework/core' +import { IndySdkModuleConfig } from '../../IndySdkModuleConfig' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' @@ -283,15 +285,20 @@ export class IndySdkHolderService implements AnonCredsHolderService { const proverDid = generateLegacyProverDidLikeString() // If a link secret is specified, use it. Otherwise, attempt to use default link secret - const linkSecretRecord = options.linkSecretId + let linkSecretRecord = options.linkSecretId ? await linkSecretRepository.getByLinkSecretId(agentContext, options.linkSecretId) : await linkSecretRepository.findDefault(agentContext) + // No default link secret. Automatically create one if set on module config if (!linkSecretRecord) { - // No default link secret - throw new AriesFrameworkError( - 'No link secret provided to createCredentialRequest and no default link secret has been found' - ) + const moduleConfig = agentContext.dependencyManager.resolve(IndySdkModuleConfig) + if (!moduleConfig.autoCreateLinkSecret) { + throw new AriesFrameworkError( + 'No link secret provided to createCredentialRequest and no default link secret has been found' + ) + } + const { linkSecretId } = await this.createLinkSecret(agentContext, {}) + linkSecretRecord = await storeLinkSecret(agentContext, { linkSecretId, setAsDefault: true }) } try { diff --git a/tests/e2e-test.ts b/tests/e2e-test.ts index ffacd8be0a..b1958e1b8a 100644 --- a/tests/e2e-test.ts +++ b/tests/e2e-test.ts @@ -50,9 +50,6 @@ export async function e2eTest({ const [recipientSenderConnection, senderRecipientConnection] = await makeConnection(recipientAgent, senderAgent) expect(recipientSenderConnection).toBeConnectedWith(senderRecipientConnection) - // Create link secret with default options. This should create a default link secret. - await recipientAgent.modules.anoncreds.createLinkSecret() - // Issue credential from sender to recipient const { credentialDefinition } = await prepareForAnonCredsIssuance(senderAgent, { attributeNames: ['name', 'age', 'dateOfBirth'], From 9e69cf441a75bf7a3c5556cf59e730ee3fce8c28 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 25 Jul 2023 12:36:03 +0200 Subject: [PATCH 09/10] feat: oob without handhsake improvements and routing (#1511) Signed-off-by: Timo Glastra --- packages/action-menu/src/ActionMenuApi.ts | 20 +- .../credentials/v1/V1CredentialProtocol.ts | 42 +-- .../V1CredentialProtocolCred.test.ts | 2 +- .../v1-connectionless-credentials.e2e.test.ts | 5 +- .../v1/handlers/V1IssueCredentialHandler.ts | 41 +-- .../v1/handlers/V1OfferCredentialHandler.ts | 56 +-- .../v1/handlers/V1ProposeCredentialHandler.ts | 8 +- .../v1/handlers/V1RequestCredentialHandler.ts | 42 +-- .../protocols/proofs/v1/V1ProofProtocol.ts | 40 +-- .../v1-connectionless-proofs.e2e.test.ts | 16 +- .../v1/handlers/V1PresentationHandler.ts | 58 +--- .../handlers/V1RequestPresentationHandler.ts | 61 +--- packages/core/src/agent/AgentMessage.ts | 8 +- packages/core/src/agent/BaseAgent.ts | 5 +- packages/core/src/agent/MessageSender.ts | 7 + .../src/agent/getOutboundMessageContext.ts | 321 ++++++++++++++++++ .../decorators/service/ServiceDecorator.ts | 8 + packages/core/src/index.ts | 1 + .../connections/DidExchangeProtocol.ts | 2 +- .../__tests__/ConnectionService.test.ts | 153 +++++---- .../connections/services/ConnectionService.ts | 107 +++--- .../src/modules/credentials/CredentialsApi.ts | 266 +++++---------- .../protocol/v2/V2CredentialProtocol.ts | 36 +- .../V2CredentialProtocolCred.test.ts | 3 +- .../v2/handlers/V2IssueCredentialHandler.ts | 43 +-- .../v2/handlers/V2OfferCredentialHandler.ts | 54 +-- .../v2/handlers/V2RequestCredentialHandler.ts | 52 +-- .../services/DidCommDocumentService.ts | 6 +- .../__tests__/DidCommDocumentService.test.ts | 5 +- packages/core/src/modules/oob/OutOfBandApi.ts | 108 +++--- .../core/src/modules/oob/OutOfBandService.ts | 33 +- .../oob/__tests__/OutOfBandService.test.ts | 6 +- .../src/modules/oob/__tests__/helpers.test.ts | 4 +- .../oob/domain/OutOfBandDidCommService.ts | 21 +- .../modules/oob/repository/OutOfBandRecord.ts | 16 +- .../outOfBandRecordMetadataTypes.ts | 12 + packages/core/src/modules/proofs/ProofsApi.ts | 262 +++++--------- .../proofs/protocol/v2/V2ProofProtocol.ts | 46 +-- .../v2/handlers/V2PresentationHandler.ts | 32 +- .../handlers/V2RequestPresentationHandler.ts | 47 +-- .../services/MediationRecipientService.ts | 1 - .../MediationRecipientService.test.ts | 14 +- packages/core/src/storage/BaseRecord.ts | 7 + .../storage/didcomm/DidCommMessageRecord.ts | 6 +- .../didcomm/DidCommMessageRepository.ts | 5 +- .../__tests__/__snapshots__/0.1.test.ts.snap | 7 + .../migration/updates/0.1-0.2/credential.ts | 4 +- .../migration/updates/0.2-0.3/proof.ts | 4 +- packages/core/src/utils/parseInvitation.ts | 32 +- packages/core/src/utils/thread.ts | 5 + packages/core/tests/helpers.ts | 5 +- packages/core/tests/oob.test.ts | 214 ++++++++++-- .../question-answer/src/QuestionAnswerApi.ts | 14 +- samples/extension-module/dummy/DummyApi.ts | 14 +- .../dummy/handlers/DummyRequestHandler.ts | 9 +- 55 files changed, 1321 insertions(+), 1075 deletions(-) create mode 100644 packages/core/src/agent/getOutboundMessageContext.ts create mode 100644 packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts create mode 100644 packages/core/src/utils/thread.ts diff --git a/packages/action-menu/src/ActionMenuApi.ts b/packages/action-menu/src/ActionMenuApi.ts index 6abe1b3fac..086ba1115b 100644 --- a/packages/action-menu/src/ActionMenuApi.ts +++ b/packages/action-menu/src/ActionMenuApi.ts @@ -11,8 +11,8 @@ import { AriesFrameworkError, ConnectionService, MessageSender, - OutboundMessageContext, injectable, + getOutboundMessageContext, } from '@aries-framework/core' import { ActionMenuRole } from './ActionMenuRole' @@ -66,10 +66,10 @@ export class ActionMenuApi { connection, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: record, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -92,10 +92,10 @@ export class ActionMenuApi { menu: options.menu, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: record, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -126,10 +126,10 @@ export class ActionMenuApi { performedAction: options.performedAction, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: record, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) diff --git a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts index 12f464d3f3..775c47aff5 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/V1CredentialProtocol.ts @@ -215,18 +215,18 @@ export class V1CredentialProtocol credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.OfferSent) - const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1ProposeCredentialMessage, }) - const previousSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1OfferCredentialMessage, }) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) await this.indyCredentialFormat.processProposal(messageContext.agentContext, { @@ -257,7 +257,7 @@ export class V1CredentialProtocol }) // Assert - connectionService.assertConnectionOrServiceDecorator(messageContext) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) // Save record await credentialRepository.save(messageContext.agentContext, credentialRecord) @@ -506,11 +506,11 @@ export class V1CredentialProtocol } if (credentialRecord) { - const previousSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1ProposeCredentialMessage, }) - const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { associatedRecordId: credentialRecord.id, messageClass: V1OfferCredentialMessage, }) @@ -518,9 +518,9 @@ export class V1CredentialProtocol // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.ProposalSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) await this.indyCredentialFormat.processOffer(messageContext.agentContext, { @@ -546,7 +546,7 @@ export class V1CredentialProtocol }) // Assert - connectionService.assertConnectionOrServiceDecorator(messageContext) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) await this.indyCredentialFormat.processOffer(messageContext.agentContext, { credentialRecord, @@ -750,9 +750,9 @@ export class V1CredentialProtocol // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.OfferSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proposalMessage ?? undefined, - previousSentMessage: offerMessage ?? undefined, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalMessage ?? undefined, + lastSentMessage: offerMessage ?? undefined, }) const requestAttachment = requestMessage.getRequestAttachmentById(INDY_CREDENTIAL_REQUEST_ATTACHMENT_ID) @@ -886,9 +886,9 @@ export class V1CredentialProtocol // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.RequestSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: offerCredentialMessage, - previousSentMessage: requestCredentialMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: offerCredentialMessage, + lastSentMessage: requestCredentialMessage, }) const issueAttachment = issueMessage.getCredentialAttachmentById(INDY_CREDENTIAL_ATTACHMENT_ID) @@ -981,9 +981,9 @@ export class V1CredentialProtocol // Assert credentialRecord.assertProtocolVersion('v1') credentialRecord.assertState(CredentialState.CredentialIssued) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: requestCredentialMessage, - previousSentMessage: issueCredentialMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: requestCredentialMessage, + lastSentMessage: issueCredentialMessage, }) // Update record diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts index eb255070cc..0abcbba515 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/V1CredentialProtocolCred.test.ts @@ -126,7 +126,7 @@ const credentialIssueMessage = new V1IssueCredentialMessage({ const didCommMessageRecord = new DidCommMessageRecord({ associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4', - message: {}, + message: { '@id': '123', '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential' }, role: DidCommMessageRole.Receiver, }) diff --git a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts index 5825f0610c..d19d391ddf 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/__tests__/v1-connectionless-credentials.e2e.test.ts @@ -59,13 +59,13 @@ describe('V1 Connectionless Credentials', () => { protocolVersion: 'v1', }) - const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + const { invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ recordId: faberCredentialRecord.id, message, domain: 'https://a-domain.com', }) - await aliceAgent.receiveMessage(offerMessage.toJSON()) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) let aliceCredentialRecord = await waitForCredentialRecordSubject(aliceReplay, { threadId: faberCredentialRecord.threadId, @@ -162,7 +162,6 @@ describe('V1 Connectionless Credentials', () => { }) const { message: offerMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ - recordId: faberCredentialRecord.id, message, domain: 'https://a-domain.com', }) diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts index e828fb2258..de3835c170 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1IssueCredentialHandler.ts @@ -1,9 +1,9 @@ import type { V1CredentialProtocol } from '../V1CredentialProtocol' import type { MessageHandler, MessageHandlerInboundMessage, CredentialExchangeRecord } from '@aries-framework/core' -import { DidCommMessageRepository, OutboundMessageContext } from '@aries-framework/core' +import { AriesFrameworkError, getOutboundMessageContext } from '@aries-framework/core' -import { V1IssueCredentialMessage, V1RequestCredentialMessage } from '../messages' +import { V1IssueCredentialMessage } from '../messages' export class V1IssueCredentialHandler implements MessageHandler { private credentialProtocol: V1CredentialProtocol @@ -36,31 +36,20 @@ export class V1IssueCredentialHandler implements MessageHandler { credentialRecord, }) - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - const requestMessage = await didCommMessageRepository.getAgentMessage(messageContext.agentContext, { - associatedRecordId: credentialRecord.id, - messageClass: V1RequestCredentialMessage, - }) - - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message.service && requestMessage.service) { - const recipientService = messageContext.message.service - const ourService = requestMessage.service - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) + const requestMessage = await this.credentialProtocol.findRequestMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) } - messageContext.agentContext.config.logger.error(`Could not automatically create credential ack`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) } } diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts index 8d2d847e96..483516736d 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1OfferCredentialHandler.ts @@ -1,13 +1,7 @@ import type { V1CredentialProtocol } from '../V1CredentialProtocol' import type { MessageHandler, MessageHandlerInboundMessage, CredentialExchangeRecord } from '@aries-framework/core' -import { - OutboundMessageContext, - RoutingService, - DidCommMessageRepository, - DidCommMessageRole, - ServiceDecorator, -} from '@aries-framework/core' +import { getOutboundMessageContext } from '@aries-framework/core' import { V1OfferCredentialMessage } from '../messages' @@ -37,47 +31,13 @@ export class V1OfferCredentialHandler implements MessageHandler { messageContext: MessageHandlerInboundMessage ) { messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) - if (messageContext.connection) { - const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord }) + const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord }) - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message.service) { - const routingService = messageContext.agentContext.dependencyManager.resolve(RoutingService) - const routing = await routingService.getRouting(messageContext.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = messageContext.message.service - - const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { - credentialRecord, - }) - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically create credential request`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + }) } } diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts index d4fdbec98f..5711a0b4c8 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1ProposeCredentialHandler.ts @@ -1,7 +1,7 @@ import type { V1CredentialProtocol } from '../V1CredentialProtocol' import type { CredentialExchangeRecord, MessageHandler, MessageHandlerInboundMessage } from '@aries-framework/core' -import { OutboundMessageContext } from '@aries-framework/core' +import { getOutboundMessageContext } from '@aries-framework/core' import { V1ProposeCredentialMessage } from '../messages' @@ -44,9 +44,9 @@ export class V1ProposeCredentialHandler implements MessageHandler { credentialRecord, }) - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, + return getOutboundMessageContext(messageContext.agentContext, { + message, + connectionRecord: messageContext.connection, associatedRecord: credentialRecord, }) } diff --git a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts index 00154fc8a4..c79c993f64 100644 --- a/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts +++ b/packages/anoncreds/src/protocols/credentials/v1/handlers/V1RequestCredentialHandler.ts @@ -1,7 +1,7 @@ import type { V1CredentialProtocol } from '../V1CredentialProtocol' import type { CredentialExchangeRecord, MessageHandler, MessageHandlerInboundMessage } from '@aries-framework/core' -import { DidCommMessageRepository, DidCommMessageRole, OutboundMessageContext } from '@aries-framework/core' +import { AriesFrameworkError, getOutboundMessageContext } from '@aries-framework/core' import { V1RequestCredentialMessage } from '../messages' @@ -36,40 +36,20 @@ export class V1RequestCredentialHandler implements MessageHandler { messageContext.agentContext, credentialRecord.id ) + if (!offerMessage) { + throw new AriesFrameworkError(`Could not find offer message for credential record with id ${credentialRecord.id}`) + } const { message } = await this.credentialProtocol.acceptRequest(messageContext.agentContext, { credentialRecord, }) - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message.service && offerMessage?.service) { - const recipientService = messageContext.message.service - const ourService = offerMessage.service - - // Set ~service, update message in record (for later use) - message.setService(ourService) - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically create credential request`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: offerMessage, + }) } } diff --git a/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts index 5e27debbfb..606dc6e7ce 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/V1ProofProtocol.ts @@ -179,17 +179,17 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< proofRecord.assertState(ProofState.RequestSent) proofRecord.assertProtocolVersion('v1') - const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1ProposePresentationMessage, }) - const previousSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1RequestPresentationMessage, }) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) // Update record @@ -212,7 +212,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< }) // Assert - connectionService.assertConnectionOrServiceDecorator(messageContext) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) await didCommMessageRepository.saveOrUpdateAgentMessage(agentContext, { agentMessage: proposalMessage, @@ -425,11 +425,11 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< // proof record already exists, this means we are the message is sent as reply to a proposal we sent if (proofRecord) { - const previousReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { + const lastReceivedMessage = await didCommMessageRepository.findAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1RequestPresentationMessage, }) - const previousSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1ProposePresentationMessage, }) @@ -437,9 +437,9 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< // Assert proofRecord.assertProtocolVersion('v1') proofRecord.assertState(ProofState.ProposalSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) await this.indyProofFormat.processRequest(agentContext, { @@ -475,7 +475,7 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< }) // Assert - connectionService.assertConnectionOrServiceDecorator(messageContext) + await connectionService.assertConnectionOrOutOfBandExchange(messageContext) // Save in repository await proofRepository.save(agentContext, proofRecord) @@ -764,9 +764,9 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< // Assert proofRecord.assertState(ProofState.RequestSent) proofRecord.assertProtocolVersion('v1') - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proposalMessage, - previousSentMessage: requestMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage: proposalMessage, + lastSentMessage: requestMessage, }) const presentationAttachment = presentationMessage.getPresentationAttachmentById(INDY_PROOF_ATTACHMENT_ID) @@ -839,12 +839,12 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< connection?.id ) - const previousReceivedMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + const lastReceivedMessage = await didCommMessageRepository.getAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1RequestPresentationMessage, }) - const previousSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { + const lastSentMessage = await didCommMessageRepository.getAgentMessage(agentContext, { associatedRecordId: proofRecord.id, messageClass: V1PresentationMessage, }) @@ -852,9 +852,9 @@ export class V1ProofProtocol extends BaseProofProtocol implements ProofProtocol< // Assert proofRecord.assertProtocolVersion('v1') proofRecord.assertState(ProofState.PresentationSent) - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) // Update record diff --git a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts index 4a54a41bbf..57127b9269 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/__tests__/v1-connectionless-proofs.e2e.test.ts @@ -119,12 +119,11 @@ describe('V1 Proofs - Connectionless - Indy', () => { }, }) - const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ - recordId: faberProofExchangeRecord.id, - message, - domain: 'https://a-domain.com', + const outOfBandRecord = await faberAgent.oob.createInvitation({ + messages: [message], + handshake: false, }) - await aliceAgent.receiveMessage(requestMessage.toJSON()) + await aliceAgent.oob.receiveInvitation(outOfBandRecord.outOfBandInvitation) testLogger.test('Alice waits for presentation request from Faber') let aliceProofExchangeRecord = await waitForProofExchangeRecordSubject(aliceReplay, { @@ -295,7 +294,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { agents = [aliceAgent, faberAgent] - const { message, proofRecord: faberProofExchangeRecord } = await faberAgent.proofs.createRequest({ + const { message } = await faberAgent.proofs.createRequest({ protocolVersion: 'v1', proofFormats: { indy: { @@ -328,8 +327,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { autoAcceptProof: AutoAcceptProof.ContentApproved, }) - const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ - recordId: faberProofExchangeRecord.id, + const { invitationUrl, message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ message, domain: 'https://a-domain.com', }) @@ -338,7 +336,7 @@ describe('V1 Proofs - Connectionless - Indy', () => { await faberAgent.unregisterOutboundTransport(transport) } - await aliceAgent.receiveMessage(requestMessage.toJSON()) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) await waitForProofExchangeRecordSubject(aliceReplay, { state: ProofState.Done, diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts index 41bcd9b4ae..38b7a97a78 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1PresentationHandler.ts @@ -1,9 +1,9 @@ import type { V1ProofProtocol } from '../V1ProofProtocol' import type { MessageHandler, MessageHandlerInboundMessage, ProofExchangeRecord } from '@aries-framework/core' -import { OutboundMessageContext, DidCommMessageRepository } from '@aries-framework/core' +import { AriesFrameworkError, getOutboundMessageContext } from '@aries-framework/core' -import { V1PresentationMessage, V1RequestPresentationMessage } from '../messages' +import { V1PresentationMessage } from '../messages' export class V1PresentationHandler implements MessageHandler { private proofProtocol: V1ProofProtocol @@ -32,47 +32,21 @@ export class V1PresentationHandler implements MessageHandler { ) { messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) - if (messageContext.connection) { - const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { - proofRecord, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: proofRecord, - }) - } else if (messageContext.message.service) { - const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { - proofRecord, - }) - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - const requestMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: proofRecord.id, - messageClass: V1RequestPresentationMessage, - }) - - const recipientService = messageContext.message.service - const ourService = requestMessage?.service - - if (!ourService) { - messageContext.agentContext.config.logger.error( - `Could not automatically create presentation ack. Missing ourService on request message` - ) - return - } - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) + const requestMessage = await this.proofProtocol.findRequestMessage(messageContext.agentContext, proofRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for proof record with id '${proofRecord.id}'`) } - messageContext.agentContext.config.logger.error(`Could not automatically create presentation ack`) + const { message } = await this.proofProtocol.acceptPresentation(messageContext.agentContext, { + proofRecord, + }) + + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) } } diff --git a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts index a697f0da90..49dc7f95ba 100644 --- a/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts +++ b/packages/anoncreds/src/protocols/proofs/v1/handlers/V1RequestPresentationHandler.ts @@ -1,13 +1,7 @@ import type { V1ProofProtocol } from '../V1ProofProtocol' import type { MessageHandler, MessageHandlerInboundMessage, ProofExchangeRecord } from '@aries-framework/core' -import { - OutboundMessageContext, - RoutingService, - ServiceDecorator, - DidCommMessageRepository, - DidCommMessageRole, -} from '@aries-framework/core' +import { getOutboundMessageContext } from '@aries-framework/core' import { V1RequestPresentationMessage } from '../messages' @@ -38,50 +32,15 @@ export class V1RequestPresentationHandler implements MessageHandler { ) { messageContext.agentContext.config.logger.info(`Automatically sending presentation with autoAccept on`) - if (messageContext.connection) { - const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { - proofRecord, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: proofRecord, - }) - } else if (messageContext.message.service) { - const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { - proofRecord, - }) - - const routingService = messageContext.agentContext.dependencyManager.resolve(RoutingService) - const routing = await routingService.getRouting(messageContext.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = messageContext.message.service - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: message.service.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - } + const { message } = await this.proofProtocol.acceptRequest(messageContext.agentContext, { + proofRecord, + }) - messageContext.agentContext.config.logger.error(`Could not automatically create presentation`) + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) } } diff --git a/packages/core/src/agent/AgentMessage.ts b/packages/core/src/agent/AgentMessage.ts index 9f081f3d18..320be216c4 100644 --- a/packages/core/src/agent/AgentMessage.ts +++ b/packages/core/src/agent/AgentMessage.ts @@ -1,3 +1,4 @@ +import type { PlaintextMessage } from '../types' import type { ParsedMessageType } from '../utils/messageType' import type { Constructor } from '../utils/mixins' @@ -31,10 +32,7 @@ export class AgentMessage extends Decorated { @Exclude() public readonly allowDidSovPrefix: boolean = false - public toJSON({ useDidSovPrefixWhereAllowed }: { useDidSovPrefixWhereAllowed?: boolean } = {}): Record< - string, - unknown - > { + public toJSON({ useDidSovPrefixWhereAllowed }: { useDidSovPrefixWhereAllowed?: boolean } = {}): PlaintextMessage { const json = JsonTransformer.toJSON(this) // If we have `useDidSovPrefixWhereAllowed` enabled, we want to replace the new https://didcomm.org prefix with the legacy did:sov prefix. @@ -44,7 +42,7 @@ export class AgentMessage extends Decorated { replaceNewDidCommPrefixWithLegacyDidSovOnMessage(json) } - return json + return json as PlaintextMessage } public is(Class: C): this is InstanceType { diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index ae9e6656ba..39ea8d521d 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -175,9 +175,10 @@ export abstract class BaseAgent { + agentContext.config.logger.debug( + `No previous sent message in thread for outbound message ${message.id}, setting up routing` + ) + + let routing: Routing | undefined = undefined + + // Extract routing from out of band record if possible + const oobRecordRecipientRouting = outOfBandRecord?.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) + if (oobRecordRecipientRouting) { + routing = { + recipientKey: Key.fromFingerprint(oobRecordRecipientRouting.recipientKeyFingerprint), + routingKeys: oobRecordRecipientRouting.routingKeyFingerprints.map((fingerprint) => + Key.fromFingerprint(fingerprint) + ), + endpoints: oobRecordRecipientRouting.endpoints, + mediatorId: oobRecordRecipientRouting.mediatorId, + } + } + + if (!routing) { + const routingService = agentContext.dependencyManager.resolve(RoutingService) + routing = await routingService.getRouting(agentContext, { + mediatorId: outOfBandRecord?.mediatorId, + }) + } + + return { + id: uuid(), + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + } +} + +async function addExchangeDataToMessage( + agentContext: AgentContext, + { + message, + ourService, + outOfBandRecord, + associatedRecord, + }: { + message: AgentMessage + ourService: ResolvedDidCommService + outOfBandRecord?: OutOfBandRecord + associatedRecord: BaseRecordAny + } +) { + // Set the parentThreadId on the message from the oob invitation + if (outOfBandRecord) { + if (!message.thread) { + message.setThread({ + parentThreadId: outOfBandRecord.outOfBandInvitation.id, + }) + } else { + message.thread.parentThreadId = outOfBandRecord.outOfBandInvitation.id + } + } + + // Set the service on the message and save service decorator to record (to remember our verkey) + // TODO: we should store this in the OOB record, but that would be a breaking change for now. + // We can change this in 0.5.0 + message.service = ServiceDecorator.fromResolvedDidCommService(ourService) + + await agentContext.dependencyManager.resolve(DidCommMessageRepository).saveOrUpdateAgentMessage(agentContext, { + agentMessage: message, + role: DidCommMessageRole.Sender, + associatedRecordId: associatedRecord.id, + }) +} diff --git a/packages/core/src/decorators/service/ServiceDecorator.ts b/packages/core/src/decorators/service/ServiceDecorator.ts index 0a105c4831..793509d678 100644 --- a/packages/core/src/decorators/service/ServiceDecorator.ts +++ b/packages/core/src/decorators/service/ServiceDecorator.ts @@ -46,4 +46,12 @@ export class ServiceDecorator { serviceEndpoint: this.serviceEndpoint, } } + + public static fromResolvedDidCommService(service: ResolvedDidCommService): ServiceDecorator { + return new ServiceDecorator({ + recipientKeys: service.recipientKeys.map((k) => k.publicKeyBase58), + routingKeys: service.routingKeys.map((k) => k.publicKeyBase58), + serviceEndpoint: service.serviceEndpoint, + }) + } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9a159f0eb7..4d99d06980 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,6 +16,7 @@ export { AgentMessage } from './agent/AgentMessage' export { Dispatcher } from './agent/Dispatcher' export { MessageSender } from './agent/MessageSender' export type { AgentDependencies } from './agent/AgentDependencies' +export { getOutboundMessageContext } from './agent/getOutboundMessageContext' export type { InitConfig, OutboundPackage, diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts index 2a1b2fea96..b9da15c304 100644 --- a/packages/core/src/modules/connections/DidExchangeProtocol.ts +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -98,7 +98,7 @@ export class DidExchangeProtocol { alias, state: DidExchangeState.InvitationReceived, theirLabel: outOfBandInvitation.label, - mediatorId: routing.mediatorId ?? outOfBandRecord.mediatorId, + mediatorId: routing.mediatorId, autoAcceptConnection: outOfBandRecord.autoAcceptConnection, outOfBandId: outOfBandRecord.id, invitationDid, diff --git a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts index c07c94a893..00e46a001d 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionService.test.ts @@ -30,8 +30,10 @@ import { DidCommV1Service } from '../../dids/domain/service/DidCommV1Service' import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' import { DidRecord, DidRepository } from '../../dids/repository' import { DidRegistrarService } from '../../dids/services/DidRegistrarService' +import { OutOfBandService } from '../../oob/OutOfBandService' import { OutOfBandRole } from '../../oob/domain/OutOfBandRole' import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { OutOfBandRepository } from '../../oob/repository/OutOfBandRepository' import { ConnectionRequestMessage, ConnectionResponseMessage, TrustPingMessage } from '../messages' import { Connection, @@ -48,9 +50,13 @@ import { ConnectionService } from '../services/ConnectionService' import { convertToNewDidDocument } from '../services/helpers' jest.mock('../repository/ConnectionRepository') +jest.mock('../../oob/repository/OutOfBandRepository') +jest.mock('../../oob/OutOfBandService') jest.mock('../../dids/services/DidRegistrarService') jest.mock('../../dids/repository/DidRepository') const ConnectionRepositoryMock = ConnectionRepository as jest.Mock +const OutOfBandRepositoryMock = OutOfBandRepository as jest.Mock +const OutOfBandServiceMock = OutOfBandService as jest.Mock const DidRepositoryMock = DidRepository as jest.Mock const DidRegistrarServiceMock = DidRegistrarService as jest.Mock @@ -72,9 +78,13 @@ const agentConfig = getAgentConfig('ConnectionServiceTest', { connectionImageUrl, }) +const outOfBandRepository = new OutOfBandRepositoryMock() +const outOfBandService = new OutOfBandServiceMock() + describe('ConnectionService', () => { let wallet: Wallet let connectionRepository: ConnectionRepository + let didRepository: DidRepository let connectionService: ConnectionService let eventEmitter: EventEmitter @@ -83,7 +93,14 @@ describe('ConnectionService', () => { beforeAll(async () => { wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) - agentContext = getAgentContext({ wallet, agentConfig }) + agentContext = getAgentContext({ + wallet, + agentConfig, + registerInstances: [ + [OutOfBandRepository, outOfBandRepository], + [OutOfBandService, outOfBandService], + ], + }) await wallet.createAndOpen(agentConfig.walletConfig) }) @@ -95,13 +112,7 @@ describe('ConnectionService', () => { eventEmitter = new EventEmitter(agentConfig.agentDependencies, new Subject()) connectionRepository = new ConnectionRepositoryMock() didRepository = new DidRepositoryMock() - connectionService = new ConnectionService( - agentConfig.logger, - connectionRepository, - didRepository, - didRegistrarService, - eventEmitter - ) + connectionService = new ConnectionService(agentConfig.logger, connectionRepository, didRepository, eventEmitter) myRouting = { recipientKey: Key.fromFingerprint('z6MkwFkSP4uv5PhhKJCGehtjuZedkotC7VF64xtMsxuM8R3W'), endpoints: agentConfig.endpoints ?? [], @@ -755,8 +766,8 @@ describe('ConnectionService', () => { }) }) - describe('assertConnectionOrServiceDecorator', () => { - it('should not throw an error when a connection record with state complete is present in the messageContext', () => { + describe('assertConnectionOrOutOfBandExchange', () => { + it('should not throw an error when a connection record with state complete is present in the messageContext', async () => { expect.assertions(1) const messageContext = new InboundMessageContext(new AgentMessage(), { @@ -764,10 +775,10 @@ describe('ConnectionService', () => { connection: getMockConnection({ state: DidExchangeState.Completed }), }) - expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext)).not.toThrow() + await expect(connectionService.assertConnectionOrOutOfBandExchange(messageContext)).resolves.not.toThrow() }) - it('should throw an error when a connection record is present and state not complete in the messageContext', () => { + it('should throw an error when a connection record is present and state not complete in the messageContext', async () => { expect.assertions(1) const messageContext = new InboundMessageContext(new AgentMessage(), { @@ -775,14 +786,16 @@ describe('ConnectionService', () => { connection: getMockConnection({ state: DidExchangeState.InvitationReceived }), }) - expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext)).toThrowError( + await expect(connectionService.assertConnectionOrOutOfBandExchange(messageContext)).rejects.toThrowError( 'Connection record is not ready to be used' ) }) - it('should not throw an error when no connection record is present in the messageContext and no additional data, but the message has a ~service decorator', () => { + it('should not throw an error when no connection record is present in the messageContext and no additional data, but the message has a ~service decorator', async () => { expect.assertions(1) + mockFunction(outOfBandRepository.findSingleByQuery).mockResolvedValue(null) + const message = new AgentMessage() message.setService({ recipientKeys: [], @@ -791,24 +804,24 @@ describe('ConnectionService', () => { }) const messageContext = new InboundMessageContext(message, { agentContext }) - expect(() => connectionService.assertConnectionOrServiceDecorator(messageContext)).not.toThrow() + await expect(connectionService.assertConnectionOrOutOfBandExchange(messageContext)).resolves.not.toThrow() }) - it('should not throw when a fully valid connection-less input is passed', () => { + it('should not throw when a fully valid connection-less input is passed', async () => { expect.assertions(1) const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) const senderKey = Key.fromPublicKeyBase58('79CXkde3j8TNuMXxPdV7nLUrT2g7JAEjH5TreyVY7GEZ', KeyType.Ed25519) - const previousSentMessage = new AgentMessage() - previousSentMessage.setService({ + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ recipientKeys: [recipientKey.publicKeyBase58], serviceEndpoint: '', routingKeys: [], }) - const previousReceivedMessage = new AgentMessage() - previousReceivedMessage.setService({ + const lastReceivedMessage = new AgentMessage() + lastReceivedMessage.setService({ recipientKeys: [senderKey.publicKeyBase58], serviceEndpoint: '', routingKeys: [], @@ -816,69 +829,80 @@ describe('ConnectionService', () => { const message = new AgentMessage() message.setService({ - recipientKeys: [], + recipientKeys: [senderKey.publicKeyBase58], serviceEndpoint: '', routingKeys: [], }) const messageContext = new InboundMessageContext(message, { agentContext, recipientKey, senderKey }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, - previousSentMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) - ).not.toThrow() + ).resolves.not.toThrow() }) - it('should throw an error when previousSentMessage is present, but recipientVerkey is not ', () => { + it('should throw an error when lastSentMessage is present, but recipientVerkey is not ', async () => { expect.assertions(1) - const previousSentMessage = new AgentMessage() - previousSentMessage.setService({ + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ recipientKeys: [], serviceEndpoint: '', routingKeys: [], }) const message = new AgentMessage() + message.setService({ + recipientKeys: [], + serviceEndpoint: '', + routingKeys: [], + }) const messageContext = new InboundMessageContext(message, { agentContext }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousSentMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastSentMessage, }) - ).toThrowError('Cannot verify service without recipientKey on incoming message') + ).rejects.toThrowError( + 'Incoming message must have recipientKey and senderKey (so cannot be AuthCrypt or unpacked) if there are lastSentMessage or lastReceivedMessage.' + ) }) - 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', () => { + it('should throw an error when lastSentMessage and recipientKey are present, but recipient key is not present in recipientKeys of previously sent message ~service decorator', async () => { expect.assertions(1) const recipientKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) + const senderKey = Key.fromPublicKeyBase58('8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K', KeyType.Ed25519) - const previousSentMessage = new AgentMessage() - previousSentMessage.setService({ + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ recipientKeys: ['anotherKey'], serviceEndpoint: '', routingKeys: [], }) const message = new AgentMessage() - const messageContext = new InboundMessageContext(message, { agentContext, recipientKey }) + message.setService({ + recipientKeys: [], + serviceEndpoint: '', + routingKeys: [], + }) + const messageContext = new InboundMessageContext(message, { agentContext, recipientKey, senderKey }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousSentMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastSentMessage, }) - ).toThrowError( - 'Previously sent message ~service recipientKeys does not include current received message recipient key' - ) + ).rejects.toThrowError('Recipient key 8HH5gYEeNc3z7PYXmd54d4x6qAfCNrqQqEB3nS7Zfu7K not found in our service') }) - it('should throw an error when previousReceivedMessage is present, but senderVerkey is not ', () => { + it('should throw an error when lastReceivedMessage is present, but senderVerkey is not ', async () => { expect.assertions(1) - const previousReceivedMessage = new AgentMessage() - previousReceivedMessage.setService({ + const lastReceivedMessage = new AgentMessage() + lastReceivedMessage.setService({ recipientKeys: [], serviceEndpoint: '', routingKeys: [], @@ -887,38 +911,47 @@ describe('ConnectionService', () => { const message = new AgentMessage() const messageContext = new InboundMessageContext(message, { agentContext }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, }) - ).toThrowError('Cannot verify service without senderKey on incoming message') + ).rejects.toThrowError( + 'No keys on our side to use for encrypting messages, and previous messages found (in which case our keys MUST also be present).' + ) }) - it('should throw an error when previousReceivedMessage and senderKey are present, but sender key is not present in recipientKeys of previously received message ~service decorator', () => { + it('should throw an error when lastReceivedMessage and senderKey are present, but sender key is not present in recipientKeys of previously received message ~service decorator', async () => { expect.assertions(1) const senderKey = 'senderKey' - const previousReceivedMessage = new AgentMessage() - previousReceivedMessage.setService({ + const lastReceivedMessage = new AgentMessage() + lastReceivedMessage.setService({ recipientKeys: ['anotherKey'], serviceEndpoint: '', routingKeys: [], }) + const lastSentMessage = new AgentMessage() + lastSentMessage.setService({ + recipientKeys: [senderKey], + serviceEndpoint: '', + routingKeys: [], + }) + const message = new AgentMessage() const messageContext = new InboundMessageContext(message, { agentContext, - senderKey: Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519), + senderKey: Key.fromPublicKeyBase58('randomKey', KeyType.Ed25519), + recipientKey: Key.fromPublicKeyBase58(senderKey, KeyType.Ed25519), }) - expect(() => - connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage, + await expect( + connectionService.assertConnectionOrOutOfBandExchange(messageContext, { + lastReceivedMessage, + lastSentMessage, }) - ).toThrowError( - 'Previously received message ~service recipientKeys does not include current received message sender key' - ) + ).rejects.toThrowError('Sender key randomKey not found in their service') }) }) diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 00461217ae..059c785452 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -23,14 +23,16 @@ import { Logger } from '../../../logger' import { inject, injectable } from '../../../plugins' import { JsonTransformer } from '../../../utils/JsonTransformer' import { indyDidFromPublicKeyBase58 } from '../../../utils/did' -import { DidKey, DidRegistrarService, IndyAgentService } from '../../dids' +import { DidKey, IndyAgentService } from '../../dids' import { DidDocumentRole } from '../../dids/domain/DidDocumentRole' import { didKeyToVerkey } from '../../dids/helpers' import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' import { DidRecord, DidRepository } from '../../dids/repository' import { DidRecordMetadataKeys } from '../../dids/repository/didRecordMetadataTypes' +import { OutOfBandService } from '../../oob/OutOfBandService' import { OutOfBandRole } from '../../oob/domain/OutOfBandRole' import { OutOfBandState } from '../../oob/domain/OutOfBandState' +import { OutOfBandRepository } from '../../oob/repository' import { ConnectionEventTypes } from '../ConnectionEvents' import { ConnectionProblemReportError, ConnectionProblemReportReason } from '../errors' import { ConnectionRequestMessage, ConnectionResponseMessage, TrustPingMessage } from '../messages' @@ -61,7 +63,6 @@ export interface ConnectionRequestParams { export class ConnectionService { private connectionRepository: ConnectionRepository private didRepository: DidRepository - private didRegistrarService: DidRegistrarService private eventEmitter: EventEmitter private logger: Logger @@ -69,12 +70,10 @@ export class ConnectionService { @inject(InjectionSymbols.Logger) logger: Logger, connectionRepository: ConnectionRepository, didRepository: DidRepository, - didRegistrarService: DidRegistrarService, eventEmitter: EventEmitter ) { this.connectionRepository = connectionRepository this.didRepository = didRepository - this.didRegistrarService = didRegistrarService this.eventEmitter = eventEmitter this.logger = logger } @@ -441,19 +440,18 @@ export class ConnectionService { /** * Assert that an inbound message either has a connection associated with it, - * or has everything correctly set up for connection-less exchange. + * or has everything correctly set up for connection-less exchange (optionally with out of band) * * @param messageContext - the inbound message context - * @param previousRespondence - previous sent and received message to determine if a valid service decorator is present */ - public assertConnectionOrServiceDecorator( + public async assertConnectionOrOutOfBandExchange( messageContext: InboundMessageContext, { - previousSentMessage, - previousReceivedMessage, + lastSentMessage, + lastReceivedMessage, }: { - previousSentMessage?: AgentMessage | null - previousReceivedMessage?: AgentMessage | null + lastSentMessage?: AgentMessage | null + lastReceivedMessage?: AgentMessage | null } = {} ) { const { connection, message } = messageContext @@ -472,43 +470,70 @@ export class ConnectionService { 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 (!recipientKey) { - throw new AriesFrameworkError( - 'Cannot verify service without recipientKey on incoming message (received unpacked message)' - ) - } + // set theirService to the value of lastReceivedMessage.service + let theirService = + messageContext.message?.service?.resolvedDidCommService ?? lastReceivedMessage?.service?.resolvedDidCommService + let ourService = lastSentMessage?.service?.resolvedDidCommService - // 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(recipientKey)) { - throw new AriesFrameworkError( - 'Previously sent message ~service recipientKeys does not include current received message recipient key' - ) - } + // 1. check if there's an oob record associated. + const outOfBandRepository = messageContext.agentContext.dependencyManager.resolve(OutOfBandRepository) + const outOfBandService = messageContext.agentContext.dependencyManager.resolve(OutOfBandService) + const outOfBandRecord = await outOfBandRepository.findSingleByQuery(messageContext.agentContext, { + invitationRequestsThreadIds: [message.threadId], + }) + + // If we have an out of band record, we can extract the service for our/the other party from the oob record + if (outOfBandRecord?.role === OutOfBandRole.Sender) { + ourService = await outOfBandService.getResolvedServiceForOutOfBandServices( + messageContext.agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) + } else if (outOfBandRecord?.role === OutOfBandRole.Receiver) { + theirService = await outOfBandService.getResolvedServiceForOutOfBandServices( + messageContext.agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) } - if (previousReceivedMessage) { - // If we have previously received a message, it is not allowed to receive an OOB/unpacked/AnonCrypt message - if (!senderKey) { - throw new AriesFrameworkError( - 'Cannot verify service without senderKey on incoming message (received AnonCrypt or unpacked message)' - ) - } + // theirService can be null when we receive an oob invitation and process the message. + // In this case there MUST be an oob record, otherwise there is no way for us to reply + // to the message + if (!theirService && !outOfBandRecord) { + throw new AriesFrameworkError( + 'No service for incoming connection-less message and no associated out of band record found.' + ) + } - // 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(senderKey)) { - throw new AriesFrameworkError( - 'Previously received message ~service recipientKeys does not include current received message sender key' - ) + // ourService can be null when we receive an oob invitation or legacy connectionless message and process the message. + // In this case lastSentMessage and lastReceivedMessage MUST be null, because there shouldn't be any previous exchange + if (!ourService && (lastReceivedMessage || lastSentMessage)) { + throw new AriesFrameworkError( + 'No keys on our side to use for encrypting messages, and previous messages found (in which case our keys MUST also be present).' + ) + } + + // If the message is unpacked or AuthCrypt, there cannot be any previous exchange (this must be the first message). + // All exchange after the first unpacked oob exchange MUST be encrypted. + if ((!senderKey || !recipientKey) && (lastSentMessage || lastReceivedMessage)) { + throw new AriesFrameworkError( + 'Incoming message must have recipientKey and senderKey (so cannot be AuthCrypt or unpacked) if there are lastSentMessage or lastReceivedMessage.' + ) + } + + // Check if recipientKey is in ourService + if (recipientKey && ourService) { + const recipientKeyFound = ourService.recipientKeys.some((key) => key.publicKeyBase58 === recipientKey) + if (!recipientKeyFound) { + throw new AriesFrameworkError(`Recipient key ${recipientKey} not found in our service`) } } - // If message is received unpacked/, we need to make sure it included a ~service decorator - if (!message.service && !recipientKey) { - throw new AriesFrameworkError('Message recipientKey must have ~service decorator') + // Check if senderKey is in theirService + if (senderKey && theirService) { + const senderKeyFound = theirService.recipientKeys.some((key) => key.publicKeyBase58 === senderKey) + if (!senderKeyFound) { + throw new AriesFrameworkError(`Sender key ${senderKey} not found in their service.`) + } } } } diff --git a/packages/core/src/modules/credentials/CredentialsApi.ts b/packages/core/src/modules/credentials/CredentialsApi.ts index 6e60a807ba..c8eba61ae9 100644 --- a/packages/core/src/modules/credentials/CredentialsApi.ts +++ b/packages/core/src/modules/credentials/CredentialsApi.ts @@ -24,13 +24,11 @@ import type { Query } from '../../storage/StorageService' import { AgentContext } from '../../agent' import { MessageSender } from '../../agent/MessageSender' -import { OutboundMessageContext } from '../../agent/models' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' import { InjectionSymbols } from '../../constants' -import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' -import { DidCommMessageRole } from '../../storage' import { DidCommMessageRepository } from '../../storage/didcomm/DidCommMessageRepository' import { ConnectionService } from '../connections/services' import { RoutingService } from '../routing/services/RoutingService' @@ -158,11 +156,10 @@ export class CredentialsApi implements Credent autoAcceptCredential: options.autoAcceptCredential, }) - // send the message here - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -202,10 +199,10 @@ export class CredentialsApi implements Credent }) // send the message - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -239,11 +236,11 @@ export class CredentialsApi implements Credent autoAcceptCredential: options.autoAcceptCredential, }) - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -271,10 +268,10 @@ export class CredentialsApi implements Credent }) this.logger.debug('Offer Message successfully created; message= ', message) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -295,75 +292,32 @@ export class CredentialsApi implements Credent this.logger.debug(`Got a credentialProtocol object for this version; version = ${protocol.version}`) const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + if (!offerMessage) { + throw new AriesFrameworkError(`No offer message found for credential record with id '${credentialRecord.id}'`) + } // Use connection if present - if (credentialRecord.connectionId) { - const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - - // Assert - connectionRecord.assertReady() - - const { message } = await protocol.acceptOffer(this.agentContext, { - credentialRecord, - credentialFormats: options.credentialFormats, - comment: options.comment, - autoAcceptCredential: options.autoAcceptCredential, - }) - - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, - associatedRecord: credentialRecord, - }) - await this.messageSender.sendMessage(outboundMessageContext) - - return credentialRecord - } - // Use ~service decorator otherwise - else if (offerMessage?.service) { - // Create ~service decorator - const routing = await this.routingService.getRouting(this.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = offerMessage.service - - const { message } = await protocol.acceptOffer(this.agentContext, { - credentialRecord, - credentialFormats: options.credentialFormats, - comment: options.comment, - autoAcceptCredential: options.autoAcceptCredential, - }) - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - ) + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() - return credentialRecord - } - // Cannot send message without connectionId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot accept offer for credential record without connectionId or ~service decorator on credential offer.` - ) - } + const { message } = await protocol.acceptOffer(this.agentContext, { + credentialRecord, + credentialFormats: options.credentialFormats, + comment: options.comment, + autoAcceptCredential: options.autoAcceptCredential, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: offerMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord } public async declineOffer(credentialRecordId: string): Promise { @@ -399,10 +353,10 @@ export class CredentialsApi implements Credent autoAcceptCredential: options.autoAcceptCredential, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -428,7 +382,7 @@ export class CredentialsApi implements Credent autoAcceptCredential: options.autoAcceptCredential, }) - this.logger.debug('Offer Message successfully created; message= ', message) + this.logger.debug('Offer Message successfully created', { message }) return { message, credentialRecord } } @@ -448,6 +402,21 @@ export class CredentialsApi implements Credent this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() + + const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) + } + const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) + if (!offerMessage) { + throw new AriesFrameworkError(`No offer message found for proof record with id '${credentialRecord.id}'`) + } + const { message } = await protocol.acceptRequest(this.agentContext, { credentialRecord, credentialFormats: options.credentialFormats, @@ -456,52 +425,16 @@ export class CredentialsApi implements Credent }) this.logger.debug('We have a credential message (sending outbound): ', message) - const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) - const offerMessage = await protocol.findOfferMessage(this.agentContext, credentialRecord.id) - - // Use connection if present - if (credentialRecord.connectionId) { - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, - associatedRecord: credentialRecord, - }) - await this.messageSender.sendMessage(outboundMessageContext) - - return credentialRecord - } - // Use ~service decorator otherwise - else if (requestMessage?.service && offerMessage?.service) { - const recipientService = requestMessage.service - const ourService = offerMessage.service - - message.service = ourService - await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - ) + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: requestMessage, + lastSentMessage: offerMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) - return credentialRecord - } - // Cannot send message without connectionId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot accept request for credential record without connectionId or ~service decorator on credential offer / request.` - ) - } + return credentialRecord } /** @@ -520,49 +453,36 @@ export class CredentialsApi implements Credent this.logger.debug(`Got a credentialProtocol object for version ${credentialRecord.protocolVersion}`) - const { message } = await protocol.acceptCredential(this.agentContext, { - credentialRecord, - }) + // Use connection if present + const connectionRecord = credentialRecord.connectionId + ? await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + : undefined + connectionRecord?.assertReady() const requestMessage = await protocol.findRequestMessage(this.agentContext, credentialRecord.id) - const credentialMessage = await protocol.findCredentialMessage(this.agentContext, credentialRecord.id) - - if (credentialRecord.connectionId) { - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, - associatedRecord: credentialRecord, - }) - - await this.messageSender.sendMessage(outboundMessageContext) - - return credentialRecord + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) } - // Use ~service decorator otherwise - else if (credentialMessage?.service && requestMessage?.service) { - const recipientService = credentialMessage.service - const ourService = requestMessage.service - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: false, // hard wire to be false since it's the end of the protocol so not needed here - }, - }) - ) - - return credentialRecord - } - // Cannot send message without connectionId or ~service decorator - else { + const credentialMessage = await protocol.findCredentialMessage(this.agentContext, credentialRecord.id) + if (!credentialMessage) { throw new AriesFrameworkError( - `Cannot accept credential without connectionId or ~service decorator on credential message.` + `No credential message found for credential record with id '${credentialRecord.id}'` ) } + + const { message } = await protocol.acceptCredential(this.agentContext, { + credentialRecord, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: credentialRecord, + lastReceivedMessage: credentialMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return credentialRecord } /** @@ -576,7 +496,7 @@ export class CredentialsApi implements Credent if (!credentialRecord.connectionId) { throw new AriesFrameworkError(`No connectionId found for credential record '${credentialRecord.id}'.`) } - const connection = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) + const connectionRecord = await this.connectionService.getById(this.agentContext, credentialRecord.connectionId) const protocol = this.getProtocol(credentialRecord.protocolVersion) const { message } = await protocol.createProblemReport(this.agentContext, { @@ -586,10 +506,10 @@ export class CredentialsApi implements Credent message.setThread({ threadId: credentialRecord.threadId, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: credentialRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) diff --git a/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts index 901834a106..2ab8eddc4a 100644 --- a/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts +++ b/packages/core/src/modules/credentials/protocol/v2/V2CredentialProtocol.ts @@ -189,9 +189,9 @@ export class V2CredentialProtocol ) { messageContext.agentContext.config.logger.info(`Automatically sending acknowledgement with autoAccept`) - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - - const requestMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: credentialRecord.id, - messageClass: V2RequestCredentialMessage, - }) - const { message } = await this.credentialProtocol.acceptCredential(messageContext.agentContext, { credentialRecord, }) - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (requestMessage?.service && messageContext.message.service) { - const recipientService = messageContext.message.service - const ourService = requestMessage.service - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) + const requestMessage = await this.credentialProtocol.findRequestMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for credential record with id '${credentialRecord.id}'`) } - messageContext.agentContext.config.logger.error(`Could not automatically create credential ack`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: requestMessage, + }) } } diff --git a/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts index 8320314f0a..ff2d08716a 100644 --- a/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2OfferCredentialHandler.ts @@ -3,10 +3,7 @@ import type { InboundMessageContext } from '../../../../../agent/models/InboundM import type { CredentialExchangeRecord } from '../../../repository/CredentialExchangeRecord' import type { V2CredentialProtocol } from '../V2CredentialProtocol' -import { OutboundMessageContext } from '../../../../../agent/models' -import { ServiceDecorator } from '../../../../../decorators/service/ServiceDecorator' -import { DidCommMessageRepository, DidCommMessageRole } from '../../../../../storage' -import { RoutingService } from '../../../../routing/services/RoutingService' +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' export class V2OfferCredentialHandler implements MessageHandler { @@ -36,48 +33,13 @@ export class V2OfferCredentialHandler implements MessageHandler { ) { messageContext.agentContext.config.logger.info(`Automatically sending request with autoAccept`) - if (messageContext.connection) { - const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { - credentialRecord, - }) - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message?.service) { - const routingService = messageContext.agentContext.dependencyManager.resolve(RoutingService) - const routing = await routingService.getRouting(messageContext.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = messageContext.message.service + const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { credentialRecord }) - const { message } = await this.credentialProtocol.acceptOffer(messageContext.agentContext, { - credentialRecord, - }) - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: credentialRecord.id, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically create credential request`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.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 index 244485182f..98f04deb1a 100644 --- a/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts +++ b/packages/core/src/modules/credentials/protocol/v2/handlers/V2RequestCredentialHandler.ts @@ -3,9 +3,8 @@ import type { InboundMessageContext } from '../../../../../agent/models/InboundM import type { CredentialExchangeRecord } from '../../../repository' import type { V2CredentialProtocol } from '../V2CredentialProtocol' -import { OutboundMessageContext } from '../../../../../agent/models' -import { DidCommMessageRepository, DidCommMessageRole } from '../../../../../storage' -import { V2OfferCredentialMessage } from '../messages/V2OfferCredentialMessage' +import { getOutboundMessageContext } from '../../../../../agent/getOutboundMessageContext' +import { AriesFrameworkError } from '../../../../../error' import { V2RequestCredentialMessage } from '../messages/V2RequestCredentialMessage' export class V2RequestCredentialHandler implements MessageHandler { @@ -35,44 +34,25 @@ export class V2RequestCredentialHandler implements MessageHandler { messageContext: InboundMessageContext ) { messageContext.agentContext.config.logger.info(`Automatically sending credential with autoAccept`) - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - const offerMessage = await didCommMessageRepository.findAgentMessage(messageContext.agentContext, { - associatedRecordId: credentialRecord.id, - messageClass: V2OfferCredentialMessage, - }) + const offerMessage = await this.credentialProtocol.findOfferMessage( + messageContext.agentContext, + credentialRecord.id + ) + if (!offerMessage) { + throw new AriesFrameworkError(`Could not find offer message for credential record with id ${credentialRecord.id}`) + } const { message } = await this.credentialProtocol.acceptRequest(messageContext.agentContext, { credentialRecord, }) - if (messageContext.connection) { - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - connection: messageContext.connection, - associatedRecord: credentialRecord, - }) - } else if (messageContext.message.service && offerMessage?.service) { - const recipientService = messageContext.message.service - const ourService = offerMessage.service - - // Set ~service, update message in record (for later use) - message.setService(ourService) - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - associatedRecordId: credentialRecord.id, - role: DidCommMessageRole.Sender, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically issue credential`) + return getOutboundMessageContext(messageContext.agentContext, { + connectionRecord: messageContext.connection, + message, + associatedRecord: credentialRecord, + lastReceivedMessage: messageContext.message, + lastSentMessage: offerMessage, + }) } } diff --git a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts index 4877113fa0..43b517f9a4 100644 --- a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts +++ b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts @@ -1,8 +1,6 @@ import type { AgentContext } from '../../../agent' -import type { Logger } from '../../../logger' import type { ResolvedDidCommService } from '../types' -import { AgentConfig } from '../../../agent/AgentConfig' import { KeyType } from '../../../crypto' import { injectable } from '../../../plugins' import { DidResolverService } from '../../dids' @@ -12,11 +10,9 @@ import { findMatchingEd25519Key } from '../util/matchingEd25519Key' @injectable() export class DidCommDocumentService { - private logger: Logger private didResolverService: DidResolverService - public constructor(agentConfig: AgentConfig, didResolverService: DidResolverService) { - this.logger = agentConfig.logger + public constructor(didResolverService: DidResolverService) { this.didResolverService = didResolverService } diff --git a/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts index ad57ed6372..8019274c46 100644 --- a/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts +++ b/packages/core/src/modules/didcomm/services/__tests__/DidCommDocumentService.test.ts @@ -1,7 +1,7 @@ import type { AgentContext } from '../../../../agent' import type { VerificationMethod } from '../../../dids' -import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers' +import { getAgentContext, mockFunction } from '../../../../../tests/helpers' import { Key, KeyType } from '../../../../crypto' import { DidCommV1Service, DidDocument, IndyAgentService } from '../../../dids' import { verkeyToInstanceOfKey } from '../../../dids/helpers' @@ -12,14 +12,13 @@ jest.mock('../../../dids/services/DidResolverService') const DidResolverServiceMock = DidResolverService as jest.Mock describe('DidCommDocumentService', () => { - const agentConfig = getAgentConfig('DidCommDocumentService') let didCommDocumentService: DidCommDocumentService let didResolverService: DidResolverService let agentContext: AgentContext beforeEach(async () => { didResolverService = new DidResolverServiceMock() - didCommDocumentService = new DidCommDocumentService(agentConfig, didResolverService) + didCommDocumentService = new DidCommDocumentService(didResolverService) agentContext = getAgentContext() }) diff --git a/packages/core/src/modules/oob/OutOfBandApi.ts b/packages/core/src/modules/oob/OutOfBandApi.ts index d96bfe2f16..0d6449d97c 100644 --- a/packages/core/src/modules/oob/OutOfBandApi.ts +++ b/packages/core/src/modules/oob/OutOfBandApi.ts @@ -20,14 +20,13 @@ import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' -import { DidCommMessageRepository, DidCommMessageRole } from '../../storage' +import { DidCommMessageRepository } from '../../storage' import { JsonEncoder, JsonTransformer } from '../../utils' import { parseMessageType, supportsIncomingMessageType } from '../../utils/messageType' import { parseInvitationShortUrl } from '../../utils/parseInvitation' import { ConnectionsApi, DidExchangeState, HandshakeProtocol } from '../connections' import { DidCommDocumentService } from '../didcomm' import { DidKey } from '../dids' -import { didKeyToVerkey } from '../dids/helpers' import { RoutingService } from '../routing/services/RoutingService' import { OutOfBandService } from './OutOfBandService' @@ -40,6 +39,7 @@ import { HandshakeReuseAcceptedHandler } from './handlers/HandshakeReuseAccepted import { convertToNewInvitation, convertToOldInvitation } from './helpers' import { OutOfBandInvitation } from './messages' import { OutOfBandRecord } from './repository/OutOfBandRecord' +import { OutOfBandRecordMetadataKeys } from './repository/outOfBandRecordMetadataTypes' const didCommProfiles = ['didcomm/aip1', 'didcomm/aip2;env=rfc19'] @@ -253,32 +253,32 @@ export class OutOfBandApi { } public async createLegacyConnectionlessInvitation(config: { - recordId: string + /** + * @deprecated this value is not used anymore, as the legacy connection-less exchange is now + * integrated with the out of band protocol. The value is kept to not break the API, but will + * be removed in a future version, and has no effect. + */ + recordId?: string message: Message domain: string routing?: Routing - }): Promise<{ message: Message; invitationUrl: string }> { - // Create keys (and optionally register them at the mediator) - const routing = config.routing ?? (await this.routingService.getRouting(this.agentContext)) - - // Set the service on the message - config.message.service = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey].map((key) => key.publicKeyBase58), - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), + }): Promise<{ message: Message; invitationUrl: string; outOfBandRecord: OutOfBandRecord }> { + const outOfBandRecord = await this.createInvitation({ + messages: [config.message], + routing: config.routing, }) - // We need to update the message with the new service, so we can - // retrieve it from storage later on. - await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { - agentMessage: config.message, - associatedRecordId: config.recordId, - role: DidCommMessageRole.Sender, - }) + // Resolve the service and set it on the message + const resolvedService = await this.outOfBandService.getResolvedServiceForOutOfBandServices( + this.agentContext, + outOfBandRecord.outOfBandInvitation.getServices() + ) + config.message.service = ServiceDecorator.fromResolvedDidCommService(resolvedService) return { message: config.message, invitationUrl: `${config.domain}?d_m=${JsonEncoder.toBase64URL(JsonTransformer.toJSON(config.message))}`, + outOfBandRecord, } } @@ -385,6 +385,8 @@ export class OutOfBandApi { const messages = outOfBandInvitation.getRequests() + const isConnectionless = handshakeProtocols === undefined || handshakeProtocols.length === 0 + 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.' @@ -392,7 +394,7 @@ export class OutOfBandApi { } // Make sure we haven't received this invitation before - // It's fine if we created it (means that we are connnecting to ourselves) or if it's an implicit + // It's fine if we created it (means that we are connecting to ourselves) or if it's an implicit // invitation (it allows to connect multiple times to the same public did) if (!config.isImplicit) { const existingOobRecordsFromThisId = await this.outOfBandService.findAllByQuery(this.agentContext, { @@ -431,8 +433,21 @@ export class OutOfBandApi { outOfBandInvitation: outOfBandInvitation, autoAcceptConnection, tags: { recipientKeyFingerprints }, + mediatorId: routing?.mediatorId, }) + // If we have routing, and this is a connectionless exchange, or we are not auto accepting the connection + // we need to store the routing, so it can be used when we send the first message in response to this invitation + if (routing && (isConnectionless || !autoAcceptInvitation)) { + this.logger.debug('Storing routing for out of band invitation.') + outOfBandRecord.metadata.set(OutOfBandRecordMetadataKeys.RecipientRouting, { + recipientKeyFingerprint: routing.recipientKey.fingerprint, + routingKeyFingerprints: routing.routingKeys.map((key) => key.fingerprint), + endpoints: routing.endpoints, + mediatorId: routing.mediatorId, + }) + } + await this.outOfBandService.save(this.agentContext, outOfBandRecord) this.outOfBandService.emitStateChangedEvent(this.agentContext, outOfBandRecord, null) @@ -473,6 +488,11 @@ export class OutOfBandApi { label?: string alias?: string imageUrl?: string + /** + * Routing for the exchange (either connection or connection-less exchange). + * + * If a connection is reused, the routing WILL NOT be used. + */ routing?: Routing timeoutMs?: number } @@ -480,11 +500,24 @@ export class OutOfBandApi { const outOfBandRecord = await this.outOfBandService.getById(this.agentContext, outOfBandId) const { outOfBandInvitation } = outOfBandRecord - const { label, alias, imageUrl, autoAcceptConnection, reuseConnection, routing } = config + const { label, alias, imageUrl, autoAcceptConnection, reuseConnection } = config const services = outOfBandInvitation.getServices() const messages = outOfBandInvitation.getRequests() const timeoutMs = config.timeoutMs ?? 20000 + let routing = config.routing + + // recipient routing from the receiveInvitation method. + const recipientRouting = outOfBandRecord.metadata.get(OutOfBandRecordMetadataKeys.RecipientRouting) + if (!routing && recipientRouting) { + routing = { + recipientKey: Key.fromFingerprint(recipientRouting.recipientKeyFingerprint), + routingKeys: recipientRouting.routingKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint)), + endpoints: recipientRouting.endpoints, + mediatorId: recipientRouting.mediatorId, + } + } + const { handshakeProtocols } = outOfBandInvitation const existingConnection = await this.findExistingConnection(outOfBandInvitation) @@ -747,39 +780,6 @@ export class OutOfBandApi { this.logger.debug(`Message with type ${plaintextMessage['@type']} can be processed.`) - let serviceEndpoint: string | undefined - let recipientKeys: string[] | undefined - let routingKeys: string[] = [] - - // 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') { - const [didService] = await this.didCommDocumentService.resolveServicesFromDid(this.agentContext, service) - if (didService) { - serviceEndpoint = didService.serviceEndpoint - recipientKeys = didService.recipientKeys.map((key) => key.publicKeyBase58) - routingKeys = didService.routingKeys.map((key) => key.publicKeyBase58) || [] - } - } else { - serviceEndpoint = service.serviceEndpoint - recipientKeys = service.recipientKeys.map(didKeyToVerkey) - routingKeys = service.routingKeys?.map(didKeyToVerkey) || [] - } - - if (!serviceEndpoint || !recipientKeys) { - throw new AriesFrameworkError('Service not found') - } - - const serviceDecorator = new ServiceDecorator({ - recipientKeys, - routingKeys, - serviceEndpoint, - }) - - plaintextMessage['~service'] = JsonTransformer.toJSON(serviceDecorator) this.eventEmitter.emit(this.agentContext, { type: AgentEventTypes.AgentMessageReceived, payload: { diff --git a/packages/core/src/modules/oob/OutOfBandService.ts b/packages/core/src/modules/oob/OutOfBandService.ts index bdf9fb131c..1884cb694a 100644 --- a/packages/core/src/modules/oob/OutOfBandService.ts +++ b/packages/core/src/modules/oob/OutOfBandService.ts @@ -1,3 +1,4 @@ +import type { OutOfBandDidCommService } from './domain' import type { HandshakeReusedEvent, OutOfBandStateChangedEvent } from './domain/OutOfBandEvents' import type { AgentContext } from '../../agent' import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' @@ -9,7 +10,7 @@ import type { HandshakeProtocol } from '../connections/models' import { EventEmitter } from '../../agent/EventEmitter' import { AriesFrameworkError } from '../../error' import { injectable } from '../../plugins' -import { JsonTransformer } from '../../utils' +import { DidCommDocumentService } from '../didcomm/services/DidCommDocumentService' import { DidsApi } from '../dids' import { parseDid } from '../dids/domain/parse' @@ -32,10 +33,16 @@ export interface CreateFromImplicitInvitationConfig { export class OutOfBandService { private outOfBandRepository: OutOfBandRepository private eventEmitter: EventEmitter + private didCommDocumentService: DidCommDocumentService - public constructor(outOfBandRepository: OutOfBandRepository, eventEmitter: EventEmitter) { + public constructor( + outOfBandRepository: OutOfBandRepository, + eventEmitter: EventEmitter, + didCommDocumentService: DidCommDocumentService + ) { this.outOfBandRepository = outOfBandRepository this.eventEmitter = eventEmitter + this.didCommDocumentService = didCommDocumentService } /** @@ -252,4 +259,26 @@ export class OutOfBandService { const outOfBandRecord = await this.getById(agentContext, outOfBandId) return this.outOfBandRepository.delete(agentContext, outOfBandRecord) } + + /** + * Extract a resolved didcomm service from an out of band invitation. + * + * Currently the first service that can be resolved is returned. + */ + public async getResolvedServiceForOutOfBandServices( + agentContext: AgentContext, + services: Array + ) { + for (const service of services) { + if (typeof service === 'string') { + const [didService] = await this.didCommDocumentService.resolveServicesFromDid(agentContext, service) + + if (didService) return didService + } else { + return service.resolvedDidCommService + } + } + + throw new AriesFrameworkError('Could not extract a service from the out of band invitation.') + } } diff --git a/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts b/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts index 4d89be6eaf..dafa6c5055 100644 --- a/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts +++ b/packages/core/src/modules/oob/__tests__/OutOfBandService.test.ts @@ -1,3 +1,5 @@ +import type { DidCommDocumentService } from '../../didcomm' + import { Subject } from 'rxjs' import { @@ -30,12 +32,14 @@ const agentContext = getAgentContext() describe('OutOfBandService', () => { let outOfBandRepository: OutOfBandRepository let outOfBandService: OutOfBandService + let didCommDocumentService: DidCommDocumentService let eventEmitter: EventEmitter beforeEach(async () => { eventEmitter = new EventEmitter(agentDependencies, new Subject()) outOfBandRepository = new OutOfBandRepositoryMock() - outOfBandService = new OutOfBandService(outOfBandRepository, eventEmitter) + didCommDocumentService = {} as DidCommDocumentService + outOfBandService = new OutOfBandService(outOfBandRepository, eventEmitter, didCommDocumentService) }) describe('processHandshakeReuse', () => { diff --git a/packages/core/src/modules/oob/__tests__/helpers.test.ts b/packages/core/src/modules/oob/__tests__/helpers.test.ts index cc501335b6..8ecba2a69a 100644 --- a/packages/core/src/modules/oob/__tests__/helpers.test.ts +++ b/packages/core/src/modules/oob/__tests__/helpers.test.ts @@ -1,7 +1,7 @@ import { Attachment } from '../../../decorators/attachment/Attachment' import { JsonTransformer } from '../../../utils' import { ConnectionInvitationMessage } from '../../connections' -import { DidCommV1Service } from '../../dids' +import { OutOfBandDidCommService } from '../domain' import { convertToNewInvitation, convertToOldInvitation } from '../helpers' import { OutOfBandInvitation } from '../messages' @@ -120,7 +120,7 @@ describe('convertToOldInvitation', () => { imageUrl: 'https://my-image.com', label: 'a-label', services: [ - new DidCommV1Service({ + new OutOfBandDidCommService({ id: '#inline', recipientKeys: ['did:key:z6MkmjY8GnV5i9YTDtPETC2uUAW6ejw3nk5mXF5yci5ab7th'], routingKeys: ['did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL'], diff --git a/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts b/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts index ecfd87c4d3..8c747523f1 100644 --- a/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts +++ b/packages/core/src/modules/oob/domain/OutOfBandDidCommService.ts @@ -1,9 +1,10 @@ +import type { ResolvedDidCommService } from '../../didcomm' import type { ValidationOptions } from 'class-validator' import { ArrayNotEmpty, buildMessage, IsOptional, isString, IsString, ValidateBy } from 'class-validator' import { isDid } from '../../../utils' -import { DidDocumentService } from '../../dids' +import { DidDocumentService, DidKey } from '../../dids' export class OutOfBandDidCommService extends DidDocumentService { public constructor(options: { @@ -35,6 +36,24 @@ export class OutOfBandDidCommService extends DidDocumentService { @IsString({ each: true }) @IsOptional() public accept?: string[] + + public get resolvedDidCommService(): ResolvedDidCommService { + return { + id: this.id, + recipientKeys: this.recipientKeys.map((didKey) => DidKey.fromDid(didKey).key), + routingKeys: this.routingKeys?.map((didKey) => DidKey.fromDid(didKey).key) ?? [], + serviceEndpoint: this.serviceEndpoint, + } + } + + public static fromResolvedDidCommService(service: ResolvedDidCommService) { + return new OutOfBandDidCommService({ + id: service.id, + recipientKeys: service.recipientKeys.map((key) => new DidKey(key).did), + routingKeys: service.routingKeys.map((key) => new DidKey(key).did), + serviceEndpoint: service.serviceEndpoint, + }) + } } /** diff --git a/packages/core/src/modules/oob/repository/OutOfBandRecord.ts b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts index 202e6a3886..a4dd0c670f 100644 --- a/packages/core/src/modules/oob/repository/OutOfBandRecord.ts +++ b/packages/core/src/modules/oob/repository/OutOfBandRecord.ts @@ -1,3 +1,4 @@ +import type { OutOfBandRecordMetadata } from './outOfBandRecordMetadataTypes' import type { TagsBase } from '../../../storage/BaseRecord' import type { OutOfBandRole } from '../domain/OutOfBandRole' import type { OutOfBandState } from '../domain/OutOfBandState' @@ -6,6 +7,7 @@ import { Type } from 'class-transformer' import { AriesFrameworkError } from '../../../error' import { BaseRecord } from '../../../storage/BaseRecord' +import { getThreadIdFromPlainTextMessage } from '../../../utils/thread' import { uuid } from '../../../utils/uuid' import { OutOfBandInvitation } from '../messages' @@ -14,6 +16,11 @@ type DefaultOutOfBandRecordTags = { state: OutOfBandState invitationId: string threadId?: string + /** + * The thread ids from the attached request messages from the out + * of band invitation. + */ + invitationRequestsThreadIds?: string[] } interface CustomOutOfBandRecordTags extends TagsBase { @@ -36,7 +43,11 @@ export interface OutOfBandRecordProps { threadId?: string } -export class OutOfBandRecord extends BaseRecord { +export class OutOfBandRecord extends BaseRecord< + DefaultOutOfBandRecordTags, + CustomOutOfBandRecordTags, + OutOfBandRecordMetadata +> { @Type(() => OutOfBandInvitation) public outOfBandInvitation!: OutOfBandInvitation public role!: OutOfBandRole @@ -75,6 +86,9 @@ export class OutOfBandRecord extends BaseRecord getThreadIdFromPlainTextMessage(r)), } } diff --git a/packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts b/packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts new file mode 100644 index 0000000000..f092807324 --- /dev/null +++ b/packages/core/src/modules/oob/repository/outOfBandRecordMetadataTypes.ts @@ -0,0 +1,12 @@ +export enum OutOfBandRecordMetadataKeys { + RecipientRouting = '_internal/recipientRouting', +} + +export type OutOfBandRecordMetadata = { + [OutOfBandRecordMetadataKeys.RecipientRouting]: { + recipientKeyFingerprint: string + routingKeyFingerprints: string[] + endpoints: string[] + mediatorId?: string + } +} diff --git a/packages/core/src/modules/proofs/ProofsApi.ts b/packages/core/src/modules/proofs/ProofsApi.ts index ed78afec2d..483ad5e2c1 100644 --- a/packages/core/src/modules/proofs/ProofsApi.ts +++ b/packages/core/src/modules/proofs/ProofsApi.ts @@ -29,13 +29,9 @@ import { injectable } from 'tsyringe' import { MessageSender } from '../../agent/MessageSender' import { AgentContext } from '../../agent/context/AgentContext' -import { OutboundMessageContext } from '../../agent/models' -import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' import { AriesFrameworkError } from '../../error' -import { DidCommMessageRepository } from '../../storage' -import { DidCommMessageRole } from '../../storage/didcomm/DidCommMessageRole' import { ConnectionService } from '../connections/services/ConnectionService' -import { RoutingService } from '../routing/services/RoutingService' import { ProofsModuleConfig } from './ProofsModuleConfig' import { ProofState } from './models/ProofState' @@ -98,9 +94,7 @@ export class ProofsApi implements ProofsApi { private connectionService: ConnectionService private messageSender: MessageSender - private routingService: RoutingService private proofRepository: ProofRepository - private didCommMessageRepository: DidCommMessageRepository private agentContext: AgentContext public constructor( @@ -108,16 +102,12 @@ export class ProofsApi implements ProofsApi { connectionService: ConnectionService, agentContext: AgentContext, proofRepository: ProofRepository, - routingService: RoutingService, - didCommMessageRepository: DidCommMessageRepository, config: ProofsModuleConfig ) { this.messageSender = messageSender this.connectionService = connectionService this.proofRepository = proofRepository this.agentContext = agentContext - this.routingService = routingService - this.didCommMessageRepository = didCommMessageRepository this.config = config } @@ -155,10 +145,10 @@ export class ProofsApi implements ProofsApi { parentThreadId: options.parentThreadId, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: proofRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -198,10 +188,10 @@ export class ProofsApi implements ProofsApi { }) // send the message - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: proofRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -240,10 +230,10 @@ export class ProofsApi implements ProofsApi { willConfirm: options.willConfirm, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: proofRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -274,10 +264,10 @@ export class ProofsApi implements ProofsApi { willConfirm: options.willConfirm, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, associatedRecord: proofRecord, + connectionRecord, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -298,75 +288,33 @@ export class ProofsApi implements ProofsApi { const protocol = this.getProtocol(proofRecord.protocolVersion) const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for proof record with id '${proofRecord.id}'`) + } // Use connection if present - if (proofRecord.connectionId) { - const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) - - // Assert - connectionRecord.assertReady() - - const { message } = await protocol.acceptRequest(this.agentContext, { - proofFormats: options.proofFormats, - proofRecord, - comment: options.comment, - autoAcceptProof: options.autoAcceptProof, - goalCode: options.goalCode, - }) - - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, - associatedRecord: proofRecord, - }) - await this.messageSender.sendMessage(outboundMessageContext) - - return proofRecord - } + const connectionRecord = proofRecord.connectionId + ? await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + : undefined + connectionRecord?.assertReady() - // Use ~service decorator otherwise - else if (requestMessage?.service) { - // Create ~service decorator - const routing = await this.routingService.getRouting(this.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = requestMessage.service - - const { message } = await protocol.acceptRequest(this.agentContext, { - proofFormats: options.proofFormats, - proofRecord, - comment: options.comment, - autoAcceptProof: options.autoAcceptProof, - goalCode: options.goalCode, - }) - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - await this.didCommMessageRepository.saveOrUpdateAgentMessage(this.agentContext, { - agentMessage: message, - role: DidCommMessageRole.Sender, - associatedRecordId: proofRecord.id, - }) - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: options.useReturnRoute ?? true, // defaults to true if missing - }, - }) - ) - return proofRecord - } - // Cannot send message without connectionId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot accept presentation request without connectionId or ~service decorator on presentation request.` - ) - } + const { message } = await protocol.acceptRequest(this.agentContext, { + proofFormats: options.proofFormats, + proofRecord, + comment: options.comment, + autoAcceptProof: options.autoAcceptProof, + goalCode: options.goalCode, + }) + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: proofRecord, + lastReceivedMessage: requestMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord } public async declineRequest(options: DeclineProofRequestOptions): Promise { @@ -414,13 +362,13 @@ export class ProofsApi implements ProofsApi { comment: options.comment, }) - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, associatedRecord: proofRecord, }) - await this.messageSender.sendMessage(outboundMessageContext) + return proofRecord } @@ -460,56 +408,36 @@ export class ProofsApi implements ProofsApi { const protocol = this.getProtocol(proofRecord.protocolVersion) const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for proof record with id '${proofRecord.id}'`) + } + const presentationMessage = await protocol.findPresentationMessage(this.agentContext, proofRecord.id) + if (!presentationMessage) { + throw new AriesFrameworkError(`No presentation message found for proof record with id '${proofRecord.id}'`) + } // Use connection if present - if (proofRecord.connectionId) { - const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + const connectionRecord = proofRecord.connectionId + ? await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + : undefined + connectionRecord?.assertReady() - // Assert - connectionRecord.assertReady() - - const { message } = await protocol.acceptPresentation(this.agentContext, { - proofRecord, - }) - - const outboundMessageContext = new OutboundMessageContext(message, { - agentContext: this.agentContext, - connection: connectionRecord, - associatedRecord: proofRecord, - }) - await this.messageSender.sendMessage(outboundMessageContext) + const { message } = await protocol.acceptPresentation(this.agentContext, { + proofRecord, + }) - return proofRecord - } - // Use ~service decorator otherwise - else if (requestMessage?.service && presentationMessage?.service) { - const recipientService = presentationMessage.service - const ourService = requestMessage.service - - const { message } = await protocol.acceptPresentation(this.agentContext, { - proofRecord, - }) - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(message, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - returnRoute: false, // hard wire to be false since it's the end of the protocol so not needed here - }, - }) - ) + // FIXME: returnRoute: false + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message, + connectionRecord, + associatedRecord: proofRecord, + lastSentMessage: requestMessage, + lastReceivedMessage: presentationMessage, + }) + await this.messageSender.sendMessage(outboundMessageContext) - return proofRecord - } - // Cannot send message without credentialId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot accept presentation without connectionId or ~service decorator on presentation message.` - ) - } + return proofRecord } /** @@ -570,50 +498,30 @@ export class ProofsApi implements ProofsApi { description: options.description, }) - if (proofRecord.connectionId) { - const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) - - // Assert - connectionRecord.assertReady() - - const outboundMessageContext = new OutboundMessageContext(problemReport, { - agentContext: this.agentContext, - connection: connectionRecord, - associatedRecord: proofRecord, - }) + // Use connection if present + const connectionRecord = proofRecord.connectionId + ? await this.connectionService.getById(this.agentContext, proofRecord.connectionId) + : undefined + connectionRecord?.assertReady() - await this.messageSender.sendMessage(outboundMessageContext) - return proofRecord - } else if (requestMessage?.service) { + // If there's no connection (so connection-less, we require the state to be request received) + if (!connectionRecord) { proofRecord.assertState(ProofState.RequestReceived) - // Create ~service decorator - const routing = await this.routingService.getRouting(this.agentContext) - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = requestMessage.service - - await this.messageSender.sendMessageToService( - new OutboundMessageContext(problemReport, { - agentContext: this.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }, - }) - ) - - return proofRecord - } - // Cannot send message without connectionId or ~service decorator - else { - throw new AriesFrameworkError( - `Cannot send problem report without connectionId or ~service decorator on presentation request.` - ) + if (!requestMessage) { + throw new AriesFrameworkError(`No request message found for proof record with id '${proofRecord.id}'`) + } } + + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: problemReport, + connectionRecord, + associatedRecord: proofRecord, + lastReceivedMessage: requestMessage ?? undefined, + }) + await this.messageSender.sendMessage(outboundMessageContext) + + return proofRecord } public async getFormatData(proofRecordId: string): Promise>> { diff --git a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts index 7c3bfbc88c..733fbc9f5c 100644 --- a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts +++ b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts @@ -172,11 +172,11 @@ export class V2ProofProtocol(RoutingService) - const didCommMessageRepository = messageContext.agentContext.dependencyManager.resolve(DidCommMessageRepository) - - const routing = await routingService.getRouting(messageContext.agentContext) - message.service = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.recipientKey.publicKeyBase58], - routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), - }) - const recipientService = messageContext.message.service - - await didCommMessageRepository.saveOrUpdateAgentMessage(messageContext.agentContext, { - agentMessage: message, - associatedRecordId: proofRecord.id, - role: DidCommMessageRole.Sender, - }) - - return new OutboundMessageContext(message, { - agentContext: messageContext.agentContext, - serviceParams: { - service: recipientService.resolvedDidCommService, - senderKey: message.service.resolvedDidCommService.recipientKeys[0], - returnRoute: true, - }, - }) - } - - messageContext.agentContext.config.logger.error(`Could not automatically create presentation`) + return getOutboundMessageContext(messageContext.agentContext, { + message, + lastReceivedMessage: messageContext.message, + associatedRecord: proofRecord, + connectionRecord: messageContext.connection, + }) } } diff --git a/packages/core/src/modules/routing/services/MediationRecipientService.ts b/packages/core/src/modules/routing/services/MediationRecipientService.ts index 2d12fc96fe..c61e50bd54 100644 --- a/packages/core/src/modules/routing/services/MediationRecipientService.ts +++ b/packages/core/src/modules/routing/services/MediationRecipientService.ts @@ -18,7 +18,6 @@ import { OutboundMessageContext } from '../../../agent/models' import { Key, KeyType } from '../../../crypto' import { AriesFrameworkError } from '../../../error' import { injectable } from '../../../plugins' -import { JsonTransformer } from '../../../utils' import { ConnectionType } from '../../connections/models/ConnectionType' import { ConnectionMetadataKeys } from '../../connections/repository/ConnectionMetadataTypes' import { ConnectionService } from '../../connections/services/ConnectionService' diff --git a/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts b/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts index a5ac4eff54..15831a8a54 100644 --- a/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts +++ b/packages/core/src/modules/routing/services/__tests__/MediationRecipientService.test.ts @@ -12,7 +12,6 @@ import { ConnectionMetadataKeys } from '../../../connections/repository/Connecti import { ConnectionRepository } from '../../../connections/repository/ConnectionRepository' import { ConnectionService } from '../../../connections/services/ConnectionService' import { DidRepository } from '../../../dids/repository/DidRepository' -import { DidRegistrarService } from '../../../dids/services/DidRegistrarService' import { RoutingEventTypes } from '../../RoutingEvents' import { KeylistUpdateAction, @@ -40,9 +39,6 @@ const EventEmitterMock = EventEmitter as jest.Mock jest.mock('../../../../agent/MessageSender') const MessageSenderMock = MessageSender as jest.Mock -jest.mock('../../../dids/services/DidRegistrarService') -const DidRegistrarServiceMock = DidRegistrarService as jest.Mock - const connectionImageUrl = 'https://example.com/image.png' describe('MediationRecipientService', () => { @@ -53,7 +49,6 @@ describe('MediationRecipientService', () => { let mediationRepository: MediationRepository let didRepository: DidRepository - let didRegistrarService: DidRegistrarService let eventEmitter: EventEmitter let connectionService: ConnectionService let connectionRepository: ConnectionRepository @@ -72,14 +67,7 @@ describe('MediationRecipientService', () => { eventEmitter = new EventEmitterMock() connectionRepository = new ConnectionRepositoryMock() didRepository = new DidRepositoryMock() - didRegistrarService = new DidRegistrarServiceMock() - connectionService = new ConnectionService( - config.logger, - connectionRepository, - didRepository, - didRegistrarService, - eventEmitter - ) + connectionService = new ConnectionService(config.logger, connectionRepository, didRepository, eventEmitter) mediationRepository = new MediationRepositoryMock() messageSender = new MessageSenderMock() diff --git a/packages/core/src/storage/BaseRecord.ts b/packages/core/src/storage/BaseRecord.ts index 10c16e8d5d..7f26952200 100644 --- a/packages/core/src/storage/BaseRecord.ts +++ b/packages/core/src/storage/BaseRecord.ts @@ -15,6 +15,13 @@ export type Tags = Cu export type RecordTags = ReturnType +// The BaseRecord requires a DefaultTags and CustomTags type, but we want to be +// able to use the BaseRecord without specifying these types. If we don't specify +// these types, the default TagsBase will be used, but this is not compatible +// with records that have specified a custom type. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type BaseRecordAny = BaseRecord + export abstract class BaseRecord< DefaultTags extends TagsBase = TagsBase, CustomTags extends TagsBase = TagsBase, diff --git a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts index 9f234bb15b..4de5321396 100644 --- a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts +++ b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts @@ -1,6 +1,6 @@ import type { DidCommMessageRole } from './DidCommMessageRole' import type { ConstructableAgentMessage } from '../../agent/AgentMessage' -import type { JsonObject } from '../../types' +import type { PlaintextMessage } from '../../types' import { AriesFrameworkError } from '../../error' import { JsonTransformer } from '../../utils/JsonTransformer' @@ -25,14 +25,14 @@ export type DefaultDidCommMessageTags = { export interface DidCommMessageRecordProps { role: DidCommMessageRole - message: JsonObject + message: PlaintextMessage id?: string createdAt?: Date associatedRecordId?: string } export class DidCommMessageRecord extends BaseRecord { - public message!: JsonObject + public message!: PlaintextMessage public role!: DidCommMessageRole /** diff --git a/packages/core/src/storage/didcomm/DidCommMessageRepository.ts b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts index cffa511e3a..245b621790 100644 --- a/packages/core/src/storage/didcomm/DidCommMessageRepository.ts +++ b/packages/core/src/storage/didcomm/DidCommMessageRepository.ts @@ -1,7 +1,6 @@ import type { DidCommMessageRole } from './DidCommMessageRole' import type { AgentContext } from '../../agent' import type { AgentMessage, ConstructableAgentMessage } from '../../agent/AgentMessage' -import type { JsonObject } from '../../types' import { EventEmitter } from '../../agent/EventEmitter' import { InjectionSymbols } from '../../constants' @@ -26,7 +25,7 @@ export class DidCommMessageRepository extends Repository { { role, agentMessage, associatedRecordId }: SaveAgentMessageOptions ) { const didCommMessageRecord = new DidCommMessageRecord({ - message: agentMessage.toJSON() as JsonObject, + message: agentMessage.toJSON(), role, associatedRecordId, }) @@ -45,7 +44,7 @@ export class DidCommMessageRepository extends Repository { }) if (record) { - record.message = options.agentMessage.toJSON() as JsonObject + record.message = options.agentMessage.toJSON() record.role = options.role await this.update(agentContext, record) return 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 index 0b66feb17b..543720be21 100644 --- 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 @@ -783,6 +783,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "1-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "d56fd7af-852e-458e-b750-7a4f4e53d6e6", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6MkfiPMPxCQeSDZGMkCvm1Y2rBoPsmw4ZHMv71jXtcWRRiM", ], @@ -849,6 +850,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "2-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "d939d371-3155-4d9c-87d1-46447f624f44", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6MktCZAQNGvWb4WHAjwBqPtXhZdDYorbSJkGW9vj1uhw1HD", ], @@ -915,6 +917,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "3-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "21ef606f-b25b-48c6-bafa-e79193732413", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6Mkt1tsp15cnDD7wBCFgehiR2SxHX1aPxt4sueE24twH9Bd", ], @@ -981,6 +984,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "4-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "08eb8d8b-67cf-4ce2-9aca-c7d260a5c143", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6Mkmod8vp2nURVktVC5ceQeyr2VUz26iu2ZANLNVg9pMawa", ], @@ -1047,6 +1051,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "5-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "cc67fb5e-1414-4ba6-9030-7456ccd2aaea", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6MkjDJL4X7YGoH6gjamhZR2NzowPZqtJfX5kPuNuWiVdjMr", ], @@ -1113,6 +1118,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "6-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "f0ca03d8-2e11-4ff2-a5fc-e0137a434b7e", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6Mko31DNE3gqMRZj1JNhv2BHb1caQshcd9njgKkEQXsgFRp", ], @@ -1174,6 +1180,7 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "id": "7-4e4f-41d9-94c4-f49351b811f1", "tags": { "invitationId": "1f516e35-08d3-43d8-900c-99d5239f54da", + "invitationRequestsThreadIds": undefined, "recipientKeyFingerprints": [ "z6MkuWTEmH1mUo6W96zSWyH612hFHowRzNEscPYBL2CCMyC2", ], 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 index 61c2ddb3af..25c751bd5f 100644 --- 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 @@ -1,6 +1,6 @@ import type { BaseAgent } from '../../../../agent/BaseAgent' import type { CredentialExchangeRecord } from '../../../../modules/credentials' -import type { JsonObject } from '../../../../types' +import type { JsonObject, PlaintextMessage } from '../../../../types' import { CredentialState } from '../../../../modules/credentials/models/CredentialState' import { CredentialRepository } from '../../../../modules/credentials/repository/CredentialRepository' @@ -224,7 +224,7 @@ export async function moveDidCommMessages( `Starting move of ${messageKey} from credential record with id ${credentialRecord.id} to DIDCommMessageRecord` ) const credentialRecordJson = credentialRecord as unknown as JsonObject - const message = credentialRecordJson[messageKey] as JsonObject | undefined + const message = credentialRecordJson[messageKey] as PlaintextMessage | undefined if (message) { const credentialRole = getCredentialRole(credentialRecord) diff --git a/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts b/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts index 7e923a3d48..b5eb0ec98d 100644 --- a/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts +++ b/packages/core/src/storage/migration/updates/0.2-0.3/proof.ts @@ -1,6 +1,6 @@ import type { BaseAgent } from '../../../../agent/BaseAgent' import type { ProofExchangeRecord } from '../../../../modules/proofs' -import type { JsonObject } from '../../../../types' +import type { JsonObject, PlaintextMessage } from '../../../../types' import { ProofState } from '../../../../modules/proofs/models' import { ProofRepository } from '../../../../modules/proofs/repository/ProofRepository' @@ -131,7 +131,7 @@ export async function moveDidCommMessages(agent: Agent, `Starting move of ${messageKey} from proof record with id ${proofRecord.id} to DIDCommMessageRecord` ) const proofRecordJson = proofRecord as unknown as JsonObject - const message = proofRecordJson[messageKey] as JsonObject | undefined + const message = proofRecordJson[messageKey] as PlaintextMessage | undefined if (message) { const proofRole = getProofRole(proofRecord) diff --git a/packages/core/src/utils/parseInvitation.ts b/packages/core/src/utils/parseInvitation.ts index 3a908af7bd..68db977e62 100644 --- a/packages/core/src/utils/parseInvitation.ts +++ b/packages/core/src/utils/parseInvitation.ts @@ -4,11 +4,14 @@ import type { Response } from 'node-fetch' import { AbortController } from 'abort-controller' import { parseUrl } from 'query-string' +import { AgentMessage } from '../agent/AgentMessage' import { AriesFrameworkError } from '../error' import { ConnectionInvitationMessage } from '../modules/connections' +import { OutOfBandDidCommService } from '../modules/oob/domain/OutOfBandDidCommService' import { convertToNewInvitation } from '../modules/oob/helpers' import { OutOfBandInvitation } from '../modules/oob/messages' +import { JsonEncoder } from './JsonEncoder' import { JsonTransformer } from './JsonTransformer' import { MessageValidator } from './MessageValidator' import { parseMessageType, supportsIncomingMessageType } from './messageType' @@ -102,9 +105,36 @@ export const parseInvitationShortUrl = async ( if (parsedUrl['oob']) { const outOfBandInvitation = OutOfBandInvitation.fromUrl(invitationUrl) return outOfBandInvitation - } else if (parsedUrl['c_i'] || parsedUrl['d_m']) { + } else if (parsedUrl['c_i']) { const invitation = ConnectionInvitationMessage.fromUrl(invitationUrl) return convertToNewInvitation(invitation) + } + // Legacy connectionless invitation + else if (parsedUrl['d_m']) { + const messageJson = JsonEncoder.fromBase64(parsedUrl['d_m'] as string) + const agentMessage = JsonTransformer.fromJSON(messageJson, AgentMessage) + + // ~service is required for legacy connectionless invitations + if (!agentMessage.service) { + throw new AriesFrameworkError('Invalid legacy connectionless invitation url. Missing ~service decorator.') + } + + // This destructuring removes the ~service property from the message, and + // we can can use messageWithoutService to create the out of band invitation + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { '~service': service, ...messageWithoutService } = messageJson + + // transform into out of band invitation + const invitation = new OutOfBandInvitation({ + // The label is currently required by the OutOfBandInvitation class, but not according to the specification. + // FIXME: In 0.5.0 we will make this optional: https://github.com/hyperledger/aries-framework-javascript/issues/1524 + label: '', + services: [OutOfBandDidCommService.fromResolvedDidCommService(agentMessage.service.resolvedDidCommService)], + }) + + invitation.addRequest(JsonTransformer.fromJSON(messageWithoutService, AgentMessage)) + + return invitation } else { try { return oobInvitationFromShortUrl(await fetchShortUrl(invitationUrl, dependencies)) diff --git a/packages/core/src/utils/thread.ts b/packages/core/src/utils/thread.ts new file mode 100644 index 0000000000..a8dd1a668a --- /dev/null +++ b/packages/core/src/utils/thread.ts @@ -0,0 +1,5 @@ +import type { PlaintextMessage } from '../types' + +export function getThreadIdFromPlainTextMessage(message: PlaintextMessage) { + return message['~thread']?.thid ?? message['@id'] +} diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 8537d5c35f..517c53a274 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -28,6 +28,7 @@ import { catchError, filter, map, take, timeout } from 'rxjs/operators' import { agentDependencies, IndySdkPostgresWalletScheme } from '../../node/src' import { + OutOfBandDidCommService, ConnectionsModule, ConnectionEventTypes, TypedArrayEncoder, @@ -45,7 +46,6 @@ import { TrustPingEventTypes, } from '../src' import { Key, KeyType } from '../src/crypto' -import { DidCommV1Service } from '../src/modules/dids' import { DidKey } from '../src/modules/dids/methods/key' import { OutOfBandRole } from '../src/modules/oob/domain/OutOfBandRole' import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' @@ -506,9 +506,8 @@ export function getMockOutOfBand({ accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], handshakeProtocols: [HandshakeProtocol.DidExchange], services: [ - new DidCommV1Service({ + new OutOfBandDidCommService({ id: `#inline-0`, - priority: 0, serviceEndpoint: serviceEndpoint ?? 'http://example.com', recipientKeys, routingKeys: [], diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index c05cb5c0a2..fece27bda0 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -17,8 +17,7 @@ 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 { DidCommMessageRepository, DidCommMessageRole } from '../src/storage' -import { JsonEncoder } from '../src/utils' +import { JsonEncoder, JsonTransformer } from '../src/utils' import { TestMessage } from './TestMessage' import { getAgentOptions, waitForCredentialRecord } from './helpers' @@ -87,6 +86,8 @@ describe('out of band', () => { aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() + await aliceAgent.modules.anoncreds.createLinkSecret() + const { credentialDefinition } = await prepareForAnonCredsIssuance(faberAgent, { attributeNames: ['name', 'age', 'profile_picture', 'x-ray'], }) @@ -720,50 +721,213 @@ describe('out of band', () => { }) }) - describe('createLegacyConnectionlessInvitation', () => { - test('add ~service decorator to the message and returns invitation url', async () => { - const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + describe('connection-less exchange', () => { + test('oob exchange without handshake where response is received to invitation', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + handshake: false, + messages: [message], + }) + const { outOfBandInvitation } = outOfBandRecord - const { message: offerMessage, invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + await aliceAgent.oob.receiveInvitation(outOfBandInvitation) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + await faberCredentialRecordPromise + }) + + test('oob exchange without handshake where response is received and custom routing is used on recipient', async () => { + const { message } = await faberAgent.credentials.createOffer(credentialTemplate) + const outOfBandRecord = await faberAgent.oob.createInvitation({ + handshake: false, + messages: [message], + }) + const { outOfBandInvitation } = outOfBandRecord + + const routing = await aliceAgent.mediationRecipient.getRouting({}) + + await aliceAgent.oob.receiveInvitation(outOfBandInvitation, { + routing, + }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + const faberCredentialRecord = await faberCredentialRecordPromise + + const faberCredentialRequest = await faberAgent.credentials.findRequestMessage(faberCredentialRecord.id) + + expect(JsonTransformer.toJSON(faberCredentialRequest?.service)).toEqual({ + recipientKeys: [routing.recipientKey.publicKeyBase58], + serviceEndpoint: routing.endpoints[0], + routingKeys: routing.routingKeys.map((r) => r.publicKeyBase58), + }) + }) + + test('legacy connectionless exchange where response is received to invitation', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + const { invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + domain: 'http://example.com', + message, recordId: credentialRecord.id, - domain: 'https://test.com', + }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + await faberCredentialRecordPromise + }) + + test('legacy connectionless exchange where response is received to invitation and custom routing is used on recipient', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + const { invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + domain: 'http://example.com', message, + recordId: credentialRecord.id, }) - expect(offerMessage.service).toMatchObject({ - serviceEndpoint: expect.any(String), - recipientKeys: [expect.any(String)], - routingKeys: [], + const routing = await aliceAgent.mediationRecipient.getRouting({}) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, }) + await aliceAgent.oob.receiveInvitationFromUrl(invitationUrl, { routing }) - expect(invitationUrl).toEqual(expect.stringContaining('https://test.com?d_m=')) + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) - const messageBase64 = invitationUrl.split('https://test.com?d_m=')[1] + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) - expect(JsonEncoder.fromBase64(messageBase64)).toMatchObject({ - '@id': expect.any(String), - '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) + + const faberCredentialRecord = await faberCredentialRecordPromise + + const faberCredentialRequest = await faberAgent.credentials.findRequestMessage(faberCredentialRecord.id) + + expect(JsonTransformer.toJSON(faberCredentialRequest?.service)).toEqual({ + recipientKeys: [routing.recipientKey.publicKeyBase58], + serviceEndpoint: routing.endpoints[0], + routingKeys: routing.routingKeys.map((r) => r.publicKeyBase58), }) }) - test('updates the message in the didCommMessageRepository', async () => { + test('legacy connectionless exchange without receiving message through oob receiveInvitation, where response is received to invitation', async () => { const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) + const { message: messageWithService } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + domain: 'http://example.com', + message, + recordId: credentialRecord.id, + }) + + const aliceCredentialRecordPromise = waitForCredentialRecord(aliceAgent, { + state: CredentialState.OfferReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + await aliceAgent.receiveMessage(messageWithService.toJSON()) + + const aliceCredentialRecord = await aliceCredentialRecordPromise + expect(aliceCredentialRecord.state).toBe(CredentialState.OfferReceived) + + // If we receive the event, we know the processing went well + const faberCredentialRecordPromise = waitForCredentialRecord(faberAgent, { + state: CredentialState.RequestReceived, + threadId: message.threadId, + timeoutMs: 10000, + }) + + await aliceAgent.credentials.acceptOffer({ + credentialRecordId: aliceCredentialRecord.id, + }) - const didCommMessageRepository = faberAgent.dependencyManager.resolve(DidCommMessageRepository) + await faberCredentialRecordPromise + }) - const saveOrUpdateSpy = jest.spyOn(didCommMessageRepository, 'saveOrUpdateAgentMessage') - saveOrUpdateSpy.mockResolvedValue() + test('add ~service decorator to the message and returns invitation url in createLegacyConnectionlessInvitation', async () => { + const { message, credentialRecord } = await faberAgent.credentials.createOffer(credentialTemplate) - await faberAgent.oob.createLegacyConnectionlessInvitation({ + const { message: offerMessage, invitationUrl } = await faberAgent.oob.createLegacyConnectionlessInvitation({ recordId: credentialRecord.id, domain: 'https://test.com', message, }) - expect(saveOrUpdateSpy).toHaveBeenCalledWith(expect.anything(), { - agentMessage: message, - associatedRecordId: credentialRecord.id, - role: DidCommMessageRole.Sender, + expect(offerMessage.service).toMatchObject({ + serviceEndpoint: expect.any(String), + recipientKeys: [expect.any(String)], + routingKeys: [], + }) + + expect(invitationUrl).toEqual(expect.stringContaining('https://test.com?d_m=')) + + const messageBase64 = invitationUrl.split('https://test.com?d_m=')[1] + + expect(JsonEncoder.fromBase64(messageBase64)).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/issue-credential/1.0/offer-credential', }) }) }) diff --git a/packages/question-answer/src/QuestionAnswerApi.ts b/packages/question-answer/src/QuestionAnswerApi.ts index 97ea98c143..5c732a8b70 100644 --- a/packages/question-answer/src/QuestionAnswerApi.ts +++ b/packages/question-answer/src/QuestionAnswerApi.ts @@ -2,9 +2,9 @@ import type { QuestionAnswerRecord } from './repository' import type { Query } from '@aries-framework/core' import { + getOutboundMessageContext, AgentContext, ConnectionService, - OutboundMessageContext, injectable, MessageSender, } from '@aries-framework/core' @@ -65,10 +65,10 @@ export class QuestionAnswerApi { detail: config?.detail, } ) - const outboundMessageContext = new OutboundMessageContext(questionMessage, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: questionMessage, associatedRecord: questionAnswerRecord, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) @@ -94,10 +94,10 @@ export class QuestionAnswerApi { const connection = await this.connectionService.getById(this.agentContext, questionRecord.connectionId) - const outboundMessageContext = new OutboundMessageContext(answerMessage, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: answerMessage, associatedRecord: questionAnswerRecord, + connectionRecord: connection, }) await this.messageSender.sendMessage(outboundMessageContext) diff --git a/samples/extension-module/dummy/DummyApi.ts b/samples/extension-module/dummy/DummyApi.ts index 9d4aa765d3..b99ae9a3b6 100644 --- a/samples/extension-module/dummy/DummyApi.ts +++ b/samples/extension-module/dummy/DummyApi.ts @@ -2,7 +2,7 @@ import type { DummyRecord } from './repository/DummyRecord' import type { Query } from '@aries-framework/core' import { - OutboundMessageContext, + getOutboundMessageContext, AgentContext, ConnectionService, injectable, @@ -48,7 +48,11 @@ export class DummyApi { const { record, message } = await this.dummyService.createRequest(this.agentContext, connection) await this.messageSender.sendMessage( - new OutboundMessageContext(message, { agentContext: this.agentContext, connection }) + await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: record, + connectionRecord: connection, + }) ) await this.dummyService.updateState(this.agentContext, record, DummyState.RequestSent) @@ -69,7 +73,11 @@ export class DummyApi { const message = await this.dummyService.createResponse(this.agentContext, record) await this.messageSender.sendMessage( - new OutboundMessageContext(message, { agentContext: this.agentContext, connection, associatedRecord: record }) + await getOutboundMessageContext(this.agentContext, { + message, + associatedRecord: record, + connectionRecord: connection, + }) ) await this.dummyService.updateState(this.agentContext, record, DummyState.ResponseSent) diff --git a/samples/extension-module/dummy/handlers/DummyRequestHandler.ts b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts index b19394239f..320dd45184 100644 --- a/samples/extension-module/dummy/handlers/DummyRequestHandler.ts +++ b/samples/extension-module/dummy/handlers/DummyRequestHandler.ts @@ -1,7 +1,7 @@ import type { DummyService } from '../services' import type { MessageHandler, MessageHandlerInboundMessage } from '@aries-framework/core' -import { OutboundMessageContext } from '@aries-framework/core' +import { getOutboundMessageContext } from '@aries-framework/core' import { DummyRequestMessage } from '../messages' @@ -14,11 +14,14 @@ export class DummyRequestHandler implements MessageHandler { } public async handle(inboundMessage: MessageHandlerInboundMessage) { - const connection = inboundMessage.assertReadyConnection() + const connectionRecord = inboundMessage.assertReadyConnection() const responseMessage = await this.dummyService.processRequest(inboundMessage) if (responseMessage) { - return new OutboundMessageContext(responseMessage, { agentContext: inboundMessage.agentContext, connection }) + return getOutboundMessageContext(inboundMessage.agentContext, { + connectionRecord, + message: responseMessage, + }) } } } From 67954262b9323ce232df5c809597542e836f9205 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 25 Jul 2023 14:01:00 +0200 Subject: [PATCH 10/10] ci: persist credentials to push tags (#1523) Signed-off-by: Timo Glastra --- .github/workflows/continuous-deployment.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index 7b23381d32..5e7d654556 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -19,7 +19,6 @@ jobs: with: # pulls all commits (needed for lerna to correctly version) fetch-depth: 0 - persist-credentials: false # setup dependencies - name: Setup Libindy