From 5afcd33495d8983b5724d13caca087e6ae560756 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 13 Dec 2023 19:36:18 -0300 Subject: [PATCH] fix: only remove session if it's not the new one Signed-off-by: Ariel Gentile feat: deliver messages individually, not fetching from the queue every time Signed-off-by: Ariel Gentile chore: revert to free runners (#1662) Signed-off-by: Ry Jones Signed-off-by: Ariel Gentile chore: create settings.yml (#1663) Signed-off-by: Ry Jones Signed-off-by: Ariel Gentile chore: fix ci and add note to readme (#1669) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile docs: update active maintainers (#1664) Signed-off-by: Karim Stekelenburg Signed-off-by: Ariel Gentile feat: did:peer:2 and did:peer:4 support in DID Exchange (#1550) Signed-off-by: Ariel Gentile feat(presentation-exchange): added PresentationExchangeService (#1672) Signed-off-by: Berend Sliedrecht Signed-off-by: Ariel Gentile chore: removed jan as maintainer (#1678) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile ci: change secret (#1679) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile chore: add meta to rxjs timeout errors (#1683) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile build(deps): bump ws and @types/ws (#1686) Signed-off-by: dependabot[bot] Signed-off-by: Ariel Gentile build(deps): bump follow-redirects from 1.15.2 to 1.15.4 (#1694) Signed-off-by: dependabot[bot] Signed-off-by: Ariel Gentile chore: update shared components libraries (#1691) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile fix: properly print key class (#1684) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile feat(present-proof): add support for aries RFC 510 (#1676) Signed-off-by: Berend Sliedrecht Signed-off-by: Ariel Gentile fix(present-proof): isolated tests (#1696) Signed-off-by: Ariel Gentile feat(indy-vdr): register revocation registry definitions and status list (#1693) Signed-off-by: Ariel Gentile chore: rename to credo-ts (#1703) Signed-off-by: Ry Jones Signed-off-by: Ariel Gentile ci: fix git checkout path and update setup-node (#1709) Signed-off-by: Ariel Gentile fix: remove check for DifPresentationExchangeService dependency (#1702) Signed-off-by: Sai Ranjit Tummalapalli Signed-off-by: Ariel Gentile docs: update zoom meeting link (#1706) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile refactor(oob)!: make label optional (#1680) Signed-off-by: Timo Glastra Co-authored-by: Ariel Gentile Signed-off-by: Ariel Gentile feat: support short legacy connectionless invitations (#1705) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile feat(dids)!: did caching (#1710) feat: did caching Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile fix: jsonld document loader node 18 (#1454) Signed-off-by: Timo Glastra Signed-off-by: Ariel Gentile build(deps): bump amannn/action-semantic-pull-request from 5.3.0 to 5.4.0 (#1656) build(deps): bump amannn/action-semantic-pull-request Bumps [amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request) from 5.3.0 to 5.4.0. - [Release notes](https://github.com/amannn/action-semantic-pull-request/releases) - [Changelog](https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/action-semantic-pull-request/compare/v5.3.0...v5.4.0) --- updated-dependencies: - dependency-name: amannn/action-semantic-pull-request dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Timo Glastra Signed-off-by: Ariel Gentile feat: did rotate (#1699) Signed-off-by: Ariel Gentile refactor: pickup protocol method names Signed-off-by: Ariel Gentile --- .github/settings.yml | 18 + .github/workflows/continuous-deployment.yml | 18 +- .github/workflows/continuous-integration.yml | 20 +- .github/workflows/lint-pr.yml | 2 +- CODEOWNERS | 4 +- MAINTAINERS.md | 4 +- README.md | 12 +- demo/package.json | 6 +- package.json | 2 +- packages/anoncreds-rs/package.json | 6 +- .../anoncreds-rs/tests/LocalDidResolver.ts | 1 + .../anoncreds-rs/tests/anoncreds-flow.test.ts | 2 +- packages/anoncreds/src/index.ts | 1 + packages/anoncreds/src/models/registry.ts | 2 +- .../RevocationRegistryDefinitionOptions.ts | 19 +- .../registry/RevocationStatusListOptions.ts | 31 +- .../utils/__tests__/indyIdentifiers.test.ts | 4 +- .../anoncreds/src/utils/indyIdentifiers.ts | 16 +- .../tests/InMemoryAnonCredsRegistry.ts | 26 +- packages/askar/package.json | 6 +- .../signature-suites/BbsBlsSignature2020.ts | 29 +- .../BbsBlsSignatureProof2020.ts | 27 +- .../src/types/CanonizeOptions.ts | 6 +- .../src/types/CreateProofOptions.ts | 4 - .../src/types/CreateVerifyDataOptions.ts | 7 +- .../src/types/DeriveProofOptions.ts | 6 +- .../src/types/SuiteSignOptions.ts | 5 +- .../src/types/VerifyProofOptions.ts | 4 - .../src/types/VerifySignatureOptions.ts | 4 - .../tests/bbs-signatures.e2e.test.ts | 4 +- .../tests/bbs-signing-provider.e2e.test.ts | 4 +- packages/bbs-signatures/tests/util.ts | 4 +- ...proof.credentials.propose-offerBbs.test.ts | 4 +- packages/cheqd/src/dids/CheqdDidResolver.ts | 1 + packages/core/package.json | 11 +- packages/core/src/agent/AgentModules.ts | 2 + packages/core/src/agent/TransportService.ts | 2 +- .../core/src/agent/__tests__/Agent.test.ts | 8 +- .../src/agent/__tests__/AgentModules.test.ts | 4 + packages/core/src/crypto/Key.ts | 9 + .../src/decorators/attachment/Attachment.ts | 5 +- packages/core/src/index.ts | 1 + .../src/modules/connections/ConnectionsApi.ts | 130 ++- .../modules/connections/ConnectionsModule.ts | 11 +- .../connections/ConnectionsModuleConfig.ts | 44 + .../connections/DidExchangeProtocol.ts | 389 ++++--- .../__tests__/ConnectionsModule.test.ts | 4 +- .../__tests__/InMemoryDidRegistry.ts | 105 ++ .../__tests__/did-rotate.e2e.test.ts | 385 +++++++ .../__tests__/didexchange-numalgo.e2e.test.ts | 193 ++++ .../handlers/DidRotateAckHandler.ts | 17 + .../connections/handlers/DidRotateHandler.ts | 26 + .../handlers/DidRotateProblemReportHandler.ts | 17 + .../connections/handlers/HangupHandler.ts | 17 + .../src/modules/connections/handlers/index.ts | 4 + .../messages/DidExchangeCompleteMessage.ts | 2 +- .../DidExchangeProblemReportMessage.ts | 2 +- .../messages/DidExchangeRequestMessage.ts | 2 +- .../messages/DidExchangeResponseMessage.ts | 11 +- .../messages/DidRotateAckMessage.ts | 20 + .../connections/messages/DidRotateMessage.ts | 38 + .../messages/DidRotateProblemReportMessage.ts | 19 + .../connections/messages/HangupMessage.ts | 30 + .../src/modules/connections/messages/index.ts | 4 + .../connections/models/DidRotateRole.ts | 4 + .../connections/models/HandshakeProtocol.ts | 2 +- .../src/modules/connections/models/index.ts | 1 + .../repository/ConnectionMetadataTypes.ts | 6 + .../repository/ConnectionRecord.ts | 10 + .../repository/ConnectionRepository.ts | 10 +- .../__tests__/ConnectionRecord.test.ts | 2 + .../connections/services/ConnectionService.ts | 5 +- .../connections/services/DidRotateService.ts | 276 +++++ .../modules/connections/services/helpers.ts | 59 +- .../src/modules/connections/services/index.ts | 1 + .../services/DidCommDocumentService.ts | 4 +- packages/core/src/modules/dids/DidsApi.ts | 3 + .../modules/dids/__tests__/DidsApi.test.ts | 46 +- .../src/modules/dids/domain/DidDocument.ts | 4 +- .../src/modules/dids/domain/DidResolver.ts | 2 + .../dids/methods/jwk/JwkDidResolver.ts | 5 + .../dids/methods/key/KeyDidResolver.ts | 5 + .../dids/methods/peer/PeerDidRegistrar.ts | 44 +- .../dids/methods/peer/PeerDidResolver.ts | 21 +- .../methods/peer/__tests__/DidPeer.test.ts | 20 +- .../peer/__tests__/PeerDidRegistrar.test.ts | 109 +- .../__fixtures__/didPeer4zQmUJdJ.json | 24 + .../__fixtures__/didPeer4zQmd8Cp.json | 39 + .../peer/__tests__/peerDidNumAlgo4.test.ts | 45 + .../peer/createPeerDidDocumentFromServices.ts | 6 +- .../src/modules/dids/methods/peer/didPeer.ts | 20 +- .../dids/methods/peer/peerDidNumAlgo4.ts | 138 +++ .../dids/methods/web/WebDidResolver.ts | 2 + .../src/modules/dids/repository/DidRecord.ts | 4 + .../modules/dids/repository/DidRepository.ts | 14 +- .../dids/services/DidResolverService.ts | 65 +- .../__tests__/DidResolverService.test.ts | 71 +- packages/core/src/modules/dids/types.ts | 25 +- .../DifPresentationExchangeError.ts | 13 + .../DifPresentationExchangeModule.ts | 25 + .../DifPresentationExchangeService.ts | 548 ++++++++++ .../dif-presentation-exchange/index.ts | 4 + .../models/DifPexCredentialsForRequest.ts | 119 +++ .../dif-presentation-exchange/models/index.ts | 11 + .../utils/credentialSelection.ts | 314 ++++++ .../dif-presentation-exchange/utils/index.ts | 2 + .../utils/transform.ts | 78 ++ .../discover-features/DiscoverFeaturesApi.ts | 5 +- .../message-pickup/MessagePickupApi.ts | 79 +- .../message-pickup/MessagePickupApiOptions.ts | 10 +- .../message-pickup/MessagePickupSession.ts | 1 + .../protocol/BaseMessagePickupProtocol.ts | 4 +- .../protocol/MessagePickupProtocol.ts | 4 +- .../protocol/MessagePickupProtocolOptions.ts | 2 + .../protocol/v1/V1MessagePickupProtocol.ts | 20 +- .../protocol/v2/V2MessagePickupProtocol.ts | 26 +- .../__tests__/V2MessagePickupProtocol.test.ts | 8 +- .../services/MessagePickupSessionService.ts | 8 +- .../InMemoryMessagePickupRepository.ts | 19 +- .../storage/MessagePickupRepositoryOptions.ts | 2 - .../message-pickup/storage/QueuedMessage.ts | 1 - .../storage/QueuedMessageState.ts | 5 - .../modules/message-pickup/storage/index.ts | 1 - packages/core/src/modules/oob/OutOfBandApi.ts | 11 +- .../core/src/modules/oob/OutOfBandService.ts | 1 - packages/core/src/modules/oob/helpers.ts | 6 +- .../oob/messages/OutOfBandInvitation.ts | 4 +- .../DifPresentationExchangeProofFormat.ts | 73 ++ ...fPresentationExchangeProofFormatService.ts | 367 +++++++ ...entationExchangeProofFormatService.test.ts | 204 ++++ .../dif-presentation-exchange/index.ts | 2 + .../core/src/modules/proofs/formats/index.ts | 2 + .../proofs/protocol/v2/V2ProofProtocol.ts | 2 +- .../proofs/protocol/v2/__tests__/fixtures.ts | 13 + ...entation-exchange-presentation.e2e.test.ts | 451 ++++++++ .../core/src/modules/proofs/utils/index.ts | 1 + .../modules/routing/MediationRecipientApi.ts | 5 +- .../services/MediationRecipientService.ts | 5 +- .../routing/services/MediatorService.ts | 16 +- .../src/modules/routing/services/helpers.ts | 13 + .../W3cJsonLdCredentialService.ts | 12 +- .../W3cJsonLdCredentialService.test.ts | 21 + .../modules/vc/data-integrity/deriveProof.ts | 8 +- .../modules/vc/data-integrity/jsonldUtil.ts | 8 +- .../data-integrity/models/GetProofsOptions.ts | 4 - .../data-integrity/models/GetTypeOptions.ts | 4 - .../CredentialIssuancePurpose.ts | 2 - .../JwsLinkedDataSignature.ts | 7 - .../ed25519/Ed25519Signature2018.ts | 7 - packages/core/src/modules/vc/index.ts | 2 +- .../presentation/W3cJsonPresentation.ts | 2 + .../vc/models/presentation/W3cPresentation.ts | 6 + .../__tests__/__snapshots__/0.1.test.ts.snap | 28 + .../0.1-0.2/__tests__/connection.test.ts | 14 + .../0.2-0.3/__tests__/connection.test.ts | 8 + ...nedUrl.test.ts => parseInvitation.test.ts} | 84 +- packages/core/src/utils/objectEquality.ts | 14 +- packages/core/src/utils/parseInvitation.ts | 52 +- packages/core/tests/helpers.ts | 58 +- packages/core/tests/jsonld.ts | 3 + packages/core/tests/oob.test.ts | 2 +- .../indy-sdk-to-askar-migration/package.json | 6 +- .../services/IndySdkAnonCredsRegistry.ts | 6 +- .../utils/__tests__/identifiers.test.ts | 4 +- .../src/anoncreds/utils/identifiers.ts | 10 +- .../src/dids/IndySdkIndyDidResolver.ts | 2 + .../src/dids/IndySdkSovDidResolver.ts | 2 + packages/indy-vdr/package.json | 6 +- .../src/anoncreds/IndyVdrAnonCredsRegistry.ts | 554 ++++++++-- .../utils/__tests__/identifiers.test.ts | 4 +- .../utils/__tests__/transform.test.ts | 164 +++ .../src/anoncreds/utils/identifiers.ts | 20 +- .../indy-vdr/src/anoncreds/utils/transform.ts | 116 ++- .../src/dids/IndyVdrIndyDidResolver.ts | 2 + .../src/dids/IndyVdrSovDidResolver.ts | 2 + .../indy-vdr/tests/__fixtures__/anoncreds.ts | 11 + .../indy-vdr-anoncreds-registry.e2e.test.ts | 489 ++++----- packages/sd-jwt-vc/package.json | 2 +- packages/tenants/tests/tenants.e2e.test.ts | 8 +- tests/transport/SubjectInboundTransport.ts | 11 +- yarn.lock | 961 +++++++++++++++++- 181 files changed, 7230 insertions(+), 894 deletions(-) create mode 100644 .github/settings.yml create mode 100644 packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts create mode 100644 packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts create mode 100644 packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts create mode 100644 packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts create mode 100644 packages/core/src/modules/connections/handlers/DidRotateHandler.ts create mode 100644 packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts create mode 100644 packages/core/src/modules/connections/handlers/HangupHandler.ts create mode 100644 packages/core/src/modules/connections/messages/DidRotateAckMessage.ts create mode 100644 packages/core/src/modules/connections/messages/DidRotateMessage.ts create mode 100644 packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts create mode 100644 packages/core/src/modules/connections/messages/HangupMessage.ts create mode 100644 packages/core/src/modules/connections/models/DidRotateRole.ts create mode 100644 packages/core/src/modules/connections/services/DidRotateService.ts create mode 100644 packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json create mode 100644 packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json create mode 100644 packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts create mode 100644 packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/index.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/models/index.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/utils/index.ts create mode 100644 packages/core/src/modules/dif-presentation-exchange/utils/transform.ts delete mode 100644 packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts create mode 100644 packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts create mode 100644 packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts create mode 100644 packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts create mode 100644 packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts create mode 100644 packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts create mode 100644 packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts create mode 100644 packages/core/src/modules/proofs/utils/index.ts create mode 100644 packages/core/src/modules/routing/services/helpers.ts rename packages/core/src/utils/__tests__/{shortenedUrl.test.ts => parseInvitation.test.ts} (69%) create mode 100644 packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000000..e56ad92080 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,18 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +repository: + name: credo-ts + description: Extension libraries for Aries Framework JavaScript + homepage: https://github.com/openwallet-foundation/credo-ts + default_branch: main + has_downloads: false + has_issues: true + has_projects: false + has_wiki: false + archived: false + private: false + allow_squash_merge: true + allow_merge_commit: false + allow_rebase_merge: true diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml index c7e209bfda..1bf351d22a 100644 --- a/.github/workflows/continuous-deployment.yml +++ b/.github/workflows/continuous-deployment.yml @@ -10,11 +10,11 @@ env: jobs: release-canary: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Release Canary if: "!startsWith(github.event.head_commit.message, 'chore(release): v')" steps: - - name: Checkout aries-framework-javascript + - name: Checkout credo-ts uses: actions/checkout@v4 with: # pulls all commits (needed for lerna to correctly version) @@ -25,7 +25,7 @@ jobs: uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' @@ -40,8 +40,8 @@ jobs: export NEXT_VERSION_BUMP=$(./node_modules/.bin/ts-node ./scripts/get-next-bump.ts) yarn lerna publish --loglevel=verbose --canary $NEXT_VERSION_BUMP --exact --force-publish --yes --no-verify-access --dist-tag alpha env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_PUBLISH }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH }} - name: Get version number id: get-version @@ -61,7 +61,7 @@ jobs: git push origin v${{ steps.get-version.outputs.version }} --no-verify release-stable: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Create Stable Release # Only run if the last pushed commit is a release commit if: "startsWith(github.event.head_commit.message, 'chore(release): v')" @@ -74,7 +74,7 @@ jobs: uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' @@ -109,5 +109,5 @@ jobs: - name: Release to NPM run: yarn lerna publish from-package --loglevel=verbose --yes --no-verify-access env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_PUBLISH }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH }} diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 57edd1bb1d..c057fe5d8b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -12,7 +12,7 @@ env: TEST_AGENT_PUBLIC_DID_SEED: 000000000000000000000000Trustee9 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 + LIB_INDY_STRG_POSTGRES: /home/runner/work/credo-ts/indy-sdk/experimental/plugins/postgres_storage/target/release # for Linux 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. @@ -28,7 +28,7 @@ jobs: # validation scripts. To still be able to run the CI we can manually trigger it by adding the 'ci-test' # label to the pull request ci-trigger: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 outputs: triggered: ${{ steps.check.outputs.triggered }} steps: @@ -47,10 +47,10 @@ jobs: echo triggered="${SHOULD_RUN}" >> "$GITHUB_OUTPUT" validate: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Validate steps: - - name: Checkout aries-framework-javascript + - name: Checkout credo-ts uses: actions/checkout@v4 # setup dependencies @@ -58,7 +58,7 @@ jobs: uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' @@ -79,7 +79,7 @@ jobs: run: yarn build integration-test: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Integration Tests strategy: @@ -111,7 +111,7 @@ jobs: uses: ./.github/actions/setup-postgres-wallet-plugin - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'yarn' @@ -126,12 +126,12 @@ jobs: if: always() version-stable: - runs-on: aries-ubuntu-2004 + runs-on: ubuntu-20.04 name: Release stable needs: [integration-test, validate] if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' steps: - - name: Checkout aries-framework-javascript + - name: Checkout agent-framework-javascript uses: actions/checkout@v4 with: # pulls all commits (needed for lerna to correctly version) @@ -143,7 +143,7 @@ jobs: uses: ./.github/actions/setup-libindy - name: Setup NodeJS - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: 'yarn' diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index f02bbae69d..50b29cb22e 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@v5.3.0 + - uses: amannn/action-semantic-pull-request@v5.4.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/CODEOWNERS b/CODEOWNERS index 3d2299408b..f1b505c931 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ # SPDX-License-Identifier: Apache-2.0 -# Aries Framework Javascript maintainers -* @hyperledger/aries-framework-javascript-committers +# Agent Framework Javascript maintainers +* @openwallet-foundation/agent-framework-javascript-maintainers diff --git a/MAINTAINERS.md b/MAINTAINERS.md index ff84db7f6e..a5830b63f1 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -4,10 +4,8 @@ | name | Github | Discord | | ------------------ | ---------------------------------------------------------- | ---------------- | -| Berend Sliedrecht | [@blu3beri](https://github.com/blu3beri) | blu3beri#2230 | +| Berend Sliedrecht | [@berendsliedrecht](https://github.com/berendsliedrecht) | blu3beri#2230 | | Jakub Kočí | [@jakubkoci](https://github.com/jakubkoci) | jakubkoci#1481 | -| James Ebert | [@JamesKEbert](https://github.com/JamesKEbert) | JamesEbert#4350 | | Karim Stekelenburg | [@karimStekelenburg](https://github.com/karimStekelenburg) | ssi_karim#3505 | | Timo Glastra | [@TimoGlastra](https://github.com/TimoGlastra) | TimoGlastra#2988 | | Ariel Gentile | [@genaris](https://github.com/genaris) | GenAris#4962 | -| Jan Rietveld | [@janrtvld](https://github.com/janrtvld) | janrtvld#3868 | diff --git a/README.md b/README.md index c7a2d8cccf..2c2d8176f9 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ Aries Framework JavaScript is a framework written in TypeScript for building **SSI Agents and DIDComm services** that aims to be **compliant and interoperable** with the standards defined in the [Aries RFCs](https://github.com/hyperledger/aries-rfcs). +> **Note** +> The Aries Framework JavaScript project has recently been moved from the Hyperledger Foundation to the Open Wallet Foundation. +> We are currently in the process of changing the name of the project, and updating all the documentation and links to reflect this change. +> You may encounter some broken links, or references to the old name, but we are working hard to fix this. Once the new name has been decided +> we will update this README and all the documentation to reflect this change. +> You can follow this discussion for updates about the name: https://github.com/openwallet-foundation/agent-framework-javascript/discussions/1668 + ## Features - 🏃 Runs in React Native & Node.JS @@ -186,10 +193,7 @@ Although Aries Framework JavaScript tries to follow the standards as described i If you would like to contribute to the framework, please read the [Framework Developers README](/DEVREADME.md) and the [CONTRIBUTING](/CONTRIBUTING.md) guidelines. These documents will provide more information to get you started! -The Aries Framework JavaScript call takes place every week at Thursday, 6AM Pacific Time. See [World Time Buddy](https://www.worldtimebuddy.com/?qm=1&lid=5,2759794,8&h=5&date=2023-5-19&sln=9-10&hf=1) for the time in your timezone. The meeting is held on [Zoom](https://zoom.us/j/99751084865?pwd=TW1rU0FDVTBqUlhnWnY2NERkd1diZz09). -This meeting is for contributors to groom and plan the backlog, and discuss issues. -Meeting agendas and recordings can be found [here](https://wiki.hyperledger.org/display/ARIES/Framework+JS+Meetings). -Feel free to join! +There are regular community working groups to discuss ongoing efforts within the framework, showcase items you've built with Credo, or ask questions. See [Meeting Information](https://github.com/openwallet-foundation/credo-ts/wiki/Meeting-Information) for up to date information on the meeting schedule. Everyone is welcome to join! ## License diff --git a/demo/package.json b/demo/package.json index 4ffbef505b..4cb9fe150f 100644 --- a/demo/package.json +++ b/demo/package.json @@ -14,9 +14,9 @@ "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { - "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.5", - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", + "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.5", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "inquirer": "^8.2.5" }, "devDependencies": { diff --git a/package.json b/package.json index 3f7c1efbac..df9133150f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "next-version-bump": "ts-node ./scripts/get-next-bump.ts" }, "devDependencies": { - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "@types/cors": "^2.8.10", "@types/eslint": "^8.21.2", "@types/express": "^4.17.13", diff --git a/packages/anoncreds-rs/package.json b/packages/anoncreds-rs/package.json index aaafaf228d..494d899a09 100644 --- a/packages/anoncreds-rs/package.json +++ b/packages/anoncreds-rs/package.json @@ -32,8 +32,8 @@ "tsyringe": "^4.8.0" }, "devDependencies": { - "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4", - "@hyperledger/anoncreds-shared": "^0.2.0-dev.4", + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.5", + "@hyperledger/anoncreds-shared": "^0.2.0-dev.5", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", "reflect-metadata": "^0.1.13", @@ -41,6 +41,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/anoncreds-shared": "^0.2.0-dev.4" + "@hyperledger/anoncreds-shared": "^0.2.0-dev.5" } } diff --git a/packages/anoncreds-rs/tests/LocalDidResolver.ts b/packages/anoncreds-rs/tests/LocalDidResolver.ts index 1f50c1b3aa..c10434f205 100644 --- a/packages/anoncreds-rs/tests/LocalDidResolver.ts +++ b/packages/anoncreds-rs/tests/LocalDidResolver.ts @@ -4,6 +4,7 @@ import { DidsApi } from '@aries-framework/core' export class LocalDidResolver implements DidResolver { public readonly supportedMethods = ['sov', 'indy'] + public readonly allowsCaching = false public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} diff --git a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts index 06481055f4..01a71f1ae3 100644 --- a/packages/anoncreds-rs/tests/anoncreds-flow.test.ts +++ b/packages/anoncreds-rs/tests/anoncreds-flow.test.ts @@ -230,7 +230,7 @@ async function anonCredsFlowTest(options: { issuerId: string; revocable: boolean options: {}, }) - if (!revocationStatusListState.revocationStatusList || !revocationStatusListState.timestamp) { + if (!revocationStatusListState.revocationStatusList) { throw new Error('Failed to create revocation status list') } } diff --git a/packages/anoncreds/src/index.ts b/packages/anoncreds/src/index.ts index fad5355d54..4b8fcd8133 100644 --- a/packages/anoncreds/src/index.ts +++ b/packages/anoncreds/src/index.ts @@ -15,3 +15,4 @@ export { generateLegacyProverDidLikeString } from './utils/proverDid' export * from './utils/indyIdentifiers' export { assertBestPracticeRevocationInterval } from './utils/revocationInterval' export { storeLinkSecret } from './utils/linkSecret' +export { dateToTimestamp } from './utils' diff --git a/packages/anoncreds/src/models/registry.ts b/packages/anoncreds/src/models/registry.ts index bc4ad8a152..883fd859fe 100644 --- a/packages/anoncreds/src/models/registry.ts +++ b/packages/anoncreds/src/models/registry.ts @@ -37,7 +37,7 @@ export interface AnonCredsRevocationRegistryDefinition { export interface AnonCredsRevocationStatusList { issuerId: string revRegDefId: string - revocationList: number[] + revocationList: Array currentAccumulator: string timestamp: number } diff --git a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts index 3f7a07ed77..fee5653c1e 100644 --- a/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationRegistryDefinitionOptions.ts @@ -4,6 +4,7 @@ import type { AnonCredsOperationStateFinished, AnonCredsResolutionMetadata, Extensible, + AnonCredsOperationStateAction, } from './base' import type { AnonCredsRevocationRegistryDefinition } from '../../models/registry' @@ -19,27 +20,33 @@ export interface RegisterRevocationRegistryDefinitionOptions { options: Extensible } +export interface RegisterRevocationRegistryDefinitionReturnStateAction extends AnonCredsOperationStateAction { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId: string +} + export interface RegisterRevocationRegistryDefinitionReturnStateFailed extends AnonCredsOperationStateFailed { revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId?: string } +export interface RegisterRevocationRegistryDefinitionReturnStateWait extends AnonCredsOperationStateWait { + revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition + revocationRegistryDefinitionId?: string +} + export interface RegisterRevocationRegistryDefinitionReturnStateFinished extends AnonCredsOperationStateFinished { revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition revocationRegistryDefinitionId: string } -export interface RegisterRevocationRegistryDefinitionReturnState extends AnonCredsOperationStateWait { - revocationRegistryDefinition?: AnonCredsRevocationRegistryDefinition - revocationRegistryDefinitionId?: string -} - export interface RegisterRevocationRegistryDefinitionReturn { jobId?: string revocationRegistryDefinitionState: + | RegisterRevocationRegistryDefinitionReturnStateWait + | RegisterRevocationRegistryDefinitionReturnStateAction | RegisterRevocationRegistryDefinitionReturnStateFailed | RegisterRevocationRegistryDefinitionReturnStateFinished - | RegisterRevocationRegistryDefinitionReturnState revocationRegistryDefinitionMetadata: Extensible registrationMetadata: Extensible } diff --git a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts index 05b1353801..21ea1bca95 100644 --- a/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts +++ b/packages/anoncreds/src/services/registry/RevocationStatusListOptions.ts @@ -4,8 +4,10 @@ import type { AnonCredsOperationStateFinished, AnonCredsResolutionMetadata, Extensible, + AnonCredsOperationStateAction, } from './base' import type { AnonCredsRevocationStatusList } from '../../models/registry' +import type { Optional } from '@aries-framework/core' export interface GetRevocationStatusListReturn { revocationStatusList?: AnonCredsRevocationStatusList @@ -13,34 +15,39 @@ export interface GetRevocationStatusListReturn { revocationStatusListMetadata: Extensible } +// Timestamp is often calculated by the ledger, otherwise method should just take current time +// Return type does include the timestamp. +export type AnonCredsRevocationStatusListWithoutTimestamp = Omit +export type AnonCredsRevocationStatusListWithOptionalTimestamp = Optional + export interface RegisterRevocationStatusListOptions { - // Timestamp is often calculated by the ledger, otherwise method should just take current time - // Return type does include the timestamp. - revocationStatusList: Omit + revocationStatusList: AnonCredsRevocationStatusListWithoutTimestamp options: Extensible } +export interface RegisterRevocationStatusListReturnStateAction extends AnonCredsOperationStateAction { + revocationStatusList: AnonCredsRevocationStatusListWithOptionalTimestamp +} + export interface RegisterRevocationStatusListReturnStateFailed extends AnonCredsOperationStateFailed { - revocationStatusList?: AnonCredsRevocationStatusList - timestamp?: string + revocationStatusList?: AnonCredsRevocationStatusListWithOptionalTimestamp } -export interface RegisterRevocationStatusListReturnStateFinished extends AnonCredsOperationStateFinished { - revocationStatusList: AnonCredsRevocationStatusList - timestamp: string +export interface RegisterRevocationStatusListReturnStateWait extends AnonCredsOperationStateWait { + revocationStatusList?: AnonCredsRevocationStatusListWithOptionalTimestamp } -export interface RegisterRevocationStatusListReturnState extends AnonCredsOperationStateWait { - revocationStatusList?: AnonCredsRevocationStatusList - timestamp?: string +export interface RegisterRevocationStatusListReturnStateFinished extends AnonCredsOperationStateFinished { + revocationStatusList: AnonCredsRevocationStatusList } export interface RegisterRevocationStatusListReturn { jobId?: string revocationStatusListState: + | RegisterRevocationStatusListReturnStateWait + | RegisterRevocationStatusListReturnStateAction | RegisterRevocationStatusListReturnStateFailed | RegisterRevocationStatusListReturnStateFinished - | RegisterRevocationStatusListReturnState revocationStatusListMetadata: Extensible registrationMetadata: Extensible } diff --git a/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts b/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts index 7fba68562d..7f94486586 100644 --- a/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts +++ b/packages/anoncreds/src/utils/__tests__/indyIdentifiers.test.ts @@ -1,6 +1,6 @@ import { getUnqualifiedCredentialDefinitionId, - getUnqualifiedRevocationRegistryId, + getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedSchemaId, parseIndyCredentialDefinitionId, parseIndyRevocationRegistryId, @@ -67,7 +67,7 @@ describe('Legacy Indy Identifier Regex', () => { const credentialDefinitionTag = 'someTag' const tag = 'anotherTag' - expect(getUnqualifiedRevocationRegistryId(did, seqNo, credentialDefinitionTag, tag)).toEqual( + expect(getUnqualifiedRevocationRegistryDefinitionId(did, seqNo, credentialDefinitionTag, tag)).toEqual( '12345:4:12345:3:CL:420:someTag:CL_ACCUM:anotherTag' ) }) diff --git a/packages/anoncreds/src/utils/indyIdentifiers.ts b/packages/anoncreds/src/utils/indyIdentifiers.ts index 1e20f75c55..3cd050e7c8 100644 --- a/packages/anoncreds/src/utils/indyIdentifiers.ts +++ b/packages/anoncreds/src/utils/indyIdentifiers.ts @@ -18,7 +18,7 @@ export const didIndyCredentialDefinitionIdRegex = new RegExp( `^${didIndyAnonCredsBase.source}/CLAIM_DEF/([1-9][0-9]*)/(.+)$` ) -// :4::3:CL::CL_ACCUM: +// :4::3:CL:::CL_ACCUM: export const unqualifiedRevocationRegistryIdRegex = /^([a-zA-Z0-9]{21,22}):4:[a-zA-Z0-9]{21,22}:3:CL:([1-9][0-9]*):(.+):CL_ACCUM:(.+)$/ // did:indy::/anoncreds/v0/REV_REG_DEF/// @@ -32,18 +32,22 @@ export function getUnqualifiedSchemaId(unqualifiedDid: string, name: string, ver return `${unqualifiedDid}:2:${name}:${version}` } -export function getUnqualifiedCredentialDefinitionId(unqualifiedDid: string, seqNo: string | number, tag: string) { - return `${unqualifiedDid}:3:CL:${seqNo}:${tag}` +export function getUnqualifiedCredentialDefinitionId( + unqualifiedDid: string, + schemaSeqNo: string | number, + tag: string +) { + return `${unqualifiedDid}:3:CL:${schemaSeqNo}:${tag}` } // TZQuLp43UcYTdtc3HewcDz:4:TZQuLp43UcYTdtc3HewcDz:3:CL:98158:BaustellenzertifikateNU1:CL_ACCUM:1-100 -export function getUnqualifiedRevocationRegistryId( +export function getUnqualifiedRevocationRegistryDefinitionId( unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, credentialDefinitionTag: string, revocationRegistryTag: string ) { - return `${unqualifiedDid}:4:${unqualifiedDid}:3:CL:${seqNo}:${credentialDefinitionTag}:CL_ACCUM:${revocationRegistryTag}` + return `${unqualifiedDid}:4:${unqualifiedDid}:3:CL:${schemaSeqNo}:${credentialDefinitionTag}:CL_ACCUM:${revocationRegistryTag}` } export function isUnqualifiedCredentialDefinitionId(credentialDefinitionId: string) { diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index da51aa12ec..9c2e8bc42b 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import type { AnonCredsRegistry, GetSchemaReturn, @@ -25,12 +24,12 @@ import BigNumber from 'bn.js' import { getDidIndyCredentialDefinitionId, - getDidIndyRevocationRegistryId, + getDidIndyRevocationRegistryDefinitionId, getDidIndySchemaId, } from '../../indy-sdk/src/anoncreds/utils/identifiers' import { parseIndyCredentialDefinitionId, - getUnqualifiedRevocationRegistryId, + getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedCredentialDefinitionId, getUnqualifiedSchemaId, parseIndyDid, @@ -71,7 +70,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { this.revocationStatusLists = existingRevocationStatusLists } - public async getSchema(agentContext: AgentContext, schemaId: string): Promise { + public async getSchema(_agentContext: AgentContext, schemaId: string): Promise { const schema = this.schemas[schemaId] const parsed = parseIndySchemaId(schemaId) @@ -103,7 +102,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async registerSchema( - agentContext: AgentContext, + _agentContext: AgentContext, options: RegisterSchemaOptions ): Promise { const { namespace, namespaceIdentifier } = parseIndyDid(options.schema.issuerId) @@ -140,7 +139,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async getCredentialDefinition( - agentContext: AgentContext, + _agentContext: AgentContext, credentialDefinitionId: string ): Promise { const credentialDefinition = this.credentialDefinitions[credentialDefinitionId] @@ -165,7 +164,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async registerCredentialDefinition( - agentContext: AgentContext, + _agentContext: AgentContext, options: RegisterCredentialDefinitionOptions ): Promise { const parsedSchema = parseIndySchemaId(options.credentialDefinition.schemaId) @@ -211,7 +210,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async getRevocationRegistryDefinition( - agentContext: AgentContext, + _agentContext: AgentContext, revocationRegistryDefinitionId: string ): Promise { const revocationRegistryDefinition = this.revocationRegistryDefinitions[revocationRegistryDefinitionId] @@ -236,7 +235,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async registerRevocationRegistryDefinition( - agentContext: AgentContext, + _agentContext: AgentContext, options: RegisterRevocationRegistryDefinitionOptions ): Promise { const parsedCredentialDefinition = parseIndyCredentialDefinitionId(options.revocationRegistryDefinition.credDefId) @@ -249,7 +248,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { const { namespace, namespaceIdentifier } = parseIndyDid(options.revocationRegistryDefinition.issuerId) const legacyIssuerId = namespaceIdentifier - const didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryId( + const didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryDefinitionId( namespace, namespaceIdentifier, indyLedgerSeqNo, @@ -259,7 +258,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { this.revocationRegistryDefinitions[didIndyRevocationRegistryDefinitionId] = options.revocationRegistryDefinition - const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryId( + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( legacyIssuerId, indyLedgerSeqNo, parsedCredentialDefinition.tag, @@ -284,7 +283,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async getRevocationStatusList( - agentContext: AgentContext, + _agentContext: AgentContext, revocationRegistryId: string, timestamp: number ): Promise { @@ -322,7 +321,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { } public async registerRevocationStatusList( - agentContext: AgentContext, + _agentContext: AgentContext, options: RegisterRevocationStatusListOptions ): Promise { const timestamp = (options.options.timestamp as number) ?? dateToTimestamp(new Date()) @@ -341,7 +340,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { revocationStatusListState: { state: 'finished', revocationStatusList, - timestamp: timestamp.toString(), }, } } diff --git a/packages/askar/package.json b/packages/askar/package.json index 6a88bf3449..dbce742314 100644 --- a/packages/askar/package.json +++ b/packages/askar/package.json @@ -32,8 +32,8 @@ "tsyringe": "^4.8.0" }, "devDependencies": { - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", - "@hyperledger/aries-askar-shared": "^0.2.0-dev.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", + "@hyperledger/aries-askar-shared": "^0.2.0-dev.5", "@types/bn.js": "^5.1.0", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", @@ -42,6 +42,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/aries-askar-shared": "^0.2.0-dev.1" + "@hyperledger/aries-askar-shared": "^0.2.0-dev.5" } } diff --git a/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts b/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts index f1a4239007..5b517a1a8e 100644 --- a/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts +++ b/packages/bbs-signatures/src/signature-suites/BbsBlsSignature2020.ts @@ -113,7 +113,7 @@ export class BbsBlsSignature2020 extends LinkedDataProof { * @returns {Promise} Resolves with the created proof object. */ public async createProof(options: CreateProofOptions): Promise> { - const { document, purpose, documentLoader, expansionMap, compactProof } = options + const { document, purpose, documentLoader, compactProof } = options let proof: JsonObject @@ -121,7 +121,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { if (this.proof) { proof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { documentLoader, - expansionMap, compactToRelative: true, }) } else { @@ -159,7 +158,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, suite: this, documentLoader, - expansionMap, }) // create data to sign @@ -168,7 +166,7 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, proof, documentLoader, - expansionMap, + compactProof, }) ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) @@ -179,7 +177,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, proof, documentLoader, - expansionMap, }) delete proof['@context'] @@ -192,7 +189,7 @@ export class BbsBlsSignature2020 extends LinkedDataProof { * @returns {Promise<{object}>} Resolves with the verification result. */ public async verifyProof(options: VerifyProofOptions): Promise> { - const { proof, document, documentLoader, expansionMap, purpose } = options + const { proof, document, documentLoader, purpose } = options try { // create data to verify @@ -201,7 +198,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, proof, documentLoader, - expansionMap, compactProof: false, }) ).map((item) => new Uint8Array(TypedArrayEncoder.fromString(item))) @@ -219,7 +215,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { document, proof, documentLoader, - expansionMap, }) if (!verified) { throw new Error('Invalid signature.') @@ -231,7 +226,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { suite: this, verificationMethod, documentLoader, - expansionMap, }) if (!valid) { throw error @@ -244,24 +238,22 @@ export class BbsBlsSignature2020 extends LinkedDataProof { } public async canonize(input: Record, options: CanonizeOptions): Promise { - const { documentLoader, expansionMap, skipExpansion } = options + const { documentLoader, skipExpansion } = options return jsonld.canonize(input, { algorithm: 'URDNA2015', format: 'application/n-quads', documentLoader, - expansionMap, skipExpansion, useNative: this.useNativeCanonize, }) } public async canonizeProof(proof: Record, options: CanonizeOptions): Promise { - const { documentLoader, expansionMap } = options + const { documentLoader } = options proof = { ...proof } delete proof[this.proofSignatureKey] return this.canonize(proof, { documentLoader, - expansionMap, skipExpansion: false, }) } @@ -272,17 +264,15 @@ export class BbsBlsSignature2020 extends LinkedDataProof { * @returns {Promise<{string[]>}. */ public async createVerifyData(options: CreateVerifyDataOptions): Promise { - const { proof, document, documentLoader, expansionMap } = options + const { proof, document, documentLoader } = options const proof2 = { ...proof, '@context': document['@context'] } const proofStatements = await this.createVerifyProofData(proof2, { documentLoader, - expansionMap, }) const documentStatements = await this.createVerifyDocumentData(document, { documentLoader, - expansionMap, }) // concatenate c14n proof options and c14n document @@ -297,11 +287,10 @@ export class BbsBlsSignature2020 extends LinkedDataProof { */ public async createVerifyProofData( proof: Record, - { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + { documentLoader }: { documentLoader?: DocumentLoader } ): Promise { const c14nProofOptions = await this.canonizeProof(proof, { documentLoader, - expansionMap, }) return c14nProofOptions.split('\n').filter((_) => _.length > 0) @@ -315,11 +304,10 @@ export class BbsBlsSignature2020 extends LinkedDataProof { */ public async createVerifyDocumentData( document: Record, - { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + { documentLoader }: { documentLoader?: DocumentLoader } ): Promise { const c14nDocument = await this.canonize(document, { documentLoader, - expansionMap, }) return c14nDocument.split('\n').filter((_) => _.length > 0) @@ -329,7 +317,6 @@ export class BbsBlsSignature2020 extends LinkedDataProof { * @param document {object} to be signed. * @param proof {object} * @param documentLoader {function} - * @param expansionMap {function} */ public async getVerificationMethod({ proof, diff --git a/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts b/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts index 59d0b37eb3..2d902c8591 100644 --- a/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts +++ b/packages/bbs-signatures/src/signature-suites/BbsBlsSignatureProof2020.ts @@ -62,7 +62,7 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { * @returns {Promise} Resolves with the derived proof object. */ public async deriveProof(options: DeriveProofOptions): Promise> { - const { document, proof, revealDocument, documentLoader, expansionMap } = options + const { document, proof, revealDocument, documentLoader } = options let { nonce } = options const proofType = proof.type @@ -98,7 +98,6 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { // use proof JSON-LD document passed to API derivedProof = await jsonld.compact(this.proof, SECURITY_CONTEXT_URL, { documentLoader, - expansionMap, compactToRelative: false, }) } else { @@ -112,13 +111,11 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { // Get the input document statements const documentStatements = await suite.createVerifyDocumentData(document, { documentLoader, - expansionMap, }) // Get the proof statements const proofStatements = await suite.createVerifyProofData(proof, { documentLoader, - expansionMap, }) // Transform any blank node identifiers for the input @@ -137,7 +134,6 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { // Canonicalize the resulting reveal document const revealDocumentStatements = await suite.createVerifyDocumentData(revealDocumentResult, { documentLoader, - expansionMap, }) //Get the indicies of the revealed statements from the transformed input document offset @@ -216,7 +212,7 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { * @returns {Promise<{object}>} Resolves with the verification result. */ public async verifyProof(options: VerifyProofOptions): Promise { - const { document, documentLoader, expansionMap, purpose } = options + const { document, documentLoader, purpose } = options const { proof } = options try { @@ -227,13 +223,11 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { // Get the proof statements const proofStatements = await this.createVerifyProofData(proofIncludingDocumentContext, { documentLoader, - expansionMap, }) // Get the document statements const documentStatements = await this.createVerifyProofData(document, { documentLoader, - expansionMap, }) // Transform the blank node identifier placeholders for the document statements @@ -278,7 +272,6 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { suite: this, verificationMethod, documentLoader, - expansionMap, }) if (!valid) { throw error @@ -291,19 +284,18 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { } public async canonize(input: JsonObject, options: CanonizeOptions): Promise { - const { documentLoader, expansionMap, skipExpansion } = options + const { documentLoader, skipExpansion } = options return jsonld.canonize(input, { algorithm: 'URDNA2015', format: 'application/n-quads', documentLoader, - expansionMap, skipExpansion, useNative: this.useNativeCanonize, }) } public async canonizeProof(proof: JsonObject, options: CanonizeOptions): Promise { - const { documentLoader, expansionMap } = options + const { documentLoader } = options proof = { ...proof } delete proof.nonce @@ -311,7 +303,6 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { return this.canonize(proof, { documentLoader, - expansionMap, skipExpansion: false, }) } @@ -322,15 +313,13 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { * @returns {Promise<{string[]>}. */ public async createVerifyData(options: CreateVerifyDataOptions): Promise { - const { proof, document, documentLoader, expansionMap } = options + const { proof, document, documentLoader } = options const proofStatements = await this.createVerifyProofData(proof, { documentLoader, - expansionMap, }) const documentStatements = await this.createVerifyDocumentData(document, { documentLoader, - expansionMap, }) // concatenate c14n proof options and c14n document @@ -345,11 +334,10 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { */ public async createVerifyProofData( proof: JsonObject, - { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + { documentLoader }: { documentLoader?: DocumentLoader } ): Promise { const c14nProofOptions = await this.canonizeProof(proof, { documentLoader, - expansionMap, }) return c14nProofOptions.split('\n').filter((_) => _.length > 0) @@ -363,11 +351,10 @@ export class BbsBlsSignatureProof2020 extends LinkedDataProof { */ public async createVerifyDocumentData( document: JsonObject, - { documentLoader, expansionMap }: { documentLoader?: DocumentLoader; expansionMap?: () => void } + { documentLoader }: { documentLoader?: DocumentLoader } ): Promise { const c14nDocument = await this.canonize(document, { documentLoader, - expansionMap, }) return c14nDocument.split('\n').filter((_) => _.length > 0) diff --git a/packages/bbs-signatures/src/types/CanonizeOptions.ts b/packages/bbs-signatures/src/types/CanonizeOptions.ts index e2a4af60c8..73a4217e8c 100644 --- a/packages/bbs-signatures/src/types/CanonizeOptions.ts +++ b/packages/bbs-signatures/src/types/CanonizeOptions.ts @@ -21,11 +21,7 @@ export interface CanonizeOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - // eslint-disable-next-line - expansionMap?: () => void + /** * Indicates whether to skip expansion during canonization */ diff --git a/packages/bbs-signatures/src/types/CreateProofOptions.ts b/packages/bbs-signatures/src/types/CreateProofOptions.ts index d4acbbe0ba..e413649ced 100644 --- a/packages/bbs-signatures/src/types/CreateProofOptions.ts +++ b/packages/bbs-signatures/src/types/CreateProofOptions.ts @@ -29,10 +29,6 @@ export interface CreateProofOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void /** * Indicates whether to compact the resulting proof */ diff --git a/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts b/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts index c163eca5c6..7aff485105 100644 --- a/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts +++ b/packages/bbs-signatures/src/types/CreateVerifyDataOptions.ts @@ -21,20 +21,17 @@ export interface CreateVerifyDataOptions { * Document to create the proof for */ readonly document: JsonObject + /** * The proof */ readonly proof: JsonObject + /** * Optional custom document loader */ - documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void /** * Indicates whether to compact the proof */ diff --git a/packages/bbs-signatures/src/types/DeriveProofOptions.ts b/packages/bbs-signatures/src/types/DeriveProofOptions.ts index 23fe427798..db62925292 100644 --- a/packages/bbs-signatures/src/types/DeriveProofOptions.ts +++ b/packages/bbs-signatures/src/types/DeriveProofOptions.ts @@ -34,11 +34,7 @@ export interface DeriveProofOptions { */ // eslint-disable-next-line documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - // eslint-disable-next-line - expansionMap?: () => void + /** * Nonce to include in the derived proof */ diff --git a/packages/bbs-signatures/src/types/SuiteSignOptions.ts b/packages/bbs-signatures/src/types/SuiteSignOptions.ts index 850587dc60..44420e7221 100644 --- a/packages/bbs-signatures/src/types/SuiteSignOptions.ts +++ b/packages/bbs-signatures/src/types/SuiteSignOptions.ts @@ -25,10 +25,7 @@ export interface SuiteSignOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void + /** * The array of statements to sign */ diff --git a/packages/bbs-signatures/src/types/VerifyProofOptions.ts b/packages/bbs-signatures/src/types/VerifyProofOptions.ts index 9aa2a60ff4..decfc7a47a 100644 --- a/packages/bbs-signatures/src/types/VerifyProofOptions.ts +++ b/packages/bbs-signatures/src/types/VerifyProofOptions.ts @@ -33,8 +33,4 @@ export interface VerifyProofOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void } diff --git a/packages/bbs-signatures/src/types/VerifySignatureOptions.ts b/packages/bbs-signatures/src/types/VerifySignatureOptions.ts index 07ea80c5b8..03a0ddfb80 100644 --- a/packages/bbs-signatures/src/types/VerifySignatureOptions.ts +++ b/packages/bbs-signatures/src/types/VerifySignatureOptions.ts @@ -37,8 +37,4 @@ export interface VerifySignatureOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void } diff --git a/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts b/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts index 6aa8db303f..c5c20bf90c 100644 --- a/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts +++ b/packages/bbs-signatures/tests/bbs-signatures.e2e.test.ts @@ -31,7 +31,7 @@ import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { BbsBlsSignature2020, BbsBlsSignatureProof2020, Bls12381g2SigningProvider } from '../src' import { BbsBlsSignature2020Fixtures } from './fixtures' -import { describeSkipNode17And18 } from './util' +import { describeSkipNode18 } from './util' const { jsonldSignatures } = vcLibraries const { purposes } = jsonldSignatures @@ -61,7 +61,7 @@ const signingProviderRegistry = new SigningProviderRegistry([new Bls12381g2Signi const agentConfig = getAgentConfig('BbsSignaturesE2eTest') -describeSkipNode17And18('BBS W3cCredentialService', () => { +describeSkipNode18('BBS W3cCredentialService', () => { let wallet: Wallet let agentContext: AgentContext let w3cJsonLdCredentialService: W3cJsonLdCredentialService diff --git a/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts b/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts index 67e2112e96..a9b48d6352 100644 --- a/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts +++ b/packages/bbs-signatures/tests/bbs-signing-provider.e2e.test.ts @@ -14,7 +14,7 @@ import { IndySdkWallet } from '../../indy-sdk/src' import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { Bls12381g2SigningProvider } from '../src' -import { describeSkipNode17And18 } from './util' +import { describeSkipNode18 } from './util' // use raw key derivation method to speed up wallet creating / opening / closing between tests const walletConfig: WalletConfig = { @@ -24,7 +24,7 @@ const walletConfig: WalletConfig = { keyDerivationMethod: KeyDerivationMethod.Raw, } -describeSkipNode17And18('BBS Signing Provider', () => { +describeSkipNode18('BBS Signing Provider', () => { let wallet: Wallet const seed = TypedArrayEncoder.fromString('sample-seed-min-of-32-bytes-long') const message = TypedArrayEncoder.fromString('sample-message') diff --git a/packages/bbs-signatures/tests/util.ts b/packages/bbs-signatures/tests/util.ts index 208a6ce8ac..efe9f799bd 100644 --- a/packages/bbs-signatures/tests/util.ts +++ b/packages/bbs-signatures/tests/util.ts @@ -1,7 +1,7 @@ -export function describeSkipNode17And18(...parameters: Parameters) { +export function describeSkipNode18(...parameters: Parameters) { const version = process.version - if (version.startsWith('v17.') || version.startsWith('v18.')) { + if (version.startsWith('v18.')) { describe.skip(...parameters) } else { describe(...parameters) diff --git a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts index 78d06d862a..feaead2f3e 100644 --- a/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts +++ b/packages/bbs-signatures/tests/v2.ldproof.credentials.propose-offerBbs.test.ts @@ -9,7 +9,7 @@ import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_BBS_URL } from '../../core import { JsonTransformer } from '../../core/src/utils/JsonTransformer' import { waitForCredentialRecordSubject, setupJsonLdTests, testLogger } from '../../core/tests' -import { describeSkipNode17And18 } from './util' +import { describeSkipNode18 } from './util' let faberAgent: JsonLdTestsAgent let faberReplay: EventReplaySubject @@ -52,7 +52,7 @@ const signCredentialOptions = { }, } -describeSkipNode17And18('credentials, BBS+ signature', () => { +describeSkipNode18('credentials, BBS+ signature', () => { beforeAll(async () => { ;({ issuerAgent: faberAgent, diff --git a/packages/cheqd/src/dids/CheqdDidResolver.ts b/packages/cheqd/src/dids/CheqdDidResolver.ts index edd94c2aa1..0b87fe9061 100644 --- a/packages/cheqd/src/dids/CheqdDidResolver.ts +++ b/packages/cheqd/src/dids/CheqdDidResolver.ts @@ -19,6 +19,7 @@ import { filterResourcesByNameAndType, getClosestResourceVersion, renderResource export class CheqdDidResolver implements DidResolver { public readonly supportedMethods = ['cheqd'] + public readonly allowsCaching = true public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { const didDocumentMetadata = {} diff --git a/packages/core/package.json b/packages/core/package.json index 363143ddcd..02c30fec80 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,13 +23,16 @@ "prepublishOnly": "yarn run build" }, "dependencies": { - "@digitalcredentials/jsonld": "^5.2.1", - "@digitalcredentials/jsonld-signatures": "^9.3.1", - "@digitalcredentials/vc": "^1.1.2", + "@digitalcredentials/jsonld": "^6.0.0", + "@digitalcredentials/jsonld-signatures": "^9.4.0", + "@digitalcredentials/vc": "^6.0.1", "@multiformats/base-x": "^4.0.1", "@stablelib/ed25519": "^1.0.2", "@stablelib/random": "^1.0.1", "@stablelib/sha256": "^1.0.1", + "@sphereon/pex": "^2.2.2", + "@sphereon/pex-models": "^2.1.2", + "@sphereon/ssi-types": "^0.17.5", "@types/ws": "^8.5.4", "abort-controller": "^3.0.0", "big-integer": "^1.6.51", @@ -38,6 +41,7 @@ "class-transformer": "0.5.1", "class-validator": "0.14.0", "did-resolver": "^4.1.0", + "jsonpath": "^1.1.1", "lru_map": "^0.4.1", "luxon": "^3.3.0", "make-error": "^1.3.6", @@ -52,6 +56,7 @@ }, "devDependencies": { "@types/events": "^3.0.0", + "@types/jsonpath": "^0.2.4", "@types/luxon": "^3.2.0", "@types/object-inspect": "^1.8.0", "@types/uuid": "^9.0.1", diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index ed0afcede9..faf87ecec7 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -7,6 +7,7 @@ import { CacheModule } from '../modules/cache' import { ConnectionsModule } from '../modules/connections' import { CredentialsModule } from '../modules/credentials' import { DidsModule } from '../modules/dids' +import { DifPresentationExchangeModule } from '../modules/dif-presentation-exchange' import { DiscoverFeaturesModule } from '../modules/discover-features' import { GenericRecordsModule } from '../modules/generic-records' import { MessagePickupModule } from '../modules/message-pickup' @@ -131,6 +132,7 @@ function getDefaultAgentModules() { oob: () => new OutOfBandModule(), w3cCredentials: () => new W3cCredentialsModule(), cache: () => new CacheModule(), + pex: () => new DifPresentationExchangeModule(), } as const } diff --git a/packages/core/src/agent/TransportService.ts b/packages/core/src/agent/TransportService.ts index cf281b77b9..bd42f892a5 100644 --- a/packages/core/src/agent/TransportService.ts +++ b/packages/core/src/agent/TransportService.ts @@ -27,7 +27,7 @@ export class TransportService { if (session.connectionId) { const oldSessions = this.getExistingSessionsForConnectionIdAndType(session.connectionId, session.type) oldSessions.forEach((oldSession) => { - if (oldSession) { + if (oldSession && oldSession.id !== session.id) { this.removeSession(oldSession) } }) diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index d00e94e845..22268ccb93 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -7,6 +7,7 @@ import { getAgentOptions } from '../../../tests/helpers' import { InjectionSymbols } from '../../constants' import { BasicMessageRepository, BasicMessageService } from '../../modules/basic-messages' import { BasicMessagesApi } from '../../modules/basic-messages/BasicMessagesApi' +import { DidRotateService } from '../../modules/connections' import { ConnectionsApi } from '../../modules/connections/ConnectionsApi' import { ConnectionRepository } from '../../modules/connections/repository/ConnectionRepository' import { ConnectionService } from '../../modules/connections/services/ConnectionService' @@ -157,6 +158,7 @@ describe('Agent', () => { expect(container.resolve(ConnectionsApi)).toBeInstanceOf(ConnectionsApi) expect(container.resolve(ConnectionService)).toBeInstanceOf(ConnectionService) expect(container.resolve(ConnectionRepository)).toBeInstanceOf(ConnectionRepository) + expect(container.resolve(DidRotateService)).toBeInstanceOf(DidRotateService) expect(container.resolve(TrustPingService)).toBeInstanceOf(TrustPingService) expect(container.resolve(ProofsApi)).toBeInstanceOf(ProofsApi) @@ -198,6 +200,7 @@ describe('Agent', () => { expect(container.resolve(ConnectionService)).toBe(container.resolve(ConnectionService)) expect(container.resolve(ConnectionRepository)).toBe(container.resolve(ConnectionRepository)) expect(container.resolve(TrustPingService)).toBe(container.resolve(TrustPingService)) + expect(container.resolve(DidRotateService)).toBe(container.resolve(DidRotateService)) expect(container.resolve(ProofsApi)).toBe(container.resolve(ProofsApi)) expect(container.resolve(ProofRepository)).toBe(container.resolve(ProofRepository)) @@ -247,7 +250,8 @@ describe('Agent', () => { 'https://didcomm.org/coordinate-mediation/1.0', 'https://didcomm.org/issue-credential/2.0', 'https://didcomm.org/present-proof/2.0', - 'https://didcomm.org/didexchange/1.0', + 'https://didcomm.org/didexchange/1.1', + 'https://didcomm.org/did-rotate/1.0', 'https://didcomm.org/discover-features/1.0', 'https://didcomm.org/discover-features/2.0', 'https://didcomm.org/messagepickup/1.0', @@ -257,6 +261,6 @@ describe('Agent', () => { 'https://didcomm.org/revocation_notification/2.0', ]) ) - expect(protocols.length).toEqual(13) + expect(protocols.length).toEqual(14) }) }) diff --git a/packages/core/src/agent/__tests__/AgentModules.test.ts b/packages/core/src/agent/__tests__/AgentModules.test.ts index 7717608581..a4c3be88a3 100644 --- a/packages/core/src/agent/__tests__/AgentModules.test.ts +++ b/packages/core/src/agent/__tests__/AgentModules.test.ts @@ -5,6 +5,7 @@ import { CacheModule } from '../../modules/cache' import { ConnectionsModule } from '../../modules/connections' import { CredentialsModule } from '../../modules/credentials' import { DidsModule } from '../../modules/dids' +import { DifPresentationExchangeModule } from '../../modules/dif-presentation-exchange' import { DiscoverFeaturesModule } from '../../modules/discover-features' import { GenericRecordsModule } from '../../modules/generic-records' import { MessagePickupModule } from '../../modules/message-pickup' @@ -62,6 +63,7 @@ describe('AgentModules', () => { mediationRecipient: expect.any(MediationRecipientModule), messagePickup: expect.any(MessagePickupModule), basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), discovery: expect.any(DiscoverFeaturesModule), dids: expect.any(DidsModule), @@ -86,6 +88,7 @@ describe('AgentModules', () => { mediationRecipient: expect.any(MediationRecipientModule), messagePickup: expect.any(MessagePickupModule), basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), discovery: expect.any(DiscoverFeaturesModule), dids: expect.any(DidsModule), @@ -113,6 +116,7 @@ describe('AgentModules', () => { mediationRecipient: expect.any(MediationRecipientModule), messagePickup: expect.any(MessagePickupModule), basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), discovery: expect.any(DiscoverFeaturesModule), dids: expect.any(DidsModule), diff --git a/packages/core/src/crypto/Key.ts b/packages/core/src/crypto/Key.ts index 2ebe3651f2..ec44eead24 100644 --- a/packages/core/src/crypto/Key.ts +++ b/packages/core/src/crypto/Key.ts @@ -59,4 +59,13 @@ export class Key { public get supportsSigning() { return isSigningSupportedForKeyType(this.keyType) } + + // We return an object structure based on the key, so that when this object is + // serialized to JSON it will be nicely formatted instead of the bytes printed + private toJSON() { + return { + keyType: this.keyType, + publicKeyBase58: this.publicKeyBase58, + } + } } diff --git a/packages/core/src/decorators/attachment/Attachment.ts b/packages/core/src/decorators/attachment/Attachment.ts index 85996ff039..3a91065b56 100644 --- a/packages/core/src/decorators/attachment/Attachment.ts +++ b/packages/core/src/decorators/attachment/Attachment.ts @@ -4,6 +4,7 @@ import { Expose, Type } from 'class-transformer' import { IsDate, IsHash, IsInstance, IsInt, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator' import { AriesFrameworkError } from '../../error' +import { JsonValue } from '../../types' import { JsonEncoder } from '../../utils/JsonEncoder' import { uuid } from '../../utils/uuid' @@ -19,7 +20,7 @@ export interface AttachmentOptions { export interface AttachmentDataOptions { base64?: string - json?: Record + json?: JsonValue links?: string[] jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat sha256?: string @@ -40,7 +41,7 @@ export class AttachmentData { * Directly embedded JSON data, when representing content inline instead of via links, and when the content is natively conveyable as JSON. Optional. */ @IsOptional() - public json?: Record + public json?: JsonValue /** * A list of zero or more locations at which the content may be fetched. Optional. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3c07b50a33..4b86373206 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -80,6 +80,7 @@ export * from './crypto/' // TODO: clean up util exports export { encodeAttachment, isLinkedAttachment } from './utils/attachment' +export type { Optional } from './utils' export { Hasher, HashName } from './utils/Hasher' export { MessageValidator } from './utils/MessageValidator' export { LinkedAttachment, LinkedAttachmentOptions } from './utils/LinkedAttachment' diff --git a/packages/core/src/modules/connections/ConnectionsApi.ts b/packages/core/src/modules/connections/ConnectionsApi.ts index 5ca73ae19d..fd1051bc06 100644 --- a/packages/core/src/modules/connections/ConnectionsApi.ts +++ b/packages/core/src/modules/connections/ConnectionsApi.ts @@ -15,6 +15,7 @@ import { DidResolverService } from '../dids' import { DidRepository } from '../dids/repository' import { OutOfBandService } from '../oob/OutOfBandService' import { RoutingService } from '../routing/services/RoutingService' +import { getMediationRecordForDidDocument } from '../routing/services/helpers' import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' import { DidExchangeProtocol } from './DidExchangeProtocol' @@ -28,8 +29,13 @@ import { TrustPingMessageHandler, TrustPingResponseMessageHandler, ConnectionProblemReportHandler, + DidRotateHandler, + DidRotateAckHandler, + DidRotateProblemReportHandler, + HangupHandler, } from './handlers' import { HandshakeProtocol } from './models' +import { DidRotateService } from './services' import { ConnectionService } from './services/ConnectionService' import { TrustPingService } from './services/TrustPingService' @@ -47,6 +53,7 @@ export class ConnectionsApi { private didExchangeProtocol: DidExchangeProtocol private connectionService: ConnectionService + private didRotateService: DidRotateService private outOfBandService: OutOfBandService private messageSender: MessageSender private trustPingService: TrustPingService @@ -59,6 +66,7 @@ export class ConnectionsApi { messageHandlerRegistry: MessageHandlerRegistry, didExchangeProtocol: DidExchangeProtocol, connectionService: ConnectionService, + didRotateService: DidRotateService, outOfBandService: OutOfBandService, trustPingService: TrustPingService, routingService: RoutingService, @@ -70,6 +78,7 @@ export class ConnectionsApi { ) { this.didExchangeProtocol = didExchangeProtocol this.connectionService = connectionService + this.didRotateService = didRotateService this.outOfBandService = outOfBandService this.trustPingService = trustPingService this.routingService = routingService @@ -91,9 +100,14 @@ export class ConnectionsApi { imageUrl?: string protocol: HandshakeProtocol routing?: Routing + ourDid?: string } ) { - const { protocol, label, alias, imageUrl, autoAcceptConnection } = config + const { protocol, label, alias, imageUrl, autoAcceptConnection, ourDid } = config + + if (ourDid && config.routing) { + throw new AriesFrameworkError(`'routing' is disallowed when defining 'ourDid'`) + } const routing = config.routing || @@ -106,8 +120,13 @@ export class ConnectionsApi { alias, routing, autoAcceptConnection, + ourDid, }) } else if (protocol === HandshakeProtocol.Connections) { + if (ourDid) { + throw new AriesFrameworkError('Using an externally defined did for connections protocol is unsupported') + } + result = await this.connectionService.createRequest(this.agentContext, outOfBandRecord, { label, alias, @@ -268,6 +287,74 @@ export class ConnectionsApi { return message } + /** + * Rotate the DID used for a given connection, notifying the other party immediately. + * + * If `toDid` is not specified, a new peer did will be created. Optionally, routing + * configuration can be set. + * + * Note: any did created or imported in agent wallet can be used as `toDid`, as long as + * there are valid DIDComm services in its DID Document. + * + * @param options connectionId and optional target did and routing configuration + * @returns object containing the new did + */ + public async rotate(options: { connectionId: string; toDid?: string; routing?: Routing }) { + const { connectionId, toDid } = options + const connection = await this.connectionService.getById(this.agentContext, connectionId) + + if (toDid && options.routing) { + throw new AriesFrameworkError(`'routing' is disallowed when defining 'toDid'`) + } + + let routing = options.routing + if (!toDid && !routing) { + routing = await this.routingService.getRouting(this.agentContext, {}) + } + + const message = await this.didRotateService.createRotate(this.agentContext, { + connection, + toDid, + routing, + }) + + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + return { newDid: message.toDid } + } + + /** + * Terminate a connection by sending a hang-up message to the other party. The connection record itself and any + * keys used for mediation will only be deleted if `deleteAfterHangup` flag is set. + * + * @param options connectionId + */ + public async hangup(options: { connectionId: string; deleteAfterHangup?: boolean }) { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + const connectionBeforeHangup = connection.clone() + + // Create Hangup message and update did in connection record + const message = await this.didRotateService.createHangup(this.agentContext, { connection }) + + const outboundMessageContext = new OutboundMessageContext(message, { + agentContext: this.agentContext, + connection: connectionBeforeHangup, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + + // After hang-up message submission, delete connection if required + if (options.deleteAfterHangup) { + await this.deleteById(connection.id) + } + } + public async returnWhenIsConnected(connectionId: string, options?: { timeoutMs: number }): Promise { return this.connectionService.returnWhenIsConnected(this.agentContext, connectionId, options?.timeoutMs) } @@ -384,6 +471,39 @@ export class ConnectionsApi { return this.connectionService.deleteById(this.agentContext, connectionId) } + /** + * Remove relationship of a connection with any previous did (either ours or theirs), preventing it from accepting + * messages from them. This is usually called when a DID Rotation flow has been succesful and we are sure that no + * more messages with older keys will arrive. + * + * It will remove routing keys from mediator if applicable. + * + * Note: this will not actually delete any DID from the wallet. + * + * @param connectionId + */ + public async removePreviousDids(options: { connectionId: string }) { + const connection = await this.connectionService.getById(this.agentContext, options.connectionId) + + for (const previousDid of connection.previousDids) { + const did = await this.didResolverService.resolve(this.agentContext, previousDid) + if (!did.didDocument) continue + const mediatorRecord = await getMediationRecordForDidDocument(this.agentContext, did.didDocument) + + if (mediatorRecord) { + await this.routingService.removeRouting(this.agentContext, { + recipientKeys: did.didDocument.recipientKeys, + mediatorId: mediatorRecord.id, + }) + } + } + + connection.previousDids = [] + connection.previousTheirDids = [] + + await this.connectionService.update(this.agentContext, connection) + } + public async findAllByOutOfBandId(outOfBandId: string) { return this.connectionService.findAllByOutOfBandId(this.agentContext, outOfBandId) } @@ -450,5 +570,13 @@ export class ConnectionsApi { messageHandlerRegistry.registerMessageHandler( new DidExchangeCompleteHandler(this.didExchangeProtocol, this.outOfBandService) ) + + messageHandlerRegistry.registerMessageHandler(new DidRotateHandler(this.didRotateService, this.connectionService)) + + messageHandlerRegistry.registerMessageHandler(new DidRotateAckHandler(this.didRotateService)) + + messageHandlerRegistry.registerMessageHandler(new HangupHandler(this.didRotateService)) + + messageHandlerRegistry.registerMessageHandler(new DidRotateProblemReportHandler(this.didRotateService)) } } diff --git a/packages/core/src/modules/connections/ConnectionsModule.ts b/packages/core/src/modules/connections/ConnectionsModule.ts index 537f7695a7..dcddf81da3 100644 --- a/packages/core/src/modules/connections/ConnectionsModule.ts +++ b/packages/core/src/modules/connections/ConnectionsModule.ts @@ -7,9 +7,9 @@ import { Protocol } from '../../agent/models' import { ConnectionsApi } from './ConnectionsApi' import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' import { DidExchangeProtocol } from './DidExchangeProtocol' -import { ConnectionRole, DidExchangeRole } from './models' +import { ConnectionRole, DidExchangeRole, DidRotateRole } from './models' import { ConnectionRepository } from './repository' -import { ConnectionService, TrustPingService } from './services' +import { ConnectionService, DidRotateService, TrustPingService } from './services' export class ConnectionsModule implements Module { public readonly config: ConnectionsModuleConfig @@ -32,6 +32,7 @@ export class ConnectionsModule implements Module { // Services dependencyManager.registerSingleton(ConnectionService) dependencyManager.registerSingleton(DidExchangeProtocol) + dependencyManager.registerSingleton(DidRotateService) dependencyManager.registerSingleton(TrustPingService) // Repositories @@ -44,8 +45,12 @@ export class ConnectionsModule implements Module { roles: [ConnectionRole.Invitee, ConnectionRole.Inviter], }), new Protocol({ - id: 'https://didcomm.org/didexchange/1.0', + id: 'https://didcomm.org/didexchange/1.1', roles: [DidExchangeRole.Requester, DidExchangeRole.Responder], + }), + new Protocol({ + id: 'https://didcomm.org/did-rotate/1.0', + roles: [DidRotateRole.RotatingParty, DidRotateRole.ObservingParty], }) ) } diff --git a/packages/core/src/modules/connections/ConnectionsModuleConfig.ts b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts index e3bd0c0408..a978241c70 100644 --- a/packages/core/src/modules/connections/ConnectionsModuleConfig.ts +++ b/packages/core/src/modules/connections/ConnectionsModuleConfig.ts @@ -1,3 +1,5 @@ +import { PeerDidNumAlgo } from '../dids' + /** * ConnectionsModuleConfigOptions defines the interface for the options of the ConnectionsModuleConfig class. * This can contain optional parameters that have default values in the config class itself. @@ -13,15 +15,35 @@ export interface ConnectionsModuleConfigOptions { * @default false */ autoAcceptConnections?: boolean + + /** + * Peer did num algo to use in requests for DID exchange protocol (RFC 0023). It will be also used by default + * in responses in case that the request does not use a peer did. + * + * @default PeerDidNumAlgo.GenesisDoc + */ + peerNumAlgoForDidExchangeRequests?: PeerDidNumAlgo + + /** + * Peer did num algo to use for DID rotation (RFC 0794). + * + * @default PeerDidNumAlgo.ShortFormAndLongForm + */ + peerNumAlgoForDidRotation?: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc | PeerDidNumAlgo.ShortFormAndLongForm } export class ConnectionsModuleConfig { #autoAcceptConnections?: boolean + #peerNumAlgoForDidExchangeRequests?: PeerDidNumAlgo + #peerNumAlgoForDidRotation?: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc | PeerDidNumAlgo.ShortFormAndLongForm + private options: ConnectionsModuleConfigOptions public constructor(options?: ConnectionsModuleConfigOptions) { this.options = options ?? {} this.#autoAcceptConnections = this.options.autoAcceptConnections + this.#peerNumAlgoForDidExchangeRequests = this.options.peerNumAlgoForDidExchangeRequests + this.#peerNumAlgoForDidRotation = this.options.peerNumAlgoForDidRotation } /** See {@link ConnectionsModuleConfigOptions.autoAcceptConnections} */ @@ -33,4 +55,26 @@ export class ConnectionsModuleConfig { public set autoAcceptConnections(autoAcceptConnections: boolean) { this.#autoAcceptConnections = autoAcceptConnections } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidExchangeRequests} */ + public get peerNumAlgoForDidExchangeRequests() { + return this.#peerNumAlgoForDidExchangeRequests ?? PeerDidNumAlgo.GenesisDoc + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidExchangeRequests} */ + public set peerNumAlgoForDidExchangeRequests(peerNumAlgoForDidExchangeRequests: PeerDidNumAlgo) { + this.#peerNumAlgoForDidExchangeRequests = peerNumAlgoForDidExchangeRequests + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidRotation} */ + public get peerNumAlgoForDidRotation() { + return this.#peerNumAlgoForDidRotation ?? PeerDidNumAlgo.ShortFormAndLongForm + } + + /** See {@link ConnectionsModuleConfigOptions.peerNumAlgoForDidRotation} */ + public set peerNumAlgoForDidRotation( + peerNumAlgoForDidRotation: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc | PeerDidNumAlgo.ShortFormAndLongForm + ) { + this.#peerNumAlgoForDidRotation = peerNumAlgoForDidRotation + } } diff --git a/packages/core/src/modules/connections/DidExchangeProtocol.ts b/packages/core/src/modules/connections/DidExchangeProtocol.ts index b9da15c304..92eb8111d7 100644 --- a/packages/core/src/modules/connections/DidExchangeProtocol.ts +++ b/packages/core/src/modules/connections/DidExchangeProtocol.ts @@ -4,7 +4,6 @@ import type { AgentContext } from '../../agent' import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' import type { ParsedMessageType } from '../../utils/messageType' import type { ResolvedDidCommService } from '../didcomm' -import type { PeerDidCreateOptions } from '../dids' import type { OutOfBandRecord } from '../oob/repository' import { InjectionSymbols } from '../../constants' @@ -16,60 +15,59 @@ import { Attachment, AttachmentData } from '../../decorators/attachment/Attachme import { AriesFrameworkError } from '../../error' import { Logger } from '../../logger' import { inject, injectable } from '../../plugins' -import { isDid } from '../../utils' +import { TypedArrayEncoder, isDid, Buffer } from '../../utils' import { JsonEncoder } from '../../utils/JsonEncoder' import { JsonTransformer } from '../../utils/JsonTransformer' import { base64ToBase64URL } from '../../utils/base64' import { DidDocument, - DidRegistrarService, - DidDocumentRole, - createPeerDidDocumentFromServices, DidKey, getNumAlgoFromPeerDid, PeerDidNumAlgo, + DidsApi, + isValidPeerDid, + getAlternativeDidsForPeerDid, } from '../dids' import { getKeyFromVerificationMethod } from '../dids/domain/key-type' import { tryParseDid } from '../dids/domain/parse' import { didKeyToInstanceOfKey } from '../dids/helpers' -import { DidRecord, DidRepository } from '../dids/repository' +import { DidRepository } from '../dids/repository' import { OutOfBandRole } from '../oob/domain/OutOfBandRole' import { OutOfBandState } from '../oob/domain/OutOfBandState' +import { getMediationRecordForDidDocument } from '../routing/services/helpers' +import { ConnectionsModuleConfig } from './ConnectionsModuleConfig' import { DidExchangeStateMachine } from './DidExchangeStateMachine' import { DidExchangeProblemReportError, DidExchangeProblemReportReason } from './errors' -import { DidExchangeCompleteMessage } from './messages/DidExchangeCompleteMessage' -import { DidExchangeRequestMessage } from './messages/DidExchangeRequestMessage' -import { DidExchangeResponseMessage } from './messages/DidExchangeResponseMessage' +import { DidExchangeRequestMessage, DidExchangeResponseMessage, DidExchangeCompleteMessage } from './messages' import { DidExchangeRole, DidExchangeState, HandshakeProtocol } from './models' import { ConnectionService } from './services' +import { createPeerDidFromServices, getDidDocumentForCreatedDid, routingToServices } from './services/helpers' interface DidExchangeRequestParams { label?: string alias?: string goal?: string goalCode?: string - routing: Routing + routing?: Routing autoAcceptConnection?: boolean + ourDid?: string } @injectable() export class DidExchangeProtocol { private connectionService: ConnectionService - private didRegistrarService: DidRegistrarService private jwsService: JwsService private didRepository: DidRepository private logger: Logger public constructor( connectionService: ConnectionService, - didRegistrarService: DidRegistrarService, didRepository: DidRepository, jwsService: JwsService, @inject(InjectionSymbols.Logger) logger: Logger ) { this.connectionService = connectionService - this.didRegistrarService = didRegistrarService this.didRepository = didRepository this.jwsService = jwsService this.logger = logger @@ -84,21 +82,56 @@ export class DidExchangeProtocol { outOfBandRecord, params, }) + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) const { outOfBandInvitation } = outOfBandRecord - const { alias, goal, goalCode, routing, autoAcceptConnection } = params - + const { alias, goal, goalCode, routing, autoAcceptConnection, ourDid: did } = params // TODO: We should store only one did that we'll use to send the request message with success. // We take just the first one for now. const [invitationDid] = outOfBandInvitation.invitationDids + // Create message + const label = params.label ?? agentContext.config.label + + let didDocument, mediatorId + + // If our did is specified, make sure we have all key material for it + if (did) { + didDocument = await getDidDocumentForCreatedDid(agentContext, did) + mediatorId = (await getMediationRecordForDidDocument(agentContext, didDocument))?.id + // Otherwise, create a did:peer based on the provided routing + } else { + if (!routing) throw new AriesFrameworkError(`'routing' must be defined if 'ourDid' is not specified`) + + didDocument = await createPeerDidFromServices( + agentContext, + routingToServices(routing), + config.peerNumAlgoForDidExchangeRequests + ) + mediatorId = routing.mediatorId + } + + const parentThreadId = outOfBandRecord.outOfBandInvitation.id + + const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode }) + + // Create sign attachment containing didDoc + if (isValidPeerDid(didDocument.id) && getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { + const didDocAttach = await this.createSignedAttachment( + agentContext, + didDocument.toJSON(), + didDocument.recipientKeys.map((key) => key.publicKeyBase58) + ) + message.didDoc = didDocAttach + } + const connectionRecord = await this.connectionService.createConnection(agentContext, { protocol: HandshakeProtocol.DidExchange, role: DidExchangeRole.Requester, alias, state: DidExchangeState.InvitationReceived, theirLabel: outOfBandInvitation.label, - mediatorId: routing.mediatorId, + mediatorId, autoAcceptConnection: outOfBandRecord.autoAcceptConnection, outOfBandId: outOfBandRecord.id, invitationDid, @@ -107,21 +140,6 @@ export class DidExchangeProtocol { DidExchangeStateMachine.assertCreateMessageState(DidExchangeRequestMessage.type, connectionRecord) - // Create message - const label = params.label ?? agentContext.config.label - const didDocument = await this.createPeerDidDoc(agentContext, this.routingToServices(routing)) - const parentThreadId = outOfBandRecord.outOfBandInvitation.id - - const message = new DidExchangeRequestMessage({ label, parentThreadId, did: didDocument.id, goal, goalCode }) - - // Create sign attachment containing didDoc - if (getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { - const didDocAttach = await this.createSignedAttachment(agentContext, didDocument, [ - routing.recipientKey.publicKeyBase58, - ]) - message.didDoc = didDocAttach - } - connectionRecord.did = didDocument.id connectionRecord.threadId = message.id @@ -141,7 +159,7 @@ export class DidExchangeProtocol { messageContext: InboundMessageContext, outOfBandRecord: OutOfBandRecord ): Promise { - this.logger.debug(`Process message ${DidExchangeRequestMessage.type.messageTypeUri} start`, { + this.logger.debug(`Process message ${messageContext.message.type} start`, { message: messageContext.message, }) @@ -150,7 +168,7 @@ export class DidExchangeProtocol { // TODO check there is no connection record for particular oob record - const { message } = messageContext + const { message, agentContext } = messageContext // Check corresponding invitation ID is the request's ~thread.pthid or pthid is a public did // TODO Maybe we can do it in handler, but that actually does not make sense because we try to find oob by parent thread ID there. @@ -165,41 +183,31 @@ export class DidExchangeProtocol { } // If the responder wishes to continue the exchange, they will persist the received information in their wallet. - if (!isDid(message.did, 'peer')) { - throw new DidExchangeProblemReportError( - `Message contains unsupported did ${message.did}. Supported dids are [did:peer]`, - { - problemCode: DidExchangeProblemReportReason.RequestNotAccepted, - } - ) - } - const numAlgo = getNumAlgoFromPeerDid(message.did) - if (numAlgo !== PeerDidNumAlgo.GenesisDoc) { - throw new DidExchangeProblemReportError( - `Unsupported numalgo ${numAlgo}. Supported numalgos are [${PeerDidNumAlgo.GenesisDoc}]`, - { - problemCode: DidExchangeProblemReportReason.RequestNotAccepted, - } - ) - } - // TODO: Move this into the didcomm module, and add a method called store received did document. - // This can be called from both the did exchange and the connection protocol. - const didDocument = await this.extractDidDocument(messageContext.agentContext, message) - const didRecord = new DidRecord({ - did: message.did, - role: DidDocumentRole.Received, + // Get DID Document either from message (if it is a supported did:peer) or resolve it externally + const didDocument = await this.resolveDidDocument(agentContext, message) + + // A DID Record must be stored in order to allow for searching for its recipient keys when receiving a message + const didRecord = await this.didRepository.storeReceivedDid(messageContext.agentContext, { + did: didDocument.id, // It is important to take the did document from the PeerDid class // as it will have the id property - didDocument, + didDocument: + !isValidPeerDid(didDocument.id) || getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc + ? didDocument + : undefined, tags: { // We need to save the recipientKeys, so we can find the associated did // of a key when we receive a message from another connection. recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(didDocument.id) : undefined, }, }) - this.logger.debug('Saving DID record', { + this.logger.debug('Saved DID record', { id: didRecord.id, did: didRecord.did, role: didRecord.role, @@ -207,8 +215,6 @@ export class DidExchangeProtocol { didDocument: 'omitted...', }) - await this.didRepository.save(messageContext.agentContext, didRecord) - const connectionRecord = await this.connectionService.createConnection(messageContext.agentContext, { protocol: HandshakeProtocol.DidExchange, role: DidExchangeRole.Responder, @@ -236,15 +242,21 @@ export class DidExchangeProtocol { this.logger.debug(`Create message ${DidExchangeResponseMessage.type.messageTypeUri} start`, connectionRecord) DidExchangeStateMachine.assertCreateMessageState(DidExchangeResponseMessage.type, connectionRecord) - const { threadId } = connectionRecord + const { threadId, theirDid } = connectionRecord + + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) if (!threadId) { throw new AriesFrameworkError('Missing threadId on connection record.') } + if (!theirDid) { + throw new AriesFrameworkError('Missing theirDid on connection record.') + } + let services: ResolvedDidCommService[] = [] if (routing) { - services = this.routingToServices(routing) + services = routingToServices(routing) } else if (outOfBandRecord) { const inlineServices = outOfBandRecord.outOfBandInvitation.getInlineServices() services = inlineServices.map((service) => ({ @@ -255,13 +267,32 @@ export class DidExchangeProtocol { })) } - const didDocument = await this.createPeerDidDoc(agentContext, services) + // Use the same num algo for response as received in request + const numAlgo = isValidPeerDid(theirDid) + ? getNumAlgoFromPeerDid(theirDid) + : config.peerNumAlgoForDidExchangeRequests + + const didDocument = await createPeerDidFromServices(agentContext, services, numAlgo) const message = new DidExchangeResponseMessage({ did: didDocument.id, threadId }) - if (getNumAlgoFromPeerDid(didDocument.id) === PeerDidNumAlgo.GenesisDoc) { - const didDocAttach = await this.createSignedAttachment( + if (numAlgo === PeerDidNumAlgo.GenesisDoc) { + message.didDoc = await this.createSignedAttachment( agentContext, - didDocument, + didDocument.toJSON(), + Array.from( + new Set( + services + .map((s) => s.recipientKeys) + .reduce((acc, curr) => acc.concat(curr), []) + .map((key) => key.publicKeyBase58) + ) + ) + ) + } else { + // We assume any other case is a resolvable did (e.g. did:peer:2 or did:peer:4) + message.didRotate = await this.createSignedAttachment( + agentContext, + didDocument.id, Array.from( new Set( services @@ -271,7 +302,6 @@ export class DidExchangeProtocol { ) ) ) - message.didDoc = didDocAttach } connectionRecord.did = didDocument.id @@ -292,7 +322,7 @@ export class DidExchangeProtocol { message: messageContext.message, }) - const { connection: connectionRecord, message } = messageContext + const { connection: connectionRecord, message, agentContext } = messageContext if (!connectionRecord) { throw new AriesFrameworkError('No connection record in message context.') @@ -306,51 +336,38 @@ export class DidExchangeProtocol { }) } - if (!isDid(message.did, 'peer')) { - throw new DidExchangeProblemReportError( - `Message contains unsupported did ${message.did}. Supported dids are [did:peer]`, - { - problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, - } - ) - } - const numAlgo = getNumAlgoFromPeerDid(message.did) - if (numAlgo !== PeerDidNumAlgo.GenesisDoc) { - throw new DidExchangeProblemReportError( - `Unsupported numalgo ${numAlgo}. Supported numalgos are [${PeerDidNumAlgo.GenesisDoc}]`, - { - problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, - } - ) - } - - const didDocument = await this.extractDidDocument( - messageContext.agentContext, + // Get DID Document either from message (if it is a did:peer) or resolve it externally + const didDocument = await this.resolveDidDocument( + agentContext, message, outOfBandRecord .getTags() .recipientKeyFingerprints.map((fingerprint) => Key.fromFingerprint(fingerprint).publicKeyBase58) ) - const didRecord = new DidRecord({ - did: message.did, - role: DidDocumentRole.Received, - didDocument, - tags: { - // We need to save the recipientKeys, so we can find the associated did - // of a key when we receive a message from another connection. - recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), - }, - }) - this.logger.debug('Saving DID record', { - id: didRecord.id, - did: didRecord.did, - role: didRecord.role, - tags: didRecord.getTags(), - didDocument: 'omitted...', - }) + if (isValidPeerDid(didDocument.id)) { + const didRecord = await this.didRepository.storeReceivedDid(messageContext.agentContext, { + did: didDocument.id, + didDocument: getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc ? didDocument : undefined, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: getAlternativeDidsForPeerDid(didDocument.id), + }, + }) - await this.didRepository.save(messageContext.agentContext, didRecord) + this.logger.debug('Saved DID record', { + id: didRecord.id, + did: didRecord.did, + role: didRecord.role, + tags: didRecord.getTags(), + didDocument: 'omitted...', + }) + } connectionRecord.theirDid = message.did @@ -433,36 +450,16 @@ export class DidExchangeProtocol { return this.connectionService.updateState(agentContext, connectionRecord, nextState) } - private async createPeerDidDoc(agentContext: AgentContext, services: ResolvedDidCommService[]) { - // Create did document without the id property - const didDocument = createPeerDidDocumentFromServices(services) - - // Register did:peer document. This will generate the id property and save it to a did record - const result = await this.didRegistrarService.create(agentContext, { - method: 'peer', - didDocument, - options: { - numAlgo: PeerDidNumAlgo.GenesisDoc, - }, - }) - - if (result.didState?.state !== 'finished') { - throw new AriesFrameworkError(`Did document creation failed: ${JSON.stringify(result.didState)}`) - } - - this.logger.debug(`Did document with did ${result.didState.did} created.`, { - did: result.didState.did, - didDocument: result.didState.didDocument, - }) - - return result.didState.didDocument - } - - private async createSignedAttachment(agentContext: AgentContext, didDoc: DidDocument, verkeys: string[]) { - const didDocAttach = new Attachment({ - mimeType: 'application/json', + private async createSignedAttachment( + agentContext: AgentContext, + data: string | Record, + verkeys: string[] + ) { + const signedAttach = new Attachment({ + mimeType: typeof data === 'string' ? undefined : 'application/json', data: new AttachmentData({ - base64: JsonEncoder.toBase64(didDoc), + base64: + typeof data === 'string' ? TypedArrayEncoder.toBase64URL(Buffer.from(data)) : JsonEncoder.toBase64(data), }), }) @@ -470,7 +467,7 @@ export class DidExchangeProtocol { verkeys.map(async (verkey) => { const key = Key.fromPublicKeyBase58(verkey, KeyType.Ed25519) const kid = new DidKey(key).did - const payload = JsonEncoder.toBuffer(didDoc) + const payload = typeof data === 'string' ? TypedArrayEncoder.fromString(data) : JsonEncoder.toBuffer(data) const jws = await this.jwsService.createJws(agentContext, { payload, @@ -483,11 +480,114 @@ export class DidExchangeProtocol { jwk: getJwkFromKey(key), }, }) - didDocAttach.addJws(jws) + signedAttach.addJws(jws) }) ) - return didDocAttach + return signedAttach + } + + /** + * Resolves a did document from a given `request` or `response` message, verifying its signature or did rotate + * signature in case it is taken from message attachment. + * + * @param message DID request or DID response message + * @param invitationKeys array containing keys from connection invitation that could be used for signing of DID document + * @returns verified DID document content from message attachment + */ + + private async resolveDidDocument( + agentContext: AgentContext, + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58: string[] = [] + ) { + // The only supported case where we expect to receive a did-document attachment is did:peer algo 1 + return isDid(message.did, 'peer') && getNumAlgoFromPeerDid(message.did) === PeerDidNumAlgo.GenesisDoc + ? this.extractAttachedDidDocument(agentContext, message, invitationKeysBase58) + : this.extractResolvableDidDocument(agentContext, message, invitationKeysBase58) + } + + /** + * Extracts DID document from message (resolving it externally if required) and verifies did-rotate attachment signature + * if applicable + */ + private async extractResolvableDidDocument( + agentContext: AgentContext, + message: DidExchangeRequestMessage | DidExchangeResponseMessage, + invitationKeysBase58?: string[] + ) { + // Validate did-rotate attachment in case of DID Exchange response + if (message instanceof DidExchangeResponseMessage) { + const didRotateAttachment = message.didRotate + + if (!didRotateAttachment) { + throw new DidExchangeProblemReportError('DID Rotate attachment is missing.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + + const jws = didRotateAttachment.data.jws + + if (!jws) { + throw new DidExchangeProblemReportError('DID Rotate signature is missing.', { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + }) + } + + if (!didRotateAttachment.data.base64) { + throw new AriesFrameworkError('DID Rotate attachment is missing base64 property for signed did.') + } + + // JWS payload must be base64url encoded + const base64UrlPayload = base64ToBase64URL(didRotateAttachment.data.base64) + const signedDid = TypedArrayEncoder.fromBase64(base64UrlPayload).toString() + + if (signedDid !== message.did) { + throw new AriesFrameworkError( + `DID Rotate attachment's did ${message.did} does not correspond to message did ${message.did}` + ) + } + + const { isValid, signerKeys } = await this.jwsService.verifyJws(agentContext, { + jws: { + ...jws, + payload: base64UrlPayload, + }, + jwkResolver: ({ jws: { header } }) => { + if (typeof header.kid !== 'string' || !isDid(header.kid, 'key')) { + throw new AriesFrameworkError('JWS header kid must be a did:key DID.') + } + + const didKey = DidKey.fromDid(header.kid) + return getJwkFromKey(didKey.key) + }, + }) + + if (!isValid || !signerKeys.every((key) => invitationKeysBase58?.includes(key.publicKeyBase58))) { + throw new DidExchangeProblemReportError( + `DID Rotate signature is invalid. isValid: ${isValid} signerKeys: ${JSON.stringify( + signerKeys + )} invitationKeys:${JSON.stringify(invitationKeysBase58)}`, + { + problemCode: DidExchangeProblemReportReason.ResponseNotAccepted, + } + ) + } + } + + // Now resolve the document related to the did (which can be either a public did or an inline did) + try { + return await agentContext.dependencyManager.resolve(DidsApi).resolveDidDocument(message.did) + } catch (error) { + const problemCode = + message instanceof DidExchangeRequestMessage + ? DidExchangeProblemReportReason.RequestNotAccepted + : DidExchangeProblemReportReason.ResponseNotAccepted + + throw new DidExchangeProblemReportError(error, { + problemCode, + }) + } } /** @@ -497,7 +597,7 @@ export class DidExchangeProtocol { * @param invitationKeys array containing keys from connection invitation that could be used for signing of DID document * @returns verified DID document content from message attachment */ - private async extractDidDocument( + private async extractAttachedDidDocument( agentContext: AgentContext, message: DidExchangeRequestMessage | DidExchangeResponseMessage, invitationKeysBase58: string[] = [] @@ -526,7 +626,6 @@ export class DidExchangeProtocol { // JWS payload must be base64url encoded const base64UrlPayload = base64ToBase64URL(didDocumentAttachment.data.base64) - const json = JsonEncoder.fromBase64(didDocumentAttachment.data.base64) const { isValid, signerKeys } = await this.jwsService.verifyJws(agentContext, { jws: { @@ -543,6 +642,7 @@ export class DidExchangeProtocol { }, }) + const json = JsonEncoder.fromBase64(didDocumentAttachment.data.base64) const didDocument = JsonTransformer.fromJSON(json, DidDocument) const didDocumentKeysBase58 = didDocument.authentication ?.map((authentication) => { @@ -567,13 +667,4 @@ export class DidExchangeProtocol { return didDocument } - - private routingToServices(routing: Routing): ResolvedDidCommService[] { - return routing.endpoints.map((endpoint, index) => ({ - id: `#inline-${index}`, - serviceEndpoint: endpoint, - recipientKeys: [routing.recipientKey], - routingKeys: routing.routingKeys, - })) - } } diff --git a/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts b/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts index 34ebb476f7..8fe0127226 100644 --- a/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts +++ b/packages/core/src/modules/connections/__tests__/ConnectionsModule.test.ts @@ -6,6 +6,7 @@ import { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' import { DidExchangeProtocol } from '../DidExchangeProtocol' import { ConnectionRepository } from '../repository' import { ConnectionService, TrustPingService } from '../services' +import { DidRotateService } from '../services/DidRotateService' jest.mock('../../../plugins/DependencyManager') const DependencyManagerMock = DependencyManager as jest.Mock @@ -28,10 +29,11 @@ describe('ConnectionsModule', () => { expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(ConnectionsModuleConfig, connectionsModule.config) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(4) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(5) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ConnectionService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidExchangeProtocol) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(TrustPingService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(DidRotateService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(ConnectionRepository) }) }) diff --git a/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts b/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts new file mode 100644 index 0000000000..ba17b1bb28 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/InMemoryDidRegistry.ts @@ -0,0 +1,105 @@ +import type { AgentContext } from '../../../agent' +import type { + DidRegistrar, + DidResolver, + DidDocument, + DidCreateOptions, + DidCreateResult, + DidUpdateResult, + DidDeactivateResult, + DidResolutionResult, +} from '../../dids' + +import { DidRecord, DidDocumentRole, DidRepository } from '../../dids' + +export class InMemoryDidRegistry implements DidRegistrar, DidResolver { + public readonly supportedMethods = ['inmemory'] + + public readonly allowsCaching = false + + private dids: Record = {} + + public async create(agentContext: AgentContext, options: DidCreateOptions): Promise { + const { did, didDocument } = options + + if (!did || !didDocument) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'InMemoryDidRegistrar requires to specify both did and didDocument', + }, + } + } + + this.dids[did] = didDocument + + // Save the did so we know we created it and can use it for didcomm + const didRecord = new DidRecord({ + did: didDocument.id, + role: DidDocumentRole.Created, + didDocument, + tags: { + // We need to save the recipientKeys, so we can find the associated did + // of a key when we receive a message from another connection. + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + }, + }) + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didDocument.id, + didDocument, + }, + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:inmemory not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:inmemory not implemented yet`, + }, + } + } + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocument = this.dids[did] + + if (!didDocument) { + return { + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}'`, + }, + } + } + + return { + didDocument, + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } +} diff --git a/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts b/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts new file mode 100644 index 0000000000..14c8496094 --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/did-rotate.e2e.test.ts @@ -0,0 +1,385 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import type { ConnectionRecord } from '../repository' + +import { ReplaySubject, first, firstValueFrom, timeout } from 'rxjs' + +import { MessageSender } from '../../..//agent/MessageSender' +import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { setupSubjectTransports, testLogger } from '../../../../tests' +import { + getAgentOptions, + makeConnection, + waitForAgentMessageProcessedEvent, + waitForBasicMessage, +} from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { getOutboundMessageContext } from '../../../agent/getOutboundMessageContext' +import { RecordNotFoundError } from '../../../error' +import { uuid } from '../../../utils/uuid' +import { BasicMessage } from '../../basic-messages' +import { createPeerDidDocumentFromServices } from '../../dids' +import { ConnectionsModule } from '../ConnectionsModule' +import { DidRotateProblemReportMessage, HangupMessage, DidRotateAckMessage } from '../messages' + +import { InMemoryDidRegistry } from './InMemoryDidRegistry' + +// This is the most common flow +describe('Rotation E2E tests', () => { + let aliceAgent: Agent + let bobAgent: Agent + let aliceBobConnection: ConnectionRecord | undefined + let bobAliceConnection: ConnectionRecord | undefined + + beforeEach(async () => { + const aliceAgentOptions = getAgentOptions( + 'DID Rotate Alice', + { + label: 'alice', + endpoints: ['rxjs:alice'], + logger: testLogger, + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + } + ) + const bobAgentOptions = getAgentOptions( + 'DID Rotate Bob', + { + label: 'bob', + endpoints: ['rxjs:bob'], + logger: testLogger, + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: true, + }), + } + ) + + aliceAgent = new Agent(aliceAgentOptions) + bobAgent = new Agent(bobAgentOptions) + + setupSubjectTransports([aliceAgent, bobAgent]) + await aliceAgent.initialize() + await bobAgent.initialize() + ;[aliceBobConnection, bobAliceConnection] = await makeConnection(aliceAgent, bobAgent) + }) + + afterEach(async () => { + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + await bobAgent.shutdown() + await bobAgent.wallet.delete() + }) + + describe('Rotation from did:peer:1 to did:peer:4', () => { + test('Rotate succesfully and send messages to new did afterwards', async () => { + const oldDid = aliceBobConnection!.did + expect(bobAliceConnection!.theirDid).toEqual(oldDid) + + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Do did rotate + const { newDid } = await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Check that new did is taken into account by both parties + const newAliceBobConnection = await aliceAgent.connections.getById(aliceBobConnection!.id) + const newBobAliceConnection = await bobAgent.connections.getById(bobAliceConnection!.id) + + expect(newAliceBobConnection.did).toEqual(newDid) + expect(newBobAliceConnection.theirDid).toEqual(newDid) + + // And also they store it into previous dids array + expect(newAliceBobConnection.previousDids).toContain(oldDid) + expect(newBobAliceConnection.previousTheirDids).toContain(oldDid) + + // Send message to new did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello new did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello new did', connectionId: aliceBobConnection!.id }) + }) + + test('Rotate succesfully and send messages to previous did afterwards', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + const messageToPreviousDid = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message to previous did' }), + connectionRecord: bobAliceConnection, + }) + + // Do did rotate + await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Send message to previous did + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageToPreviousDid) + + await waitForBasicMessage(aliceAgent, { + content: 'Message to previous did', + connectionId: aliceBobConnection!.id, + }) + }) + }) + + describe('Rotation specifying did and routing externally', () => { + test('Rotate succesfully and send messages to new did afterwards', async () => { + const oldDid = aliceBobConnection!.did + expect(bobAliceConnection!.theirDid).toEqual(oldDid) + + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Create a new external did + + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + aliceAgent.dids.config.addRegistrar(didRegistry) + aliceAgent.dids.config.addResolver(didRegistry) + bobAgent.dids.config.addRegistrar(didRegistry) + bobAgent.dids.config.addResolver(didRegistry) + + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + const did = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = did + + await aliceAgent.dids.create({ + did, + didDocument, + }) + + // Do did rotate + const { newDid } = await aliceAgent.connections.rotate({ + connectionId: aliceBobConnection!.id, + toDid: did, + }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Check that new did is taken into account by both parties + const newAliceBobConnection = await aliceAgent.connections.getById(aliceBobConnection!.id) + const newBobAliceConnection = await bobAgent.connections.getById(bobAliceConnection!.id) + + expect(newAliceBobConnection.did).toEqual(newDid) + expect(newBobAliceConnection.theirDid).toEqual(newDid) + + // And also they store it into previous dids array + expect(newAliceBobConnection.previousDids).toContain(oldDid) + expect(newBobAliceConnection.previousTheirDids).toContain(oldDid) + + // Send message to new did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello new did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello new did', connectionId: aliceBobConnection!.id }) + }) + + test('Rotate succesfully and send messages to previous did afterwards', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + const messageToPreviousDid = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message to previous did' }), + connectionRecord: bobAliceConnection, + }) + + // Create a new external did + + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + aliceAgent.dids.config.addRegistrar(didRegistry) + aliceAgent.dids.config.addResolver(didRegistry) + bobAgent.dids.config.addRegistrar(didRegistry) + bobAgent.dids.config.addResolver(didRegistry) + + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + const did = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = did + + await aliceAgent.dids.create({ + did, + didDocument, + }) + + // Do did rotate + await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id, toDid: did }) + + // Wait for acknowledge + await waitForAgentMessageProcessedEvent(aliceAgent, { messageType: DidRotateAckMessage.type.messageTypeUri }) + + // Send message to previous did + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageToPreviousDid) + + await waitForBasicMessage(aliceAgent, { + content: 'Message to previous did', + connectionId: aliceBobConnection!.id, + }) + }) + + test('Rotate failed and send messages to previous did afterwards', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + const messageToPreviousDid = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message to previous did' }), + connectionRecord: bobAliceConnection, + }) + + // Create a new external did + + // Use custom registry only for Alice agent, in order to force an error on Bob side + const didRegistry = new InMemoryDidRegistry() + aliceAgent.dids.config.addRegistrar(didRegistry) + aliceAgent.dids.config.addResolver(didRegistry) + + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + const did = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = did + + await aliceAgent.dids.create({ + did, + didDocument, + }) + + // Do did rotate + await aliceAgent.connections.rotate({ connectionId: aliceBobConnection!.id, toDid: did }) + + // Wait for a problem report + await waitForAgentMessageProcessedEvent(aliceAgent, { + messageType: DidRotateProblemReportMessage.type.messageTypeUri, + }) + + // Send message to previous did + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageToPreviousDid) + + await waitForBasicMessage(aliceAgent, { + content: 'Message to previous did', + connectionId: aliceBobConnection!.id, + }) + + // Send message to stored did (should be the previous one) + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Message after did rotation failure') + + await waitForBasicMessage(aliceAgent, { + content: 'Message after did rotation failure', + connectionId: aliceBobConnection!.id, + }) + }) + }) + + describe('Hangup', () => { + test('Hangup without record deletion', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Store an outbound context so we can attempt to send a message even if the connection is terminated. + // A bit hacky, but may happen in some cases where message retry mechanisms are being used + const messageBeforeHangup = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message before hangup' }), + connectionRecord: bobAliceConnection!.clone(), + }) + + await aliceAgent.connections.hangup({ connectionId: aliceBobConnection!.id }) + + // Wait for hangup + await waitForAgentMessageProcessedEvent(bobAgent, { + messageType: HangupMessage.type.messageTypeUri, + }) + + // If Bob attempts to send a message to Alice after they received the hangup, framework should reject it + expect(bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Message after hangup')).rejects.toThrowError() + + // If Bob sends a message afterwards, Alice should still be able to receive it + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageBeforeHangup) + + await waitForBasicMessage(aliceAgent, { + content: 'Message before hangup', + connectionId: aliceBobConnection!.id, + }) + }) + + test('Hangup and delete connection record', async () => { + // Send message to initial did + await bobAgent.basicMessages.sendMessage(bobAliceConnection!.id, 'Hello initial did') + + await waitForBasicMessage(aliceAgent, { content: 'Hello initial did' }) + + // Store an outbound context so we can attempt to send a message even if the connection is terminated. + // A bit hacky, but may happen in some cases where message retry mechanisms are being used + const messageBeforeHangup = await getOutboundMessageContext(bobAgent.context, { + message: new BasicMessage({ content: 'Message before hangup' }), + connectionRecord: bobAliceConnection!.clone(), + }) + + await aliceAgent.connections.hangup({ connectionId: aliceBobConnection!.id, deleteAfterHangup: true }) + + // Verify that alice connection has been effectively deleted + expect(aliceAgent.connections.getById(aliceBobConnection!.id)).rejects.toThrowError(RecordNotFoundError) + + // Wait for hangup + await waitForAgentMessageProcessedEvent(bobAgent, { + messageType: HangupMessage.type.messageTypeUri, + }) + + // If Bob sends a message afterwards, Alice should not receive it since the connection has been deleted + await bobAgent.dependencyManager.resolve(MessageSender).sendMessage(messageBeforeHangup) + + // An error is thrown by Alice agent and, after inspecting all basic messages, it cannot be found + // TODO: Update as soon as agent sends error events upon reception of messages + const observable = aliceAgent.events.observable('AgentReceiveMessageError') + const subject = new ReplaySubject(1) + observable.pipe(first(), timeout({ first: 10000 })).subscribe(subject) + await firstValueFrom(subject) + + const aliceBasicMessages = await aliceAgent.basicMessages.findAllByQuery({}) + expect(aliceBasicMessages.find((message) => message.content === 'Message before hangup')).toBeUndefined() + }) + }) +}) diff --git a/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts new file mode 100644 index 0000000000..50cf2742ee --- /dev/null +++ b/packages/core/src/modules/connections/__tests__/didexchange-numalgo.e2e.test.ts @@ -0,0 +1,193 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { ConnectionStateChangedEvent } from '../ConnectionEvents' + +import { firstValueFrom } from 'rxjs' +import { filter, first, map, timeout } from 'rxjs/operators' + +import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { setupSubjectTransports } from '../../../../tests' +import { getAgentOptions } from '../../../../tests/helpers' +import { Agent } from '../../../agent/Agent' +import { uuid } from '../../../utils/uuid' +import { DidsModule, PeerDidNumAlgo, createPeerDidDocumentFromServices } from '../../dids' +import { ConnectionEventTypes } from '../ConnectionEvents' +import { ConnectionsModule } from '../ConnectionsModule' +import { DidExchangeState } from '../models' + +import { InMemoryDidRegistry } from './InMemoryDidRegistry' + +function waitForRequest(agent: Agent, theirLabel: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + map((event) => event.payload.connectionRecord), + // Wait for request received + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.RequestReceived && connectionRecord.theirLabel === theirLabel + ), + first(), + timeout(5000) + ) + ) +} + +function waitForResponse(agent: Agent, connectionId: string) { + return firstValueFrom( + agent.events.observable(ConnectionEventTypes.ConnectionStateChanged).pipe( + // Wait for response received + map((event) => event.payload.connectionRecord), + filter( + (connectionRecord) => + connectionRecord.state === DidExchangeState.ResponseReceived && connectionRecord.id === connectionId + ), + first(), + timeout(5000) + ) + ) +} + +describe('Did Exchange numalgo settings', () => { + test('Connect using default setting (numalgo 1)', async () => { + await didExchangeNumAlgoBaseTest({}) + }) + + test('Connect using default setting for requester and numalgo 2 for responder', async () => { + await didExchangeNumAlgoBaseTest({ responderNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc }) + }) + + test('Connect using numalgo 2 for requester and default setting for responder', async () => { + await didExchangeNumAlgoBaseTest({ requesterNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc }) + }) + + test('Connect using numalgo 2 for both requester and responder', async () => { + await didExchangeNumAlgoBaseTest({ + requesterNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + responderNumAlgoSetting: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, + }) + }) + + test('Connect using default setting for requester and numalgo 4 for responder', async () => { + await didExchangeNumAlgoBaseTest({ responderNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm }) + }) + + test('Connect using numalgo 4 for requester and default setting for responder', async () => { + await didExchangeNumAlgoBaseTest({ requesterNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm }) + }) + + test('Connect using numalgo 4 for both requester and responder', async () => { + await didExchangeNumAlgoBaseTest({ + requesterNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm, + responderNumAlgoSetting: PeerDidNumAlgo.ShortFormAndLongForm, + }) + }) + + test('Connect using an externally defined did for the requested', async () => { + await didExchangeNumAlgoBaseTest({ + createExternalDidForRequester: true, + }) + }) +}) + +async function didExchangeNumAlgoBaseTest(options: { + requesterNumAlgoSetting?: PeerDidNumAlgo + responderNumAlgoSetting?: PeerDidNumAlgo + createExternalDidForRequester?: boolean +}) { + // Make a common in-memory did registry for both agents + const didRegistry = new InMemoryDidRegistry() + + const aliceAgentOptions = getAgentOptions( + 'DID Exchange numalgo settings Alice', + { + label: 'alice', + endpoints: ['rxjs:alice'], + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: false, + peerNumAlgoForDidExchangeRequests: options.requesterNumAlgoSetting, + }), + dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), + } + ) + const faberAgentOptions = getAgentOptions( + 'DID Exchange numalgo settings Alice', + { + endpoints: ['rxjs:faber'], + }, + { + ...getIndySdkModules(), + connections: new ConnectionsModule({ + autoAcceptConnections: false, + peerNumAlgoForDidExchangeRequests: options.responderNumAlgoSetting, + }), + dids: new DidsModule({ registrars: [didRegistry], resolvers: [didRegistry] }), + } + ) + + const aliceAgent = new Agent(aliceAgentOptions) + const faberAgent = new Agent(faberAgentOptions) + + setupSubjectTransports([aliceAgent, faberAgent]) + await aliceAgent.initialize() + await faberAgent.initialize() + + const faberOutOfBandRecord = await faberAgent.oob.createInvitation({ + autoAcceptConnection: false, + multiUseInvitation: false, + }) + + const waitForAliceRequest = waitForRequest(faberAgent, 'alice') + + let ourDid, routing + if (options.createExternalDidForRequester) { + // Create did externally + const didRouting = await aliceAgent.mediationRecipient.getRouting({}) + ourDid = `did:inmemory:${uuid()}` + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [didRouting.recipientKey], + routingKeys: didRouting.routingKeys, + serviceEndpoint: didRouting.endpoints[0], + }, + ]) + didDocument.id = ourDid + + await aliceAgent.dids.create({ + did: ourDid, + didDocument, + }) + } + + let { connectionRecord: aliceConnectionRecord } = await aliceAgent.oob.receiveInvitation( + faberOutOfBandRecord.outOfBandInvitation, + { + autoAcceptInvitation: true, + autoAcceptConnection: false, + routing, + ourDid, + } + ) + + let faberAliceConnectionRecord = await waitForAliceRequest + + const waitForAliceResponse = waitForResponse(aliceAgent, aliceConnectionRecord!.id) + + await faberAgent.connections.acceptRequest(faberAliceConnectionRecord.id) + + aliceConnectionRecord = await waitForAliceResponse + await aliceAgent.connections.acceptResponse(aliceConnectionRecord!.id) + + aliceConnectionRecord = await aliceAgent.connections.returnWhenIsConnected(aliceConnectionRecord!.id) + faberAliceConnectionRecord = await faberAgent.connections.returnWhenIsConnected(faberAliceConnectionRecord!.id) + + expect(aliceConnectionRecord).toBeConnectedWith(faberAliceConnectionRecord) + + await aliceAgent.wallet.delete() + await aliceAgent.shutdown() + + await faberAgent.wallet.delete() + await faberAgent.shutdown() +} diff --git a/packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts b/packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts new file mode 100644 index 0000000000..ec05c71e8e --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidRotateAckHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' + +import { DidRotateAckMessage } from '../messages' + +export class DidRotateAckHandler implements MessageHandler { + private didRotateService: DidRotateService + public supportedMessages = [DidRotateAckMessage] + + public constructor(didRotateService: DidRotateService) { + this.didRotateService = didRotateService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + await this.didRotateService.processRotateAck(inboundMessage) + } +} diff --git a/packages/core/src/modules/connections/handlers/DidRotateHandler.ts b/packages/core/src/modules/connections/handlers/DidRotateHandler.ts new file mode 100644 index 0000000000..9533253572 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidRotateHandler.ts @@ -0,0 +1,26 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' +import type { ConnectionService } from '../services/ConnectionService' + +import { AriesFrameworkError } from '../../../error' +import { DidRotateMessage } from '../messages' + +export class DidRotateHandler implements MessageHandler { + private didRotateService: DidRotateService + private connectionService: ConnectionService + public supportedMessages = [DidRotateMessage] + + public constructor(didRotateService: DidRotateService, connectionService: ConnectionService) { + this.didRotateService = didRotateService + this.connectionService = connectionService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const { connection, recipientKey } = messageContext + if (!connection) { + throw new AriesFrameworkError(`Connection for verkey ${recipientKey?.fingerprint} not found!`) + } + + return this.didRotateService.processRotate(messageContext) + } +} diff --git a/packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts b/packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts new file mode 100644 index 0000000000..2f68e748bd --- /dev/null +++ b/packages/core/src/modules/connections/handlers/DidRotateProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' + +import { DidRotateProblemReportMessage } from '../messages' + +export class DidRotateProblemReportHandler implements MessageHandler { + private didRotateService: DidRotateService + public supportedMessages = [DidRotateProblemReportMessage] + + public constructor(didRotateService: DidRotateService) { + this.didRotateService = didRotateService + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + await this.didRotateService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/connections/handlers/HangupHandler.ts b/packages/core/src/modules/connections/handlers/HangupHandler.ts new file mode 100644 index 0000000000..5e66ee2944 --- /dev/null +++ b/packages/core/src/modules/connections/handlers/HangupHandler.ts @@ -0,0 +1,17 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' +import type { DidRotateService } from '../services' + +import { HangupMessage } from '../messages' + +export class HangupHandler implements MessageHandler { + private didRotateService: DidRotateService + public supportedMessages = [HangupMessage] + + public constructor(didRotateService: DidRotateService) { + this.didRotateService = didRotateService + } + + public async handle(inboundMessage: MessageHandlerInboundMessage) { + await this.didRotateService.processHangup(inboundMessage) + } +} diff --git a/packages/core/src/modules/connections/handlers/index.ts b/packages/core/src/modules/connections/handlers/index.ts index edd1a26766..0aeb955bdc 100644 --- a/packages/core/src/modules/connections/handlers/index.ts +++ b/packages/core/src/modules/connections/handlers/index.ts @@ -7,3 +7,7 @@ export * from './DidExchangeRequestHandler' export * from './DidExchangeResponseHandler' export * from './DidExchangeCompleteHandler' export * from './ConnectionProblemReportHandler' +export * from './DidRotateHandler' +export * from './DidRotateAckHandler' +export * from './DidRotateProblemReportHandler' +export * from './HangupHandler' diff --git a/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts index 3c142e76e8..754f049a71 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeCompleteMessage.ts @@ -26,5 +26,5 @@ export class DidExchangeCompleteMessage extends AgentMessage { @IsValidMessageType(DidExchangeCompleteMessage.type) public readonly type = DidExchangeCompleteMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/complete') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/complete') } diff --git a/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts index ec8baf9880..3f948aa768 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeProblemReportMessage.ts @@ -15,5 +15,5 @@ export class DidExchangeProblemReportMessage extends ProblemReportMessage { @IsValidMessageType(DidExchangeProblemReportMessage.type) public readonly type = DidExchangeProblemReportMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/problem-report') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/problem-report') } diff --git a/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts index c22729a272..353ac079a2 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeRequestMessage.ts @@ -42,7 +42,7 @@ export class DidExchangeRequestMessage extends AgentMessage { @IsValidMessageType(DidExchangeRequestMessage.type) public readonly type = DidExchangeRequestMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/request') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/request') @IsString() public readonly label?: string diff --git a/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts index d0de9b6ec8..fe62a6393a 100644 --- a/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts +++ b/packages/core/src/modules/connections/messages/DidExchangeResponseMessage.ts @@ -1,5 +1,5 @@ import { Type, Expose } from 'class-transformer' -import { IsString, ValidateNested } from 'class-validator' +import { IsOptional, IsString, ValidateNested } from 'class-validator' import { AgentMessage } from '../../../agent/AgentMessage' import { Attachment } from '../../../decorators/attachment/Attachment' @@ -36,13 +36,20 @@ export class DidExchangeResponseMessage extends AgentMessage { @IsValidMessageType(DidExchangeResponseMessage.type) public readonly type = DidExchangeResponseMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.0/response') + public static readonly type = parseMessageType('https://didcomm.org/didexchange/1.1/response') @IsString() public readonly did!: string @Expose({ name: 'did_doc~attach' }) + @IsOptional() @Type(() => Attachment) @ValidateNested() public didDoc?: Attachment + + @Expose({ name: 'did_rotate~attach' }) + @IsOptional() + @Type(() => Attachment) + @ValidateNested() + public didRotate?: Attachment } diff --git a/packages/core/src/modules/connections/messages/DidRotateAckMessage.ts b/packages/core/src/modules/connections/messages/DidRotateAckMessage.ts new file mode 100644 index 0000000000..363c7b1e45 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidRotateAckMessage.ts @@ -0,0 +1,20 @@ +import type { AckMessageOptions } from '../../common/messages/AckMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { AckMessage } from '../../common/messages/AckMessage' + +export type DidRotateAckMessageOptions = AckMessageOptions + +export class DidRotateAckMessage extends AckMessage { + /** + * Create new CredentialAckMessage instance. + * @param options + */ + public constructor(options: DidRotateAckMessageOptions) { + super(options) + } + + @IsValidMessageType(DidRotateAckMessage.type) + public readonly type = DidRotateAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/ack') +} diff --git a/packages/core/src/modules/connections/messages/DidRotateMessage.ts b/packages/core/src/modules/connections/messages/DidRotateMessage.ts new file mode 100644 index 0000000000..a33db94a71 --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidRotateMessage.ts @@ -0,0 +1,38 @@ +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface DidRotateMessageOptions { + id?: string + toDid: string +} + +/** + * Message to communicate the DID a party wish to rotate to. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0794-did-rotate#rotate + */ +export class DidRotateMessage extends AgentMessage { + /** + * Create new RotateMessage instance. + * @param options + */ + public constructor(options: DidRotateMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.toDid = options.toDid + } + } + + @IsValidMessageType(DidRotateMessage.type) + public readonly type = DidRotateMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/rotate') + + @Expose({ name: 'to_did' }) + @IsString() + public readonly toDid!: string +} diff --git a/packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts b/packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts new file mode 100644 index 0000000000..2eaf0c027d --- /dev/null +++ b/packages/core/src/modules/connections/messages/DidRotateProblemReportMessage.ts @@ -0,0 +1,19 @@ +import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage' + +export type DidRotateProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class DidRotateProblemReportMessage extends ProblemReportMessage { + public constructor(options: DidRotateProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(DidRotateProblemReportMessage.type) + public readonly type = DidRotateProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/problem-report') +} diff --git a/packages/core/src/modules/connections/messages/HangupMessage.ts b/packages/core/src/modules/connections/messages/HangupMessage.ts new file mode 100644 index 0000000000..b9ab2bd510 --- /dev/null +++ b/packages/core/src/modules/connections/messages/HangupMessage.ts @@ -0,0 +1,30 @@ +import { AgentMessage } from '../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' + +export interface HangupMessageOptions { + id?: string +} + +/** + * This message is sent by the rotating_party to inform the observing_party that they are done + * with the relationship and will no longer be responding. + * + * @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0794-did-rotate#hangup + */ +export class HangupMessage extends AgentMessage { + /** + * Create new HangupMessage instance. + * @param options + */ + public constructor(options: HangupMessageOptions) { + super() + + if (options) { + this.id = options.id || this.generateId() + } + } + + @IsValidMessageType(HangupMessage.type) + public readonly type = HangupMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/did-rotate/1.0/hangup') +} diff --git a/packages/core/src/modules/connections/messages/index.ts b/packages/core/src/modules/connections/messages/index.ts index 7507e5ed56..1a3acdf7c8 100644 --- a/packages/core/src/modules/connections/messages/index.ts +++ b/packages/core/src/modules/connections/messages/index.ts @@ -8,3 +8,7 @@ export * from './DidExchangeRequestMessage' export * from './DidExchangeResponseMessage' export * from './DidExchangeCompleteMessage' export * from './DidExchangeProblemReportMessage' +export * from './DidRotateProblemReportMessage' +export * from './DidRotateMessage' +export * from './DidRotateAckMessage' +export * from './HangupMessage' diff --git a/packages/core/src/modules/connections/models/DidRotateRole.ts b/packages/core/src/modules/connections/models/DidRotateRole.ts new file mode 100644 index 0000000000..310c124ed4 --- /dev/null +++ b/packages/core/src/modules/connections/models/DidRotateRole.ts @@ -0,0 +1,4 @@ +export enum DidRotateRole { + RotatingParty = 'rotating_party', + ObservingParty = 'observing_party', +} diff --git a/packages/core/src/modules/connections/models/HandshakeProtocol.ts b/packages/core/src/modules/connections/models/HandshakeProtocol.ts index a433bd87f5..cee69c7fcd 100644 --- a/packages/core/src/modules/connections/models/HandshakeProtocol.ts +++ b/packages/core/src/modules/connections/models/HandshakeProtocol.ts @@ -1,4 +1,4 @@ export enum HandshakeProtocol { Connections = 'https://didcomm.org/connections/1.0', - DidExchange = 'https://didcomm.org/didexchange/1.0', + DidExchange = 'https://didcomm.org/didexchange/1.1', } diff --git a/packages/core/src/modules/connections/models/index.ts b/packages/core/src/modules/connections/models/index.ts index 69752df9c7..72a4635768 100644 --- a/packages/core/src/modules/connections/models/index.ts +++ b/packages/core/src/modules/connections/models/index.ts @@ -3,6 +3,7 @@ export * from './ConnectionRole' export * from './ConnectionState' export * from './DidExchangeState' export * from './DidExchangeRole' +export * from './DidRotateRole' export * from './HandshakeProtocol' export * from './did' export * from './ConnectionType' diff --git a/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts b/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts index 9609097515..f16ea2d043 100644 --- a/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts +++ b/packages/core/src/modules/connections/repository/ConnectionMetadataTypes.ts @@ -1,9 +1,15 @@ export enum ConnectionMetadataKeys { UseDidKeysForProtocol = '_internal/useDidKeysForProtocol', + DidRotate = '_internal/didRotate', } export type ConnectionMetadata = { [ConnectionMetadataKeys.UseDidKeysForProtocol]: { [protocolUri: string]: boolean } + [ConnectionMetadataKeys.DidRotate]: { + did: string + threadId: string + mediatorId?: string + } } diff --git a/packages/core/src/modules/connections/repository/ConnectionRecord.ts b/packages/core/src/modules/connections/repository/ConnectionRecord.ts index f05106e5c9..2f0fa23059 100644 --- a/packages/core/src/modules/connections/repository/ConnectionRecord.ts +++ b/packages/core/src/modules/connections/repository/ConnectionRecord.ts @@ -27,6 +27,8 @@ export interface ConnectionRecordProps { outOfBandId?: string invitationDid?: string connectionTypes?: Array + previousDids?: Array + previousTheirDids?: Array } export type CustomConnectionTags = TagsBase @@ -40,6 +42,8 @@ export type DefaultConnectionTags = { outOfBandId?: string invitationDid?: string connectionTypes?: Array + previousDids?: Array + previousTheirDids?: Array } export class ConnectionRecord @@ -66,6 +70,8 @@ export class ConnectionRecord public invitationDid?: string public connectionTypes: string[] = [] + public previousDids: string[] = [] + public previousTheirDids: string[] = [] public static readonly type = 'ConnectionRecord' public readonly type = ConnectionRecord.type @@ -92,6 +98,8 @@ export class ConnectionRecord this.protocol = props.protocol this.outOfBandId = props.outOfBandId this.connectionTypes = props.connectionTypes ?? [] + this.previousDids = props.previousDids ?? [] + this.previousTheirDids = props.previousTheirDids ?? [] } } @@ -107,6 +115,8 @@ export class ConnectionRecord outOfBandId: this.outOfBandId, invitationDid: this.invitationDid, connectionTypes: this.connectionTypes, + previousDids: this.previousDids, + previousTheirDids: this.previousTheirDids, } } diff --git a/packages/core/src/modules/connections/repository/ConnectionRepository.ts b/packages/core/src/modules/connections/repository/ConnectionRepository.ts index 071d7e90cb..158c75e7a8 100644 --- a/packages/core/src/modules/connections/repository/ConnectionRepository.ts +++ b/packages/core/src/modules/connections/repository/ConnectionRepository.ts @@ -20,8 +20,14 @@ export class ConnectionRepository extends Repository { public async findByDids(agentContext: AgentContext, { ourDid, theirDid }: { ourDid: string; theirDid: string }) { return this.findSingleByQuery(agentContext, { - did: ourDid, - theirDid, + $or: [ + { + did: ourDid, + theirDid, + }, + { did: ourDid, previousTheirDids: [theirDid] }, + { previousDids: [ourDid], theirDid }, + ], }) } diff --git a/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts b/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts index 229a99d1e6..e052bfc594 100644 --- a/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts +++ b/packages/core/src/modules/connections/repository/__tests__/ConnectionRecord.test.ts @@ -25,6 +25,8 @@ describe('ConnectionRecord', () => { outOfBandId: 'a-out-of-band-id', invitationDid: 'a-invitation-did', connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) }) diff --git a/packages/core/src/modules/connections/services/ConnectionService.ts b/packages/core/src/modules/connections/services/ConnectionService.ts index 05ec71bc77..b87c4f6a33 100644 --- a/packages/core/src/modules/connections/services/ConnectionService.ts +++ b/packages/core/src/modules/connections/services/ConnectionService.ts @@ -898,7 +898,10 @@ export class ConnectionService { filterContextCorrelationId(agentContext.contextCorrelationId), map((e) => e.payload.connectionRecord), first(isConnected), // Do not wait for longer than specified timeout - timeout(timeoutMs) + timeout({ + first: timeoutMs, + meta: 'ConnectionService.returnWhenIsConnected', + }) ) .subscribe(subject) diff --git a/packages/core/src/modules/connections/services/DidRotateService.ts b/packages/core/src/modules/connections/services/DidRotateService.ts new file mode 100644 index 0000000000..23b9a5b7d5 --- /dev/null +++ b/packages/core/src/modules/connections/services/DidRotateService.ts @@ -0,0 +1,276 @@ +import type { Routing } from './ConnectionService' +import type { AgentContext } from '../../../agent' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { ConnectionRecord } from '../repository/ConnectionRecord' + +import { OutboundMessageContext } from '../../../agent/models' +import { InjectionSymbols } from '../../../constants' +import { AriesFrameworkError } from '../../../error' +import { Logger } from '../../../logger' +import { inject, injectable } from '../../../plugins' +import { AckStatus } from '../../common' +import { + DidRepository, + DidResolverService, + PeerDidNumAlgo, + getAlternativeDidsForPeerDid, + getNumAlgoFromPeerDid, + isValidPeerDid, +} from '../../dids' +import { getMediationRecordForDidDocument } from '../../routing/services/helpers' +import { ConnectionsModuleConfig } from '../ConnectionsModuleConfig' +import { DidRotateMessage, DidRotateAckMessage, DidRotateProblemReportMessage, HangupMessage } from '../messages' +import { ConnectionMetadataKeys } from '../repository/ConnectionMetadataTypes' + +import { ConnectionService } from './ConnectionService' +import { createPeerDidFromServices, getDidDocumentForCreatedDid, routingToServices } from './helpers' + +@injectable() +export class DidRotateService { + private didResolverService: DidResolverService + private logger: Logger + + public constructor(didResolverService: DidResolverService, @inject(InjectionSymbols.Logger) logger: Logger) { + this.didResolverService = didResolverService + this.logger = logger + } + + public async createRotate( + agentContext: AgentContext, + options: { connection: ConnectionRecord; toDid?: string; routing?: Routing } + ) { + const { connection, toDid, routing } = options + + const config = agentContext.dependencyManager.resolve(ConnectionsModuleConfig) + + // Do not allow to receive concurrent did rotation flows + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (didRotateMetadata) { + throw new AriesFrameworkError( + `There is already an existing opened did rotation flow for connection id ${connection.id}` + ) + } + + let didDocument, mediatorId + // If did is specified, make sure we have all key material for it + if (toDid) { + didDocument = await getDidDocumentForCreatedDid(agentContext, toDid) + mediatorId = (await getMediationRecordForDidDocument(agentContext, didDocument))?.id + + // Otherwise, create a did:peer based on the provided routing + } else { + if (!routing) { + throw new AriesFrameworkError('Routing configuration must be defined when rotating to a new peer did') + } + + didDocument = await createPeerDidFromServices( + agentContext, + routingToServices(routing), + config.peerNumAlgoForDidRotation + ) + mediatorId = routing.mediatorId + } + + const message = new DidRotateMessage({ toDid: didDocument.id }) + + // We set new info into connection metadata for further 'sealing' it once we receive an acknowledge + // All messages sent in-between will be using previous connection information + connection.metadata.set(ConnectionMetadataKeys.DidRotate, { + threadId: message.threadId, + did: didDocument.id, + mediatorId, + }) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + + return message + } + + public async createHangup(agentContext: AgentContext, options: { connection: ConnectionRecord }) { + const { connection } = options + + const message = new HangupMessage({}) + + // Remove did to indicate termination status for this connection + if (connection.did) { + connection.previousDids = [...connection.previousDids, connection.did] + } + + connection.did = undefined + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + + return message + } + + /** + * Process a Hangup message and mark connection's theirDid as undefined so it is effectively terminated. + * Connection Record itself is not deleted (TODO: config parameter to automatically do so) + * + * Its previous did will be stored in record in order to be able to recognize any message received + * afterwards. + * + * @param messageContext + */ + public async processHangup(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const { agentContext } = messageContext + + if (connection.theirDid) { + connection.previousTheirDids = [...connection.previousTheirDids, connection.theirDid] + } + + connection.theirDid = undefined + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } + + /** + * Process an incoming DID Rotate message and update connection if success. Any acknowledge + * or problem report will be sent to the prior DID, so the created context will take former + * connection record data + * + * @param param + * @param connection + * @returns + */ + public async processRotate(messageContext: InboundMessageContext) { + const connection = messageContext.assertReadyConnection() + const { message, agentContext } = messageContext + + // Check and store their new did + const newDid = message.toDid + + // DID Rotation not supported for peer:1 dids, as we need explicit did document information + if (isValidPeerDid(newDid) && getNumAlgoFromPeerDid(newDid) === PeerDidNumAlgo.GenesisDoc) { + this.logger.error(`Unable to resolve DID Document for '${newDid}`) + + const response = new DidRotateProblemReportMessage({ + description: { en: 'DID Method Unsupported', code: 'e.did.method_unsupported' }, + }) + return new OutboundMessageContext(response, { agentContext, connection }) + } + + const didDocument = (await this.didResolverService.resolve(agentContext, newDid)).didDocument + + // Cannot resolve did + if (!didDocument) { + this.logger.error(`Unable to resolve DID Document for '${newDid}`) + + const response = new DidRotateProblemReportMessage({ + description: { en: 'DID Unresolvable', code: 'e.did.unresolvable' }, + }) + return new OutboundMessageContext(response, { agentContext, connection }) + } + + // Did is resolved but no compatible DIDComm services found + if (!didDocument.didCommServices) { + const response = new DidRotateProblemReportMessage({ + description: { en: 'DID Document Unsupported', code: 'e.did.doc_unsupported' }, + }) + return new OutboundMessageContext(response, { agentContext, connection }) + } + + // Send acknowledge to previous did and persist new did. Previous did will be stored in connection record in + // order to still accept messages from it + const outboundMessageContext = new OutboundMessageContext( + new DidRotateAckMessage({ + threadId: message.threadId, + status: AckStatus.OK, + }), + { agentContext, connection: connection.clone() } + ) + + // Store received did and update connection for further message processing + await agentContext.dependencyManager.resolve(DidRepository).storeReceivedDid(agentContext, { + did: didDocument.id, + didDocument, + tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + + // For did:peer, store any alternative dids (like short form did:peer:4), + // it may have in order to relate any message referencing it + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(didDocument.id) : undefined, + }, + }) + + if (connection.theirDid) { + connection.previousTheirDids = [...connection.previousTheirDids, connection.theirDid] + } + + connection.theirDid = newDid + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + + return outboundMessageContext + } + + public async processRotateAck(inboundMessage: InboundMessageContext) { + const { agentContext, message } = inboundMessage + + const connection = inboundMessage.assertReadyConnection() + + // Update connection info based on metadata set when creating the rotate message + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (!didRotateMetadata) { + throw new AriesFrameworkError(`No did rotation data found for connection with id '${connection.id}'`) + } + + if (didRotateMetadata.threadId !== message.threadId) { + throw new AriesFrameworkError( + `Existing did rotation flow thread id '${didRotateMetadata.threadId} does not match incoming message'` + ) + } + + // Store previous did in order to still accept out-of-order messages that arrived later using it + if (connection.did) connection.previousDids = [...connection.previousDids, connection.did] + + connection.did = didRotateMetadata.did + connection.mediatorId = didRotateMetadata.mediatorId + connection.metadata.delete(ConnectionMetadataKeys.DidRotate) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } + + /** + * Process a problem report related to did rotate protocol, by simply deleting any temporary metadata. + * + * No specific event is thrown other than generic message processing + * + * @param messageContext + */ + public async processProblemReport( + messageContext: InboundMessageContext + ): Promise { + const { message, agentContext } = messageContext + + const connection = messageContext.assertReadyConnection() + + this.logger.debug(`Processing problem report with id ${message.id}`) + + // Delete any existing did rotation metadata in order to 'reset' the connection + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (!didRotateMetadata) { + throw new AriesFrameworkError(`No did rotation data found for connection with id '${connection.id}'`) + } + + connection.metadata.delete(ConnectionMetadataKeys.DidRotate) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } + + public async clearDidRotationData(agentContext: AgentContext, connection: ConnectionRecord) { + const didRotateMetadata = connection.metadata.get(ConnectionMetadataKeys.DidRotate) + + if (!didRotateMetadata) { + throw new AriesFrameworkError(`No did rotation data found for connection with id '${connection.id}'`) + } + + connection.metadata.delete(ConnectionMetadataKeys.DidRotate) + + await agentContext.dependencyManager.resolve(ConnectionService).update(agentContext, connection) + } +} diff --git a/packages/core/src/modules/connections/services/helpers.ts b/packages/core/src/modules/connections/services/helpers.ts index 3dbcc9c837..e0dc91a93a 100644 --- a/packages/core/src/modules/connections/services/helpers.ts +++ b/packages/core/src/modules/connections/services/helpers.ts @@ -1,9 +1,20 @@ -import type { DidDocument } from '../../dids' +import type { Routing } from './ConnectionService' +import type { AgentContext } from '../../../agent' +import type { ResolvedDidCommService } from '../../didcomm' +import type { DidDocument, PeerDidNumAlgo } from '../../dids' import type { DidDoc, PublicKey } from '../models' import { Key, KeyType } from '../../../crypto' import { AriesFrameworkError } from '../../../error' -import { IndyAgentService, DidCommV1Service, DidDocumentBuilder, getEd25519VerificationKey2018 } from '../../dids' +import { + IndyAgentService, + DidCommV1Service, + DidDocumentBuilder, + getEd25519VerificationKey2018, + DidRepository, + DidsApi, + createPeerDidDocumentFromServices, +} from '../../dids' import { didDocumentJsonToNumAlgo1Did } from '../../dids/methods/peer/peerDidNumAlgo1' import { EmbeddedAuthentication } from '../models' @@ -109,3 +120,47 @@ function convertPublicKeyToVerificationMethod(publicKey: PublicKey) { controller: '#id', }) } + +export function routingToServices(routing: Routing): ResolvedDidCommService[] { + return routing.endpoints.map((endpoint, index) => ({ + id: `#inline-${index}`, + serviceEndpoint: endpoint, + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + })) +} + +export async function getDidDocumentForCreatedDid(agentContext: AgentContext, did: string) { + const didRecord = await agentContext.dependencyManager.resolve(DidRepository).findCreatedDid(agentContext, did) + + if (!didRecord?.didDocument) { + throw new AriesFrameworkError(`Could not get DidDocument for created did ${did}`) + } + return didRecord.didDocument +} + +export async function createPeerDidFromServices( + agentContext: AgentContext, + services: ResolvedDidCommService[], + numAlgo: PeerDidNumAlgo +) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + // Create did document without the id property + const didDocument = createPeerDidDocumentFromServices(services) + // Register did:peer document. This will generate the id property and save it to a did record + + const result = await didsApi.create({ + method: 'peer', + didDocument, + options: { + numAlgo, + }, + }) + + if (result.didState?.state !== 'finished') { + throw new AriesFrameworkError(`Did document creation failed: ${JSON.stringify(result.didState)}`) + } + + return result.didState.didDocument +} diff --git a/packages/core/src/modules/connections/services/index.ts b/packages/core/src/modules/connections/services/index.ts index 3520b11146..db9fe13ac4 100644 --- a/packages/core/src/modules/connections/services/index.ts +++ b/packages/core/src/modules/connections/services/index.ts @@ -1,2 +1,3 @@ export * from './ConnectionService' +export * from './DidRotateService' export * from './TrustPingService' diff --git a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts index ca1b1a88d4..e51a55c5ef 100644 --- a/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts +++ b/packages/core/src/modules/didcomm/services/DidCommDocumentService.ts @@ -29,7 +29,7 @@ export class DidCommDocumentService { // FIXME: we currently retrieve did documents for all didcomm services in the did document, and we don't have caching // yet so this will re-trigger ledger resolves for each one. Should we only resolve the first service, then the second service, etc...? for (const didCommService of didCommServices) { - if (didCommService instanceof IndyAgentService) { + if (didCommService.type === IndyAgentService.type) { // IndyAgentService (DidComm v0) has keys encoded as raw publicKeyBase58 (verkeys) resolvedServices.push({ id: didCommService.id, @@ -37,7 +37,7 @@ export class DidCommDocumentService { routingKeys: didCommService.routingKeys?.map(verkeyToInstanceOfKey) || [], serviceEndpoint: didCommService.serviceEndpoint, }) - } else if (didCommService instanceof DidCommV1Service) { + } else if (didCommService.type === DidCommV1Service.type) { // Resolve dids to DIDDocs to retrieve routingKeys const routingKeys = [] for (const routingKey of didCommService.routingKeys ?? []) { diff --git a/packages/core/src/modules/dids/DidsApi.ts b/packages/core/src/modules/dids/DidsApi.ts index e20ef573e2..4f0cf294bf 100644 --- a/packages/core/src/modules/dids/DidsApi.ts +++ b/packages/core/src/modules/dids/DidsApi.ts @@ -15,6 +15,7 @@ import { injectable } from '../../plugins' import { WalletKeyExistsError } from '../../wallet/error' import { DidsModuleConfig } from './DidsModuleConfig' +import { getAlternativeDidsForPeerDid, isValidPeerDid } from './methods' import { DidRepository } from './repository' import { DidRegistrarService, DidResolverService } from './services' @@ -157,6 +158,7 @@ export class DidsApi { existingDidRecord.didDocument = didDocument existingDidRecord.setTags({ recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, }) await this.didRepository.update(this.agentContext, existingDidRecord) @@ -169,6 +171,7 @@ export class DidsApi { didDocument, tags: { recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: isValidPeerDid(didDocument.id) ? getAlternativeDidsForPeerDid(did) : undefined, }, }) } diff --git a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts index 41993e9d43..8485899723 100644 --- a/packages/core/src/modules/dids/__tests__/DidsApi.test.ts +++ b/packages/core/src/modules/dids/__tests__/DidsApi.test.ts @@ -2,8 +2,16 @@ import { IndySdkModule } from '../../../../../indy-sdk/src' import { indySdk } from '../../../../tests' import { getAgentOptions } from '../../../../tests/helpers' import { Agent } from '../../../agent/Agent' +import { isLongFormDidPeer4, isShortFormDidPeer4 } from '../methods/peer/peerDidNumAlgo4' -import { DidDocument, DidDocumentService, KeyType, TypedArrayEncoder } from '@aries-framework/core' +import { + DidDocument, + DidDocumentService, + KeyType, + PeerDidNumAlgo, + TypedArrayEncoder, + createPeerDidDocumentFromServices, +} from '@aries-framework/core' const agentOptions = getAgentOptions( 'DidsApi', @@ -227,4 +235,40 @@ describe('DidsApi', () => { ], }) }) + + test('create and resolve did:peer:4 in short and long form', async () => { + const routing = await agent.mediationRecipient.getRouting({}) + const didDocument = createPeerDidDocumentFromServices([ + { + id: 'didcomm', + recipientKeys: [routing.recipientKey], + routingKeys: routing.routingKeys, + serviceEndpoint: routing.endpoints[0], + }, + ]) + + const result = await agent.dids.create({ + method: 'peer', + didDocument, + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + const longFormDid = result.didState.did + const shortFormDid = result.didState.didDocument?.alsoKnownAs + ? result.didState.didDocument?.alsoKnownAs[0] + : undefined + + if (!longFormDid) fail('Long form did not defined') + if (!shortFormDid) fail('Short form did not defined') + + expect(isLongFormDidPeer4(longFormDid)).toBeTruthy() + expect(isShortFormDidPeer4(shortFormDid)).toBeTruthy() + + const didDocumentFromLongFormDid = await agent.dids.resolveDidDocument(longFormDid) + const didDocumentFromShortFormDid = await agent.dids.resolveDidDocument(shortFormDid) + + expect(didDocumentFromLongFormDid).toEqual(didDocumentFromShortFormDid) + }) }) diff --git a/packages/core/src/modules/dids/domain/DidDocument.ts b/packages/core/src/modules/dids/domain/DidDocument.ts index 08963613af..e67dbd6a1b 100644 --- a/packages/core/src/modules/dids/domain/DidDocument.ts +++ b/packages/core/src/modules/dids/domain/DidDocument.ts @@ -186,12 +186,12 @@ export class DidDocument { let recipientKeys: Key[] = [] for (const service of this.didCommServices) { - if (service instanceof IndyAgentService) { + if (service.type === IndyAgentService.type) { recipientKeys = [ ...recipientKeys, ...service.recipientKeys.map((publicKeyBase58) => Key.fromPublicKeyBase58(publicKeyBase58, KeyType.Ed25519)), ] - } else if (service instanceof DidCommV1Service) { + } else if (service.type === DidCommV1Service.type) { recipientKeys = [ ...recipientKeys, ...service.recipientKeys.map((recipientKey) => keyReferenceToKey(this, recipientKey)), diff --git a/packages/core/src/modules/dids/domain/DidResolver.ts b/packages/core/src/modules/dids/domain/DidResolver.ts index 050ea2cd97..6154030023 100644 --- a/packages/core/src/modules/dids/domain/DidResolver.ts +++ b/packages/core/src/modules/dids/domain/DidResolver.ts @@ -3,6 +3,8 @@ import type { ParsedDid, DidResolutionResult, DidResolutionOptions } from '../ty export interface DidResolver { readonly supportedMethods: string[] + readonly allowsCaching: boolean + resolve( agentContext: AgentContext, did: string, diff --git a/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts b/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts index 8207814895..5ac705c860 100644 --- a/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts +++ b/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts @@ -7,6 +7,11 @@ import { DidJwk } from './DidJwk' export class JwkDidResolver implements DidResolver { public readonly supportedMethods = ['jwk'] + /** + * No remote resolving done, did document is dynamically constructed. To not pollute the cache we don't allow caching + */ + public readonly allowsCaching = false + public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} diff --git a/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts index 41f4a0e221..7719c29d1c 100644 --- a/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts +++ b/packages/core/src/modules/dids/methods/key/KeyDidResolver.ts @@ -7,6 +7,11 @@ import { DidKey } from './DidKey' export class KeyDidResolver implements DidResolver { public readonly supportedMethods = ['key'] + /** + * No remote resolving done, did document is dynamically constructed. To not pollute the cache we don't allow caching + */ + public readonly allowsCaching = false + public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts index fcae22119e..b1e9172dd9 100644 --- a/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts +++ b/packages/core/src/modules/dids/methods/peer/PeerDidRegistrar.ts @@ -9,19 +9,26 @@ import { DidDocument } from '../../domain' import { DidDocumentRole } from '../../domain/DidDocumentRole' import { DidRepository, DidRecord } from '../../repository' -import { PeerDidNumAlgo } from './didPeer' +import { PeerDidNumAlgo, getAlternativeDidsForPeerDid } from './didPeer' import { keyToNumAlgo0DidDocument } from './peerDidNumAlgo0' import { didDocumentJsonToNumAlgo1Did } from './peerDidNumAlgo1' import { didDocumentToNumAlgo2Did } from './peerDidNumAlgo2' +import { didDocumentToNumAlgo4Did } from './peerDidNumAlgo4' export class PeerDidRegistrar implements DidRegistrar { public readonly supportedMethods = ['peer'] public async create( agentContext: AgentContext, - options: PeerDidNumAlgo0CreateOptions | PeerDidNumAlgo1CreateOptions | PeerDidNumAlgo2CreateOptions + options: + | PeerDidNumAlgo0CreateOptions + | PeerDidNumAlgo1CreateOptions + | PeerDidNumAlgo2CreateOptions + | PeerDidNumAlgo4CreateOptions ): Promise { const didRepository = agentContext.dependencyManager.resolve(DidRepository) + + let did: string let didDocument: DidDocument try { @@ -50,16 +57,27 @@ export class PeerDidRegistrar implements DidRegistrar { // TODO: validate did:peer document didDocument = keyToNumAlgo0DidDocument(key) + did = didDocument.id } else if (isPeerDidNumAlgo1CreateOptions(options)) { const didDocumentJson = options.didDocument.toJSON() - const did = didDocumentJsonToNumAlgo1Did(didDocumentJson) + did = didDocumentJsonToNumAlgo1Did(didDocumentJson) didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) } else if (isPeerDidNumAlgo2CreateOptions(options)) { const didDocumentJson = options.didDocument.toJSON() - const did = didDocumentToNumAlgo2Did(options.didDocument) + did = didDocumentToNumAlgo2Did(options.didDocument) didDocument = JsonTransformer.fromJSON({ ...didDocumentJson, id: did }, DidDocument) + } else if (isPeerDidNumAlgo4CreateOptions(options)) { + const didDocumentJson = options.didDocument.toJSON() + + const { longFormDid, shortFormDid } = didDocumentToNumAlgo4Did(options.didDocument) + + did = longFormDid + didDocument = JsonTransformer.fromJSON( + { ...didDocumentJson, id: longFormDid, alsoKnownAs: [shortFormDid] }, + DidDocument + ) } else { return { didDocumentMetadata: {}, @@ -73,13 +91,14 @@ export class PeerDidRegistrar implements DidRegistrar { // Save the did so we know we created it and can use it for didcomm const didRecord = new DidRecord({ - did: didDocument.id, + did, role: DidDocumentRole.Created, didDocument: isPeerDidNumAlgo1CreateOptions(options) ? didDocument : undefined, tags: { // We need to save the recipientKeys, so we can find the associated did // of a key when we receive a message from another connection. recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: getAlternativeDidsForPeerDid(did), }, }) await didRepository.save(agentContext, didRecord) @@ -149,10 +168,15 @@ function isPeerDidNumAlgo2CreateOptions(options: PeerDidCreateOptions): options return options.options.numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc } +function isPeerDidNumAlgo4CreateOptions(options: PeerDidCreateOptions): options is PeerDidNumAlgo4CreateOptions { + return options.options.numAlgo === PeerDidNumAlgo.ShortFormAndLongForm +} + export type PeerDidCreateOptions = | PeerDidNumAlgo0CreateOptions | PeerDidNumAlgo1CreateOptions | PeerDidNumAlgo2CreateOptions + | PeerDidNumAlgo4CreateOptions export interface PeerDidNumAlgo0CreateOptions extends DidCreateOptions { method: 'peer' @@ -188,6 +212,16 @@ export interface PeerDidNumAlgo2CreateOptions extends DidCreateOptions { secret?: undefined } +export interface PeerDidNumAlgo4CreateOptions extends DidCreateOptions { + method: 'peer' + did?: never + didDocument: DidDocument + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm + } + secret?: undefined +} + // Update and Deactivate not supported for did:peer export type PeerDidUpdateOptions = never export type PeerDidDeactivateOptions = never diff --git a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts index fa3f2ed9d2..4e15bee8fd 100644 --- a/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts +++ b/packages/core/src/modules/dids/methods/peer/PeerDidResolver.ts @@ -9,10 +9,16 @@ import { DidRepository } from '../../repository' import { getNumAlgoFromPeerDid, isValidPeerDid, PeerDidNumAlgo } from './didPeer' import { didToNumAlgo0DidDocument } from './peerDidNumAlgo0' import { didToNumAlgo2DidDocument } from './peerDidNumAlgo2' +import { didToNumAlgo4DidDocument, isShortFormDidPeer4 } from './peerDidNumAlgo4' export class PeerDidResolver implements DidResolver { public readonly supportedMethods = ['peer'] + /** + * No remote resolving done, did document is fetched from storage. To not pollute the cache we don't allow caching + */ + public readonly allowsCaching = false + public async resolve(agentContext: AgentContext, did: string): Promise { const didRepository = agentContext.dependencyManager.resolve(DidRepository) @@ -48,9 +54,22 @@ export class PeerDidResolver implements DidResolver { didDocument = didDocumentRecord.didDocument } // For Method 2, generate from did - else { + else if (numAlgo === PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) { didDocument = didToNumAlgo2DidDocument(did) } + // For Method 4, if short form is received, attempt to get the didDocument from stored record + else { + if (isShortFormDidPeer4(did)) { + const [didRecord] = await didRepository.findAllByDid(agentContext, did) + + if (!didRecord) { + throw new AriesFrameworkError(`No did record found for peer did ${did}.`) + } + didDocument = didToNumAlgo4DidDocument(didRecord.did) + } else { + didDocument = didToNumAlgo4DidDocument(did) + } + } return { didDocument, diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts index 99716995f5..160860bf03 100644 --- a/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts +++ b/packages/core/src/modules/dids/methods/peer/__tests__/DidPeer.test.ts @@ -9,10 +9,21 @@ describe('didPeer', () => { 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' ) ).toBe(true) + expect( + isValidPeerDid( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(true) + expect(isValidPeerDid('did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn')).toBe(true) + expect( + isValidPeerDid( + 'did:peer:4z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(false) expect( isValidPeerDid( - 'did:peer:4.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' + 'did:peer:5.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' ) ).toBe(false) }) @@ -35,6 +46,13 @@ describe('didPeer', () => { 'did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc.Vz6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V.Vz6MkgoLTnTypo3tDRwCkZXSccTPHRLhF4ZnjhueYAFpEX6vg.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9lbmRwb2ludCIsInIiOlsiZGlkOmV4YW1wbGU6c29tZW1lZGlhdG9yI3NvbWVrZXkiXSwiYSI6WyJkaWRjb21tL3YyIiwiZGlkY29tbS9haXAyO2Vudj1yZmM1ODciXX0' ) ).toBe(PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc) + + // NumAlgo 4 + expect( + getNumAlgoFromPeerDid( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + ).toBe(PeerDidNumAlgo.ShortFormAndLongForm) }) }) }) diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts index 2daa05729b..2c1076f397 100644 --- a/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts +++ b/packages/core/src/modules/dids/methods/peer/__tests__/PeerDidRegistrar.test.ts @@ -318,17 +318,110 @@ describe('DidRegistrar', () => { }) }) - it('should return an error state if an unsupported numAlgo is provided', async () => { - const result = await peerDidRegistrar.create( - agentContext, - // @ts-expect-error - this is not a valid numAlgo - { + describe('did:peer:4', () => { + const key = Key.fromFingerprint('z6LShxJc8afmt8L1HKjUE56hXwmAkUhdQygrH1VG2jmb1WRz') + const verificationMethod = getEd25519VerificationKey2018({ + key, + controller: '#id', + // Use relative id for peer dids + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + }) + + const didDocument = new DidDocumentBuilder('') + .addVerificationMethod(verificationMethod) + .addAuthentication(verificationMethod.id) + .addService( + new DidCommV1Service({ + id: '#service-0', + recipientKeys: [verificationMethod.id], + serviceEndpoint: 'https://example.com', + accept: ['didcomm/aip2;env=rfc19'], + }) + ) + .build() + + it('should correctly create a did:peer:4 document from a did document', async () => { + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + didDocument: didDocument, + options: { + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + const longFormDid = + 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e' + const shortFormDid = 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4' + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: longFormDid, + didDocument: { + '@context': ['https://w3id.org/did/v1'], + id: longFormDid, + alsoKnownAs: [shortFormDid], + service: [ + { + serviceEndpoint: 'https://example.com', + type: 'did-communication', + priority: 0, + recipientKeys: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + accept: ['didcomm/aip2;env=rfc19'], + id: '#service-0', + }, + ], + verificationMethod: [ + { + id: '#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16', + type: 'Ed25519VerificationKey2018', + controller: '#id', + publicKeyBase58: '7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE', + }, + ], + authentication: ['#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16'], + }, + secret: {}, + }, + }) + }) + + it('should store the did without the did document', async () => { + const longFormDid = + 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e' + const shortFormDid = 'did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4' + await peerDidRegistrar.create(agentContext, { method: 'peer', + didDocument, options: { - numAlgo: 4, + numAlgo: PeerDidNumAlgo.ShortFormAndLongForm, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did: longFormDid, + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key) => key.fingerprint), + alternativeDids: [shortFormDid], }, - } - ) + didDocument: undefined, + }) + }) + }) + + it('should return an error state if an unsupported numAlgo is provided', async () => { + // @ts-expect-error - this is not a valid numAlgo + const result = await peerDidRegistrar.create(agentContext, { + method: 'peer', + options: { + numAlgo: 5, + }, + }) expect(JsonTransformer.toJSON(result)).toMatchObject({ didDocumentMetadata: {}, diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json new file mode 100644 index 0000000000..79a8c2a0d1 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmUJdJ.json @@ -0,0 +1,24 @@ +{ + "@context": ["https://w3id.org/did/v1"], + "verificationMethod": [ + { + "id": "#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "7H8ScGrunfcGBwMhhRakDMYguLAWiNWhQ2maYH84J8fE", + "controller": "did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e" + } + ], + "service": [ + { + "id": "#service-0", + "serviceEndpoint": "https://example.com", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16"], + "accept": ["didcomm/aip2;env=rfc19"] + } + ], + "authentication": ["#41fb2ec7-1f8b-42bf-91a2-4ef9092ddc16"], + "id": "did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4:zD6dcwCdYV2zR4EBGTpxfEaRDLEq3ncjbutZpYTrMcGqaWip2P8vT6LrSH4cCVWfTdZgpuzBV4qY3ZasBMAs8M12JWstLTQHRVtu5ongsGvHCaWdWGS5cQaK6KLABnpBB5KgjPAN391Eekn1Zm4e14atfuj6gKHGp6V41GEumQFGM3YDwijVH82prvah5CqhRx6gXh4CYXu8MJVKiY5HBFdWyNLBtzaPWasGSEdLXYx6FcDv21igJfpcVbwQHwbU43wszfPypKiL9GDyys2n5zAWek5nQFGmDwrF65Vqy74CMFt8fZcvfBc1PTXSexhEwZkUY5inmeBbLXjbJU33FpWK6GxyDANxq5opQeRtAzUCtqeWxdafK56LYUes1THq6DzEKN2VirvvqygtnfPSJUfQWcRYixXq6bGGk5bjt14YygT7mALy5Ne6APGysjnNfH1MA3hrfEM9Ho8tuGSA2JeDvqYebV41chQDfKWoJrsG2bdFwZGgnkb3aBPHd4qyPvEdWiFLawR4mNj8qrtTagX1CyWvcAiWMKbspo5mVvCqP1SJuuT451X4uRBXazC9JGD2k7P63p71HU25zff4LvYkLeU8izcdBva1Tu4RddJN7jMFg4ifkTeZscFfbLPejFTmEDNRFswK1e", + "alsoKnownAs": ["did:peer:4zQmUJdJN7h66RpdeNEkNQ1tpUpN9nr2LcDz4Ftd3xKSgmn4"] +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json new file mode 100644 index 0000000000..fc0529d74e --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/__fixtures__/didPeer4zQmd8Cp.json @@ -0,0 +1,39 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/x25519-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "verificationMethod": [ + { + "id": "#6LSqPZfn", + "type": "X25519KeyAgreementKey2020", + "publicKeyMultibase": "z6LSqPZfn9krvgXma2icTMKf2uVcYhKXsudCmPoUzqGYW24U", + "controller": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" + }, + { + "id": "#6MkrCD1c", + "type": "Ed25519VerificationKey2020", + "publicKeyMultibase": "z6MkrCD1csqtgdj8sjrsu8jxcbeyP6m7LiK87NzhfWqio5yr", + "controller": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" + } + ], + "service": [ + { + "id": "#didcommmessaging-0", + "type": "DIDCommMessaging", + "serviceEndpoint": { + "uri": "didcomm:transport/queue", + "accept": ["didcomm/v2"], + "routingKeys": [] + } + } + ], + "authentication": ["#6MkrCD1c"], + "keyAgreement": ["#6LSqPZfn"], + "assertionMethod": ["#6MkrCD1c"], + "capabilityDelegation": ["#6MkrCD1c"], + "capabilityInvocation": ["#6MkrCD1c"], + "alsoKnownAs": ["did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd"], + "id": "did:peer:4zQmd8CpeFPci817KDsbSAKWcXAE2mjvCQSasRewvbSF54Bd:z2M1k7h4psgp4CmJcnQn2Ljp7Pz7ktsd7oBhMU3dWY5s4fhFNj17qcRTQ427C7QHNT6cQ7T3XfRh35Q2GhaNFZmWHVFq4vL7F8nm36PA9Y96DvdrUiRUaiCuXnBFrn1o7mxFZAx14JL4t8vUWpuDPwQuddVo1T8myRiVH7wdxuoYbsva5x6idEpCQydJdFjiHGCpNc2UtjzPQ8awSXkctGCnBmgkhrj5gto3D4i3EREXYq4Z8r2cWGBr2UzbSmnxW2BuYddFo9Yfm6mKjtJyLpF74ytqrF5xtf84MnGFg1hMBmh1xVx1JwjZ2BeMJs7mNS8DTZhKC7KH38EgqDtUZzfjhpjmmUfkXg2KFEA3EGbbVm1DPqQXayPYKAsYPS9AyKkcQ3fzWafLPP93UfNhtUPL8JW5pMcSV3P8v6j3vPXqnnGknNyBprD6YGUVtgLiAqDBDUF3LSxFQJCVYYtghMTv8WuSw9h1a1SRFrDQLGHE4UrkgoRvwaGWr64aM87T1eVGkP5Dt4L1AbboeK2ceLArPScrdYGTpi3BpTkLwZCdjdiFSfTy9okL1YNRARqUf2wm8DvkVGUU7u5nQA3ZMaXWJAewk6k1YUxKd7LvofGUK4YEDtoxN5vb6r1Q2godrGqaPkjfL3RoYPpDYymf9XhcgG8Kx3DZaA6cyTs24t45KxYAfeCw4wqUpCH9HbpD78TbEUr9PPAsJgXBvBj2VVsxnr7FKbK4KykGcg1W8M1JPz21Z4Y72LWgGQCmixovrkHktcTX1uNHjAvKBqVD5C7XmVfHgXCHj7djCh3vzLNuVLtEED8J1hhqsB1oCBGiuh3xXr7fZ9wUjJCQ1HYHqxLJKdYKtoCiPmgKM7etVftXkmTFETZmpM19aRyih3bao76LdpQtbw636r7a3qt8v4WfxsXJetSL8c7t24SqQBcAY89FBsbEnFNrQCMK3JEseKHVaU388ctvRD45uQfe5GndFxthj4iSDomk4uRFd1uRbywoP1tRuabHTDX42UxPjz" +} diff --git a/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts new file mode 100644 index 0000000000..60af15b4f2 --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/__tests__/peerDidNumAlgo4.test.ts @@ -0,0 +1,45 @@ +import { JsonTransformer } from '../../../../../utils' +import { OutOfBandDidCommService } from '../../../../oob/domain/OutOfBandDidCommService' +import { DidDocument } from '../../../domain' +import { didDocumentToNumAlgo4Did, didToNumAlgo4DidDocument, outOfBandServiceToNumAlgo4Did } from '../peerDidNumAlgo4' + +import didPeer4zQmUJdJ from './__fixtures__/didPeer4zQmUJdJ.json' +import didPeer4zQmd8Cp from './__fixtures__/didPeer4zQmd8Cp.json' + +describe('peerDidNumAlgo4', () => { + describe('didDocumentToNumAlgo4Did', () => { + test('transforms method 4 peer did to a did document', async () => { + expect(didToNumAlgo4DidDocument(didPeer4zQmd8Cp.id).toJSON()).toMatchObject(didPeer4zQmd8Cp) + }) + }) + + describe('didDocumentToNumAlgo4Did', () => { + test('transforms method 4 peer did document to a did', async () => { + const longFormDid = didPeer4zQmUJdJ.id + const shortFormDid = didPeer4zQmUJdJ.alsoKnownAs[0] + + const didDocument = JsonTransformer.fromJSON(didPeer4zQmUJdJ, DidDocument) + + expect(didDocumentToNumAlgo4Did(didDocument)).toEqual({ longFormDid, shortFormDid }) + }) + }) + + describe('outOfBandServiceToNumAlgo4Did', () => { + test('transforms a did comm service into a valid method 4 did', () => { + const service = new OutOfBandDidCommService({ + id: '#service-0', + serviceEndpoint: 'https://example.com/endpoint', + recipientKeys: ['did:key:z6MkqRYqQiSgvZQdnBytw86Qbs2ZWUkGv22od935YF4s8M7V'], + routingKeys: ['did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'], + accept: ['didcomm/v2', 'didcomm/aip2;env=rfc587'], + }) + const { longFormDid } = outOfBandServiceToNumAlgo4Did(service) + const peerDidDocument = didToNumAlgo4DidDocument(longFormDid) + + expect(longFormDid).toBe( + 'did:peer:4zQmXU3HDFaMvdiuUh7eC2hUzFxZHgaKUJpiCAkSDfRE6qSn:z2gxx5mnuv7Tuc5GxjJ3BgJ69g1ucM27iVW9xYSg9tbBjjGLKsWGSpEwqQPbCdCt4qs1aoB3HSM4eoUQALBvR52hCEq2quLwo5RzuZBjZZmuNf6FXvVCrRLQdMG52QJ285W5MUd3hK9QGCUoCNAHJprhtpvcJpoohcg5otvuHeZiffYDRWrfxKUGS83X4X7Hp2vYqdFPgBQcwoveyJcyYByu7zT3Fn8faMffCE5oP125gwsHxjkquEnCy3RMbf64NVL9bLDDk391k7W4HyScbLyh7ooJcWaDDjiFMtoi1J856cDocYtxZ7rjmWmG15pgTcBLX7o8ebKhWCrFSMWtspRuKs9VFaY366Sjce5ZxTUsBWUMCpWhQZxeZQ2h42UST5XiJJ7TV1E13a3ttWrHijPcHgX1MvvDAPGKVgU2jXSgH8bCL4mKuVjdEm4Kx5wMdDW88ougUFuLfwhXkDfP7sYAfuaCFWx286kWqkfYdopcGntPjCvDu6uonghRmxeC2qNfXkYmk3ZQJXzsxgQToixevEvfxQgFY1uuNo5288zJPQcfLHtTvgxEhHxD5wwYYeGFqgV6FTg9mZVU5xqg7w6456cLuZNPuARkfpZK78xMEUHtnr95tK91UY' + ) + expect(longFormDid).toBe(peerDidDocument.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts index 4e23c98d3d..7a194d4c4c 100644 --- a/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts +++ b/packages/core/src/modules/dids/methods/peer/createPeerDidDocumentFromServices.ts @@ -5,7 +5,6 @@ import { convertPublicKeyToX25519 } from '@stablelib/ed25519' import { Key } from '../../../../crypto/Key' import { KeyType } from '../../../../crypto/KeyType' import { AriesFrameworkError } from '../../../../error' -import { uuid } from '../../../../utils/uuid' import { getEd25519VerificationKey2018, getX25519KeyAgreementKey2019 } from '../../domain' import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' import { DidCommV1Service } from '../../domain/service/DidCommV1Service' @@ -30,13 +29,14 @@ export function createPeerDidDocumentFromServices(services: ResolvedDidCommServi } const x25519Key = Key.fromPublicKey(convertPublicKeyToX25519(recipientKey.publicKey), KeyType.X25519) + // Remove prefix from id as it is not included in did peer identifiers const ed25519VerificationMethod = getEd25519VerificationKey2018({ - id: `#${uuid()}`, + id: `#${recipientKey.fingerprint.substring(1)}`, key: recipientKey, controller: '#id', }) const x25519VerificationMethod = getX25519KeyAgreementKey2019({ - id: `#${uuid()}`, + id: `#${x25519Key.fingerprint.substring(1)}`, key: x25519Key, controller: '#id', }) diff --git a/packages/core/src/modules/dids/methods/peer/didPeer.ts b/packages/core/src/modules/dids/methods/peer/didPeer.ts index 9aaf294ba5..622b5b6a9c 100644 --- a/packages/core/src/modules/dids/methods/peer/didPeer.ts +++ b/packages/core/src/modules/dids/methods/peer/didPeer.ts @@ -1,7 +1,9 @@ import { AriesFrameworkError } from '../../../../error' +import { getAlternativeDidsForNumAlgo4Did } from './peerDidNumAlgo4' + const PEER_DID_REGEX = new RegExp( - '^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))+(.(S)[0-9a-zA-Z=]*)?)))$' + '^did:peer:(([01](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))|(2((.[AEVID](z)([1-9a-km-zA-HJ-NP-Z]{5,200}))+(.(S)[0-9a-zA-Z=]*)?))|([4](z[1-9a-km-zA-HJ-NP-Z]{46})(:z[1-9a-km-zA-HJ-NP-Z]{6,}){0,1}))$' ) export function isValidPeerDid(did: string): boolean { @@ -14,6 +16,7 @@ export enum PeerDidNumAlgo { InceptionKeyWithoutDoc = 0, GenesisDoc = 1, MultipleInceptionKeyWithoutDoc = 2, + ShortFormAndLongForm = 4, } export function getNumAlgoFromPeerDid(did: string) { @@ -22,10 +25,23 @@ export function getNumAlgoFromPeerDid(did: string) { if ( numAlgo !== PeerDidNumAlgo.InceptionKeyWithoutDoc && numAlgo !== PeerDidNumAlgo.GenesisDoc && - numAlgo !== PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc + numAlgo !== PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc && + numAlgo !== PeerDidNumAlgo.ShortFormAndLongForm ) { throw new AriesFrameworkError(`Invalid peer did numAlgo: ${numAlgo}`) } return numAlgo as PeerDidNumAlgo } + +/** + * Given a peer did, returns any alternative forms equivalent to it. + * + * @param did + * @returns array of alternative dids or undefined if not applicable + */ +export function getAlternativeDidsForPeerDid(did: string) { + if (getNumAlgoFromPeerDid(did) === PeerDidNumAlgo.ShortFormAndLongForm) { + return getAlternativeDidsForNumAlgo4Did(did) + } +} diff --git a/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts new file mode 100644 index 0000000000..cb8d61598d --- /dev/null +++ b/packages/core/src/modules/dids/methods/peer/peerDidNumAlgo4.ts @@ -0,0 +1,138 @@ +import type { OutOfBandDidCommService } from '../../../oob/domain/OutOfBandDidCommService' + +import { AriesFrameworkError } from '../../../../error' +import { + JsonEncoder, + JsonTransformer, + MultiBaseEncoder, + MultiHashEncoder, + TypedArrayEncoder, + VarintEncoder, +} from '../../../../utils' +import { Buffer } from '../../../../utils/buffer' +import { DidDocument, DidCommV1Service } from '../../domain' +import { DidDocumentBuilder } from '../../domain/DidDocumentBuilder' +import { parseDid } from '../../domain/parse' +import { DidKey } from '../key' + +const LONG_RE = new RegExp(`^did:peer:4(z[1-9a-km-zA-HJ-NP-Z]{46}):(z[1-9a-km-zA-HJ-NP-Z]{6,})$`) +const SHORT_RE = new RegExp(`^did:peer:4(z[1-9a-km-zA-HJ-NP-Z]{46})$`) +const JSON_MULTICODEC_VARINT = 0x0200 + +export const isShortFormDidPeer4 = (did: string) => SHORT_RE.test(did) +export const isLongFormDidPeer4 = (did: string) => LONG_RE.test(did) + +const hashEncodedDocument = (encodedDocument: string) => + MultiBaseEncoder.encode( + MultiHashEncoder.encode(TypedArrayEncoder.fromString(encodedDocument), 'sha2-256'), + 'base58btc' + ) + +export function getAlternativeDidsForNumAlgo4Did(did: string) { + const match = did.match(LONG_RE) + if (!match) return + const [, hash] = match + return [`did:peer:4${hash}`] +} + +export function didToNumAlgo4DidDocument(did: string) { + const parsed = parseDid(did) + + const match = parsed.did.match(LONG_RE) + if (!match) { + throw new AriesFrameworkError(`Invalid long form algo 4 did:peer: ${parsed.did}`) + } + const [, hash, encodedDocument] = match + if (hash !== hashEncodedDocument(encodedDocument)) { + throw new AriesFrameworkError(`Hash is invalid for did: ${did}`) + } + + const { data } = MultiBaseEncoder.decode(encodedDocument) + const [multiCodecValue] = VarintEncoder.decode(data.subarray(0, 2)) + if (multiCodecValue !== JSON_MULTICODEC_VARINT) { + throw new AriesFrameworkError(`Not a JSON multicodec data`) + } + const didDocumentJson = JsonEncoder.fromBuffer(data.subarray(2)) + + didDocumentJson.id = parsed.did + didDocumentJson.alsoKnownAs = [parsed.did.slice(0, did.lastIndexOf(':'))] + + // Populate all verification methods without controller + const addControllerIfNotPresent = (item: unknown) => { + if (Array.isArray(item)) item.forEach(addControllerIfNotPresent) + + if (item && typeof item === 'object' && (item as Record).controller === undefined) { + ;(item as Record).controller = parsed.did + } + } + + addControllerIfNotPresent(didDocumentJson.verificationMethod) + addControllerIfNotPresent(didDocumentJson.authentication) + addControllerIfNotPresent(didDocumentJson.assertionMethod) + addControllerIfNotPresent(didDocumentJson.keyAgreement) + addControllerIfNotPresent(didDocumentJson.capabilityDelegation) + addControllerIfNotPresent(didDocumentJson.capabilityInvocation) + + const didDocument = JsonTransformer.fromJSON(didDocumentJson, DidDocument) + return didDocument +} + +export function didDocumentToNumAlgo4Did(didDocument: DidDocument) { + const didDocumentJson = didDocument.toJSON() + + // Build input document based on did document, without any + // reference to controller + const deleteControllerIfPresent = (item: unknown) => { + if (Array.isArray(item)) { + item.forEach((method: { controller?: string }) => { + if (method.controller === '#id' || method.controller === didDocument.id) delete method.controller + }) + } + } + delete didDocumentJson.id + delete didDocumentJson.alsoKnownAs + deleteControllerIfPresent(didDocumentJson.verificationMethod) + deleteControllerIfPresent(didDocumentJson.authentication) + deleteControllerIfPresent(didDocumentJson.assertionMethod) + deleteControllerIfPresent(didDocumentJson.keyAgreement) + deleteControllerIfPresent(didDocumentJson.capabilityDelegation) + deleteControllerIfPresent(didDocumentJson.capabilityInvocation) + + // Construct encoded document by prefixing did document with multicodec prefix for JSON + const buffer = Buffer.concat([ + VarintEncoder.encode(JSON_MULTICODEC_VARINT), + Buffer.from(JSON.stringify(didDocumentJson)), + ]) + + const encodedDocument = MultiBaseEncoder.encode(buffer, 'base58btc') + + const shortFormDid = `did:peer:4${hashEncodedDocument(encodedDocument)}` + const longFormDid = `${shortFormDid}:${encodedDocument}` + + return { shortFormDid, longFormDid } +} + +export function outOfBandServiceToNumAlgo4Did(service: OutOfBandDidCommService) { + // FIXME: add the key entries for the recipientKeys to the did document. + const didDocument = new DidDocumentBuilder('') + .addService( + new DidCommV1Service({ + id: service.id, + serviceEndpoint: service.serviceEndpoint, + accept: service.accept, + // FIXME: this should actually be local key references, not did:key:123#456 references + recipientKeys: service.recipientKeys.map((recipientKey) => { + const did = DidKey.fromDid(recipientKey) + return `${did.did}#${did.key.fingerprint}` + }), + // Map did:key:xxx to actual did:key:xxx#123 + routingKeys: service.routingKeys?.map((routingKey) => { + const did = DidKey.fromDid(routingKey) + return `${did.did}#${did.key.fingerprint}` + }), + }) + ) + .build() + + return didDocumentToNumAlgo4Did(didDocument) +} diff --git a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts index 77d9b1e295..f56dbebbfd 100644 --- a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts +++ b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts @@ -11,6 +11,8 @@ import { DidDocument } from '../../domain' export class WebDidResolver implements DidResolver { public readonly supportedMethods + public readonly allowsCaching = true + // FIXME: Would be nice if we don't have to provide a did resolver instance private _resolverInstance = new Resolver() private resolver = didWeb.getResolver() diff --git a/packages/core/src/modules/dids/repository/DidRecord.ts b/packages/core/src/modules/dids/repository/DidRecord.ts index 5f8b9cc375..3f22751648 100644 --- a/packages/core/src/modules/dids/repository/DidRecord.ts +++ b/packages/core/src/modules/dids/repository/DidRecord.ts @@ -23,6 +23,10 @@ export interface DidRecordProps { export interface CustomDidTags extends TagsBase { recipientKeyFingerprints?: string[] + + // Alternative forms of the did, allowed to be queried by them. + // Relationship must be verified both ways before setting this tag. + alternativeDids?: string[] } type DefaultDidTags = { diff --git a/packages/core/src/modules/dids/repository/DidRepository.ts b/packages/core/src/modules/dids/repository/DidRepository.ts index 11a6c60b9a..0851390d87 100644 --- a/packages/core/src/modules/dids/repository/DidRepository.ts +++ b/packages/core/src/modules/dids/repository/DidRepository.ts @@ -48,22 +48,28 @@ export class DidRepository extends Repository { } public findAllByDid(agentContext: AgentContext, did: string) { - return this.findByQuery(agentContext, { did }) + return this.findByQuery(agentContext, { $or: [{ alternativeDids: [did] }, { did }] }) } public findReceivedDid(agentContext: AgentContext, receivedDid: string) { - return this.findSingleByQuery(agentContext, { did: receivedDid, role: DidDocumentRole.Received }) + return this.findSingleByQuery(agentContext, { + $or: [{ alternativeDids: [receivedDid] }, { did: receivedDid }], + role: DidDocumentRole.Received, + }) } public findCreatedDid(agentContext: AgentContext, createdDid: string) { - return this.findSingleByQuery(agentContext, { did: createdDid, role: DidDocumentRole.Created }) + return this.findSingleByQuery(agentContext, { + $or: [{ alternativeDids: [createdDid] }, { did: createdDid }], + role: DidDocumentRole.Created, + }) } public getCreatedDids(agentContext: AgentContext, { method, did }: { method?: string; did?: string }) { return this.findByQuery(agentContext, { role: DidDocumentRole.Created, method, - did, + $or: did ? [{ alternativeDids: [did] }, { did }] : undefined, }) } diff --git a/packages/core/src/modules/dids/services/DidResolverService.ts b/packages/core/src/modules/dids/services/DidResolverService.ts index 7f97d3f9d1..a24c8908ba 100644 --- a/packages/core/src/modules/dids/services/DidResolverService.ts +++ b/packages/core/src/modules/dids/services/DidResolverService.ts @@ -6,7 +6,10 @@ import { InjectionSymbols } from '../../../constants' import { AriesFrameworkError } from '../../../error' import { Logger } from '../../../logger' import { injectable, inject } from '../../../plugins' +import { JsonTransformer } from '../../../utils' +import { CacheModuleConfig } from '../../cache' import { DidsModuleConfig } from '../DidsModuleConfig' +import { DidDocument } from '../domain' import { parseDid } from '../domain/parse' @injectable() @@ -53,9 +56,69 @@ export class DidResolverService { } } - return resolver.resolve(agentContext, parsed.did, parsed, options) + // extract caching options and set defaults + const { useCache = true, cacheDurationInSeconds = 300, persistInCache = true } = options + const cacheKey = `did:resolver:${parsed.did}` + + if (resolver.allowsCaching && useCache) { + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + // FIXME: in multi-tenancy it can be that the same cache is used for different agent contexts + // This may become a problem when resolving dids, as you can get back a cache hit for a different + // tenant. did:peer has disabled caching, and I think we should just recommend disabling caching + // for these private dids + // We could allow providing a custom cache prefix in the resolver options, so that the cache key + // can be recognized in the cache implementation + const cachedDidDocument = await cache.get }>( + agentContext, + cacheKey + ) + + if (cachedDidDocument) { + return { + ...cachedDidDocument, + didDocument: JsonTransformer.fromJSON(cachedDidDocument.didDocument, DidDocument), + didResolutionMetadata: { + ...cachedDidDocument.didResolutionMetadata, + servedFromCache: true, + }, + } + } + } + + let resolutionResult = await resolver.resolve(agentContext, parsed.did, parsed, options) + // Avoid overwriting existing document + resolutionResult = { + ...resolutionResult, + didResolutionMetadata: { + ...resolutionResult.didResolutionMetadata, + resolutionTime: Date.now(), + // Did resolver implementation might use did method specific caching strategy + // We only set to false if not defined by the resolver + servedFromCache: resolutionResult.didResolutionMetadata.servedFromCache ?? false, + }, + } + + if (resolutionResult.didDocument && resolver.allowsCaching && persistInCache) { + const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache + await cache.set( + agentContext, + cacheKey, + { + ...resolutionResult, + didDocument: resolutionResult.didDocument.toJSON(), + }, + // Set cache duration + cacheDurationInSeconds + ) + } + + return resolutionResult } + /** + * Resolve a did document. This uses the default resolution options, and thus + * will use caching if available. + */ public async resolveDidDocument(agentContext: AgentContext, did: string) { const { didDocument, diff --git a/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts b/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts index 00b17ad458..76573d0ed3 100644 --- a/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts +++ b/packages/core/src/modules/dids/services/__tests__/DidResolverService.test.ts @@ -2,6 +2,7 @@ import type { DidResolver } from '../../domain' import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers' import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { CacheModuleConfig, InMemoryLruCache } from '../../../cache' import { DidsModuleConfig } from '../../DidsModuleConfig' import didKeyEd25519Fixture from '../../__tests__/__fixtures__/didKeyEd25519.json' import { DidDocument } from '../../domain' @@ -9,12 +10,16 @@ import { parseDid } from '../../domain/parse' import { DidResolverService } from '../DidResolverService' const didResolverMock = { + allowsCaching: true, supportedMethods: ['key'], resolve: jest.fn(), } as DidResolver +const cache = new InMemoryLruCache({ limit: 10 }) const agentConfig = getAgentConfig('DidResolverService') -const agentContext = getAgentContext() +const agentContext = getAgentContext({ + registerInstances: [[CacheModuleConfig, new CacheModuleConfig({ cache })]], +}) describe('DidResolverService', () => { const didResolverService = new DidResolverService( @@ -22,6 +27,10 @@ describe('DidResolverService', () => { new DidsModuleConfig({ resolvers: [didResolverMock] }) ) + afterEach(() => { + jest.clearAllMocks() + }) + it('should correctly find and call the correct resolver for a specified did', async () => { const returnValue = { didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument), @@ -33,7 +42,14 @@ describe('DidResolverService', () => { mockFunction(didResolverMock.resolve).mockResolvedValue(returnValue) const result = await didResolverService.resolve(agentContext, 'did:key:xxxx', { someKey: 'string' }) - expect(result).toEqual(returnValue) + expect(result).toEqual({ + ...returnValue, + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: false, + }, + }) expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) expect(didResolverMock.resolve).toHaveBeenCalledWith(agentContext, 'did:key:xxxx', parseDid('did:key:xxxx'), { @@ -41,6 +57,57 @@ describe('DidResolverService', () => { }) }) + it('should return cached did document when resolved multiple times within caching duration', async () => { + const returnValue = { + didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument), + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + } + mockFunction(didResolverMock.resolve).mockResolvedValue(returnValue) + + const result = await didResolverService.resolve(agentContext, 'did:key:cached', { someKey: 'string' }) + const cachedValue = await cache.get(agentContext, 'did:resolver:did:key:cached') + + expect(result).toEqual({ + ...returnValue, + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: false, + }, + }) + + expect(cachedValue).toEqual({ + ...returnValue, + didDocument: returnValue.didDocument.toJSON(), + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: false, + }, + }) + + expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) + expect(didResolverMock.resolve).toHaveBeenCalledWith(agentContext, 'did:key:cached', parseDid('did:key:cached'), { + someKey: 'string', + }) + + const resultCached = await didResolverService.resolve(agentContext, 'did:key:cached', { someKey: 'string' }) + expect(resultCached).toEqual({ + ...returnValue, + didResolutionMetadata: { + ...returnValue.didResolutionMetadata, + resolutionTime: expect.any(Number), + servedFromCache: true, + }, + }) + + // Still called once because served from cache + expect(didResolverMock.resolve).toHaveBeenCalledTimes(1) + }) + it("should return an error with 'invalidDid' if the did string couldn't be parsed", async () => { const did = 'did:__Asd:asdfa' diff --git a/packages/core/src/modules/dids/types.ts b/packages/core/src/modules/dids/types.ts index 359616a818..ccbc53fa70 100644 --- a/packages/core/src/modules/dids/types.ts +++ b/packages/core/src/modules/dids/types.ts @@ -2,11 +2,34 @@ import type { DidDocument } from './domain' import type { DIDDocumentMetadata, DIDResolutionMetadata, DIDResolutionOptions, ParsedDID } from 'did-resolver' export type ParsedDid = ParsedDID -export type DidResolutionOptions = DIDResolutionOptions export type DidDocumentMetadata = DIDDocumentMetadata +export interface DidResolutionOptions extends DIDResolutionOptions { + /** + * Whether to resolve the did document from the cache. + * + * @default true + */ + useCache?: boolean + + /** + * Whether to persist the did document in the cache. + * + * @default true + */ + persistInCache?: boolean + + /** + * How many seconds to persist the resolved document + * + * @default 3600 + */ + cacheDurationInSeconds?: number +} + export interface DidResolutionMetadata extends DIDResolutionMetadata { message?: string + servedFromCache?: boolean } export interface DidResolutionResult { diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts new file mode 100644 index 0000000000..5c06ec420a --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts @@ -0,0 +1,13 @@ +import { AriesFrameworkError } from '../../error' + +export class DifPresentationExchangeError extends AriesFrameworkError { + public additionalMessages?: Array + + public constructor( + message: string, + { cause, additionalMessages }: { cause?: Error; additionalMessages?: Array } = {} + ) { + super(message, { cause }) + this.additionalMessages = additionalMessages + } +} diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts new file mode 100644 index 0000000000..7cb2c86c5a --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts @@ -0,0 +1,25 @@ +import type { DependencyManager, Module } from '../../plugins' + +import { AgentConfig } from '../../agent/AgentConfig' + +import { DifPresentationExchangeService } from './DifPresentationExchangeService' + +/** + * @public + */ +export class DifPresentationExchangeModule implements Module { + /** + * Registers the dependencies of the presentation-exchange module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The 'DifPresentationExchangeModule' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + ) + + // service + dependencyManager.registerSingleton(DifPresentationExchangeService) + } +} diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts new file mode 100644 index 0000000000..eab8642230 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -0,0 +1,548 @@ +import type { + DifPexInputDescriptorToCredentials, + DifPexCredentialsForRequest, + DifPresentationExchangeDefinition, + DifPresentationExchangeDefinitionV1, + DifPresentationExchangeSubmission, + DifPresentationExchangeDefinitionV2, +} from './models' +import type { AgentContext } from '../../agent' +import type { Query } from '../../storage/StorageService' +import type { VerificationMethod } from '../dids' +import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresentation } from '../vc' +import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@sphereon/pex' +import type { InputDescriptorV2, PresentationDefinitionV1 } from '@sphereon/pex-models' +import type { OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types' + +import { Status, PEVersion, PEX } from '@sphereon/pex' +import { injectable } from 'tsyringe' + +import { getJwkFromKey } from '../../crypto' +import { AriesFrameworkError } from '../../error' +import { JsonTransformer } from '../../utils' +import { DidsApi, getKeyFromVerificationMethod } from '../dids' +import { + ClaimFormat, + SignatureSuiteRegistry, + W3cCredentialRepository, + W3cCredentialService, + W3cPresentation, +} from '../vc' + +import { DifPresentationExchangeError } from './DifPresentationExchangeError' +import { DifPresentationExchangeSubmissionLocation } from './models' +import { + getCredentialsForRequest, + getSphereonOriginalVerifiableCredential, + getSphereonW3cVerifiablePresentation, + getW3cVerifiablePresentationInstance, +} from './utils' + +export type ProofStructure = Record>> + +@injectable() +export class DifPresentationExchangeService { + private pex = new PEX() + + public async getCredentialsForRequest( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinition + ): Promise { + const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) + + // FIXME: why are we resolving all created dids here? + // If we want to do this we should extract all dids from the credential records and only + // fetch the dids for the queried credential records + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didRecords = await didsApi.getCreatedDids() + const holderDids = didRecords.map((didRecord) => didRecord.did) + + return getCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) + } + + /** + * Selects the credentials to use based on the output from `getCredentialsForRequest` + * Use this method if you don't want to manually select the credentials yourself. + */ + public selectCredentialsForRequest( + credentialsForRequest: DifPexCredentialsForRequest + ): DifPexInputDescriptorToCredentials { + if (!credentialsForRequest.areRequirementsSatisfied) { + throw new AriesFrameworkError('Could not find the required credentials for the presentation submission') + } + + const credentials: DifPexInputDescriptorToCredentials = {} + + for (const requirement of credentialsForRequest.requirements) { + for (const submission of requirement.submissionEntry) { + if (!credentials[submission.inputDescriptorId]) { + credentials[submission.inputDescriptorId] = [] + } + + // We pick the first matching VC if we are auto-selecting + credentials[submission.inputDescriptorId].push(submission.verifiableCredentials[0].credential) + } + } + + return credentials + } + + public validatePresentationDefinition(presentationDefinition: DifPresentationExchangeDefinition) { + const validation = PEX.validateDefinition(presentationDefinition) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new DifPresentationExchangeError(`Invalid presentation definition`, { additionalMessages: errorMessages }) + } + } + + public validatePresentationSubmission(presentationSubmission: DifPresentationExchangeSubmission) { + const validation = PEX.validateSubmission(presentationSubmission) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new DifPresentationExchangeError(`Invalid presentation submission`, { additionalMessages: errorMessages }) + } + } + + public validatePresentation( + presentationDefinition: DifPresentationExchangeDefinition, + presentation: W3cVerifiablePresentation + ) { + const { errors } = this.pex.evaluatePresentation( + presentationDefinition, + presentation.encoded as OriginalVerifiablePresentation + ) + + if (errors) { + const errorMessages = this.formatValidated(errors as Validated) + if (errorMessages.length > 0) { + throw new DifPresentationExchangeError(`Invalid presentation`, { additionalMessages: errorMessages }) + } + } + } + + private formatValidated(v: Validated) { + const validated = Array.isArray(v) ? v : [v] + return validated + .filter((r) => r.tag === Status.ERROR) + .map((r) => r.message) + .filter((r): r is string => Boolean(r)) + } + + /** + * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the + * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. + */ + private async queryCredentialForPresentationDefinition( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinition + ): Promise> { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const query: Array> = [] + const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) + + if (!presentationDefinitionVersion.version) { + throw new DifPresentationExchangeError( + `Unable to determine the Presentation Exchange version from the presentation definition + `, + presentationDefinitionVersion.error ? { additionalMessages: [presentationDefinitionVersion.error] } : {} + ) + } + + if (presentationDefinitionVersion.version === PEVersion.v1) { + const pd = presentationDefinition as PresentationDefinitionV1 + + // The schema.uri can contain either an expanded type, or a context uri + for (const inputDescriptor of pd.input_descriptors) { + for (const schema of inputDescriptor.schema) { + query.push({ + $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], + }) + } + } + } else if (presentationDefinitionVersion.version === PEVersion.v2) { + // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. + // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need + // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. + } else { + throw new DifPresentationExchangeError( + `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` + ) + } + + // query the wallet ourselves first to avoid the need to query the pex library for all + // credentials for every proof request + const credentialRecords = + query.length > 0 + ? await w3cCredentialRepository.findByQuery(agentContext, { + $or: query, + }) + : await w3cCredentialRepository.getAll(agentContext) + + return credentialRecords + } + + private addCredentialToSubjectInputDescriptor( + subjectsToInputDescriptors: ProofStructure, + subjectId: string, + inputDescriptorId: string, + credential: W3cVerifiableCredential + ) { + const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} + const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] + + credentials.push(credential) + inputDescriptorsToCredentials[inputDescriptorId] = credentials + subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials + } + + private getPresentationFormat( + presentationDefinition: DifPresentationExchangeDefinition, + credentials: Array + ): ClaimFormat.JwtVp | ClaimFormat.LdpVp { + const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') + const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') + + const inputDescriptorsNotSupportingJwtVc = ( + presentationDefinition.input_descriptors as Array + ).filter((d) => d.format && d.format.jwt_vc === undefined) + + const inputDescriptorsNotSupportingLdpVc = ( + presentationDefinition.input_descriptors as Array + ).filter((d) => d.format && d.format.ldp_vc === undefined) + + if ( + allCredentialsAreJwtVc && + (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && + inputDescriptorsNotSupportingJwtVc.length === 0 + ) { + return ClaimFormat.JwtVp + } else if ( + allCredentialsAreLdpVc && + (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && + inputDescriptorsNotSupportingLdpVc.length === 0 + ) { + return ClaimFormat.LdpVp + } else { + throw new DifPresentationExchangeError( + 'No suitable presentation format found for the given presentation definition, and credentials' + ) + } + } + + public async createPresentation( + agentContext: AgentContext, + options: { + credentialsForInputDescriptor: DifPexInputDescriptorToCredentials + presentationDefinition: DifPresentationExchangeDefinition + /** + * Defaults to {@link DifPresentationExchangeSubmissionLocation.PRESENTATION} + */ + presentationSubmissionLocation?: DifPresentationExchangeSubmissionLocation + challenge?: string + domain?: string + nonce?: string + } + ) { + const { presentationDefinition, challenge, nonce, domain, presentationSubmissionLocation } = options + + const proofStructure: ProofStructure = {} + + Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { + credentials.forEach((credential) => { + const subjectId = credential.credentialSubjectIds[0] + if (!subjectId) { + throw new DifPresentationExchangeError('Missing required credential subject for creating the presentation.') + } + + this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) + }) + }) + + const verifiablePresentationResultsWithFormat: Array<{ + verifiablePresentationResult: VerifiablePresentationResult + format: ClaimFormat.LdpVp | ClaimFormat.JwtVp + }> = [] + + const subjectToInputDescriptors = Object.entries(proofStructure) + for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) + + if (!verificationMethod) { + throw new DifPresentationExchangeError(`No verification method found for subject id '${subjectId}'.`) + } + + // We create a presentation for each subject + // Thus for each subject we need to filter all the related input descriptors and credentials + // FIXME: cast to V1, as tsc errors for strange reasons if not + const inputDescriptorsForSubject = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( + (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials + ) + + // Get all the credentials associated with the input descriptors + const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) + .flat() + .map(getSphereonOriginalVerifiableCredential) + + const presentationDefinitionForSubject: DifPresentationExchangeDefinition = { + ...presentationDefinition, + input_descriptors: inputDescriptorsForSubject, + + // We remove the submission requirements, as it will otherwise fail to create the VP + submission_requirements: undefined, + } + + const format = this.getPresentationFormat(presentationDefinitionForSubject, credentialsForSubject) + + // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? + // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? + const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( + presentationDefinitionForSubject, + credentialsForSubject, + this.getPresentationSignCallback(agentContext, verificationMethod, format), + { + holderDID: subjectId, + proofOptions: { challenge, domain, nonce }, + signatureOptions: { verificationMethod: verificationMethod?.id }, + presentationSubmissionLocation: + presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION, + } + ) + + verifiablePresentationResultsWithFormat.push({ verifiablePresentationResult, format }) + } + + if (!verifiablePresentationResultsWithFormat[0]) { + throw new DifPresentationExchangeError('No verifiable presentations created') + } + + if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { + throw new DifPresentationExchangeError('Invalid amount of verifiable presentations created') + } + + const presentationSubmission: DifPresentationExchangeSubmission = { + id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, + definition_id: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, + descriptor_map: [], + } + + for (const vpf of verifiablePresentationResultsWithFormat) { + const { verifiablePresentationResult } = vpf + presentationSubmission.descriptor_map.push(...verifiablePresentationResult.presentationSubmission.descriptor_map) + } + + return { + verifiablePresentations: verifiablePresentationResultsWithFormat.map((r) => + getW3cVerifiablePresentationInstance(r.verifiablePresentationResult.verifiablePresentation) + ), + presentationSubmission, + presentationSubmissionLocation: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, + } + } + + private getSigningAlgorithmFromVerificationMethod( + verificationMethod: VerificationMethod, + suitableAlgorithms?: Array + ) { + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + if (suitableAlgorithms) { + const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) + if (!possibleAlgorithms || possibleAlgorithms.length === 0) { + throw new DifPresentationExchangeError( + [ + `Found no suitable signing algorithm.`, + `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, + `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, + ].join('\n') + ) + } + } + + const alg = jwk.supportedSignatureAlgorithms[0] + if (!alg) throw new DifPresentationExchangeError(`No supported algs for key type: ${key.keyType}`) + return alg + } + + private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition: Array, + inputDescriptorAlgorithms: Array> + ) { + const allDescriptorAlgorithms = inputDescriptorAlgorithms.flat() + const algorithmsSatisfyingDescriptors = allDescriptorAlgorithms.filter((alg) => + inputDescriptorAlgorithms.every((descriptorAlgorithmSet) => descriptorAlgorithmSet.includes(alg)) + ) + + const algorithmsSatisfyingPdAndDescriptorRestrictions = algorithmsSatisfyingDefinition.filter((alg) => + algorithmsSatisfyingDescriptors.includes(alg) + ) + + if ( + algorithmsSatisfyingDefinition.length > 0 && + algorithmsSatisfyingDescriptors.length > 0 && + algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 + ) { + throw new DifPresentationExchangeError( + `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors` + ) + } + + if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { + throw new DifPresentationExchangeError( + `No signature algorithm found for satisfying restrictions of the input descriptors` + ) + } + + let suitableAlgorithms: Array | undefined + if (algorithmsSatisfyingPdAndDescriptorRestrictions.length > 0) { + suitableAlgorithms = algorithmsSatisfyingPdAndDescriptorRestrictions + } else if (algorithmsSatisfyingDescriptors.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDescriptors + } else if (algorithmsSatisfyingDefinition.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDefinition + } + + return suitableAlgorithms + } + + private getSigningAlgorithmForJwtVc( + presentationDefinition: DifPresentationExchangeDefinitionV1 | DifPresentationExchangeDefinitionV2, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.jwt_vc?.alg ?? []) + .filter((alg) => alg.length > 0) + + const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) + } + + private getProofTypeForLdpVc( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinitionV1 | DifPresentationExchangeDefinitionV2, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type ?? []) + .filter((alg) => alg.length > 0) + + const suitableSignatureSuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + // For each of the supported algs, find the key types, then find the proof types + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!supportedSignatureSuite) { + throw new DifPresentationExchangeError( + `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'` + ) + } + + if (suitableSignatureSuites) { + if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { + throw new DifPresentationExchangeError( + [ + 'No possible signature suite found for the given verification method.', + `Verification method type: ${verificationMethod.type}`, + `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, + `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, + ].join('\n') + ) + } + + return supportedSignatureSuite.proofType + } + + return supportedSignatureSuite.proofType + } + + public getPresentationSignCallback( + agentContext: AgentContext, + verificationMethod: VerificationMethod, + vpFormat: ClaimFormat.LdpVp | ClaimFormat.JwtVp + ) { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + return async (callBackParams: PresentationSignCallBackParams) => { + // The created partial proof and presentation, as well as original supplied options + const { presentation: presentationJson, options, presentationDefinition } = callBackParams + const { challenge, domain, nonce } = options.proofOptions ?? {} + const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} + + if (verificationMethodId && verificationMethodId !== verificationMethod.id) { + throw new DifPresentationExchangeError( + `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}` + ) + } + + let signedPresentation: W3cVerifiablePresentation + if (vpFormat === 'jwt_vp') { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.JwtVp, + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else if (vpFormat === 'ldp_vp') { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.LdpVp, + proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), + proofPurpose: 'authentication', + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else { + throw new DifPresentationExchangeError( + `Only JWT credentials or JSONLD credentials are supported for a single presentation` + ) + } + + return getSphereonW3cVerifiablePresentation(signedPresentation) + } + } + + private async getVerificationMethodForSubjectId(agentContext: AgentContext, subjectId: string) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + if (!subjectId.startsWith('did:')) { + throw new DifPresentationExchangeError( + `Only dids are supported as credentialSubject id. ${subjectId} is not a valid did` + ) + } + + const didDocument = await didsApi.resolveDidDocument(subjectId) + + if (!didDocument.authentication || didDocument.authentication.length === 0) { + throw new DifPresentationExchangeError( + `No authentication verificationMethods found for did ${subjectId} in did document` + ) + } + + // the signature suite to use for the presentation is dependant on the credentials we share. + // 1. Get the verification method for this given proof purpose in this DID document + let [verificationMethod] = didDocument.authentication + if (typeof verificationMethod === 'string') { + verificationMethod = didDocument.dereferenceKey(verificationMethod, ['authentication']) + } + + return verificationMethod + } +} diff --git a/packages/core/src/modules/dif-presentation-exchange/index.ts b/packages/core/src/modules/dif-presentation-exchange/index.ts new file mode 100644 index 0000000000..4f4e4b3923 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/index.ts @@ -0,0 +1,4 @@ +export * from './DifPresentationExchangeError' +export * from './DifPresentationExchangeModule' +export * from './DifPresentationExchangeService' +export * from './models' diff --git a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts new file mode 100644 index 0000000000..ec2e83d17e --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts @@ -0,0 +1,119 @@ +import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../vc' + +export interface DifPexCredentialsForRequest { + /** + * Whether all requirements have been satisfied by the credentials in the wallet. + */ + areRequirementsSatisfied: boolean + + /** + * The requirements for the presentation definition. If the `areRequirementsSatisfied` value + * is `false`, this list will still be populated with requirements, but won't contain credentials + * for all requirements. This can be useful to display the missing credentials for a presentation + * definition to be satisfied. + * + * NOTE: Presentation definition requirements can be really complex as there's a lot of different + * combinations that are possible. The structure doesn't include all possible combinations yet that + * could satisfy a presentation definition. + */ + requirements: DifPexCredentialsForRequestRequirement[] + + /** + * Name of the presentation definition + */ + name?: string + + /** + * Purpose of the presentation definition. + */ + purpose?: string +} + +/** + * A requirement for the presentation submission. A requirement + * is a group of input descriptors that together fulfill a requirement + * from the presentation definition. + * + * Each submission represents a input descriptor. + */ +export interface DifPexCredentialsForRequestRequirement { + /** + * Whether the requirement is satisfied. + * + * If the requirement is not satisfied, the submission will still contain + * entries, but the `verifiableCredentials` list will be empty. + */ + isRequirementSatisfied: boolean + + /** + * Name of the requirement + */ + name?: string + + /** + * Purpose of the requirement + */ + purpose?: string + + /** + * Array of objects, where each entry contains one or more credentials that will be part + * of the submission. + * + * NOTE: if the `isRequirementSatisfied` is `false` the submission list will + * contain entries where the verifiable credential list is empty. In this case it could also + * contain more entries than are actually needed (as you sometimes can choose from + * e.g. 4 types of credentials and need to submit at least two). If + * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value + * to see how many of those submissions needed. + */ + submissionEntry: DifPexCredentialsForRequestSubmissionEntry[] + + /** + * The number of submission entries that are needed to fulfill the requirement. + * If `isRequirementSatisfied` is `true`, the submission list will always be equal + * to the number of `needsCount`. If `isRequirementSatisfied` is `false` the list of + * submissions could be longer. + */ + needsCount: number + + /** + * The rule that is used to select the credentials for the submission. + * If the rule is `pick`, the user can select which credentials to use for the submission. + * If the rule is `all`, all credentials that satisfy the input descriptor will be used. + */ + rule: 'pick' | 'all' +} + +/** + * A submission entry that satisfies a specific input descriptor from the + * presentation definition. + */ +export interface DifPexCredentialsForRequestSubmissionEntry { + /** + * The id of the input descriptor + */ + inputDescriptorId: string + + /** + * Name of the input descriptor + */ + name?: string + + /** + * Purpose of the input descriptor + */ + purpose?: string + + /** + * The verifiable credentials that satisfy the input descriptor. + * + * If the value is an empty list, it means the input descriptor could + * not be satisfied. + */ + verifiableCredentials: W3cCredentialRecord[] +} + +/** + * Mapping of selected credentials for an input descriptor + */ +export type DifPexInputDescriptorToCredentials = Record> diff --git a/packages/core/src/modules/dif-presentation-exchange/models/index.ts b/packages/core/src/modules/dif-presentation-exchange/models/index.ts new file mode 100644 index 0000000000..01ce9d6767 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/models/index.ts @@ -0,0 +1,11 @@ +export * from './DifPexCredentialsForRequest' +import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' + +import { PresentationSubmissionLocation } from '@sphereon/pex' + +// Re-export some types from sphereon library, but under more explicit names +export type DifPresentationExchangeDefinition = PresentationDefinitionV1 | PresentationDefinitionV2 +export type DifPresentationExchangeDefinitionV1 = PresentationDefinitionV1 +export type DifPresentationExchangeDefinitionV2 = PresentationDefinitionV2 +export type DifPresentationExchangeSubmission = PresentationSubmission +export { PresentationSubmissionLocation as DifPresentationExchangeSubmissionLocation } diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts new file mode 100644 index 0000000000..1fca34b943 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -0,0 +1,314 @@ +import type { W3cCredentialRecord } from '../../vc' +import type { + DifPexCredentialsForRequest, + DifPexCredentialsForRequestRequirement, + DifPexCredentialsForRequestSubmissionEntry, +} from '../models' +import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' +import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' + +import { PEX } from '@sphereon/pex' +import { Rules } from '@sphereon/pex-models' +import { default as jp } from 'jsonpath' + +import { deepEquality } from '../../../utils' +import { DifPresentationExchangeError } from '../DifPresentationExchangeError' + +import { getSphereonOriginalVerifiableCredential } from './transform' + +export async function getCredentialsForRequest( + presentationDefinition: IPresentationDefinition, + credentialRecords: Array, + holderDIDs: Array +): Promise { + if (!presentationDefinition) { + throw new DifPresentationExchangeError('Presentation Definition is required to select credentials for submission.') + } + + const pex = new PEX() + + const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) + + // FIXME: there is a function for this in the VP library, but it is not usable atm + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { + holderDIDs, + // limitDisclosureSignatureSuites: [], + // restrictToDIDMethods, + // restrictToFormats + }) + + const selectResults = { + ...selectResultsRaw, + // Map the encoded credential to their respective w3c credential record + verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { + const credentialRecord = credentialRecords.find((record) => { + const originalVc = getSphereonOriginalVerifiableCredential(record.credential) + return deepEquality(originalVc, encoded) + }) + + if (!credentialRecord) { + throw new DifPresentationExchangeError('Unable to find credential in credential records.') + } + + return credentialRecord + }), + } + + const presentationSubmission: DifPexCredentialsForRequest = { + requirements: [], + areRequirementsSatisfied: false, + name: presentationDefinition.name, + purpose: presentationDefinition.purpose, + } + + // If there's no submission requirements, ALL input descriptors MUST be satisfied + if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { + presentationSubmission.requirements = getSubmissionRequirementsForAllInputDescriptors( + presentationDefinition.input_descriptors, + selectResults + ) + } else { + presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) + } + + // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error + // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) + // I see this more as the fault of the presentation definition, as it should have at least some requirements. + if (presentationSubmission.requirements.length === 0) { + throw new DifPresentationExchangeError( + 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' + ) + } + if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { + return presentationSubmission + } + + return { + ...presentationSubmission, + + // If all requirements are satisfied, the presentation submission is satisfied + areRequirementsSatisfied: presentationSubmission.requirements.every( + (requirement) => requirement.isRequirementSatisfied + ), + } +} + +function getSubmissionRequirements( + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + // There are submission requirements, so we need to select the input_descriptors + // based on the submission requirements + for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { + // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet + if (submissionRequirement.from_nested) { + throw new DifPresentationExchangeError( + "Presentation definition contains requirement using 'from_nested', which is not supported yet." + ) + } + + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match") + } + + if (submissionRequirement.rule === Rules.All) { + const selectedSubmission = getSubmissionRequirementRuleAll( + submissionRequirement, + presentationDefinition, + selectResults + ) + submissionRequirements.push(selectedSubmission) + } else { + const selectedSubmission = getSubmissionRequirementRulePick( + submissionRequirement, + presentationDefinition, + selectResults + ) + + submissionRequirements.push(selectedSubmission) + } + } + + // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) + // We use minimization strategy, and thus only disclose the minimum amount of information + const requirementsWithCredentials = submissionRequirements.filter((requirement) => requirement.needsCount > 0) + + return requirementsWithCredentials +} + +function getSubmissionRequirementsForAllInputDescriptors( + inputDescriptors: Array | Array, + selectResults: W3cCredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + for (const inputDescriptor of inputDescriptors) { + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + submissionRequirements.push({ + rule: Rules.Pick, + needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, + submissionEntry: [submission], + isRequirementSatisfied: submission.verifiableCredentials.length >= 1, + }) + } + + return submissionRequirements +} + +function getSubmissionRequirementRuleAll( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match.") + + const selectedSubmission: DifPexCredentialsForRequestRequirement = { + rule: Rules.All, + needsCount: 0, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + submissionEntry: [], + isRequirementSatisfied: false, + } + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + // Rule ALL, so for every input descriptor that matches in this group, we need to add it + selectedSubmission.needsCount += 1 + selectedSubmission.submissionEntry.push(submission) + } + + return { + ...selectedSubmission, + + // If all submissions have a credential, the requirement is satisfied + isRequirementSatisfied: selectedSubmission.submissionEntry.every( + (submission) => submission.verifiableCredentials.length >= 1 + ), + } +} + +function getSubmissionRequirementRulePick( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match.") + } + + const selectedSubmission: DifPexCredentialsForRequestRequirement = { + rule: Rules.Pick, + needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + // If there's no count, min, or max we assume one credential is required for submission + // however, the exact behavior is not specified in the spec + submissionEntry: [], + isRequirementSatisfied: false, + } + + const satisfiedSubmissions: Array = [] + const unsatisfiedSubmissions: Array = [] + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + if (submission.verifiableCredentials.length >= 1) { + satisfiedSubmissions.push(submission) + } else { + unsatisfiedSubmissions.push(submission) + } + + // If we have found enough credentials to satisfy the requirement, we could stop + // but the user may not want the first x that match, so we continue and return all matches + // if (satisfiedSubmissions.length === selectedSubmission.needsCount) break + } + + return { + ...selectedSubmission, + + // If there are enough satisfied submissions, the requirement is satisfied + isRequirementSatisfied: satisfiedSubmissions.length >= selectedSubmission.needsCount, + + // if the requirement is satisfied, we only need to return the satisfied submissions + // however if the requirement is not satisfied, we include all entries so the wallet could + // render which credentials are missing. + submission: + satisfiedSubmissions.length >= selectedSubmission.needsCount + ? satisfiedSubmissions + : [...satisfiedSubmissions, ...unsatisfiedSubmissions], + } +} + +function getSubmissionForInputDescriptor( + inputDescriptor: InputDescriptorV1 | InputDescriptorV2, + selectResults: W3cCredentialRecordSelectResults +): DifPexCredentialsForRequestSubmissionEntry { + // https://github.com/Sphereon-Opensource/PEX/issues/116 + // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it + const matchesForInputDescriptor = selectResults.matches?.filter( + (m) => + m.name === inputDescriptor.id || + // FIXME: this is not collision proof as the name doesn't have to be unique + m.name === inputDescriptor.name + ) + + const submissionEntry: DifPexCredentialsForRequestSubmissionEntry = { + inputDescriptorId: inputDescriptor.id, + name: inputDescriptor.name, + purpose: inputDescriptor.purpose, + verifiableCredentials: [], + } + + // return early if no matches. + if (!matchesForInputDescriptor?.length) return submissionEntry + + // FIXME: This can return multiple credentials for multiple input_descriptors, + // which I think is a bug in the PEX library + // Extract all credentials from the match + const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => + extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) + ) + + submissionEntry.verifiableCredentials = verifiableCredentials + + return submissionEntry +} + +function extractCredentialsFromMatch( + match: SubmissionRequirementMatch, + availableCredentials?: Array +) { + const verifiableCredentials: Array = [] + + for (const vcPath of match.vc_path) { + const [verifiableCredential] = jp.query({ verifiableCredential: availableCredentials }, vcPath) as [ + W3cCredentialRecord + ] + verifiableCredentials.push(verifiableCredential) + } + + return verifiableCredentials +} + +/** + * Custom SelectResults that include the W3cCredentialRecord instead of the encoded verifiable credential + */ +export type W3cCredentialRecordSelectResults = Omit & { + verifiableCredential?: Array +} diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/index.ts b/packages/core/src/modules/dif-presentation-exchange/utils/index.ts new file mode 100644 index 0000000000..aaf44fa1b6 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/index.ts @@ -0,0 +1,2 @@ +export * from './transform' +export * from './credentialSelection' diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts new file mode 100644 index 0000000000..e4d5f694c9 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts @@ -0,0 +1,78 @@ +import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' +import type { + OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, + W3CVerifiableCredential as SphereonW3cVerifiableCredential, + W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, +} from '@sphereon/ssi-types' + +import { JsonTransformer } from '../../../utils' +import { + W3cJsonLdVerifiableCredential, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiableCredential, + W3cJwtVerifiablePresentation, + ClaimFormat, +} from '../../vc' +import { DifPresentationExchangeError } from '../DifPresentationExchangeError' + +export function getSphereonOriginalVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonOriginalVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonOriginalVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new DifPresentationExchangeError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getSphereonW3cVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonW3cVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new DifPresentationExchangeError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getSphereonW3cVerifiablePresentation( + w3cVerifiablePresentation: W3cVerifiablePresentation +): SphereonW3cVerifiablePresentation { + if (w3cVerifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + return JsonTransformer.toJSON(w3cVerifiablePresentation) as SphereonW3cVerifiablePresentation + } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { + return w3cVerifiablePresentation.serializedJwt + } else { + throw new DifPresentationExchangeError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getW3cVerifiablePresentationInstance( + w3cVerifiablePresentation: SphereonW3cVerifiablePresentation +): W3cVerifiablePresentation { + if (typeof w3cVerifiablePresentation === 'string') { + return W3cJwtVerifiablePresentation.fromSerializedJwt(w3cVerifiablePresentation) + } else { + return JsonTransformer.fromJSON(w3cVerifiablePresentation, W3cJsonLdVerifiablePresentation) + } +} + +export function getW3cVerifiableCredentialInstance( + w3cVerifiableCredential: SphereonW3cVerifiableCredential +): W3cVerifiableCredential { + if (typeof w3cVerifiableCredential === 'string') { + return W3cJwtVerifiableCredential.fromSerializedJwt(w3cVerifiableCredential) + } else { + return JsonTransformer.fromJSON(w3cVerifiableCredential, W3cJsonLdVerifiableCredential) + } +} diff --git a/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts b/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts index 3d074f9d18..2ab4550b52 100644 --- a/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts +++ b/packages/core/src/modules/discover-features/DiscoverFeaturesApi.ts @@ -121,7 +121,10 @@ export class DiscoverFeaturesApi< // Return disclosures map((e) => e.payload.disclosures), // If we don't have an answer in timeoutMs miliseconds (no response, not supported, etc...) error - timeout(options.awaitDisclosuresTimeoutMs ?? 7000), // TODO: Harmonize default timeouts across the framework + timeout({ + first: options.awaitDisclosuresTimeoutMs ?? 7000, + meta: 'DiscoverFeaturesApi.queryFeatures', + }), // TODO: Harmonize default timeouts across the framework // We want to return false if an error occurred catchError(() => of([])) ) diff --git a/packages/core/src/modules/message-pickup/MessagePickupApi.ts b/packages/core/src/modules/message-pickup/MessagePickupApi.ts index f854f7e0be..b2e1950090 100644 --- a/packages/core/src/modules/message-pickup/MessagePickupApi.ts +++ b/packages/core/src/modules/message-pickup/MessagePickupApi.ts @@ -1,5 +1,6 @@ import type { - DeliverQueuedMessagesOptions, + DeliverMessagesOptions, + DeliverMessagesFromQueueOptions, PickupMessagesOptions, PickupMessagesReturnType, QueueMessageOptions, @@ -7,6 +8,7 @@ import type { SetLiveDeliveryModeOptions, SetLiveDeliveryModeReturnType, } from './MessagePickupApiOptions' +import type { MessagePickupSessionRole } from './MessagePickupSession' import type { V1MessagePickupProtocol, V2MessagePickupProtocol } from './protocol' import type { MessagePickupProtocol } from './protocol/MessagePickupProtocol' import type { MessagePickupRepository } from './storage/MessagePickupRepository' @@ -88,32 +90,79 @@ export class MessagePickupApi { connectionId: string protocolVersion: MessagePickupProtocolVersionType diff --git a/packages/core/src/modules/message-pickup/MessagePickupSession.ts b/packages/core/src/modules/message-pickup/MessagePickupSession.ts index eddca7ad4f..26cf49ff1c 100644 --- a/packages/core/src/modules/message-pickup/MessagePickupSession.ts +++ b/packages/core/src/modules/message-pickup/MessagePickupSession.ts @@ -6,6 +6,7 @@ export enum MessagePickupSessionRole { MessageHolder = 'MessageHolder', } export type MessagePickupSession = { + id: string connectionId: string protocolVersion: MessagePickupProtocolVersionType role: MessagePickupSessionRole diff --git a/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts index a6d5f52703..686cdccc90 100644 --- a/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/BaseMessagePickupProtocol.ts @@ -19,12 +19,12 @@ import type { DependencyManager } from '../../../plugins' export abstract class BaseMessagePickupProtocol implements MessagePickupProtocol { public abstract readonly version: string - public abstract pickupMessages( + public abstract createPickupMessage( agentContext: AgentContext, options: PickupMessagesProtocolOptions ): Promise> - public abstract deliverMessages( + public abstract createDeliveryMessage( agentContext: AgentContext, options: DeliverMessagesProtocolOptions ): Promise | void> diff --git a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts index d4ea5f8861..df11b80547 100644 --- a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocol.ts @@ -14,12 +14,12 @@ import type { DependencyManager } from '../../../plugins' export interface MessagePickupProtocol { readonly version: string - pickupMessages( + createPickupMessage( agentContext: AgentContext, options: PickupMessagesProtocolOptions ): Promise> - deliverMessages( + createDeliveryMessage( agentContext: AgentContext, options: DeliverMessagesProtocolOptions ): Promise | void> diff --git a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts index 37b8feac30..41391f3286 100644 --- a/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts +++ b/packages/core/src/modules/message-pickup/protocol/MessagePickupProtocolOptions.ts @@ -1,5 +1,6 @@ import type { AgentMessage } from '../../../agent/AgentMessage' import type { ConnectionRecord } from '../../connections' +import type { QueuedMessage } from '../storage' export interface PickupMessagesProtocolOptions { connectionRecord: ConnectionRecord @@ -9,6 +10,7 @@ export interface PickupMessagesProtocolOptions { export interface DeliverMessagesProtocolOptions { connectionRecord: ConnectionRecord + messages?: QueuedMessage[] recipientKey?: string batchSize?: number } diff --git a/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts index 33954964ea..0a9b5711ba 100644 --- a/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/v1/V1MessagePickupProtocol.ts @@ -44,7 +44,7 @@ export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { ) } - public async pickupMessages( + public async createPickupMessage( agentContext: AgentContext, options: PickupMessagesProtocolOptions ): Promise> { @@ -59,23 +59,25 @@ export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { return { message } } - public async deliverMessages( + public async createDeliveryMessage( agentContext: AgentContext, options: DeliverMessagesProtocolOptions ): Promise | void> { - const { connectionRecord, batchSize } = options + const { connectionRecord, batchSize, messages } = options connectionRecord.assertReady() const pickupMessageQueue = agentContext.dependencyManager.resolve( InjectionSymbols.MessagePickupRepository ) - const messages = await pickupMessageQueue.takeFromQueue({ - connectionId: connectionRecord.id, - limit: batchSize, // TODO: Define as config parameter for message holder side - }) + const messagesToDeliver = + messages ?? + (await pickupMessageQueue.takeFromQueue({ + connectionId: connectionRecord.id, + limit: batchSize, // TODO: Define as config parameter for message holder side + })) - const batchMessages = messages.map( + const batchMessages = messagesToDeliver.map( (msg) => new BatchMessageMessage({ id: msg.id, @@ -83,7 +85,7 @@ export class V1MessagePickupProtocol extends BaseMessagePickupProtocol { }) ) - if (messages.length > 0) { + if (messagesToDeliver.length > 0) { const message = new V1BatchMessage({ messages: batchMessages, }) diff --git a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts index 78936f8b8f..38a0194baa 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/V2MessagePickupProtocol.ts @@ -74,7 +74,7 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { ) } - public async pickupMessages( + public async createPickupMessage( agentContext: AgentContext, options: PickupMessagesProtocolOptions ): Promise> { @@ -88,11 +88,11 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { return { message } } - public async deliverMessages( + public async createDeliveryMessage( agentContext: AgentContext, options: DeliverMessagesProtocolOptions ): Promise | void> { - const { connectionRecord, recipientKey } = options + const { connectionRecord, recipientKey, messages } = options connectionRecord.assertReady() const messagePickupRepository = agentContext.dependencyManager.resolve( @@ -100,18 +100,20 @@ export class V2MessagePickupProtocol extends BaseMessagePickupProtocol { ) // Get available messages from queue, but don't delete them - const messages = await messagePickupRepository.takeFromQueue({ - connectionId: connectionRecord.id, - recipientKey, - limit: 10, // TODO: Define as config parameter - keepMessages: true, - }) - - if (messages.length === 0) { + const messagesToDeliver = + messages ?? + (await messagePickupRepository.takeFromQueue({ + connectionId: connectionRecord.id, + recipientKey, + limit: 10, // TODO: Define as config parameter + keepMessages: true, + })) + + if (messagesToDeliver.length === 0) { return } - const attachments = messages.map( + const attachments = messagesToDeliver.map( (msg) => new Attachment({ id: msg.id, diff --git a/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts index 424552bb00..ec8fd4b62d 100644 --- a/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts +++ b/packages/core/src/modules/message-pickup/protocol/v2/__tests__/V2MessagePickupProtocol.test.ts @@ -29,7 +29,7 @@ const mockConnection = getMockConnection({ }) // Mock classes -jest.mock('../../../storage/InMemoryMessagePickupQueue') +jest.mock('../../../storage/InMemoryMessagePickupRepository') jest.mock('../../../../../agent/EventEmitter') jest.mock('../../../../../agent/MessageSender') jest.mock('../../../../connections/services/ConnectionService') @@ -71,7 +71,7 @@ const queuedMessages = [ { id: '3', encryptedMessage }, ] -describe('V2MessagePickupService', () => { +describe('V2MessagePickupProtocol', () => { let pickupProtocol: V2MessagePickupProtocol beforeEach(async () => { @@ -299,9 +299,9 @@ describe('V2MessagePickupService', () => { }) }) - describe('pickupMessages', () => { + describe('createPickupMessage', () => { it('creates a status request message', async () => { - const { message: statusRequestMessage } = await pickupProtocol.pickupMessages(agentContext, { + const { message: statusRequestMessage } = await pickupProtocol.createPickupMessage(agentContext, { connectionRecord: mockConnection, recipientKey: 'a-key', }) diff --git a/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts b/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts index bfec964e2c..7e726c7c8a 100644 --- a/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts +++ b/packages/core/src/modules/message-pickup/services/MessagePickupSessionService.ts @@ -9,6 +9,7 @@ import { EventEmitter } from '../../../agent/EventEmitter' import { InjectionSymbols } from '../../../constants' import { injectable } from '../../../plugins' import { TransportEventTypes } from '../../../transport' +import { uuid } from '../../../utils/uuid' import { MessagePickupEventTypes } from '../MessagePickupEvents' /** @@ -42,7 +43,11 @@ export class MessagePickupSessionService { }) } - public getLiveSession( + public getLiveSession(agentContext: AgentContext, sessionId: string) { + return this.sessions.find((session) => session.id === sessionId) + } + + public getLiveSessionByConnectionId( agentContext: AgentContext, options: { connectionId: string; role?: MessagePickupSessionRole } ) { @@ -63,6 +68,7 @@ export class MessagePickupSessionService { this.removeLiveSession(agentContext, { connectionId }) const session = { + id: uuid(), connectionId, protocolVersion, role, diff --git a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts index 5346de4e2a..84a3557f85 100644 --- a/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts +++ b/packages/core/src/modules/message-pickup/storage/InMemoryMessagePickupRepository.ts @@ -15,6 +15,7 @@ import { uuid } from '../../../utils/uuid' interface InMemoryQueuedMessage extends QueuedMessage { connectionId: string recipientKeys: string[] + state: 'pending' | 'sending' } @injectable() @@ -37,17 +38,28 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository return messages.length } - public takeFromQueue(options: TakeFromQueueOptions) { + public takeFromQueue(options: TakeFromQueueOptions): QueuedMessage[] { const { connectionId, recipientKey, limit, keepMessages } = options - const messages = this.messages.filter( + let messages = this.messages.filter( (msg) => - msg.connectionId === connectionId && (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) + msg.connectionId === connectionId && + msg.state === 'pending' && + (recipientKey === undefined || msg.recipientKeys.includes(recipientKey)) ) const messagesToTake = limit ?? messages.length + + messages = messages.slice(0, messagesToTake) + this.logger.debug(`Taking ${messagesToTake} messages from queue for connection ${connectionId}`) + // Mark taken messages in order to prevent them of being retrieved again + messages.forEach((msg) => { + const index = this.messages.findIndex((item) => item.id === msg.id) + if (index !== -1) this.messages[index].state = 'sending' + }) + if (!keepMessages) { this.removeMessages({ connectionId, messageIds: messages.map((msg) => msg.id) }) } @@ -64,6 +76,7 @@ export class InMemoryMessagePickupRepository implements MessagePickupRepository connectionId, encryptedMessage: payload, recipientKeys, + state: 'pending', }) return id diff --git a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts index 108ccc6a3d..e580949c32 100644 --- a/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts +++ b/packages/core/src/modules/message-pickup/storage/MessagePickupRepositoryOptions.ts @@ -1,10 +1,8 @@ -import type { QueuedMessageState } from './QueuedMessageState' import type { EncryptedMessage } from '../../../types' export interface GetAvailableMessageCountOptions { connectionId: string recipientKey?: string - state?: QueuedMessageState } export interface TakeFromQueueOptions { diff --git a/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts b/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts index dae90e7d5f..b554e08184 100644 --- a/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts +++ b/packages/core/src/modules/message-pickup/storage/QueuedMessage.ts @@ -3,5 +3,4 @@ import type { EncryptedMessage } from '../../../types' export type QueuedMessage = { id: string encryptedMessage: EncryptedMessage - state?: string } diff --git a/packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts b/packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts deleted file mode 100644 index dc4065d85a..0000000000 --- a/packages/core/src/modules/message-pickup/storage/QueuedMessageState.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum QueuedMessageState { - Pending = 'pending', - Sending = 'sending', - Delivered = 'delivered', -} diff --git a/packages/core/src/modules/message-pickup/storage/index.ts b/packages/core/src/modules/message-pickup/storage/index.ts index 6c733de292..1894b67d72 100644 --- a/packages/core/src/modules/message-pickup/storage/index.ts +++ b/packages/core/src/modules/message-pickup/storage/index.ts @@ -2,4 +2,3 @@ export * from './InMemoryMessagePickupRepository' export * from './MessagePickupRepository' export * from './MessagePickupRepositoryOptions' export * from './QueuedMessage' -export * from './QueuedMessageState' diff --git a/packages/core/src/modules/oob/OutOfBandApi.ts b/packages/core/src/modules/oob/OutOfBandApi.ts index b6f48b97e2..1a0ae3cf4a 100644 --- a/packages/core/src/modules/oob/OutOfBandApi.ts +++ b/packages/core/src/modules/oob/OutOfBandApi.ts @@ -77,6 +77,7 @@ interface BaseReceiveOutOfBandInvitationConfig { routing?: Routing acceptInvitationTimeoutMs?: number isImplicit?: boolean + ourDid?: string } export type ReceiveOutOfBandInvitationConfig = Omit @@ -479,6 +480,7 @@ export class OutOfBandApi { reuseConnection, routing, timeoutMs: config.acceptInvitationTimeoutMs, + ourDid: config.ourDid, }) } @@ -514,12 +516,13 @@ export class OutOfBandApi { */ routing?: Routing timeoutMs?: number + ourDid?: string } ) { const outOfBandRecord = await this.outOfBandService.getById(this.agentContext, outOfBandId) const { outOfBandInvitation } = outOfBandRecord - const { label, alias, imageUrl, autoAcceptConnection, reuseConnection } = config + const { label, alias, imageUrl, autoAcceptConnection, reuseConnection, ourDid } = config const services = outOfBandInvitation.getServices() const messages = outOfBandInvitation.getRequests() const timeoutMs = config.timeoutMs ?? 20000 @@ -585,6 +588,7 @@ export class OutOfBandApi { autoAcceptConnection, protocol: handshakeProtocol, routing, + ourDid, }) } @@ -871,7 +875,10 @@ export class OutOfBandApi { ), // If the event is found, we return the value true map(() => true), - timeout(15000), + timeout({ + first: 15000, + meta: 'OutOfBandApi.handleHandshakeReuse', + }), // If timeout is reached, we return false catchError(() => of(false)) ) diff --git a/packages/core/src/modules/oob/OutOfBandService.ts b/packages/core/src/modules/oob/OutOfBandService.ts index 1884cb694a..93fbb26c9e 100644 --- a/packages/core/src/modules/oob/OutOfBandService.ts +++ b/packages/core/src/modules/oob/OutOfBandService.ts @@ -67,7 +67,6 @@ export class OutOfBandService { // initiating the flow const outOfBandInvitation = new OutOfBandInvitation({ id: did, - label: '', services: [did], handshakeProtocols, }) diff --git a/packages/core/src/modules/oob/helpers.ts b/packages/core/src/modules/oob/helpers.ts index ccd35e7a31..be2fe3f0b4 100644 --- a/packages/core/src/modules/oob/helpers.ts +++ b/packages/core/src/modules/oob/helpers.ts @@ -45,7 +45,8 @@ export function convertToOldInvitation(newInvitation: OutOfBandInvitation) { if (typeof service === 'string') { options = { id: newInvitation.id, - label: newInvitation.label, + // label is optional + label: newInvitation.label ?? '', did: service, imageUrl: newInvitation.imageUrl, appendedAttachments: newInvitation.appendedAttachments, @@ -53,7 +54,8 @@ export function convertToOldInvitation(newInvitation: OutOfBandInvitation) { } else { options = { id: newInvitation.id, - label: newInvitation.label, + // label is optional + label: newInvitation.label ?? '', recipientKeys: service.recipientKeys.map(didKeyToVerkey), routingKeys: service.routingKeys?.map(didKeyToVerkey), serviceEndpoint: service.serviceEndpoint, diff --git a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts index 80118119a0..d900284329 100644 --- a/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts +++ b/packages/core/src/modules/oob/messages/OutOfBandInvitation.ts @@ -17,7 +17,7 @@ import { OutOfBandDidCommService } from '../domain/OutOfBandDidCommService' export interface OutOfBandInvitationOptions { id?: string - label: string + label?: string goalCode?: string goal?: string accept?: string[] @@ -124,7 +124,7 @@ export class OutOfBandInvitation extends AgentMessage { public readonly type = OutOfBandInvitation.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/out-of-band/1.1/invitation') - public readonly label!: string + public readonly label?: string @Expose({ name: 'goal_code' }) public readonly goalCode?: string diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts new file mode 100644 index 0000000000..ca7e908a76 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts @@ -0,0 +1,73 @@ +import type { + DifPexInputDescriptorToCredentials, + DifPexCredentialsForRequest, + DifPresentationExchangeDefinitionV1, +} from '../../../dif-presentation-exchange' +import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' +import type { ProofFormat } from '../ProofFormat' + +export type DifPresentationExchangeProposal = DifPresentationExchangeDefinitionV1 + +export type DifPresentationExchangeRequest = { + options?: { + challenge?: string + domain?: string + } + presentation_definition: DifPresentationExchangeDefinitionV1 +} + +export type DifPresentationExchangePresentation = + | W3cJsonPresentation + // NOTE: this is not spec compliant, as it doesn't describe how to submit + // JWT VPs but to support JWT VPs we also allow the value to be a string + | string + +export interface DifPresentationExchangeProofFormat extends ProofFormat { + formatKey: 'presentationExchange' + + proofFormats: { + createProposal: { + presentationDefinition: DifPresentationExchangeDefinitionV1 + } + + acceptProposal: { + options?: { + challenge?: string + domain?: string + } + } + + createRequest: { + presentationDefinition: DifPresentationExchangeDefinitionV1 + options?: { + challenge?: string + domain?: string + } + } + + acceptRequest: { + credentials?: DifPexInputDescriptorToCredentials + } + + getCredentialsForRequest: { + input: never + // Presentation submission details which the options that are available + output: DifPexCredentialsForRequest + } + + selectCredentialsForRequest: { + input: never + // Input descriptor to credentials specifically details which credentials + // should be used for which input descriptor + output: { + credentials: DifPexInputDescriptorToCredentials + } + } + } + + formatData: { + proposal: DifPresentationExchangeProposal + request: DifPresentationExchangeRequest + presentation: DifPresentationExchangePresentation + } +} diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts new file mode 100644 index 0000000000..6e3a85438a --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -0,0 +1,367 @@ +import type { + DifPresentationExchangePresentation, + DifPresentationExchangeProofFormat, + DifPresentationExchangeProposal, + DifPresentationExchangeRequest, +} from './DifPresentationExchangeProofFormat' +import type { AgentContext } from '../../../../agent' +import type { JsonValue } from '../../../../types' +import type { DifPexInputDescriptorToCredentials } from '../../../dif-presentation-exchange' +import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' +import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' +import type { ProofFormatService } from '../ProofFormatService' +import type { + ProofFormatCreateProposalOptions, + ProofFormatCreateReturn, + ProofFormatProcessOptions, + ProofFormatAcceptProposalOptions, + FormatCreateRequestOptions, + ProofFormatAcceptRequestOptions, + ProofFormatProcessPresentationOptions, + ProofFormatGetCredentialsForRequestOptions, + ProofFormatSelectCredentialsForRequestOptions, + ProofFormatAutoRespondProposalOptions, + ProofFormatAutoRespondRequestOptions, + ProofFormatAutoRespondPresentationOptions, +} from '../ProofFormatServiceOptions' + +import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../error' +import { deepEquality, JsonTransformer } from '../../../../utils' +import { DifPresentationExchangeService } from '../../../dif-presentation-exchange' +import { + W3cCredentialService, + ClaimFormat, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiablePresentation, +} from '../../../vc' +import { ProofFormatSpec } from '../../models' + +const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0' +const PRESENTATION_EXCHANGE_PRESENTATION_REQUEST = 'dif/presentation-exchange/definitions@v1.0' +const PRESENTATION_EXCHANGE_PRESENTATION = 'dif/presentation-exchange/submission@v1.0' + +export class PresentationExchangeProofFormatService implements ProofFormatService { + public readonly formatKey = 'presentationExchange' as const + + private presentationExchangeService(agentContext: AgentContext) { + return agentContext.dependencyManager.resolve(DifPresentationExchangeService) + } + + public supportsFormat(formatIdentifier: string): boolean { + return [ + PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, + PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + PRESENTATION_EXCHANGE_PRESENTATION, + ].includes(formatIdentifier) + } + + public async createProposal( + agentContext: AgentContext, + { proofFormats, attachmentId }: ProofFormatCreateProposalOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const pexFormat = proofFormats.presentationExchange + if (!pexFormat) { + throw new AriesFrameworkError('Missing Presentation Exchange format in create proposal attachment format') + } + + const { presentationDefinition } = pexFormat + + ps.validatePresentationDefinition(presentationDefinition) + + const format = new ProofFormatSpec({ format: PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, attachmentId }) + + const attachment = this.getFormatData(presentationDefinition, format.attachmentId) + + return { format, attachment } + } + + public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const ps = this.presentationExchangeService(agentContext) + const proposal = attachment.getDataAsJson() + ps.validatePresentationDefinition(proposal) + } + + public async acceptProposal( + agentContext: AgentContext, + { + attachmentId, + proposalAttachment, + proofFormats, + }: ProofFormatAcceptProposalOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const presentationExchangeFormat = proofFormats?.presentationExchange + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + attachmentId, + }) + + const presentationDefinition = proposalAttachment.getDataAsJson() + ps.validatePresentationDefinition(presentationDefinition) + + const attachment = this.getFormatData( + { + presentation_definition: presentationDefinition, + options: { + // NOTE: we always want to include a challenge to prevent replay attacks + challenge: presentationExchangeFormat?.options?.challenge ?? (await agentContext.wallet.generateNonce()), + domain: presentationExchangeFormat?.options?.domain, + }, + } satisfies DifPresentationExchangeRequest, + format.attachmentId + ) + + return { format, attachment } + } + + public async createRequest( + agentContext: AgentContext, + { attachmentId, proofFormats }: FormatCreateRequestOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const presentationExchangeFormat = proofFormats.presentationExchange + if (!presentationExchangeFormat) { + throw Error('Missing presentation exchange format in create request attachment format') + } + + const { presentationDefinition, options } = presentationExchangeFormat + + ps.validatePresentationDefinition(presentationDefinition) + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + attachmentId, + }) + + const attachment = this.getFormatData( + { + presentation_definition: presentationDefinition, + options: { + // NOTE: we always want to include a challenge to prevent replay attacks + challenge: options?.challenge ?? (await agentContext.wallet.generateNonce()), + domain: options?.domain, + }, + } satisfies DifPresentationExchangeRequest, + format.attachmentId + ) + + return { attachment, format } + } + + public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const ps = this.presentationExchangeService(agentContext) + const { presentation_definition: presentationDefinition } = + attachment.getDataAsJson() + ps.validatePresentationDefinition(presentationDefinition) + } + + public async acceptRequest( + agentContext: AgentContext, + { + attachmentId, + requestAttachment, + proofFormats, + }: ProofFormatAcceptRequestOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION, + attachmentId, + }) + + const { presentation_definition: presentationDefinition, options } = + requestAttachment.getDataAsJson() + + const credentials: DifPexInputDescriptorToCredentials = proofFormats?.presentationExchange?.credentials ?? {} + if (Object.keys(credentials).length === 0) { + const { areRequirementsSatisfied, requirements } = await ps.getCredentialsForRequest( + agentContext, + presentationDefinition + ) + + if (!areRequirementsSatisfied) { + throw new AriesFrameworkError('Requirements of the presentation definition could not be satisfied') + } + + requirements.forEach((r) => { + r.submissionEntry.forEach((r) => { + credentials[r.inputDescriptorId] = r.verifiableCredentials.map((c) => c.credential) + }) + }) + } + + const presentation = await ps.createPresentation(agentContext, { + presentationDefinition, + credentialsForInputDescriptor: credentials, + challenge: options?.challenge, + domain: options?.domain, + }) + + if (presentation.verifiablePresentations.length > 1) { + throw new AriesFrameworkError('Invalid amount of verifiable presentations. Only one is allowed.') + } + + const firstPresentation = presentation.verifiablePresentations[0] + const attachmentData = firstPresentation.encoded as DifPresentationExchangePresentation + const attachment = this.getFormatData(attachmentData, format.attachmentId) + + return { attachment, format } + } + + public async processPresentation( + agentContext: AgentContext, + { requestAttachment, attachment }: ProofFormatProcessPresentationOptions + ): Promise { + const ps = this.presentationExchangeService(agentContext) + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + const request = requestAttachment.getDataAsJson() + const presentation = attachment.getDataAsJson() + let parsedPresentation: W3cVerifiablePresentation + let jsonPresentation: W3cJsonPresentation + + // TODO: we should probably move this transformation logic into the VC module, so it + // can be reused in AFJ when we need to go from encoded -> parsed + if (typeof presentation === 'string') { + parsedPresentation = W3cJwtVerifiablePresentation.fromSerializedJwt(presentation) + jsonPresentation = parsedPresentation.presentation.toJSON() + } else { + parsedPresentation = JsonTransformer.fromJSON(presentation, W3cJsonLdVerifiablePresentation) + jsonPresentation = parsedPresentation.toJSON() + } + + if (!jsonPresentation.presentation_submission) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format without presentation submission. This should not happen.' + ) + return false + } + + if (!request.options?.challenge) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format without challenge. This should not happen.' + ) + return false + } + + try { + ps.validatePresentationDefinition(request.presentation_definition) + ps.validatePresentationSubmission(jsonPresentation.presentation_submission) + ps.validatePresentation(request.presentation_definition, parsedPresentation) + + let verificationResult: W3cVerifyPresentationResult + + // FIXME: for some reason it won't accept the input if it doesn't know + // whether it's a JWT or JSON-LD VP even though the input is the same. + // Not sure how to fix + if (parsedPresentation.claimFormat === ClaimFormat.JwtVp) { + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) + } else { + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) + } + + if (!verificationResult.isValid) { + agentContext.config.logger.error( + `Received presentation in PEX proof format that could not be verified: ${verificationResult.error}`, + { verificationResult } + ) + return false + } + + return true + } catch (e) { + agentContext.config.logger.error(`Failed to verify presentation in PEX proof format service: ${e.message}`, { + cause: e, + }) + return false + } + } + + public async getCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment }: ProofFormatGetCredentialsForRequestOptions + ) { + const ps = this.presentationExchangeService(agentContext) + const { presentation_definition: presentationDefinition } = + requestAttachment.getDataAsJson() + + ps.validatePresentationDefinition(presentationDefinition) + + const presentationSubmission = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + return presentationSubmission + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + { requestAttachment }: ProofFormatSelectCredentialsForRequestOptions + ) { + const ps = this.presentationExchangeService(agentContext) + const { presentation_definition: presentationDefinition } = + requestAttachment.getDataAsJson() + + const credentialsForRequest = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + return { credentials: ps.selectCredentialsForRequest(credentialsForRequest) } + } + + public async shouldAutoRespondToProposal( + _agentContext: AgentContext, + { requestAttachment, proposalAttachment }: ProofFormatAutoRespondProposalOptions + ): Promise { + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() + + return deepEquality(requestData.presentation_definition, proposalData) + } + + public async shouldAutoRespondToRequest( + _agentContext: AgentContext, + { requestAttachment, proposalAttachment }: ProofFormatAutoRespondRequestOptions + ): Promise { + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() + + return deepEquality(requestData.presentation_definition, proposalData) + } + + /** + * + * The presentation is already verified in processPresentation, so we can just return true here. + * It's only an ack, so it's just that we received the presentation. + * + */ + public async shouldAutoRespondToPresentation( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _options: ProofFormatAutoRespondPresentationOptions + ): Promise { + return true + } + + private getFormatData(data: unknown, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + json: data as JsonValue, + }), + }) + + return attachment + } +} diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts new file mode 100644 index 0000000000..316927aaf8 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -0,0 +1,204 @@ +import type { DifPresentationExchangeDefinitionV1 } from '../../../../dif-presentation-exchange' +import type { ProofFormatService } from '../../ProofFormatService' +import type { DifPresentationExchangeProofFormat } from '../DifPresentationExchangeProofFormat' + +import { PresentationSubmissionLocation } from '@sphereon/pex' + +import { getIndySdkModules } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' +import { agentDependencies, getAgentConfig } from '../../../../../../tests' +import { Agent } from '../../../../../agent/Agent' +import { DifPresentationExchangeModule, DifPresentationExchangeService } from '../../../../dif-presentation-exchange' +import { + W3cJsonLdVerifiableCredential, + W3cCredentialRecord, + W3cCredentialRepository, + CREDENTIALS_CONTEXT_V1_URL, + W3cJsonLdVerifiablePresentation, +} from '../../../../vc' +import { ProofsModule } from '../../../ProofsModule' +import { ProofState } from '../../../models' +import { V2ProofProtocol } from '../../../protocol' +import { ProofExchangeRecord } from '../../../repository' +import { PresentationExchangeProofFormatService } from '../DifPresentationExchangeProofFormatService' + +const mockProofRecord = () => + new ProofExchangeRecord({ + state: ProofState.ProposalSent, + threadId: 'add7e1a0-109e-4f37-9caa-cfd0fcdfe540', + protocolVersion: 'v2', + }) + +const mockPresentationDefinition = (): DifPresentationExchangeDefinitionV1 => ({ + id: '32f54163-7166-48f1-93d8-ff217bdb0653', + input_descriptors: [ + { + schema: [{ uri: 'https://www.w3.org/2018/credentials/examples/v1' }], + id: 'wa_driver_license', + name: 'Washington State Business License', + purpose: 'We can only allow licensed Washington State business representatives into the WA Business Conference', + constraints: { + fields: [ + { + path: ['$.credentialSubject.id'], + }, + ], + }, + }, + ], +}) + +const mockCredentialRecord = new W3cCredentialRecord({ + tags: {}, + credential: new W3cJsonLdVerifiableCredential({ + id: 'did:some:id', + context: [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }, + proof: { + type: 'Ed25519Signature2020', + created: '2021-11-13T18:19:39Z', + verificationMethod: 'https://example.edu/issuers/14#key-1', + proofPurpose: 'assertionMethod', + proofValue: 'z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz', + }, + }), +}) + +const presentationSubmission = { id: 'did:id', definition_id: 'my-id', descriptor_map: [] } +jest.spyOn(W3cCredentialRepository.prototype, 'findByQuery').mockResolvedValue([mockCredentialRecord]) +jest.spyOn(DifPresentationExchangeService.prototype, 'createPresentation').mockResolvedValue({ + presentationSubmission, + verifiablePresentations: [ + new W3cJsonLdVerifiablePresentation({ + verifiableCredential: [mockCredentialRecord.credential], + proof: { + type: 'Ed25519Signature2020', + created: '2021-11-13T18:19:39Z', + verificationMethod: 'https://example.edu/issuers/14#key-1', + proofPurpose: 'assertionMethod', + proofValue: 'z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz', + }, + }), + ], + presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION, +}) + +describe('Presentation Exchange ProofFormatService', () => { + let pexFormatService: ProofFormatService + let agent: Agent + + beforeAll(async () => { + agent = new Agent({ + config: getAgentConfig('PresentationExchangeProofFormatService'), + modules: { + someModule: new DifPresentationExchangeModule(), + proofs: new ProofsModule({ + proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], + }), + ...getIndySdkModules(), + }, + dependencies: agentDependencies, + }) + + await agent.initialize() + + pexFormatService = agent.dependencyManager.resolve(PresentationExchangeProofFormatService) + }) + + describe('Create Presentation Exchange Proof Proposal / Request', () => { + test('Creates Presentation Exchange Proposal', async () => { + const presentationDefinition = mockPresentationDefinition() + const { format, attachment } = await pexFormatService.createProposal(agent.context, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition } }, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: presentationDefinition, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }) + }) + + test('Creates Presentation Exchange Request', async () => { + const presentationDefinition = mockPresentationDefinition() + const { format, attachment } = await pexFormatService.createRequest(agent.context, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition } }, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + options: { + challenge: expect.any(String), + }, + presentation_definition: presentationDefinition, + }, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }) + }) + }) + + describe('Accept Proof Request', () => { + test('Accept a Presentation Exchange Proof Request', async () => { + const presentationDefinition = mockPresentationDefinition() + const { attachment: requestAttachment } = await pexFormatService.createRequest(agent.context, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition } }, + }) + + const { attachment, format } = await pexFormatService.acceptRequest(agent.context, { + proofRecord: mockProofRecord(), + requestAttachment, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + '@context': expect.any(Array), + type: expect.any(Array), + verifiableCredential: [ + { + '@context': expect.any(Array), + id: expect.any(String), + type: expect.any(Array), + issuer: expect.any(String), + issuanceDate: expect.any(String), + credentialSubject: { + id: expect.any(String), + }, + proof: expect.any(Object), + }, + ], + }, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/submission@v1.0', + }) + }) + }) +}) diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts new file mode 100644 index 0000000000..b8a8c35e4e --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts @@ -0,0 +1,2 @@ +export * from './DifPresentationExchangeProofFormat' +export * from './DifPresentationExchangeProofFormatService' diff --git a/packages/core/src/modules/proofs/formats/index.ts b/packages/core/src/modules/proofs/formats/index.ts index a28e77d623..a2cc952c57 100644 --- a/packages/core/src/modules/proofs/formats/index.ts +++ b/packages/core/src/modules/proofs/formats/index.ts @@ -2,6 +2,8 @@ export * from './ProofFormat' export * from './ProofFormatService' export * from './ProofFormatServiceOptions' +export * from './dif-presentation-exchange' + import * as ProofFormatServiceOptions from './ProofFormatServiceOptions' export { ProofFormatServiceOptions } diff --git a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts index ffec69b8a3..3ce7e2c9d8 100644 --- a/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts +++ b/packages/core/src/modules/proofs/protocol/v2/V2ProofProtocol.ts @@ -42,7 +42,7 @@ import { ProofsModuleConfig } from '../../ProofsModuleConfig' import { PresentationProblemReportReason } from '../../errors/PresentationProblemReportReason' import { AutoAcceptProof, ProofState } from '../../models' import { ProofExchangeRecord, ProofRepository } from '../../repository' -import { composeAutoAccept } from '../../utils/composeAutoAccept' +import { composeAutoAccept } from '../../utils' import { BaseProofProtocol } from '../BaseProofProtocol' import { ProofFormatCoordinator } from './ProofFormatCoordinator' diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts new file mode 100644 index 0000000000..0b3d8c39b9 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts @@ -0,0 +1,13 @@ +import type { InputDescriptorV1 } from '@sphereon/pex-models' + +export const TEST_INPUT_DESCRIPTORS_CITIZENSHIP = { + constraints: { + fields: [ + { + path: ['$.credentialSubject.degree.type'], + }, + ], + }, + id: 'citizenship_input_1', + schema: [{ uri: 'https://www.w3.org/2018/credentials/examples/v1' }], +} satisfies InputDescriptorV1 diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts new file mode 100644 index 0000000000..a0901029a6 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts @@ -0,0 +1,451 @@ +import type { getJsonLdModules } from '../../../../../../tests' +import type { Agent } from '../../../../../agent/Agent' + +import { waitForCredentialRecord, setupJsonLdTests, waitForProofExchangeRecord } from '../../../../../../tests' +import testLogger from '../../../../../../tests/logger' +import { KeyType } from '../../../../../crypto' +import { DidCommMessageRepository } from '../../../../../storage' +import { TypedArrayEncoder } from '../../../../../utils' +import { AutoAcceptCredential, CredentialState } from '../../../../credentials' +import { CREDENTIALS_CONTEXT_V1_URL } from '../../../../vc' +import { ProofState } from '../../../models/ProofState' +import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' +import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' + +import { TEST_INPUT_DESCRIPTORS_CITIZENSHIP } from './fixtures' + +const jsonld = { + credential: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, +} + +describe('Present Proof', () => { + let proverAgent: Agent> + let issuerAgent: Agent> + let verifierAgent: Agent> + + let issuerProverConnectionId: string + let proverVerifierConnectionId: string + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + holderAgent: proverAgent, + issuerAgent, + verifierAgent, + issuerHolderConnectionId: issuerProverConnectionId, + holderVerifierConnectionId: proverVerifierConnectionId, + } = await setupJsonLdTests({ + holderName: 'presentation exchange prover agent', + issuerName: 'presentation exchange issuer agent', + verifierName: 'presentation exchange verifier agent', + createConnections: true, + autoAcceptCredentials: AutoAcceptCredential.Always, + })) + + await issuerAgent.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + + await proverAgent.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + + await issuerAgent.credentials.offerCredential({ + connectionId: issuerProverConnectionId, + protocolVersion: 'v2', + credentialFormats: { jsonld }, + }) + + await waitForCredentialRecord(proverAgent, { state: CredentialState.Done }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await proverAgent.shutdown() + await proverAgent.wallet.delete() + await verifierAgent.shutdown() + await verifierAgent.wallet.delete() + }) + + test(`Prover Creates and sends Proof Proposal to a Verifier`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + testLogger.test('Verifier waits for presentation from the Prover') + const verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const didCommMessageRepository = + proverAgent.dependencyManager.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage(verifierAgent.context, { + associatedRecordId: verifierProofExchangeRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + input_descriptors: expect.any(Array), + }, + }, + }, + ], + id: expect.any(String), + comment: 'V2 Presentation Exchange propose proof test', + }) + expect(verifierProofExchangeRecord.id).not.toBeNull() + expect(verifierProofExchangeRecord).toMatchObject({ + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + }) + + test(`Verifier accepts the Proposal send by the Prover`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + let proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + let verifierProofExchangeRecord = await verifierPresentationRecordPromise + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + const didCommMessageRepository = + proverAgent.dependencyManager.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage(proverAgent.context, { + associatedRecordId: proverProofExchangeRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + id: expect.any(String), + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + presentation_definition: { + id: expect.any(String), + input_descriptors: [ + { + id: TEST_INPUT_DESCRIPTORS_CITIZENSHIP.id, + constraints: { + fields: TEST_INPUT_DESCRIPTORS_CITIZENSHIP.constraints.fields, + }, + }, + ], + }, + }, + }, + }, + ], + }) + + expect(proverProofExchangeRecord).toMatchObject({ + id: expect.any(String), + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + }) + + test(`Prover accepts presentation request from the Verifier`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + let proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + const verifierProposalReceivedPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + let verifierProofExchangeRecord = await verifierProposalReceivedPresentationRecordPromise + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + // Prover retrieves the requested credentials and accepts the presentation request + testLogger.test('Prover accepts presentation request from Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await proverAgent.proofs.acceptRequest({ + proofRecordId: proverProofExchangeRecord.id, + }) + + // Verifier waits for the presentation from the Prover + testLogger.test('Verifier waits for presentation from the Prover') + verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const didCommMessageRepository = + verifierAgent.dependencyManager.resolve(DidCommMessageRepository) + + const presentation = await didCommMessageRepository.findAgentMessage(verifierAgent.context, { + associatedRecordId: verifierProofExchangeRecord.id, + messageClass: V2PresentationMessage, + }) + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/submission@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + '@context': expect.any(Array), + type: expect.any(Array), + presentation_submission: { + id: expect.any(String), + definition_id: expect.any(String), + descriptor_map: [ + { + id: 'citizenship_input_1', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + ], + }, + verifiableCredential: [ + { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://www.w3.org/2018/credentials/examples/v1', + ], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: expect.any(String), + issuanceDate: expect.any(String), + credentialSubject: { + id: expect.any(String), + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: expect.any(String), + type: 'Ed25519Signature2018', + created: expect.any(String), + proofPurpose: 'assertionMethod', + jws: expect.any(String), + }, + }, + ], + proof: { + verificationMethod: expect.any(String), + type: 'Ed25519Signature2018', + created: expect.any(String), + proofPurpose: 'authentication', + challenge: expect.any(String), + jws: expect.any(String), + }, + }, + }, + }, + ], + id: expect.any(String), + thread: { + threadId: verifierProofExchangeRecord.threadId, + }, + }) + + expect(verifierProofExchangeRecord.id).not.toBeNull() + expect(verifierProofExchangeRecord).toMatchObject({ + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + }) + + test(`Verifier accepts the presentation provided by the Prover`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + let proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + const verifierProposalReceivedPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + let verifierProofExchangeRecord = await verifierProposalReceivedPresentationRecordPromise + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + // Prover retrieves the requested credentials and accepts the presentation request + testLogger.test('Prover accepts presentation request from Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + await proverAgent.proofs.acceptRequest({ + proofRecordId: proverProofExchangeRecord.id, + }) + + // Verifier waits for the presentation from the Prover + testLogger.test('Verifier waits for presentation from the Prover') + verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const proverProofExchangeRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Verifier accepts the presentation provided by by the Prover + testLogger.test('Verifier accepts the presentation provided by the Prover') + await verifierAgent.proofs.acceptPresentation({ proofRecordId: verifierProofExchangeRecord.id }) + + // Prover waits until she received a presentation acknowledgement + testLogger.test('Prover waits until she receives a presentation acknowledgement') + proverProofExchangeRecord = await proverProofExchangeRecordPromise + + expect(verifierProofExchangeRecord).toMatchObject({ + id: expect.any(String), + createdAt: expect.any(Date), + threadId: proverProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(proverProofExchangeRecord).toMatchObject({ + id: expect.any(String), + createdAt: expect.any(Date), + threadId: verifierProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/utils/index.ts b/packages/core/src/modules/proofs/utils/index.ts new file mode 100644 index 0000000000..e9685c62fe --- /dev/null +++ b/packages/core/src/modules/proofs/utils/index.ts @@ -0,0 +1 @@ +export * from './composeAutoAccept' diff --git a/packages/core/src/modules/routing/MediationRecipientApi.ts b/packages/core/src/modules/routing/MediationRecipientApi.ts index c86d917638..be8e5d137d 100644 --- a/packages/core/src/modules/routing/MediationRecipientApi.ts +++ b/packages/core/src/modules/routing/MediationRecipientApi.ts @@ -418,7 +418,10 @@ export class MediationRecipientApi { // Only wait for first event that matches the criteria first(), // Do not wait for longer than specified timeout - timeout(timeoutMs) + timeout({ + first: timeoutMs, + meta: 'MediationRecipientApi.requestAndAwaitGrant', + }) ) .subscribe(subject) diff --git a/packages/core/src/modules/routing/services/MediationRecipientService.ts b/packages/core/src/modules/routing/services/MediationRecipientService.ts index c61e50bd54..dc464289b8 100644 --- a/packages/core/src/modules/routing/services/MediationRecipientService.ts +++ b/packages/core/src/modules/routing/services/MediationRecipientService.ts @@ -186,7 +186,10 @@ export class MediationRecipientService { // Only wait for first event that matches the criteria first(), // Do not wait for longer than specified timeout - timeout(timeoutMs) + timeout({ + first: timeoutMs, + meta: 'MediationRecipientService.keylistUpdateAndAwait', + }) ) .subscribe(subject) diff --git a/packages/core/src/modules/routing/services/MediatorService.ts b/packages/core/src/modules/routing/services/MediatorService.ts index d54b3720c3..5645df7ba2 100644 --- a/packages/core/src/modules/routing/services/MediatorService.ts +++ b/packages/core/src/modules/routing/services/MediatorService.ts @@ -15,7 +15,8 @@ import { injectable, inject } from '../../../plugins' import { ConnectionService } from '../../connections' import { ConnectionMetadataKeys } from '../../connections/repository/ConnectionMetadataTypes' import { didKeyToVerkey, isDidKey, verkeyToDidKey } from '../../dids/helpers' -import { MessagePickupApi } from '../../message-pìckup' +import { MessagePickupApi } from '../../message-pickup' +import { MessagePickupSessionRole } from '../../message-pickup/MessagePickupSession' import { MediatorModuleConfig } from '../MediatorModuleConfig' import { MessageForwardingStrategy } from '../MessageForwardingStrategy' import { RoutingEventTypes } from '../RoutingEvents' @@ -99,17 +100,24 @@ export class MediatorService { message: message.message, }) break - case MessageForwardingStrategy.QueueAndDeliver: + case MessageForwardingStrategy.QueueAndDeliver: { await this.messagePickupApi.queueMessage({ connectionId: mediationRecord.connectionId, recipientKeys: [verkeyToDidKey(message.to)], message: message.message, }) - await this.messagePickupApi.deliverQueuedMessages({ + const session = await this.messagePickupApi.getLiveModeSession({ connectionId: mediationRecord.connectionId, - recipientKey: verkeyToDidKey(message.to), + role: MessagePickupSessionRole.MessageHolder, }) + if (session) { + await this.messagePickupApi.deliverMessagesFromQueue({ + pickupSessionId: session.id, + recipientKey: verkeyToDidKey(message.to), + }) + } break + } case MessageForwardingStrategy.DeliverOnly: // The message inside the forward message is packed so we just send the packed // message to the connection associated with it diff --git a/packages/core/src/modules/routing/services/helpers.ts b/packages/core/src/modules/routing/services/helpers.ts new file mode 100644 index 0000000000..0a3bb4fe42 --- /dev/null +++ b/packages/core/src/modules/routing/services/helpers.ts @@ -0,0 +1,13 @@ +import type { AgentContext } from '../../../agent' +import type { DidDocument } from '../../dids' + +import { MediationRecipientService } from './MediationRecipientService' + +export async function getMediationRecordForDidDocument(agentContext: AgentContext, didDocument: DidDocument) { + const [mediatorRecord] = await agentContext.dependencyManager + .resolve(MediationRecipientService) + .findAllMediatorsByQuery(agentContext, { + recipientKeys: didDocument.recipientKeys.map((key) => key.publicKeyBase58), + }) + return mediatorRecord +} diff --git a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts index 841271fa39..f2777463dc 100644 --- a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts +++ b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts @@ -9,6 +9,7 @@ import type { W3cJsonLdVerifyPresentationOptions, } from '../W3cCredentialServiceOptions' import type { W3cVerifyCredentialResult, W3cVerifyPresentationResult } from '../models' +import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' import { createWalletKeyPairClass } from '../../../crypto/WalletKeyPair' import { AriesFrameworkError } from '../../../error' @@ -108,8 +109,9 @@ export class W3cJsonLdCredentialService { credential: JsonTransformer.toJSON(options.credential), suite: suites, documentLoader: this.w3cCredentialsModuleConfig.documentLoader(agentContext), - checkStatus: () => { - if (verifyCredentialStatus) { + checkStatus: ({ credential }: { credential: W3cJsonCredential }) => { + // Only throw error if credentialStatus is present + if (verifyCredentialStatus && 'credentialStatus' in credential) { throw new AriesFrameworkError( 'Verifying credential status for JSON-LD credentials is currently not supported' ) @@ -129,6 +131,12 @@ export class W3cJsonLdCredentialService { const { verified: isValid, ...remainingResult } = result + if (!isValid) { + agentContext.config.logger.debug(`Credential verification failed: ${result.error?.message}`, { + stack: result.error?.stack, + }) + } + // We map the result to our own result type to make it easier to work with // however, for now we just add a single vcJs validation result as we don't // have access to the internal validation results of vc-js diff --git a/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts index 80f4e42526..0e1a416dd2 100644 --- a/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts +++ b/packages/core/src/modules/vc/data-integrity/__tests__/W3cJsonLdCredentialService.test.ts @@ -156,6 +156,27 @@ describe('W3cJsonLdCredentialsService', () => { vcJs: { isValid: true, results: expect.any(Array), + log: [ + { + id: 'expiration', + valid: true, + }, + { + id: 'valid_signature', + valid: true, + }, + { + id: 'issuer_did_resolves', + valid: true, + }, + { + id: 'revocation_status', + valid: true, + }, + ], + statusResult: { + verified: true, + }, }, }, }) diff --git a/packages/core/src/modules/vc/data-integrity/deriveProof.ts b/packages/core/src/modules/vc/data-integrity/deriveProof.ts index a98bf1a064..fe89595115 100644 --- a/packages/core/src/modules/vc/data-integrity/deriveProof.ts +++ b/packages/core/src/modules/vc/data-integrity/deriveProof.ts @@ -38,7 +38,7 @@ export interface W3cJsonLdDeriveProofOptions { export const deriveProof = async ( proofDocument: JsonObject, revealDocument: JsonObject, - { suite, skipProofCompaction, documentLoader, expansionMap, nonce }: any + { suite, skipProofCompaction, documentLoader, nonce }: any ): Promise => { if (!suite) { throw new TypeError('"options.suite" is required.') @@ -52,7 +52,6 @@ export const deriveProof = async ( document: proofDocument, proofType: suite.supportedDeriveProofType, documentLoader, - expansionMap, }) if (proofs.length === 0) { @@ -64,7 +63,7 @@ export const deriveProof = async ( proof: proofs[0], revealDocument, documentLoader, - expansionMap, + nonce, }) @@ -82,7 +81,6 @@ export const deriveProof = async ( proof, revealDocument, documentLoader, - expansionMap, }) derivedProof.proof.push(additionalDerivedProofValue.proof) } @@ -99,7 +97,6 @@ export const deriveProof = async ( // account for type-scoped `proof` definition by getting document types const { types, alias } = await getTypeInfo(derivedProof.document, { documentLoader, - expansionMap, }) expandedProof['@type'] = types @@ -108,7 +105,6 @@ export const deriveProof = async ( const compactProof = await jsonld.compact(expandedProof, ctx, { documentLoader, - expansionMap, compactToRelative: false, }) diff --git a/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts b/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts index 7fbd3a753c..6975e92c99 100644 --- a/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts +++ b/packages/core/src/modules/vc/data-integrity/jsonldUtil.ts @@ -67,7 +67,7 @@ const PROOF_PROPERTY = 'proof' * @returns {GetProofsResult} An object containing the matched proofs and the JSON-LD document */ export const getProofs = async (options: GetProofsOptions): Promise => { - const { proofType, skipProofCompaction, documentLoader, expansionMap } = options + const { proofType, skipProofCompaction, documentLoader } = options let { document } = options let proofs @@ -76,7 +76,6 @@ export const getProofs = async (options: GetProofsOptions): Promise => { - const { documentLoader, expansionMap } = options + const { documentLoader } = options // determine `@type` alias, if any // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -124,7 +123,6 @@ export const getTypeInfo = async ( const compacted = await jsonld.compact({ '@type': '_:b0' }, context, { documentLoader, - expansionMap, }) delete compacted['@context'] @@ -139,7 +137,7 @@ export const getTypeInfo = async ( // @ts-ignore - needed because getValues is not part of the public API. toExpand['@type'] = jsonld.getValues(document, '@type').concat(jsonld.getValues(document, alias)) - const expanded = (await jsonld.expand(toExpand, { documentLoader, expansionMap }))[0] || {} + const expanded = (await jsonld.expand(toExpand, { documentLoader }))[0] || {} // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - needed because getValues is not part of the public API. diff --git a/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts b/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts index 0ed1214404..76e9dfccb5 100644 --- a/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts +++ b/packages/core/src/modules/vc/data-integrity/models/GetProofsOptions.ts @@ -30,10 +30,6 @@ export interface GetProofsOptions { * Optional custom document loader */ documentLoader?(): DocumentLoader - /** - * Optional expansion map - */ - expansionMap?(): () => void /** * Optional property to indicate whether to skip compacting the resulting proof */ diff --git a/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts b/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts index f39854f02b..da40540e38 100644 --- a/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts +++ b/packages/core/src/modules/vc/data-integrity/models/GetTypeOptions.ts @@ -21,8 +21,4 @@ export interface GetTypeOptions { * Optional custom document loader */ documentLoader?: DocumentLoader - /** - * Optional expansion map - */ - expansionMap?: () => void } diff --git a/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts b/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts index 950f5b3790..3f79fe92b7 100644 --- a/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts +++ b/packages/core/src/modules/vc/data-integrity/proof-purposes/CredentialIssuancePurpose.ts @@ -40,7 +40,6 @@ export class CredentialIssuancePurpose extends AssertionProofPurpose { * @param {string} options.verificationMethod - Key id URL to the paired * public key. * @param {object} [options.documentLoader] - A document loader. - * @param {object} [options.expansionMap] - An expansion map. * * @throws {Error} If verification method not authorized by controller. * @throws {Error} If proof's created timestamp is out of range. @@ -54,7 +53,6 @@ export class CredentialIssuancePurpose extends AssertionProofPurpose { suite: typeof LinkedDataProof verificationMethod: string documentLoader?: DocumentLoader - expansionMap?: () => void } // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise<{ valid: boolean; error?: any }> { diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts index cf1f25ad06..db012f8555 100644 --- a/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/JwsLinkedDataSignature.ts @@ -208,11 +208,6 @@ export class JwsLinkedDataSignature extends LinkedDataSignature { * recommended to use one that provides static known documents, instead of * fetching from the web) for returning contexts, controller documents, * keys, and other relevant URLs needed for the proof. - * @param [options.expansionMap] - A custom expansion map that is - * passed to the JSON-LD processor; by default a function that will throw - * an error when unmapped properties are detected in the input, use `false` - * to turn this off and allow unmapped properties to be dropped or use a - * custom function. * * @returns Whether a match for the proof was found. */ @@ -222,14 +217,12 @@ export class JwsLinkedDataSignature extends LinkedDataSignature { // eslint-disable-next-line @typescript-eslint/no-explicit-any purpose: any documentLoader?: DocumentLoader - expansionMap?: () => void }) { const proofMatches = await super.matchProof({ proof: options.proof, document: options.document, purpose: options.purpose, documentLoader: options.documentLoader, - expansionMap: options.expansionMap, }) if (!proofMatches) { return false diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts index 329b71c47c..3feed55441 100644 --- a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2018.ts @@ -155,11 +155,6 @@ export class Ed25519Signature2018 extends JwsLinkedDataSignature { * recommended to use one that provides static known documents, instead of * fetching from the web) for returning contexts, controller documents, * keys, and other relevant URLs needed for the proof. - * @param {Function} [options.expansionMap] - A custom expansion map that is - * passed to the JSON-LD processor; by default a function that will throw - * an error when unmapped properties are detected in the input, use `false` - * to turn this off and allow unmapped properties to be dropped or use a - * custom function. * * @returns {Promise} Whether a match for the proof was found. */ @@ -169,7 +164,6 @@ export class Ed25519Signature2018 extends JwsLinkedDataSignature { // eslint-disable-next-line @typescript-eslint/no-explicit-any purpose: any documentLoader?: DocumentLoader - expansionMap?: () => void }) { if (!_includesCompatibleContext({ document: options.document })) { return false @@ -179,7 +173,6 @@ export class Ed25519Signature2018 extends JwsLinkedDataSignature { document: options.document, purpose: options.purpose, documentLoader: options.documentLoader, - expansionMap: options.expansionMap, }) } } diff --git a/packages/core/src/modules/vc/index.ts b/packages/core/src/modules/vc/index.ts index 47571cdecb..84a0da17c7 100644 --- a/packages/core/src/modules/vc/index.ts +++ b/packages/core/src/modules/vc/index.ts @@ -1,6 +1,6 @@ export * from './W3cCredentialService' export * from './W3cCredentialServiceOptions' -export * from './repository/W3cCredentialRecord' +export * from './repository' export * from './W3cCredentialsModule' export * from './W3cCredentialsApi' export * from './models' diff --git a/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts index 5625627ef8..a47b3e90dc 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts @@ -1,4 +1,5 @@ import type { JsonObject } from '../../../../types' +import type { DifPresentationExchangeSubmission } from '../../../dif-presentation-exchange' import type { W3cJsonCredential } from '../credential/W3cJsonCredential' export interface W3cJsonPresentation { @@ -7,5 +8,6 @@ export interface W3cJsonPresentation { type: Array holder: string | { id?: string } verifiableCredential: Array + presentation_submission?: DifPresentationExchangeSubmission [key: string]: unknown } diff --git a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts index 299d0fbbc4..cf37fc434b 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts @@ -1,4 +1,5 @@ import type { W3cHolderOptions } from './W3cHolder' +import type { W3cJsonPresentation } from './W3cJsonPresentation' import type { JsonObject } from '../../../../types' import type { W3cVerifiableCredential } from '../credential/W3cVerifiableCredential' import type { ValidationOptions } from 'class-validator' @@ -6,6 +7,7 @@ import type { ValidationOptions } from 'class-validator' import { Expose } from 'class-transformer' import { ValidateNested, buildMessage, IsOptional, ValidateBy } from 'class-validator' +import { JsonTransformer } from '../../../../utils' import { SingleOrArray } from '../../../../utils/type' import { IsUri, IsInstanceOrArrayOfInstances } from '../../../../utils/validators' import { CREDENTIALS_CONTEXT_V1_URL, VERIFIABLE_PRESENTATION_TYPE } from '../../constants' @@ -64,6 +66,10 @@ export class W3cPresentation { return this.holder instanceof W3cHolder ? this.holder.id : this.holder } + + public toJSON() { + return JsonTransformer.toJSON(this) as W3cJsonPresentation + } } // Custom validators 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 83449887ab..c06c6515c6 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 @@ -1256,6 +1256,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "EZdqDkqBSfiepgMZ15jsZvtxTwjiz5diBtjJBnvvMvQF", "mediatorId": undefined, "outOfBandId": "3-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "request-received", "theirDid": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", @@ -1273,6 +1275,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3QxdHNwMTVjbkREN3dCQ0ZnZWhpUjJTeEhYMWFQeHQ0c3VlRTI0dHdIOUJkI3o2TWt0MXRzcDE1Y25ERDd3QkNGZ2VoaVIyU3hIWDFhUHh0NHN1ZUUyNHR3SDlCZCJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "3-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "request-received", "theirDid": "did:peer:1zQmc3BZoTinpVaG3oZ4PmRVN4JMdNZGCmPkS6smmTNLnvEZ", @@ -1290,6 +1294,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "2G8JohwyJtj69ruWFC3hBkdoaJW5eg31E66ohceVWCvy", "mediatorId": undefined, "outOfBandId": "1-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "completed", "theirDid": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", @@ -1307,6 +1313,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa2ZpUE1QeENRZVNEWkdNa0N2bTFZMnJCb1BzbXc0WkhNdjcxalh0Y1dSUmlNI3o2TWtmaVBNUHhDUWVTRFpHTWtDdm0xWTJyQm9Qc213NFpITXY3MWpYdGNXUlJpTSJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "1-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "completed", "theirDid": "did:peer:1zQmeHpGaZ48DnAP2k3KntXB1vmd8MgLEdcb4EQzqWJDHcbX", @@ -1324,6 +1332,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "EkJ7p82VB3a3AfuEWGS3gc1dPyY1BZ4PaVEztjwh1nVq", "mediatorId": undefined, "outOfBandId": "2-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "completed", "theirDid": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", @@ -1340,6 +1350,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3RDWkFRTkd2V2I0V0hBandCcVB0WGhaZERZb3JiU0prR1c5dmoxdWh3MUhEI3o2TWt0Q1pBUU5HdldiNFdIQWp3QnFQdFhoWmREWW9yYlNKa0dXOXZqMXVodzFIRCJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "2-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "completed", "theirDid": "did:peer:1zQmXYj3nNwsF37WXXdb8XkCAtsTCBpJJbsLKPPGfi2PWCTU", @@ -1369,6 +1381,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "G4CCB2mL9Fc32c9jqQKF9w9FUEfaaUzWvNdFVkEBSkQe", "mediatorId": undefined, "outOfBandId": "7-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "invitation-sent", "theirDid": undefined, @@ -1385,6 +1399,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa3VXVEVtSDFtVW82Vzk2elNXeUg2MTJoRkhvd1J6TkVzY1BZQkwyQ0NNeUMyI3o2TWt1V1RFbUgxbVVvNlc5NnpTV3lINjEyaEZIb3dSek5Fc2NQWUJMMkNDTXlDMiJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "7-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "invitation-sent", "updatedAt": "2022-01-21T22:50:20.522Z", @@ -1399,6 +1415,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "9akAmyoFVow6cWTg2M4LSVTckqbrCjuS3fQpQ8Zrm2eS", "mediatorId": undefined, "outOfBandId": "6-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "response-sent", "theirDid": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", @@ -1416,6 +1434,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczphbGljZSIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa28zMURORTNncU1SWmoxSk5odjJCSGIxY2FRc2hjZDluamdLa0VRWHNnRlJwI3o2TWtvMzFETkUzZ3FNUlpqMUpOaHYyQkhiMWNhUXNoY2Q5bmpnS2tFUVhzZ0ZScCJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "6-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "responder", "state": "response-sent", "theirDid": "did:peer:1zQmadmBfngrYSWhYYxZ24fpW29iwhKhQ6CB6euLabbSK6ga", @@ -2395,6 +2415,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "5m3HUGs6wFndaEk51zTBXuFwZza2tnGj4NzT5EkUiWaU", "mediatorId": undefined, "outOfBandId": "5-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "response-received", "theirDid": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", @@ -2412,6 +2434,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa2pESkw0WDdZR29INmdqYW1oWlIyTnpvd1BacXRKZlg1a1B1TnVXaVZkak1yI3o2TWtqREpMNFg3WUdvSDZnamFtaFpSMk56b3dQWnF0SmZYNWtQdU51V2lWZGpNciJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "5-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "response-received", "theirDid": "did:peer:1zQmcXZepLE55VGCMELEFjMd4nKrzp3GGyRR3r3MYermagui", @@ -2429,6 +2453,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationKey": "8MN6LZnM8t1HmzMNw5Sp8kUVfQkFK1nCUMRSfQBoSNAC", "mediatorId": undefined, "outOfBandId": "4-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "request-sent", "theirDid": undefined, @@ -2444,6 +2470,8 @@ exports[`UpdateAssistant | v0.1 - v0.2 should correctly update the connection re "invitationDid": "did:peer:2.SeyJzIjoicnhqczpmYWJlciIsInQiOiJkaWQtY29tbXVuaWNhdGlvbiIsInByaW9yaXR5IjowLCJyZWNpcGllbnRLZXlzIjpbImRpZDprZXk6ejZNa21vZDh2cDJuVVJWa3RWQzVjZVFleXIyVlV6MjZpdTJaQU5MTlZnOXBNYXdhI3o2TWttb2Q4dnAyblVSVmt0VkM1Y2VRZXlyMlZVejI2aXUyWkFOTE5WZzlwTWF3YSJdLCJyIjpbXX0", "metadata": {}, "outOfBandId": "4-4e4f-41d9-94c4-f49351b811f1", + "previousDids": [], + "previousTheirDids": [], "role": "requester", "state": "request-sent", "theirLabel": "Agent: PopulateWallet2", diff --git a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts index 70c72c9c2a..af1886a094 100644 --- a/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts +++ b/packages/core/src/storage/migration/updates/0.1-0.2/__tests__/connection.test.ts @@ -142,6 +142,8 @@ describe('0.1-0.2 | Connection', () => { theirDid: didPeer4kgVt6CidfKgo1MoWMqsQX.id, outOfBandId: expect.any(String), connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) }) @@ -173,6 +175,8 @@ describe('0.1-0.2 | Connection', () => { label: 'test', }, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) }) @@ -202,6 +206,8 @@ describe('0.1-0.2 | Connection', () => { label: 'test', }, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) @@ -275,6 +281,8 @@ describe('0.1-0.2 | Connection', () => { metadata: {}, _tags: {}, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) @@ -346,6 +354,8 @@ describe('0.1-0.2 | Connection', () => { label: 'test', }, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) }) @@ -373,6 +383,8 @@ describe('0.1-0.2 | Connection', () => { autoAcceptConnection: true, mediatorId: 'a-mediator-id', connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) @@ -499,6 +511,8 @@ describe('0.1-0.2 | Connection', () => { mediatorId: 'a-mediator-id', outOfBandId: outOfBandRecord.id, connectionTypes: [], + previousDids: [], + previousTheirDids: [], }) }) diff --git a/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts b/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts index 0e397e3419..64ada8c5d2 100644 --- a/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts +++ b/packages/core/src/storage/migration/updates/0.2-0.3/__tests__/connection.test.ts @@ -104,6 +104,8 @@ describe('0.2-0.3 | Connection', () => { _tags: { connectionType: undefined, }, + previousDids: [], + previousTheirDids: [], metadata: {}, }) }) @@ -128,6 +130,8 @@ describe('0.2-0.3 | Connection', () => { _tags: { connectionType: undefined, }, + previousDids: [], + previousTheirDids: [], metadata: {}, }) }) @@ -146,6 +150,8 @@ describe('0.2-0.3 | Connection', () => { expect(connectionRecord.toJSON()).toEqual({ ...connectionRecordProps, connectionTypes: [], + previousDids: [], + previousTheirDids: [], _tags: { connectionType: undefined, }, @@ -174,6 +180,8 @@ describe('0.2-0.3 | Connection', () => { connectionType: undefined, }, metadata: {}, + previousDids: [], + previousTheirDids: [], }) }) }) diff --git a/packages/core/src/utils/__tests__/shortenedUrl.test.ts b/packages/core/src/utils/__tests__/parseInvitation.test.ts similarity index 69% rename from packages/core/src/utils/__tests__/shortenedUrl.test.ts rename to packages/core/src/utils/__tests__/parseInvitation.test.ts index ffe4a2934a..b9966f52cd 100644 --- a/packages/core/src/utils/__tests__/shortenedUrl.test.ts +++ b/packages/core/src/utils/__tests__/parseInvitation.test.ts @@ -1,9 +1,11 @@ +import { agentDependencies } from '../../../tests' import { ConnectionInvitationMessage } from '../../modules/connections' import { InvitationType, OutOfBandInvitation } from '../../modules/oob' import { convertToNewInvitation } from '../../modules/oob/helpers' +import { JsonEncoder } from '../JsonEncoder' import { JsonTransformer } from '../JsonTransformer' import { MessageValidator } from '../MessageValidator' -import { oobInvitationFromShortUrl } from '../parseInvitation' +import { oobInvitationFromShortUrl, parseInvitationShortUrl } from '../parseInvitation' const mockOobInvite = { '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation', @@ -21,6 +23,16 @@ const mockConnectionInvite = { recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], } +const mockLegacyConnectionless = { + '@id': '035b6404-f496-4cb6-a2b5-8bd09e8c92c1', + '@type': 'https://didcomm.org/some-protocol/1.0/some-message', + '~service': { + recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + routingKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'], + serviceEndpoint: 'https://example.com/endpoint', + }, +} + const header = new Headers() const dummyHeader = new Headers() @@ -49,6 +61,13 @@ const mockedResponseOobUrl = { dummyHeader.forEach(mockedResponseOobUrl.headers.append) +const mockedLegacyConnectionlessInvitationJson = { + status: 200, + ok: true, + json: async () => mockLegacyConnectionless, + headers: header, +} as Response + const mockedResponseConnectionJson = { status: 200, ok: true, @@ -103,15 +122,78 @@ describe('shortened urls resolving to oob invitations', () => { }) }) +describe('legacy connectionless', () => { + test('parse url containing d_m ', async () => { + const parsed = await parseInvitationShortUrl( + `https://example.com?d_m=${JsonEncoder.toBase64URL(mockLegacyConnectionless)}`, + agentDependencies + ) + expect(parsed.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + label: undefined, + 'requests~attach': [ + { + '@id': expect.any(String), + data: { + base64: + 'eyJAaWQiOiIwMzViNjQwNC1mNDk2LTRjYjYtYTJiNS04YmQwOWU4YzkyYzEiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvc29tZS1wcm90b2NvbC8xLjAvc29tZS1tZXNzYWdlIn0=', + }, + 'mime-type': 'application/json', + }, + ], + services: [ + { + id: expect.any(String), + recipientKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + routingKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + ], + }) + }) + + test('parse short url returning legacy connectionless invitation to out of band invitation', async () => { + const parsed = await oobInvitationFromShortUrl(mockedLegacyConnectionlessInvitationJson) + expect(parsed.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/out-of-band/1.1/invitation', + label: undefined, + 'requests~attach': [ + { + '@id': expect.any(String), + data: { + base64: + 'eyJAaWQiOiIwMzViNjQwNC1mNDk2LTRjYjYtYTJiNS04YmQwOWU4YzkyYzEiLCJAdHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvc29tZS1wcm90b2NvbC8xLjAvc29tZS1tZXNzYWdlIn0=', + }, + 'mime-type': 'application/json', + }, + ], + services: [ + { + id: expect.any(String), + recipientKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + routingKeys: ['did:key:z6MkijBsFPbW4fQyvnpM9Yt2AhHYTh7N1zH6xp1mPrJJfZe1'], + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + }, + ], + }) + }) +}) + describe('shortened urls resolving to connection invitations', () => { test('Resolve a mocked response in the form of a connection invitation as a json object', async () => { const short = await oobInvitationFromShortUrl(mockedResponseConnectionJson) expect(short).toEqual(connectionInvitationToNew) }) + test('Resolve a mocked Response in the form of a connection invitation encoded in an url c_i query parameter', async () => { const short = await oobInvitationFromShortUrl(mockedResponseConnectionUrl) expect(short).toEqual(connectionInvitationToNew) }) + test('Resolve a mocked Response in the form of a connection invitation encoded in an url oob query parameter', async () => { const mockedResponseConnectionInOobUrl = { status: 200, diff --git a/packages/core/src/utils/objectEquality.ts b/packages/core/src/utils/objectEquality.ts index 5288d1a52d..33db64084a 100644 --- a/packages/core/src/utils/objectEquality.ts +++ b/packages/core/src/utils/objectEquality.ts @@ -1,14 +1,16 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function areObjectsEqual(a: any, b: any): boolean { +export function areObjectsEqual(a: A, b: B): boolean { if (typeof a == 'object' && a != null && typeof b == 'object' && b != null) { - if (Object.keys(a).length !== Object.keys(b).length) return false - for (const key in a) { - if (!(key in b) || !areObjectsEqual(a[key], b[key])) { + const definedA = Object.fromEntries(Object.entries(a).filter(([, value]) => value !== undefined)) + const definedB = Object.fromEntries(Object.entries(b).filter(([, value]) => value !== undefined)) + if (Object.keys(definedA).length !== Object.keys(definedB).length) return false + for (const key in definedA) { + if (!(key in definedB) || !areObjectsEqual(definedA[key], definedB[key])) { return false } } - for (const key in b) { - if (!(key in a) || !areObjectsEqual(b[key], a[key])) { + for (const key in definedB) { + if (!(key in definedA) || !areObjectsEqual(definedB[key], definedA[key])) { return false } } diff --git a/packages/core/src/utils/parseInvitation.ts b/packages/core/src/utils/parseInvitation.ts index 1fb9d99b03..4b5b5232a8 100644 --- a/packages/core/src/utils/parseInvitation.ts +++ b/packages/core/src/utils/parseInvitation.ts @@ -59,6 +59,9 @@ export const parseInvitationJson = (invitationJson: Record): Ou const outOfBandInvitation = convertToNewInvitation(invitation) outOfBandInvitation.invitationType = InvitationType.Connection return outOfBandInvitation + } else if (invitationJson['~service']) { + // This is probably a legacy connectionless invitation + return transformLegacyConnectionlessInvitationToOutOfBandInvitation(invitationJson) } else { throw new AriesFrameworkError(`Invitation with '@type' ${parsedMessageType.messageTypeUri} not supported.`) } @@ -105,6 +108,30 @@ export const oobInvitationFromShortUrl = async (response: Response): Promise) { + 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({ + services: [OutOfBandDidCommService.fromResolvedDidCommService(agentMessage.service.resolvedDidCommService)], + }) + + invitation.invitationType = InvitationType.Connectionless + invitation.addRequest(JsonTransformer.fromJSON(messageWithoutService, AgentMessage)) + + return invitation +} + /** * Parses URL containing encoded invitation and returns invitation message. Compatible with * parsing short Urls @@ -126,30 +153,7 @@ export const parseInvitationShortUrl = async ( // 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.invitationType = InvitationType.Connectionless - invitation.addRequest(JsonTransformer.fromJSON(messageWithoutService, AgentMessage)) - - return invitation + return transformLegacyConnectionlessInvitationToOutOfBandInvitation(messageJson) } else { try { const outOfBandInvitation = await oobInvitationFromShortUrl(await fetchShortUrl(invitationUrl, dependencies)) diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 4ad2392ceb..b1312050a8 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -15,6 +15,8 @@ import type { ConnectionStateChangedEvent, Buffer, RevocationNotificationReceivedEvent, + AgentMessageProcessedEvent, + AgentMessageReceivedEvent, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { TrustPingReceivedEvent, TrustPingResponseReceivedEvent } from '../src/modules/connections/TrustPingEvents' @@ -45,6 +47,7 @@ import { InjectionSymbols, ProofEventTypes, TrustPingEventTypes, + AgentEventTypes, } from '../src' import { Key, KeyType } from '../src/crypto' import { DidKey } from '../src/modules/dids/methods/key' @@ -218,6 +221,8 @@ const isTrustPingReceivedEvent = (e: BaseEvent): e is TrustPingReceivedEvent => e.type === TrustPingEventTypes.TrustPingReceivedEvent const isTrustPingResponseReceivedEvent = (e: BaseEvent): e is TrustPingResponseReceivedEvent => e.type === TrustPingEventTypes.TrustPingResponseReceivedEvent +const isAgentMessageProcessedEvent = (e: BaseEvent): e is AgentMessageProcessedEvent => + e.type === AgentEventTypes.AgentMessageProcessed export function waitForProofExchangeRecordSubject( subject: ReplaySubject | Observable, @@ -344,6 +349,50 @@ export function waitForTrustPingResponseReceivedEventSubject( ) } +export async function waitForAgentMessageProcessedEvent( + agent: Agent, + options: { + threadId?: string + messageType?: string + timeoutMs?: number + } +) { + const observable = agent.events.observable(AgentEventTypes.AgentMessageProcessed) + + return waitForAgentMessageProcessedEventSubject(observable, options) +} + +export function waitForAgentMessageProcessedEventSubject( + subject: ReplaySubject | Observable, + { + threadId, + timeoutMs = 10000, + messageType, + }: { + threadId?: string + messageType?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject + return firstValueFrom( + observable.pipe( + filter(isAgentMessageProcessedEvent), + filter((e) => threadId === undefined || e.payload.message.threadId === threadId), + filter((e) => messageType === undefined || e.payload.message.type === messageType), + timeout(timeoutMs), + catchError(() => { + throw new Error( + `AgentMessageProcessedEvent event not emitted within specified timeout: ${timeoutMs} + threadId: ${threadId}, messageType: ${messageType} +}` + ) + }), + map((e) => e.payload.message) + ) + ) +} + export function waitForCredentialRecordSubject( subject: ReplaySubject | Observable, { @@ -440,12 +489,17 @@ export async function waitForConnectionRecord( return waitForConnectionRecordSubject(observable, options) } -export async function waitForBasicMessage(agent: Agent, { content }: { content?: string }): Promise { +export async function waitForBasicMessage( + agent: Agent, + { content, connectionId }: { content?: string; connectionId?: string } +): Promise { return new Promise((resolve) => { const listener = (event: BasicMessageStateChangedEvent) => { const contentMatches = content === undefined || event.payload.message.content === content + const connectionIdMatches = + connectionId === undefined || event.payload.basicMessageRecord.connectionId === connectionId - if (contentMatches) { + if (contentMatches && connectionIdMatches) { agent.events.off(BasicMessageEventTypes.BasicMessageStateChanged, listener) resolve(event.payload.message) diff --git a/packages/core/tests/jsonld.ts b/packages/core/tests/jsonld.ts index 9b0f211097..1cfb263ab0 100644 --- a/packages/core/tests/jsonld.ts +++ b/packages/core/tests/jsonld.ts @@ -5,6 +5,8 @@ import { BbsModule } from '../../bbs-signatures/src/BbsModule' import { IndySdkModule } from '../../indy-sdk/src' import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { + PresentationExchangeProofFormatService, + V2ProofProtocol, CacheModule, CredentialEventTypes, InMemoryLruCache, @@ -38,6 +40,7 @@ export const getJsonLdModules = ({ }), proofs: new ProofsModule({ autoAcceptProofs, + proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], }), cache: new CacheModule({ cache: new InMemoryLruCache({ limit: 100 }), diff --git a/packages/core/tests/oob.test.ts b/packages/core/tests/oob.test.ts index 2911d9d406..fddbb253ba 100644 --- a/packages/core/tests/oob.test.ts +++ b/packages/core/tests/oob.test.ts @@ -692,7 +692,7 @@ describe('out of band', () => { await expect(aliceAgent.oob.receiveInvitation(outOfBandInvitation, receiveInvitationConfig)).rejects.toEqual( new AriesFrameworkError( - `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.0,https://didcomm.org/connections/1.0]` + `Handshake protocols [${unsupportedProtocol}] are not supported. Supported protocols are [https://didcomm.org/didexchange/1.1,https://didcomm.org/connections/1.0]` ) ) }) diff --git a/packages/indy-sdk-to-askar-migration/package.json b/packages/indy-sdk-to-askar-migration/package.json index a8e1c6e0a4..1f8cf4d3f9 100644 --- a/packages/indy-sdk-to-askar-migration/package.json +++ b/packages/indy-sdk-to-askar-migration/package.json @@ -31,13 +31,13 @@ }, "devDependencies": { "@aries-framework/indy-sdk": "0.4.2", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", - "@hyperledger/aries-askar-shared": "^0.2.0-dev.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", + "@hyperledger/aries-askar-shared": "^0.2.0-dev.5", "indy-sdk": "^1.16.0-dev-1655", "rimraf": "^4.4.0", "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/aries-askar-shared": "^0.2.0-dev.1" + "@hyperledger/aries-askar-shared": "^0.2.0-dev.5" } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index a386675b1e..0b9fdb0718 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -18,7 +18,7 @@ import type { Schema as IndySdkSchema } from 'indy-sdk' import { getUnqualifiedCredentialDefinitionId, - getUnqualifiedRevocationRegistryId, + getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedSchemaId, parseIndyCredentialDefinitionId, parseIndyDid, @@ -394,7 +394,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { `Using ledger '${pool.didIndyNamespace}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` ) - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryId( + const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryDefinitionId( namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, @@ -492,7 +492,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { `Using ledger '${pool.didIndyNamespace}' to retrieve revocation registry deltas with revocation registry definition id '${revocationRegistryId}' until ${timestamp}` ) - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryId( + const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryDefinitionId( namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, diff --git a/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts b/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts index 9b9a54ba83..546579330b 100644 --- a/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts +++ b/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts @@ -1,6 +1,6 @@ import { getDidIndyCredentialDefinitionId, - getDidIndyRevocationRegistryId, + getDidIndyRevocationRegistryDefinitionId, getDidIndySchemaId, indySdkAnonCredsRegistryIdentifierRegex, } from '../identifiers' @@ -72,7 +72,7 @@ describe('identifiers', () => { const credentialDefinitionTag = 'someTag' const tag = 'anotherTag' - expect(getDidIndyRevocationRegistryId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( + expect(getDidIndyRevocationRegistryDefinitionId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag' ) }) diff --git a/packages/indy-sdk/src/anoncreds/utils/identifiers.ts b/packages/indy-sdk/src/anoncreds/utils/identifiers.ts index 4cedb11ff4..ccc7433e34 100644 --- a/packages/indy-sdk/src/anoncreds/utils/identifiers.ts +++ b/packages/indy-sdk/src/anoncreds/utils/identifiers.ts @@ -46,18 +46,18 @@ export function getDidIndySchemaId(namespace: string, unqualifiedDid: string, na export function getDidIndyCredentialDefinitionId( namespace: string, unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, tag: string ) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${seqNo}/${tag}` + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` } -export function getDidIndyRevocationRegistryId( +export function getDidIndyRevocationRegistryDefinitionId( namespace: string, unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, credentialDefinitionTag: string, revocationRegistryTag: string ) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${seqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` } diff --git a/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts b/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts index 1c486eb3aa..c84999a8c1 100644 --- a/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts +++ b/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts @@ -16,6 +16,8 @@ import { addServicesFromEndpointsAttrib } from './didSovUtil' export class IndySdkIndyDidResolver implements DidResolver { public readonly supportedMethods = ['indy'] + public readonly allowsCaching = true + public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} diff --git a/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts b/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts index ff6afc9571..3a881db50e 100644 --- a/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts +++ b/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts @@ -12,6 +12,8 @@ import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovU export class IndySdkSovDidResolver implements DidResolver { public readonly supportedMethods = ['sov'] + public readonly allowsCaching = true + public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { const didDocumentMetadata = {} diff --git a/packages/indy-vdr/package.json b/packages/indy-vdr/package.json index e49286f040..d558c83dd0 100644 --- a/packages/indy-vdr/package.json +++ b/packages/indy-vdr/package.json @@ -28,8 +28,8 @@ "@aries-framework/core": "0.4.2" }, "devDependencies": { - "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.5", - "@hyperledger/indy-vdr-shared": "^0.2.0-dev.5", + "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.6", + "@hyperledger/indy-vdr-shared": "^0.2.0-dev.6", "@stablelib/ed25519": "^1.0.2", "@types/ref-array-di": "^1.2.6", "@types/ref-struct-di": "^1.1.10", @@ -38,6 +38,6 @@ "typescript": "~4.9.5" }, "peerDependencies": { - "@hyperledger/indy-vdr-shared": "^0.2.0-dev.5" + "@hyperledger/indy-vdr-shared": "^0.2.0-dev.6" } } diff --git a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts index 5251362983..15db83beee 100644 --- a/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts +++ b/packages/indy-vdr/src/anoncreds/IndyVdrAnonCredsRegistry.ts @@ -1,3 +1,4 @@ +import type { RevocationRegistryDelta } from './utils/transform' import type { AnonCredsRegistry, GetCredentialDefinitionReturn, @@ -19,21 +20,33 @@ import type { RegisterCredentialDefinitionReturnStateWait, RegisterCredentialDefinitionReturnStateFinished, RegisterCredentialDefinitionReturnStateFailed, + RegisterRevocationRegistryDefinitionReturnStateFinished, + RegisterRevocationRegistryDefinitionReturnStateFailed, + RegisterRevocationRegistryDefinitionReturnStateWait, + RegisterRevocationRegistryDefinitionReturnStateAction, + RegisterRevocationStatusListReturnStateFinished, + RegisterRevocationStatusListReturnStateFailed, + RegisterRevocationStatusListReturnStateWait, + RegisterRevocationStatusListReturnStateAction, + RegisterRevocationStatusListOptions, } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { SchemaResponse } from '@hyperledger/indy-vdr-shared' import { getUnqualifiedCredentialDefinitionId, - getUnqualifiedRevocationRegistryId, + getUnqualifiedRevocationRegistryDefinitionId, getUnqualifiedSchemaId, parseIndyCredentialDefinitionId, parseIndyDid, parseIndyRevocationRegistryId, parseIndySchemaId, + dateToTimestamp, } from '@aries-framework/anoncreds' import { AriesFrameworkError } from '@aries-framework/core' import { + RevocationRegistryEntryRequest, + RevocationRegistryDefinitionRequest, GetSchemaRequest, SchemaRequest, GetCredentialDefinitionRequest, @@ -52,8 +65,10 @@ import { indyVdrAnonCredsRegistryIdentifierRegex, getDidIndySchemaId, getDidIndyCredentialDefinitionId, + getDidIndyRevocationRegistryDefinitionId, + getDidIndyRevocationRegistryEntryId, } from './utils/identifiers' -import { anonCredsRevocationStatusListFromIndyVdr } from './utils/transform' +import { indyVdrCreateLatestRevocationDelta, anonCredsRevocationStatusListFromIndyVdr } from './utils/transform' export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { public readonly methodName = 'indy' @@ -277,7 +292,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { : schema.schema.schemaId return { - credentialDefinitionId: credentialDefinitionId, + credentialDefinitionId, credentialDefinition: { issuerId: did, schemaId, @@ -329,7 +344,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { let writeRequest: CustomRequest let didIndyCredentialDefinitionId: string - let seqNo: number + let schemaSeqNo: number const endorsedTransaction = options.options.endorsedTransaction if (endorsedTransaction) { @@ -340,8 +355,13 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) const operation = JSON.parse(endorsedTransaction)?.operation // extract the seqNo from the endorsed transaction, which is contained in the ref field of the operation - seqNo = Number(operation?.ref) - didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId(namespace, namespaceIdentifier, seqNo, tag) + schemaSeqNo = Number(operation?.ref) + didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + tag + ) } else { // TODO: this will bypass caching if done on a higher level. const { schemaMetadata, resolutionMetadata } = await this.getSchema(agentContext, schemaId) @@ -359,20 +379,31 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { }, } } - seqNo = schemaMetadata.indyLedgerSeqNo - - const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(issuerId, seqNo, tag) - didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId(namespace, namespaceIdentifier, seqNo, tag) + schemaSeqNo = schemaMetadata.indyLedgerSeqNo + + // FIXME: we need to check if schemaId has same namespace as issuerId and it should not be a legacy identifier + // FIXME: in the other methods we also need to add checks. E.g. when creating a revocation + // status list, you can only create a revocation status list for a credential definition registry that is created + // under the same namespace and by the same issuer id (you can create a cred def for a schema created by another issuer + // but you can't create a revocation registry based on a cred def created by another issuer. We need to add these checks + // to all register methods in this file) + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId(issuerId, schemaSeqNo, tag) + didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + tag + ) const credentialDefinitionRequest = new CredentialDefinitionRequest({ submitterDid: namespaceIdentifier, credentialDefinition: { ver: '1.0', id: legacyCredentialDefinitionId, - schemaId: `${seqNo}`, + schemaId: schemaSeqNo.toString(), type: 'CL', - tag: tag, - value: value, + tag, + value, }, }) @@ -457,7 +488,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { `Using ledger '${pool.indyNamespace}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` ) - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryId( + const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryDefinitionId( namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, @@ -483,7 +514,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { return { resolutionMetadata: { error: 'notFound', - message: `unable to resolve revocation registry definition`, + message: 'unable to resolve revocation registry definition', }, revocationRegistryDefinitionId, revocationRegistryDefinitionMetadata: {}, @@ -552,48 +583,178 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } } - public async registerRevocationRegistryDefinition(): Promise { - throw new AriesFrameworkError('Not implemented!') - } - - public async getRevocationStatusList( + public async registerRevocationRegistryDefinition( agentContext: AgentContext, - revocationRegistryId: string, - timestamp: number - ): Promise { + { options, revocationRegistryDefinition }: IndyVdrRegisterRevocationRegistryDefinition + ): Promise { try { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) - - const { did, namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = - parseIndyRevocationRegistryId(revocationRegistryId) - const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) + // This will throw an error if trying to register a credential definition with a legacy indy identifier. We only support did:indy + // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { namespaceIdentifier, namespace } = parseIndyDid(revocationRegistryDefinition.issuerId) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const pool = indyVdrPoolService.getPoolForNamespace(namespace) agentContext.config.logger.debug( - `Using ledger '${pool.indyNamespace}' to retrieve revocation registry deltas with revocation registry definition id '${revocationRegistryId}' until ${timestamp}` + `Registering revocation registry definition on ledger '${namespace}' with did '${revocationRegistryDefinition.issuerId}'`, + revocationRegistryDefinition ) - const legacyRevocationRegistryId = getUnqualifiedRevocationRegistryId( - namespaceIdentifier, - schemaSeqNo, - credentialDefinitionTag, - revocationRegistryTag - ) - const request = new GetRevocationRegistryDeltaRequest({ - revocationRegistryId: legacyRevocationRegistryId, - toTs: timestamp, - }) + let writeRequest: CustomRequest + let didIndyRevocationRegistryDefinitionId: string - agentContext.config.logger.trace( - `Submitting get revocation registry delta request for revocation registry '${revocationRegistryId}' to ledger` + const { schemaSeqNo, tag: credentialDefinitionTag } = parseIndyCredentialDefinitionId( + revocationRegistryDefinition.credDefId ) - const response = await pool.submitRequest(request) + const { endorsedTransaction, endorserDid, endorserMode } = options + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${revocationRegistryDefinition.issuerId}'`, + revocationRegistryDefinition + ) + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryDefinition.tag + ) + } else { + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryDefinition.tag + ) + + didIndyRevocationRegistryDefinitionId = getDidIndyRevocationRegistryDefinitionId( + namespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryDefinition.tag + ) + + const legacyCredentialDefinitionId = getUnqualifiedCredentialDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag + ) + + const revocationRegistryDefinitionRequest = new RevocationRegistryDefinitionRequest({ + submitterDid: namespaceIdentifier, + revocationRegistryDefinitionV1: { + id: legacyRevocationRegistryDefinitionId, + ver: '1.0', + credDefId: legacyCredentialDefinitionId, + tag: revocationRegistryDefinition.tag, + revocDefType: revocationRegistryDefinition.revocDefType, + value: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + ...revocationRegistryDefinition.value, + }, + }, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, revocationRegistryDefinition.issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + revocationRegistryDefinitionRequest, + submitterKey, + endorserDid !== revocationRegistryDefinition.issuerId ? endorserDid : undefined + ) + + if (endorserMode === 'external') { + return { + jobId: didIndyRevocationRegistryDefinitionId, + revocationRegistryDefinitionState: { + state: 'action', + action: 'endorseIndyTransaction', + revocationRegistryDefinition, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + revocationRegistryDefinitionRequest: writeRequest.body, + }, + registrationMetadata: {}, + revocationRegistryDefinitionMetadata: {}, + } + } + + if (endorserMode === 'internal' && endorserDid !== revocationRegistryDefinition.issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } + + const response = await pool.submitRequest(writeRequest) agentContext.config.logger.debug( - `Got revocation registry deltas '${revocationRegistryId}' until timestamp ${timestamp} from ledger` + `Registered revocation registry definition '${didIndyRevocationRegistryDefinitionId}' on ledger '${pool.indyNamespace}'`, + { + response, + revocationRegistryDefinition, + } ) + return { + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error registering revocation registry definition for credential definition '${revocationRegistryDefinition.credDefId}'`, + { + error, + did: revocationRegistryDefinition.issuerId, + revocationRegistryDefinition, + } + ) + + return { + revocationRegistryDefinitionMetadata: {}, + registrationMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition, + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async getRevocationStatusList( + agentContext: AgentContext, + revocationRegistryDefinitionId: string, + timestamp: number + ): Promise { + try { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const { did } = parseIndyRevocationRegistryId(revocationRegistryDefinitionId) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + + const revocationDelta = await this.fetchIndyRevocationDelta( + agentContext, + revocationRegistryDefinitionId, + timestamp + ) + + if (!revocationDelta) { + return { + resolutionMetadata: { + error: 'notFound', + message: `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation`, + }, + revocationStatusListMetadata: {}, + } + } + const { revocationRegistryDefinition, resolutionMetadata, revocationRegistryDefinitionMetadata } = - await this.getRevocationRegistryDefinition(agentContext, revocationRegistryId) + await this.getRevocationRegistryDefinition(agentContext, revocationRegistryDefinitionId) if ( !revocationRegistryDefinition || @@ -602,7 +763,7 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { ) { return { resolutionMetadata: { - error: `error resolving revocation registry definition with id ${revocationRegistryId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, + error: `error resolving revocation registry definition with id ${revocationRegistryDefinitionId}: ${resolutionMetadata.error} ${resolutionMetadata.message}`, }, revocationStatusListMetadata: { didIndyNamespace: pool.indyNamespace, @@ -612,29 +773,12 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { const isIssuanceByDefault = revocationRegistryDefinitionMetadata.issuanceType === 'ISSUANCE_BY_DEFAULT' - if (!response.result.data) { - return { - resolutionMetadata: { - error: 'notFound', - message: `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation`, - }, - revocationStatusListMetadata: {}, - } - } - - const revocationRegistryDelta = { - accum: response.result.data.value.accum_to.value.accum, - issued: response.result.data.value.issued, - revoked: response.result.data.value.revoked, - } - return { resolutionMetadata: {}, revocationStatusList: anonCredsRevocationStatusListFromIndyVdr( - revocationRegistryId, + revocationRegistryDefinitionId, revocationRegistryDefinition, - revocationRegistryDelta, - response.result.data.value.accum_to.txnTime, + revocationDelta, isIssuanceByDefault ), revocationStatusListMetadata: { @@ -643,25 +787,178 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { } } catch (error) { agentContext.config.logger.error( - `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation?"`, + `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation?"`, { error, - revocationRegistryId: revocationRegistryId, + revocationRegistryId: revocationRegistryDefinitionId, } ) return { resolutionMetadata: { error: 'notFound', - message: `Error retrieving revocation registry delta '${revocationRegistryId}' from ledger, potentially revocation interval ends before revocation registry creation: ${error.message}`, + message: `Error retrieving revocation registry delta '${revocationRegistryDefinitionId}' from ledger, potentially revocation interval ends before revocation registry creation: ${error.message}`, }, revocationStatusListMetadata: {}, } } } - public async registerRevocationStatusList(): Promise { - throw new AriesFrameworkError('Not implemented!') + public async registerRevocationStatusList( + agentContext: AgentContext, + { options, revocationStatusList }: IndyVdrRegisterRevocationStatusList + ): Promise { + try { + // This will throw an error if trying to register a revocation status list with a legacy indy identifier. We only support did:indy + // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { endorsedTransaction, endorserDid, endorserMode } = options + const { namespaceIdentifier, namespace } = parseIndyDid(revocationStatusList.issuerId) + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + const pool = indyVdrPoolService.getPoolForNamespace(namespace) + + agentContext.config.logger.debug( + `Registering revocation status list on ledger '${namespace}' with did '${revocationStatusList.issuerId}'`, + revocationStatusList + ) + + let writeRequest: CustomRequest + + // Parse the revocation registry id + const { + schemaSeqNo, + credentialDefinitionTag, + namespaceIdentifier: revocationRegistryNamespaceIdentifier, + revocationRegistryTag, + namespace: revocationRegistryNamespace, + } = parseIndyRevocationRegistryId(revocationStatusList.revRegDefId) + + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + const didIndyRevocationRegistryEntryId = getDidIndyRevocationRegistryEntryId( + namespace, + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + if (revocationRegistryNamespace && revocationRegistryNamespace !== namespace) { + throw new AriesFrameworkError( + `Issued id '${revocationStatusList.issuerId}' does not have the same namespace (${namespace}) as the revocation registry definition '${revocationRegistryNamespace}'` + ) + } + + if (revocationRegistryNamespaceIdentifier !== namespaceIdentifier) { + throw new AriesFrameworkError( + `Cannot register revocation registry definition using a different DID. Revocation registry definition contains '${revocationRegistryNamespaceIdentifier}', but DID used was '${namespaceIdentifier}'` + ) + } + + if (endorsedTransaction) { + agentContext.config.logger.debug( + `Preparing endorsed tx '${endorsedTransaction}' for submission on ledger '${namespace}' with did '${revocationStatusList.issuerId}'`, + revocationStatusList + ) + + writeRequest = new CustomRequest({ customRequest: endorsedTransaction }) + } else { + const previousDelta = await this.fetchIndyRevocationDelta( + agentContext, + legacyRevocationRegistryDefinitionId, + // Fetch revocation delta for current timestamp + dateToTimestamp(new Date()) + ) + + const revocationRegistryDefinitionEntryValue = indyVdrCreateLatestRevocationDelta( + revocationStatusList.currentAccumulator, + revocationStatusList.revocationList, + previousDelta ?? undefined + ) + + const revocationRegistryDefinitionRequest = new RevocationRegistryEntryRequest({ + submitterDid: namespaceIdentifier, + revocationRegistryEntry: { + ver: '1.0', + value: revocationRegistryDefinitionEntryValue, + }, + revocationRegistryDefinitionType: 'CL_ACCUM', + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, + }) + + const submitterKey = await verificationKeyForIndyDid(agentContext, revocationStatusList.issuerId) + writeRequest = await pool.prepareWriteRequest( + agentContext, + revocationRegistryDefinitionRequest, + submitterKey, + endorserDid !== revocationStatusList.issuerId ? endorserDid : undefined + ) + + if (endorserMode === 'external') { + return { + jobId: didIndyRevocationRegistryEntryId, + revocationStatusListState: { + state: 'action', + action: 'endorseIndyTransaction', + revocationStatusList, + revocationStatusListRequest: writeRequest.body, + }, + registrationMetadata: {}, + revocationStatusListMetadata: {}, + } + } + + if (endorserMode === 'internal' && endorserDid !== revocationStatusList.issuerId) { + const endorserKey = await verificationKeyForIndyDid(agentContext, endorserDid as string) + await multiSignRequest(agentContext, writeRequest, endorserKey, parseIndyDid(endorserDid).namespaceIdentifier) + } + } + + const response = await pool.submitRequest( + writeRequest as RevocationRegistryEntryRequest + ) + agentContext.config.logger.debug( + `Registered revocation status list '${didIndyRevocationRegistryEntryId}' on ledger '${pool.indyNamespace}'`, + { + response, + revocationStatusList, + } + ) + + return { + revocationStatusListMetadata: {}, + revocationStatusListState: { + revocationStatusList: { + ...revocationStatusList, + timestamp: response.result.txnMetadata.txnTime, + }, + state: 'finished', + }, + registrationMetadata: {}, + } + } catch (error) { + agentContext.config.logger.error( + `Error registering revocation status list for revocation registry definition '${revocationStatusList.revRegDefId}}'`, + { + error, + did: revocationStatusList.issuerId, + } + ) + + return { + registrationMetadata: {}, + revocationStatusListMetadata: {}, + revocationStatusListState: { + revocationStatusList, + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } } private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, seqNo: number, did: string) { @@ -697,6 +994,56 @@ export class IndyVdrAnonCredsRegistry implements AnonCredsRegistry { indyNamespace: pool.indyNamespace, } } + + private async fetchIndyRevocationDelta( + agentContext: AgentContext, + revocationRegistryDefinitionId: string, + toTs: number + ): Promise { + const indyVdrPoolService = agentContext.dependencyManager.resolve(IndyVdrPoolService) + + const { did, namespaceIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = + parseIndyRevocationRegistryId(revocationRegistryDefinitionId) + const { pool } = await indyVdrPoolService.getPoolForDid(agentContext, did) + + agentContext.config.logger.debug( + `Using ledger '${pool.indyNamespace}' to retrieve revocation registry deltas with revocation registry definition id '${revocationRegistryDefinitionId}' until ${toTs}` + ) + + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + namespaceIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + + const deltaRequest = new GetRevocationRegistryDeltaRequest({ + toTs, + submitterDid: namespaceIdentifier, + revocationRegistryId: legacyRevocationRegistryDefinitionId, + }) + + agentContext.config.logger.trace(`Submitting get transaction request to ledger '${pool.indyNamespace}'`) + const response = await pool.submitRequest(deltaRequest) + const { + result: { data, type, txnTime }, + } = response + + // Indicating there are no deltas + if (type !== '117' || data === null || !txnTime) { + agentContext.config.logger.warn( + `Could not get any deltas from ledger for revocation registry definition '${revocationRegistryDefinitionId}' from ledger '${pool.indyNamespace}'` + ) + return null + } + + return { + revoked: data.value.revoked, + issued: data.value.issued, + accum: data.value.accum_to.value.accum, + txnTime, + } + } } interface SchemaType { @@ -781,3 +1128,72 @@ export type IndyVdrRegisterCredentialDefinition = | IndyVdrRegisterCredentialDefinitionExternalSubmitOptions export type IndyVdrRegisterCredentialDefinitionOptions = IndyVdrRegisterCredentialDefinition['options'] + +export interface IndyVdrRegisterRevocationRegistryDefinitionInternalOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: InternalEndorsement +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionExternalCreateOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionExternalSubmitOptions { + revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionReturnStateAction + extends RegisterRevocationRegistryDefinitionReturnStateAction { + action: 'endorseIndyTransaction' + revocationRegistryDefinitionRequest: string +} + +export interface IndyVdrRegisterRevocationRegistryDefinitionReturn extends RegisterRevocationRegistryDefinitionReturn { + revocationRegistryDefinitionState: + | IndyVdrRegisterRevocationRegistryDefinitionReturnStateAction + | RegisterRevocationRegistryDefinitionReturnStateWait + | RegisterRevocationRegistryDefinitionReturnStateFinished + | RegisterRevocationRegistryDefinitionReturnStateFailed +} + +export type IndyVdrRegisterRevocationRegistryDefinition = + | IndyVdrRegisterRevocationRegistryDefinitionInternalOptions + | IndyVdrRegisterRevocationRegistryDefinitionExternalCreateOptions + | IndyVdrRegisterRevocationRegistryDefinitionExternalSubmitOptions + +export type IndyVdrRegisterRevocationRegistryDefinitionOptions = IndyVdrRegisterRevocationRegistryDefinition['options'] + +export interface IndyVdrRegisterRevocationStatusListInternalOptions extends RegisterRevocationStatusListOptions { + options: InternalEndorsement +} + +export interface IndyVdrRegisterRevocationStatusListExternalCreateOptions extends RegisterRevocationStatusListOptions { + options: ExternalEndorsementCreate +} + +export interface IndyVdrRegisterRevocationStatusListExternalSubmitOptions extends RegisterRevocationStatusListOptions { + options: ExternalEndorsementSubmit +} + +export interface IndyVdrRegisterRevocationStatusListReturnStateAction + extends RegisterRevocationStatusListReturnStateAction { + action: 'endorseIndyTransaction' + revocationStatusListRequest: string +} + +export interface IndyVdrRegisterRevocationStatusListReturn extends RegisterRevocationStatusListReturn { + revocationStatusListState: + | IndyVdrRegisterRevocationStatusListReturnStateAction + | RegisterRevocationStatusListReturnStateWait + | RegisterRevocationStatusListReturnStateFinished + | RegisterRevocationStatusListReturnStateFailed +} + +export type IndyVdrRegisterRevocationStatusList = + | IndyVdrRegisterRevocationStatusListInternalOptions + | IndyVdrRegisterRevocationStatusListExternalCreateOptions + | IndyVdrRegisterRevocationStatusListExternalSubmitOptions + +export type IndyVdrRegisterRevocationStatusListOptions = IndyVdrRegisterRevocationStatusList['options'] diff --git a/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts b/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts index b96720611b..0028a3bfa2 100644 --- a/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts +++ b/packages/indy-vdr/src/anoncreds/utils/__tests__/identifiers.test.ts @@ -1,6 +1,6 @@ import { getDidIndyCredentialDefinitionId, - getDidIndyRevocationRegistryId, + getDidIndyRevocationRegistryDefinitionId, getDidIndySchemaId, indyVdrAnonCredsRegistryIdentifierRegex, } from '../identifiers' @@ -72,7 +72,7 @@ describe('identifiers', () => { const credentialDefinitionTag = 'someTag' const tag = 'anotherTag' - expect(getDidIndyRevocationRegistryId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( + expect(getDidIndyRevocationRegistryDefinitionId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag' ) }) diff --git a/packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts b/packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts new file mode 100644 index 0000000000..c5f71e8868 --- /dev/null +++ b/packages/indy-vdr/src/anoncreds/utils/__tests__/transform.test.ts @@ -0,0 +1,164 @@ +import type { RevocationRegistryDelta } from '../transform' +import type { AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' + +import { indyVdrCreateLatestRevocationDelta, anonCredsRevocationStatusListFromIndyVdr } from '../transform' + +const createRevocationRegistryDefinition = (maxCreds: number): AnonCredsRevocationRegistryDefinition => ({ + value: { + tailsHash: 'hash', + maxCredNum: maxCreds, + publicKeys: { + accumKey: { + z: 'key', + }, + }, + tailsLocation: 'nowhere', + }, + revocDefType: 'CL_ACCUM', + tag: 'REV_TAG', + issuerId: 'does:not:matter', + credDefId: 'does:not:matter', +}) + +describe('transform', () => { + const accum = 'does not matter' + const revocationRegistryDefinitionId = 'does:not:matter' + + describe('indy vdr delta to anoncreds revocation status list', () => { + test('issued and revoked are filled', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [0, 1, 2, 3, 4], + revoked: [5, 6, 7, 8, 9], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + const statusList = anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + true + ) + + expect(statusList.revocationList).toStrictEqual([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) + }) + + test('issued and revoked are empty (issuance by default)', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [], + revoked: [], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + const statusList = anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + true + ) + + expect(statusList.revocationList).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + }) + + test('issued and revoked are empty (issuance on demand)', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [], + revoked: [], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + const statusList = anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + false + ) + + expect(statusList.revocationList).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + }) + + test('issued index is too high', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [200], + revoked: [5, 6, 7, 8, 9], + txnTime: 1, + } + + const revocationRegistryDefinition = createRevocationRegistryDefinition(10) + + expect(() => + anonCredsRevocationStatusListFromIndyVdr( + revocationRegistryDefinitionId, + revocationRegistryDefinition, + delta, + true + ) + ).toThrowError() + }) + }) + + describe('create latest indy vdr delta from status list and previous delta', () => { + test('delta and status list are equal', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [0, 1, 2, 3, 4], + revoked: [5, 6, 7, 8, 9], + txnTime: 1, + } + + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta) + + expect(revoked).toStrictEqual([]) + expect(issued).toStrictEqual([]) + }) + + test('no previous delta', () => { + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList) + + expect(issued).toStrictEqual([0, 1, 2, 3, 4]) + expect(revoked).toStrictEqual([5, 6, 7, 8, 9]) + }) + + test('status list and previous delta are out of sync', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [0], + revoked: [5], + txnTime: 1, + } + + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + const { revoked, issued } = indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta) + + expect(issued).toStrictEqual([1, 2, 3, 4]) + expect(revoked).toStrictEqual([6, 7, 8, 9]) + }) + + test('previous delta index exceeds length of revocation status list', () => { + const delta: RevocationRegistryDelta = { + accum, + issued: [200], + revoked: [5], + txnTime: 1, + } + + const revocationStatusList = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] + + expect(() => indyVdrCreateLatestRevocationDelta(accum, revocationStatusList, delta)).toThrowError() + }) + }) +}) diff --git a/packages/indy-vdr/src/anoncreds/utils/identifiers.ts b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts index 1e9d6a8fd3..46bebaadf7 100644 --- a/packages/indy-vdr/src/anoncreds/utils/identifiers.ts +++ b/packages/indy-vdr/src/anoncreds/utils/identifiers.ts @@ -46,18 +46,28 @@ export function getDidIndySchemaId(namespace: string, unqualifiedDid: string, na export function getDidIndyCredentialDefinitionId( namespace: string, unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, tag: string ) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${seqNo}/${tag}` + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/${tag}` } -export function getDidIndyRevocationRegistryId( +export function getDidIndyRevocationRegistryDefinitionId( namespace: string, unqualifiedDid: string, - seqNo: string | number, + schemaSeqNo: string | number, credentialDefinitionTag: string, revocationRegistryTag: string ) { - return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${seqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` +} + +export function getDidIndyRevocationRegistryEntryId( + namespace: string, + unqualifiedDid: string, + schemaSeqNo: string | number, + credentialDefinitionTag: string, + revocationRegistryTag: string +) { + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_ENTRY/${schemaSeqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` } diff --git a/packages/indy-vdr/src/anoncreds/utils/transform.ts b/packages/indy-vdr/src/anoncreds/utils/transform.ts index 36f4628bb4..87180e50d3 100644 --- a/packages/indy-vdr/src/anoncreds/utils/transform.ts +++ b/packages/indy-vdr/src/anoncreds/utils/transform.ts @@ -1,26 +1,51 @@ import type { AnonCredsRevocationStatusList, AnonCredsRevocationRegistryDefinition } from '@aries-framework/anoncreds' +import { AriesFrameworkError } from '@aries-framework/core' + +export type RevocationRegistryDelta = { + accum: string + issued: number[] + revoked: number[] + txnTime: number +} + +enum RevocationState { + Active, + Revoked, +} + export function anonCredsRevocationStatusListFromIndyVdr( revocationRegistryDefinitionId: string, revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition, - delta: RevocRegDelta, - timestamp: number, + delta: RevocationRegistryDelta, isIssuanceByDefault: boolean ): AnonCredsRevocationStatusList { + // Check whether the highest delta index is supported in the `maxCredNum` field of the + // revocation registry definition. This will likely also be checked on other levels as well + // by the ledger or the indy-vdr library itself + if (Math.max(...delta.issued, ...delta.revoked) >= revocationRegistryDefinition.value.maxCredNum) { + throw new AriesFrameworkError( + `Highest delta index '${Math.max( + ...delta.issued, + ...delta.revoked + )}' is too large for the Revocation registry maxCredNum '${revocationRegistryDefinition.value.maxCredNum}' ` + ) + } + // 0 means unrevoked, 1 means revoked - const defaultState = isIssuanceByDefault ? 0 : 1 + const defaultState = isIssuanceByDefault ? RevocationState.Active : RevocationState.Revoked // Fill with default value const revocationList = new Array(revocationRegistryDefinition.value.maxCredNum).fill(defaultState) // Set all `issuer` indexes to 0 (not revoked) for (const issued of delta.issued ?? []) { - revocationList[issued] = 0 + revocationList[issued] = RevocationState.Active } // Set all `revoked` indexes to 1 (revoked) for (const revoked of delta.revoked ?? []) { - revocationList[revoked] = 1 + revocationList[revoked] = RevocationState.Revoked } return { @@ -28,12 +53,83 @@ export function anonCredsRevocationStatusListFromIndyVdr( currentAccumulator: delta.accum, revRegDefId: revocationRegistryDefinitionId, revocationList, - timestamp, + timestamp: delta.txnTime, } } -interface RevocRegDelta { - accum: string - issued: number[] - revoked: number[] +/** + * + * Transforms the previous deltas and the full revocation status list into the latest delta + * + * ## Example + * + * input: + * + * revocationStatusList: [0, 1, 1, 1, 0, 0, 0, 1, 1, 0] + * previousDelta: + * - issued: [1, 2, 5, 8, 9] + * - revoked: [0, 3, 4, 6, 7] + * + * output: + * - issued: [5, 9] + * - revoked: [3, 7] + * + */ +export function indyVdrCreateLatestRevocationDelta( + currentAccumulator: string, + revocationStatusList: Array, + previousDelta?: RevocationRegistryDelta +) { + if (previousDelta && Math.max(...previousDelta.issued, ...previousDelta.revoked) > revocationStatusList.length - 1) { + throw new AriesFrameworkError( + `Indy Vdr delta contains an index '${Math.max( + ...previousDelta.revoked, + ...previousDelta.issued + )}' that exceeds the length of the revocation status list '${revocationStatusList.length}'` + ) + } + + const issued: Array = [] + const revoked: Array = [] + + if (previousDelta) { + for (const issuedIdx of previousDelta.issued) { + // Check whether the revocationStatusList has a different state compared to the delta + if (revocationStatusList[issuedIdx] !== RevocationState.Active) { + issued.push(issuedIdx) + } + } + + for (const revokedIdx of previousDelta.revoked) { + // Check whether the revocationStatusList has a different state compared to the delta + if (revocationStatusList[revokedIdx] !== RevocationState.Revoked) { + revoked.push(revokedIdx) + } + } + + revocationStatusList.forEach((revocationStatus, idx) => { + // Check whether the revocationStatusList entry is not included in the previous delta issued indices + if (revocationStatus === RevocationState.Active && !previousDelta.issued.includes(idx)) { + issued.push(idx) + } + + // Check whether the revocationStatusList entry is not included in the previous delta revoked indices + if (revocationStatus === RevocationState.Revoked && !previousDelta.revoked.includes(idx)) { + revoked.push(idx) + } + }) + } else { + // No delta is provided, initial state, so the entire revocation status list is converted to two list of indices + revocationStatusList.forEach((revocationStatus, idx) => { + if (revocationStatus === RevocationState.Active) issued.push(idx) + if (revocationStatus === RevocationState.Revoked) revoked.push(idx) + }) + } + + return { + issued, + revoked, + accum: currentAccumulator, + prevAccum: previousDelta?.accum, + } } diff --git a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts index 89271f5fdd..7b2dfd768c 100644 --- a/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts @@ -9,6 +9,8 @@ import { buildDidDocument } from './didIndyUtil' export class IndyVdrIndyDidResolver implements DidResolver { public readonly supportedMethods = ['indy'] + public readonly allowsCaching = true + public async resolve(agentContext: AgentContext, did: string): Promise { const didDocumentMetadata = {} try { diff --git a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts index 4484586078..2563bbbd18 100644 --- a/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts +++ b/packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts @@ -12,6 +12,8 @@ import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovU export class IndyVdrSovDidResolver implements DidResolver { public readonly supportedMethods = ['sov'] + public readonly allowsCaching = true + public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise { const didDocumentMetadata = {} diff --git a/packages/indy-vdr/tests/__fixtures__/anoncreds.ts b/packages/indy-vdr/tests/__fixtures__/anoncreds.ts index fea36d5fcb..f4b57c21f0 100644 --- a/packages/indy-vdr/tests/__fixtures__/anoncreds.ts +++ b/packages/indy-vdr/tests/__fixtures__/anoncreds.ts @@ -28,3 +28,14 @@ export const credentialDefinitionValue = { y: '1 1BF97F07270EC21A89E43BCA645D86A755F846B547238F1DA379E088CDD9B40D 1 146BB00F56FFC0DEF6541CEB484C718559B398DB1547B52850E46B23144161F1 1 079A1BEF8DFFA4E6352F701D476664340E7FBE5D3F46B897412BD2B5F10E33D7 1 02FDC508AEF90FB11961AF332BE4037973C76B954FFA48848F7E0588E93FCA8C 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', }, } + +export const revocationRegistryDefinitionValue = { + maxCredNum: 11, + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', +} diff --git a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts index c98a807b91..f55b43fd8d 100644 --- a/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts +++ b/packages/indy-vdr/tests/indy-vdr-anoncreds-registry.e2e.test.ts @@ -1,14 +1,12 @@ import type { IndyVdrDidCreateOptions, IndyVdrDidCreateResult } from '../src/dids/IndyVdrIndyDidRegistrar' -import type { RevocationRegistryEntryResponse } from '@hyperledger/indy-vdr-shared' -import { parseIndyDid } from '@aries-framework/anoncreds' +import { + getUnqualifiedRevocationRegistryDefinitionId, + parseIndyDid, + parseIndyRevocationRegistryId, +} from '@aries-framework/anoncreds' import { Agent, DidsModule, TypedArrayEncoder } from '@aries-framework/core' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' -import { - CustomRequest, - RevocationRegistryDefinitionRequest, - RevocationRegistryEntryRequest, -} from '@hyperledger/indy-vdr-shared' import { agentDependencies, getAgentConfig, importExistingIndyDidFromPrivateKey } from '../../core/tests/helpers' import { IndySdkModule } from '../../indy-sdk/src' @@ -16,10 +14,9 @@ import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { IndyVdrIndyDidResolver, IndyVdrModule, IndyVdrSovDidResolver } from '../src' import { IndyVdrAnonCredsRegistry } from '../src/anoncreds/IndyVdrAnonCredsRegistry' import { IndyVdrIndyDidRegistrar } from '../src/dids/IndyVdrIndyDidRegistrar' -import { verificationKeyForIndyDid } from '../src/dids/didIndyUtil' import { IndyVdrPoolService } from '../src/pool' -import { credentialDefinitionValue } from './__fixtures__/anoncreds' +import { credentialDefinitionValue, revocationRegistryDefinitionValue } from './__fixtures__/anoncreds' import { indyVdrModuleConfig } from './helpers' const endorserConfig = getAgentConfig('IndyVdrAnonCredsRegistryEndorser') @@ -64,7 +61,6 @@ const agent = new Agent({ }) const indyVdrPoolService = endorser.dependencyManager.resolve(IndyVdrPoolService) -const pool = indyVdrPoolService.getPoolForNamespace('pool:localtest') describe('IndyVdrAnonCredsRegistry', () => { let endorserDid: string @@ -104,7 +100,6 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyIssuerId = didCreateResult.didState.did const { namespaceIdentifier: legacyIssuerId } = parseIndyDid(didIndyIssuerId) const dynamicVersion = `1.${Math.random() * 100}` - const signingKey = await verificationKeyForIndyDid(endorser.context, didIndyIssuerId) const legacySchemaId = `${legacyIssuerId}:2:test:${dynamicVersion}` const didIndySchemaId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` @@ -120,6 +115,7 @@ describe('IndyVdrAnonCredsRegistry', () => { version: dynamicVersion, }, }) + const schemaSeqNo = schemaResult.schemaMetadata.indyLedgerSeqNo as number expect(schemaResult).toMatchObject({ schemaState: { @@ -174,8 +170,8 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) - const legacyCredentialDefinitionId = `${legacyIssuerId}:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG` - const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/CLAIM_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG` + const legacyCredentialDefinitionId = `${legacyIssuerId}:3:CL:${schemaSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/TAG` const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(endorser.context, { credentialDefinition: { issuerId: didIndyIssuerId, @@ -250,18 +246,15 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) - // We don't support creating a revocation registry using AFJ yet, so we directly use indy-vdr to create the revocation registry - const legacyRevocationRegistryId = `${legacyIssuerId}:4:${legacyIssuerId}:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` - const didIndyRevocationRegistryId = `did:indy:pool:localtest:${legacyIssuerId}/anoncreds/v0/REV_REG_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` - const revocationRegistryRequest = new RevocationRegistryDefinitionRequest({ - submitterDid: legacyIssuerId, - revocationRegistryDefinitionV1: { - credDefId: legacyCredentialDefinitionId, - id: legacyRevocationRegistryId, + const { + revocationRegistryDefinitionState: { revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId }, + } = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(endorser.context, { + revocationRegistryDefinition: { + tag: 'REV_TAG', + issuerId: didIndyIssuerId, + credDefId: didIndyCredentialDefinitionId, revocDefType: 'CL_ACCUM', - tag: 'tag', value: { - issuanceType: 'ISSUANCE_BY_DEFAULT', maxCredNum: 100, publicKeys: { accumKey: { @@ -272,55 +265,37 @@ describe('IndyVdrAnonCredsRegistry', () => { tailsLocation: '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', }, - ver: '1.0', }, - }) - - // After this call, the revocation registry should now be resolvable - const writeRequest = await pool.prepareWriteRequest( - endorser.context, - revocationRegistryRequest, - signingKey, - endorserDid - ) - const endorsedRequest = await endorser.modules.indyVdr.endorseTransaction(writeRequest.body, endorserDid) - await pool.submitRequest(new CustomRequest({ customRequest: endorsedRequest })) - - // Also create a revocation registry entry - const revocationEntryRequest = new RevocationRegistryEntryRequest({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, - revocationRegistryDefinitionType: 'CL_ACCUM', - revocationRegistryEntry: { - ver: '1.0', - value: { - accum: '1', - }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, }, - submitterDid: legacyIssuerId, }) - // After this call we can query the revocation registry entries (using timestamp now) + if (!didIndyRevocationRegistryDefinitionId) { + throw Error('revocation registry definition was not created correctly') + } - const revocationEntryWriteRequest = await pool.prepareWriteRequest( - endorser.context, - revocationEntryRequest, - signingKey, - endorserDid + const { credentialDefinitionTag, revocationRegistryTag } = parseIndyRevocationRegistryId( + didIndyRevocationRegistryDefinitionId ) - const endorsedRevocationEntryWriteRequest = await endorser.modules.indyVdr.endorseTransaction( - revocationEntryWriteRequest.body, - endorserDid + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + legacyIssuerId, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag ) - const entryResponse = (await pool.submitRequest( - new CustomRequest({ customRequest: endorsedRevocationEntryWriteRequest }) - )) as RevocationRegistryEntryResponse + + // Wait some time before resolving revocation registry definition object + await new Promise((res) => setTimeout(res, 1000)) const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( endorser.context, - legacyRevocationRegistryId + legacyRevocationRegistryDefinitionId ) + expect(legacyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: legacyIssuerId, revocDefType: 'CL_ACCUM', @@ -335,7 +310,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }, }, - tag: 'tag', + tag: 'REV_TAG', credDefId: legacyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -347,10 +322,11 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( endorser.context, - didIndyRevocationRegistryId + didIndyRevocationRegistryDefinitionId ) + expect(didIndyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: didIndyRevocationRegistryId, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: didIndyIssuerId, revocDefType: 'CL_ACCUM', @@ -365,7 +341,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }, }, - tag: 'tag', + tag: 'REV_TAG', credDefId: didIndyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -375,10 +351,30 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) + const registerStatusListResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(endorser.context, { + revocationStatusList: { + issuerId: didIndyIssuerId, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: new Array(100).fill(0), + currentAccumulator: '1', + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + if (registerStatusListResult.revocationStatusListState.state !== 'finished') { + throw new Error(`Unable to register status list: ${JSON.stringify(registerStatusListResult)}`) + } + + // Wait some time before resolving revocation status list object + await new Promise((res) => setTimeout(res, 1000)) + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( endorser.context, - legacyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + legacyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp ) expect(legacyRevocationStatusList).toMatchObject({ @@ -386,13 +382,13 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: legacyIssuerId, currentAccumulator: '1', - revRegDefId: legacyRevocationRegistryId, + revRegDefId: legacyRevocationRegistryDefinitionId, revocationList: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], - timestamp: entryResponse.result.txnMetadata.txnTime, + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -401,8 +397,8 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( endorser.context, - didIndyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + didIndyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp ) expect(didIndyRevocationStatusList).toMatchObject({ @@ -410,13 +406,13 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: didIndyIssuerId, currentAccumulator: '1', - revRegDefId: didIndyRevocationRegistryId, + revRegDefId: didIndyRevocationRegistryDefinitionId, revocationList: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], - timestamp: entryResponse.result.txnMetadata.txnTime, + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -429,8 +425,6 @@ describe('IndyVdrAnonCredsRegistry', () => { const legacyIssuerId = 'DJKobikPAaYWAu9vfhEEo5' const didIndyIssuerId = 'did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5' - const signingKey = await verificationKeyForIndyDid(agent.context, didIndyIssuerId) - const legacySchemaId = `DJKobikPAaYWAu9vfhEEo5:2:test:${dynamicVersion}` const didIndySchemaId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/SCHEMA/test/${dynamicVersion}` @@ -447,6 +441,8 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) + const schemaSeqNo = schemaResult.schemaMetadata.indyLedgerSeqNo as number + expect(schemaResult).toMatchObject({ schemaState: { state: 'finished', @@ -500,8 +496,8 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) - const legacyCredentialDefinitionId = `DJKobikPAaYWAu9vfhEEo5:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG` - const didIndyCredentialDefinitionId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/CLAIM_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG` + const legacyCredentialDefinitionId = `DJKobikPAaYWAu9vfhEEo5:3:CL:${schemaSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/CLAIM_DEF/${schemaSeqNo}/TAG` const credentialDefinitionResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(endorser.context, { credentialDefinition: { issuerId: didIndyIssuerId, @@ -576,18 +572,15 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) - // We don't support creating a revocation registry using AFJ yet, so we directly use indy-vdr to create the revocation registry - const legacyRevocationRegistryId = `DJKobikPAaYWAu9vfhEEo5:4:DJKobikPAaYWAu9vfhEEo5:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` - const didIndyRevocationRegistryId = `did:indy:pool:localtest:DJKobikPAaYWAu9vfhEEo5/anoncreds/v0/REV_REG_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` - const revocationRegistryRequest = new RevocationRegistryDefinitionRequest({ - submitterDid: 'DJKobikPAaYWAu9vfhEEo5', - revocationRegistryDefinitionV1: { - credDefId: legacyCredentialDefinitionId, - id: legacyRevocationRegistryId, + const { + revocationRegistryDefinitionState: { revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId }, + } = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(endorser.context, { + revocationRegistryDefinition: { + tag: 'REV_TAG', + issuerId: didIndyIssuerId, + credDefId: didIndyCredentialDefinitionId, revocDefType: 'CL_ACCUM', - tag: 'tag', value: { - issuanceType: 'ISSUANCE_BY_DEFAULT', maxCredNum: 100, publicKeys: { accumKey: { @@ -598,42 +591,37 @@ describe('IndyVdrAnonCredsRegistry', () => { tailsLocation: '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', }, - ver: '1.0', }, - }) - - // After this call, the revocation registry should now be resolvable - const writeRequest = await pool.prepareWriteRequest(endorser.context, revocationRegistryRequest, signingKey) - await pool.submitRequest(writeRequest) - - // Also create a revocation registry entry - const revocationEntryRequest = new RevocationRegistryEntryRequest({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, - revocationRegistryDefinitionType: 'CL_ACCUM', - revocationRegistryEntry: { - ver: '1.0', - value: { - accum: '1', - }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, }, - submitterDid: legacyIssuerId, }) - // After this call we can query the revocation registry entries (using timestamp now) + if (!didIndyRevocationRegistryDefinitionId) { + throw Error('revocation registry definition was not created correctly') + } - const revocationEntryWriteRequest = await pool.prepareWriteRequest( - endorser.context, - revocationEntryRequest, - signingKey + const { credentialDefinitionTag, revocationRegistryTag } = parseIndyRevocationRegistryId( + didIndyRevocationRegistryDefinitionId + ) + const legacyRevocationRegistryDefinitionId = getUnqualifiedRevocationRegistryDefinitionId( + legacyIssuerId, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag ) - const entryResponse = await pool.submitRequest(revocationEntryWriteRequest) + + // Wait some time before resolving revocation registry definition object + await new Promise((res) => setTimeout(res, 1000)) const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( endorser.context, - legacyRevocationRegistryId + legacyRevocationRegistryDefinitionId ) + expect(legacyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: legacyIssuerId, revocDefType: 'CL_ACCUM', @@ -648,7 +636,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }, }, - tag: 'tag', + tag: 'REV_TAG', credDefId: legacyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -660,10 +648,11 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( endorser.context, - didIndyRevocationRegistryId + didIndyRevocationRegistryDefinitionId ) + expect(didIndyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: didIndyRevocationRegistryId, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: didIndyIssuerId, revocDefType: 'CL_ACCUM', @@ -678,7 +667,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }, }, - tag: 'tag', + tag: 'REV_TAG', credDefId: didIndyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -688,10 +677,30 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) + const registerStatusListResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(endorser.context, { + revocationStatusList: { + issuerId: didIndyIssuerId, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: new Array(100).fill(0), + currentAccumulator: '1', + }, + options: { + endorserMode: 'internal', + endorserDid: endorserDid, + }, + }) + + if (registerStatusListResult.revocationStatusListState.state !== 'finished') { + throw new Error(`Unable to register status list: ${JSON.stringify(registerStatusListResult)}`) + } + + // Wait some time before resolving revocation status list object + await new Promise((res) => setTimeout(res, 1000)) + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( endorser.context, - legacyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + legacyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp ) expect(legacyRevocationStatusList).toMatchObject({ @@ -699,13 +708,13 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: legacyIssuerId, currentAccumulator: '1', - revRegDefId: legacyRevocationRegistryId, + revRegDefId: legacyRevocationRegistryDefinitionId, revocationList: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], - timestamp: entryResponse.result.txnMetadata.txnTime, + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -714,8 +723,8 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( endorser.context, - didIndyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + didIndyRevocationRegistryDefinitionId, + registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp ) expect(didIndyRevocationStatusList).toMatchObject({ @@ -723,13 +732,13 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: didIndyIssuerId, currentAccumulator: '1', - revRegDefId: didIndyRevocationRegistryId, + revRegDefId: didIndyRevocationRegistryDefinitionId, revocationList: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], - timestamp: entryResponse.result.txnMetadata.txnTime, + timestamp: registerStatusListResult.revocationStatusListState.revocationStatusList.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -737,7 +746,10 @@ describe('IndyVdrAnonCredsRegistry', () => { }) }) - test('register and resolve a schema and credential definition (external)', async () => { + test('register and resolve a schema, credential definition, revocation registry and status list (external)', async () => { + // ---- + // CREATE DID + // ---- const didCreateTxResult = (await agent.dids.create({ method: 'indy', options: { @@ -773,8 +785,10 @@ describe('IndyVdrAnonCredsRegistry', () => { const legacyIssuerId = namespaceIdentifier const didIndyIssuerId = agentDid - const signingKey = await verificationKeyForIndyDid(agent.context, didIndyIssuerId) + // ---- + // CREATE SCHEMA + // ---- const legacySchemaId = `${namespaceIdentifier}:2:test:${dynamicVersion}` const didIndySchemaId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/SCHEMA/test/${dynamicVersion}` @@ -859,6 +873,9 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) + // ---- + // CREATE CREDENTIAL DEFINITION + // ---- const legacyCredentialDefinitionId = `${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG` const didIndyCredentialDefinitionId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/CLAIM_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG` @@ -879,13 +896,13 @@ describe('IndyVdrAnonCredsRegistry', () => { const { credentialDefinitionState } = createCredDefTxResult if (credentialDefinitionState.state !== 'action' || credentialDefinitionState.action !== 'endorseIndyTransaction') - throw Error('unexpected schema state') + throw Error('unexpected credential definition state') const endorsedCredDefTx = await endorser.modules.indyVdr.endorseTransaction( credentialDefinitionState.credentialDefinitionRequest, endorserDid ) - const SubmitCredDefTxResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { + const submitCredDefTxResult = await indyVdrAnonCredsRegistry.registerCredentialDefinition(agent.context, { credentialDefinition: credentialDefinitionState.credentialDefinition, options: { endorserMode: 'external', @@ -893,7 +910,7 @@ describe('IndyVdrAnonCredsRegistry', () => { }, }) - expect(SubmitCredDefTxResult).toMatchObject({ + expect(submitCredDefTxResult).toMatchObject({ credentialDefinitionMetadata: {}, credentialDefinitionState: { credentialDefinition: { @@ -953,92 +970,76 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) - // We don't support creating a revocation registry using AFJ yet, so we directly use indy-vdr to create the revocation registry - const legacyRevocationRegistryId = `${namespaceIdentifier}:4:${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` - const didIndyRevocationRegistryId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` - const revocationRegistryRequest = new RevocationRegistryDefinitionRequest({ - submitterDid: namespaceIdentifier, - revocationRegistryDefinitionV1: { - credDefId: legacyCredentialDefinitionId, - id: legacyRevocationRegistryId, + // ---- + // CREATE REVOCATION REGISTRY + // ---- + const legacyRevocationRegistryDefinitionId = `${namespaceIdentifier}:4:${namespaceIdentifier}:3:CL:${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:REV_TAG` + const didIndyRevocationRegistryDefinitionId = `did:indy:pool:localtest:${namespaceIdentifier}/anoncreds/v0/REV_REG_DEF/${submitSchemaTxResult.schemaMetadata.indyLedgerSeqNo}/TAG/REV_TAG` + + const createRevRegDefTxResult = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(agent.context, { + revocationRegistryDefinition: { + tag: 'REV_TAG', revocDefType: 'CL_ACCUM', - tag: 'tag', - value: { - issuanceType: 'ISSUANCE_BY_DEFAULT', - maxCredNum: 100, - publicKeys: { - accumKey: { - z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', - }, - }, - tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - tailsLocation: - '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - }, - ver: '1.0', + credDefId: didIndyCredentialDefinitionId, + issuerId: didIndyIssuerId, + value: revocationRegistryDefinitionValue, + }, + options: { + endorserMode: 'external', + endorserDid, }, }) - // After this call, the revocation registry should now be resolvable - const writeRequest = await pool.prepareWriteRequest( - agent.context, - revocationRegistryRequest, - signingKey, + const { revocationRegistryDefinitionState } = createRevRegDefTxResult + + if ( + revocationRegistryDefinitionState.state !== 'action' || + revocationRegistryDefinitionState.action !== 'endorseIndyTransaction' + ) { + throw Error('unexpected revocation registry definition state') + } + + const endorsedRevRegDefTx = await endorser.modules.indyVdr.endorseTransaction( + revocationRegistryDefinitionState.revocationRegistryDefinitionRequest, endorserDid ) - const endorsedRequest = await endorser.modules.indyVdr.endorseTransaction(writeRequest.body, endorserDid) - await pool.submitRequest(new CustomRequest({ customRequest: endorsedRequest })) - - // Also create a revocation registry entry - const revocationEntryRequest = new RevocationRegistryEntryRequest({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, - revocationRegistryDefinitionType: 'CL_ACCUM', - revocationRegistryEntry: { - ver: '1.0', - value: { - accum: '1', - }, + const submitRevRegDefTxResult = await indyVdrAnonCredsRegistry.registerRevocationRegistryDefinition(agent.context, { + revocationRegistryDefinition: revocationRegistryDefinitionState.revocationRegistryDefinition, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedRevRegDefTx, }, - submitterDid: legacyIssuerId, }) - // After this call we can query the revocation registry entries (using timestamp now) + expect(submitRevRegDefTxResult).toMatchObject({ + revocationRegistryDefinitionMetadata: {}, + revocationRegistryDefinitionState: { + revocationRegistryDefinition: { + credDefId: didIndyCredentialDefinitionId, + issuerId: didIndyIssuerId, + tag: 'REV_TAG', + value: revocationRegistryDefinitionValue, + }, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, + state: 'finished', + }, + registrationMetadata: {}, + }) - const revocationEntryWriteRequest = await pool.prepareWriteRequest( - agent.context, - revocationEntryRequest, - signingKey, - endorserDid - ) - const endorsedRevEntryWriteRequest = await endorser.modules.indyVdr.endorseTransaction( - revocationEntryWriteRequest.body, - endorserDid - ) - const entryResponse = (await pool.submitRequest( - new CustomRequest({ customRequest: endorsedRevEntryWriteRequest }) - )) as RevocationRegistryEntryResponse + await new Promise((res) => setTimeout(res, 1000)) const legacyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( agent.context, - legacyRevocationRegistryId + legacyRevocationRegistryDefinitionId ) + expect(legacyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinitionId: legacyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: legacyIssuerId, revocDefType: 'CL_ACCUM', - value: { - maxCredNum: 100, - tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - tailsLocation: - '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - publicKeys: { - accumKey: { - z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', - }, - }, - }, - tag: 'tag', + value: revocationRegistryDefinitionValue, + tag: 'REV_TAG', credDefId: legacyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -1050,25 +1051,16 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationRegistryDefinition = await indyVdrAnonCredsRegistry.getRevocationRegistryDefinition( agent.context, - didIndyRevocationRegistryId + didIndyRevocationRegistryDefinitionId ) + expect(didIndyRevocationRegistryDefinition).toMatchObject({ - revocationRegistryDefinitionId: didIndyRevocationRegistryId, + revocationRegistryDefinitionId: didIndyRevocationRegistryDefinitionId, revocationRegistryDefinition: { issuerId: didIndyIssuerId, revocDefType: 'CL_ACCUM', - value: { - maxCredNum: 100, - tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - tailsLocation: - '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', - publicKeys: { - accumKey: { - z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', - }, - }, - }, - tag: 'tag', + value: revocationRegistryDefinitionValue, + tag: 'REV_TAG', credDefId: didIndyCredentialDefinitionId, }, revocationRegistryDefinitionMetadata: { @@ -1078,10 +1070,59 @@ describe('IndyVdrAnonCredsRegistry', () => { resolutionMetadata: {}, }) + // ---- + // CREATE REVOCATION STATUS LIST + // ---- + const revocationStatusListValue = { + issuerId: didIndyIssuerId, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: [1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0], + currentAccumulator: '1', + } + + const createRevStatusListTxResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(agent.context, { + options: { + endorserMode: 'external', + endorserDid, + }, + revocationStatusList: revocationStatusListValue, + }) + + const { revocationStatusListState } = createRevStatusListTxResult + + if (revocationStatusListState.state !== 'action' || revocationStatusListState.action !== 'endorseIndyTransaction') { + throw Error('unexpected revocation status list state') + } + + const endorsedRevStatusListTx = await endorser.modules.indyVdr.endorseTransaction( + revocationStatusListState.revocationStatusListRequest, + endorserDid + ) + + const submitRevStatusListTxResult = await indyVdrAnonCredsRegistry.registerRevocationStatusList(agent.context, { + revocationStatusList: revocationStatusListState.revocationStatusList, + options: { + endorserMode: 'external', + endorsedTransaction: endorsedRevStatusListTx, + }, + }) + + expect(submitRevStatusListTxResult).toMatchObject({ + revocationStatusListMetadata: {}, + revocationStatusListState: { + revocationStatusList: { ...revocationStatusListValue, timestamp: expect.any(Number) }, + state: 'finished', + }, + registrationMetadata: {}, + }) + + // Wait some time before resolving status list + await new Promise((res) => setTimeout(res, 1000)) + const legacyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( agent.context, - legacyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + legacyRevocationRegistryDefinitionId, + submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp as number ) expect(legacyRevocationStatusList).toMatchObject({ @@ -1089,13 +1130,9 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: legacyIssuerId, currentAccumulator: '1', - revRegDefId: legacyRevocationRegistryId, - revocationList: [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - timestamp: entryResponse.result.txnMetadata.txnTime, + revRegDefId: legacyRevocationRegistryDefinitionId, + revocationList: revocationStatusListValue.revocationList, + timestamp: submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', @@ -1104,8 +1141,8 @@ describe('IndyVdrAnonCredsRegistry', () => { const didIndyRevocationStatusList = await indyVdrAnonCredsRegistry.getRevocationStatusList( agent.context, - didIndyRevocationRegistryId, - entryResponse.result.txnMetadata.txnTime + didIndyRevocationRegistryDefinitionId, + submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp as number ) expect(didIndyRevocationStatusList).toMatchObject({ @@ -1113,13 +1150,9 @@ describe('IndyVdrAnonCredsRegistry', () => { revocationStatusList: { issuerId: didIndyIssuerId, currentAccumulator: '1', - revRegDefId: didIndyRevocationRegistryId, - revocationList: [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - timestamp: entryResponse.result.txnMetadata.txnTime, + revRegDefId: didIndyRevocationRegistryDefinitionId, + revocationList: revocationStatusListValue.revocationList, + timestamp: submitRevStatusListTxResult.revocationStatusListState.revocationStatusList?.timestamp, }, revocationStatusListMetadata: { didIndyNamespace: 'pool:localtest', diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json index df62927318..1f973c648e 100644 --- a/packages/sd-jwt-vc/package.json +++ b/packages/sd-jwt-vc/package.json @@ -31,7 +31,7 @@ "jwt-sd": "^0.1.2" }, "devDependencies": { - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", "reflect-metadata": "^0.1.13", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/tenants/tests/tenants.e2e.test.ts b/packages/tenants/tests/tenants.e2e.test.ts index f37e962449..ffd2518f5b 100644 --- a/packages/tenants/tests/tenants.e2e.test.ts +++ b/packages/tenants/tests/tenants.e2e.test.ts @@ -1,6 +1,6 @@ import type { InitConfig } from '@aries-framework/core' -import { ConnectionsModule, OutOfBandRecord, Agent } from '@aries-framework/core' +import { ConnectionsModule, OutOfBandRecord, Agent, CacheModule, InMemoryLruCache } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' @@ -39,6 +39,9 @@ const agent1 = new Agent({ connections: new ConnectionsModule({ autoAcceptConnections: true, }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 500 }), + }), }, dependencies: agentDependencies, }) @@ -51,6 +54,9 @@ const agent2 = new Agent({ connections: new ConnectionsModule({ autoAcceptConnections: true, }), + cache: new CacheModule({ + cache: new InMemoryLruCache({ limit: 500 }), + }), }, dependencies: agentDependencies, }) diff --git a/tests/transport/SubjectInboundTransport.ts b/tests/transport/SubjectInboundTransport.ts index 6e3b5468a2..7cf6932dec 100644 --- a/tests/transport/SubjectInboundTransport.ts +++ b/tests/transport/SubjectInboundTransport.ts @@ -48,7 +48,16 @@ export class SubjectInboundTransport implements InboundTransport { }) } - await messageReceiver.receiveMessage(message, { session }) + // This emits a custom error in order to easily catch in E2E tests when a message + // reception throws an error. TODO: Remove as soon as agent throws errors automatically + try { + await messageReceiver.receiveMessage(message, { session }) + } catch (error) { + agent.events.emit(agent.context, { + type: 'AgentReceiveMessageError', + payload: error, + }) + } }, }) } diff --git a/yarn.lock b/yarn.lock index abae25902c..59c30b61a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,6 +32,13 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@astronautlabs/jsonpath@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@astronautlabs/jsonpath/-/jsonpath-1.1.2.tgz#af19bb4a7d13dcfbc60c3c998ee1e73d7c2ddc38" + integrity sha512-FqL/muoreH7iltYC1EB5Tvox5E8NSOOPGkgns4G+qxRKl6k5dxEVljUjB5NcKESzkqwnUqWjSZkL61XGYOuV+A== + dependencies: + static-eval "2.0.2" + "@azure/core-asynciterator-polyfill@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz#0dd3849fb8d97f062a39db0e5cadc9ffaf861fec" @@ -52,11 +59,24 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" +"@babel/code-frame@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== + dependencies: + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" + "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.4.tgz#457ffe647c480dff59c2be092fc3acf71195c87f" integrity sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g== +"@babel/compat-data@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" + integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== + "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.13.16", "@babel/core@^7.20.0": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz#c6dc73242507b8e2a27fd13a9c1814f9fa34a659" @@ -78,6 +98,27 @@ json5 "^2.2.2" semver "^6.3.0" +"@babel/core@^7.14.6": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz#b028820718000f267870822fec434820e9b1e4d1" + integrity sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helpers" "^7.23.9" + "@babel/parser" "^7.23.9" + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.20.0", "@babel/generator@^7.21.4", "@babel/generator@^7.7.2": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" @@ -98,6 +139,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== + dependencies: + "@babel/types" "^7.23.6" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -116,6 +167,17 @@ lru-cache "^5.1.1" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz#3a017163dc3c2ba7deb9a7950849a9586ea24c18" @@ -197,6 +259,13 @@ dependencies: "@babel/types" "^7.21.4" +"@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + "@babel/helper-module-transforms@^7.21.2": version "7.21.2" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" @@ -211,6 +280,17 @@ "@babel/traverse" "^7.21.2" "@babel/types" "^7.21.2" +"@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" @@ -223,6 +303,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" @@ -252,6 +337,13 @@ dependencies: "@babel/types" "^7.20.2" +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers@^7.20.0": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" @@ -283,6 +375,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-string-parser@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" + integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" @@ -298,6 +395,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + "@babel/helper-wrap-function@^7.18.9": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" @@ -317,6 +419,15 @@ "@babel/traverse" "^7.21.0" "@babel/types" "^7.21.0" +"@babel/helpers@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" + integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== + dependencies: + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -335,6 +446,15 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" @@ -345,6 +465,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== +"@babel/parser@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== + "@babel/plugin-proposal-async-generator-functions@^7.0.0": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" @@ -371,6 +496,14 @@ "@babel/helper-plugin-utils" "^7.18.9" "@babel/plugin-syntax-export-default-from" "^7.18.6" +"@babel/plugin-proposal-export-namespace-from@^7.14.5": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-proposal-nullish-coalescing-operator@^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" @@ -442,6 +575,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-flow@^7.0.0", "@babel/plugin-syntax-flow@^7.18.0", "@babel/plugin-syntax-flow@^7.18.6": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.21.4.tgz#3e37fca4f06d93567c1cd9b75156422e90a67107" @@ -633,6 +773,15 @@ "@babel/helper-plugin-utils" "^7.20.2" "@babel/helper-simple-access" "^7.20.2" +"@babel/plugin-transform-modules-commonjs@^7.14.5": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz#661ae831b9577e52be57dd8356b734f9700b53b4" + integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA== + dependencies: + "@babel/helper-module-transforms" "^7.23.3" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex@^7.0.0": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" @@ -815,6 +964,15 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/template@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" + integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/traverse@^7.20.0", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" @@ -831,6 +989,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4" @@ -849,6 +1023,15 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.23.6", "@babel/types@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" + integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1071,11 +1254,93 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@digitalbazaar/bitstring@^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/bitstring/-/bitstring-3.1.0.tgz#bbbacb80eaaa53594723a801879b3a95a0401b11" + integrity sha512-Cii+Sl++qaexOvv3vchhgZFfSmtHPNIPzGegaq4ffPnflVXFu+V2qrJ17aL2+gfLxrlC/zazZFuAltyKTPq7eg== + dependencies: + base64url-universal "^2.0.0" + pako "^2.0.4" + +"@digitalbazaar/http-client@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@digitalbazaar/http-client/-/http-client-3.4.1.tgz#5116fc44290d647cfe4b615d1f3fad9d6005e44d" + integrity sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g== + dependencies: + ky "^0.33.3" + ky-universal "^0.11.0" + undici "^5.21.2" + "@digitalbazaar/security-context@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@digitalbazaar/security-context/-/security-context-1.0.0.tgz#23624692cfadc6d97e1eb787ad38a19635d89297" integrity sha512-mlj+UmodxTAdMCHGxnGVTRLHcSLyiEOVRiz3J6yiRliJWyrgeXs34wlWjBorDIEMDIjK2JwZrDuFEKO9bS5nKQ== +"@digitalbazaar/vc-status-list-context@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@digitalbazaar/vc-status-list-context/-/vc-status-list-context-3.0.1.tgz#0507b7b6f6ee8b5e7e4d402e7a2905efdc70a316" + integrity sha512-vQsqQXpmSXKNy/C0xxFUOBzz60dHh6oupQam1xRC8IspVC11hYJiX9SAhmbI0ulHvX1R2JfqZaJHZjmAyMZ/aA== + +"@digitalbazaar/vc-status-list@^7.0.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/vc-status-list/-/vc-status-list-7.1.0.tgz#1d585a1766106e1586e1e2f87092dd0381b3f036" + integrity sha512-p5uxKJlX13N8TcTuv9qFDeej+6bndU+Rh1Cez2MT+bXQE6Jpn5t336FBSHmcECB4yUfZQpkmV/LOcYU4lW8Ojw== + dependencies: + "@digitalbazaar/bitstring" "^3.0.0" + "@digitalbazaar/vc" "^5.0.0" + "@digitalbazaar/vc-status-list-context" "^3.0.1" + credentials-context "^2.0.0" + +"@digitalbazaar/vc@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@digitalbazaar/vc/-/vc-5.0.0.tgz#20180fb492cb755eb2c6b6df9a17f7407d5e4b5a" + integrity sha512-XmLM7Ag5W+XidGnFuxFIyUFSMnHnWEMJlHei602GG94+WzFJ6Ik8txzPQL8T18egSoiTsd1VekymbIlSimhuaQ== + dependencies: + credentials-context "^2.0.0" + jsonld "^8.0.0" + jsonld-signatures "^11.0.0" + +"@digitalcredentials/base58-universal@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/base58-universal/-/base58-universal-1.0.1.tgz#41b5a16cdeaac9cf01b23f1e564c560c2599b607" + integrity sha512-1xKdJnfITMvrF/sCgwBx2C4p7qcNAARyIvrAOZGqIHmBaT/hAenpC8bf44qVY+UIMuCYP23kqpIfJQebQDThDQ== + +"@digitalcredentials/base64url-universal@^2.0.2": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@digitalcredentials/base64url-universal/-/base64url-universal-2.0.6.tgz#43c59c62a33b024e7adc3c56403d18dbcb61ec61" + integrity sha512-QJyK6xS8BYNnkKLhEAgQc6Tb9DMe+GkHnBAWJKITCxVRXJAFLhJnr+FsJnCThS3x2Y0UiiDAXoWjwMqtUrp4Kg== + dependencies: + base64url "^3.0.1" + +"@digitalcredentials/bitstring@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/bitstring/-/bitstring-2.0.1.tgz#bb887f1d0999980598754e426d831c96a26a3863" + integrity sha512-9priXvsEJGI4LYHPwLqf5jv9HtQGlG0MgeuY8Q4NHN+xWz5rYMylh1TYTVThKa3XI6xF2pR2oEfKZD21eWXveQ== + dependencies: + "@digitalcredentials/base64url-universal" "^2.0.2" + pako "^2.0.4" + +"@digitalcredentials/ed25519-signature-2020@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/ed25519-signature-2020/-/ed25519-signature-2020-3.0.2.tgz#2df8fb6f814a1964b40ebb3402d41630c30120da" + integrity sha512-R8IrR21Dh+75CYriQov3nVHKaOVusbxfk9gyi6eCAwLHKn6fllUt+2LQfuUrL7Ts/sGIJqQcev7YvkX9GvyYRA== + dependencies: + "@digitalcredentials/base58-universal" "^1.0.1" + "@digitalcredentials/ed25519-verification-key-2020" "^3.1.1" + "@digitalcredentials/jsonld-signatures" "^9.3.1" + ed25519-signature-2018-context "^1.1.0" + ed25519-signature-2020-context "^1.0.1" + +"@digitalcredentials/ed25519-verification-key-2020@^3.1.1": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/ed25519-verification-key-2020/-/ed25519-verification-key-2020-3.2.2.tgz#cdf271bf4bb44dd2c417dcde6d7a0436e31d84ca" + integrity sha512-ZfxNFZlA379MZpf+gV2tUYyiZ15eGVgjtCQLWlyu3frWxsumUgv++o0OJlMnrDsWGwzFMRrsXcosd5+752rLOA== + dependencies: + "@digitalcredentials/base58-universal" "^1.0.1" + "@stablelib/ed25519" "^1.0.1" + base64url-universal "^1.1.0" + crypto-ld "^6.0.0" + "@digitalcredentials/http-client@^1.0.0": version "1.2.2" resolved "https://registry.yarnpkg.com/@digitalcredentials/http-client/-/http-client-1.2.2.tgz#8b09ab6f1e3aa8878d91d3ca51946ca8265cc92e" @@ -1095,6 +1360,17 @@ isomorphic-webcrypto "^2.3.8" serialize-error "^8.0.1" +"@digitalcredentials/jsonld-signatures@^9.3.2", "@digitalcredentials/jsonld-signatures@^9.4.0": + version "9.4.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld-signatures/-/jsonld-signatures-9.4.0.tgz#d5881122c4202449b88a7e2384f8e615ae55582c" + integrity sha512-DnR+HDTm7qpcDd0wcD1w6GdlAwfHjQSgu+ahion8REkCkkMRywF+CLunU7t8AZpFB2Gr/+N8naUtiEBNje1Oew== + dependencies: + "@digitalbazaar/security-context" "^1.0.0" + "@digitalcredentials/jsonld" "^6.0.0" + fast-text-encoding "^1.0.3" + isomorphic-webcrypto "^2.3.8" + serialize-error "^8.0.1" + "@digitalcredentials/jsonld@^5.2.1": version "5.2.1" resolved "https://registry.yarnpkg.com/@digitalcredentials/jsonld/-/jsonld-5.2.1.tgz#60acf587bec8331e86324819fd19692939118775" @@ -1115,6 +1391,11 @@ canonicalize "^1.0.1" lru-cache "^6.0.0" +"@digitalcredentials/open-badges-context@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/open-badges-context/-/open-badges-context-2.1.0.tgz#cefd29af4642adf8feeed5bb7ede663b14913c2f" + integrity sha512-VK7X5u6OoBFxkyIFplNqUPVbo+8vFSAEoam8tSozpj05KPfcGw41Tp5p9fqMnY38oPfwtZR2yDNSctj/slrE0A== + "@digitalcredentials/rdf-canonize@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@digitalcredentials/rdf-canonize/-/rdf-canonize-1.0.0.tgz#6297d512072004c2be7f280246383a9c4b0877ff" @@ -1123,15 +1404,39 @@ fast-text-encoding "^1.0.3" isomorphic-webcrypto "^2.3.8" -"@digitalcredentials/vc@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@digitalcredentials/vc/-/vc-1.1.2.tgz#868a56962f5137c29eb51eea1ba60251ebf69ad1" - integrity sha512-TSgny9XUh+W7uFjdcpvZzN7I35F9YMTv6jVINXr7UaLNgrinIjy6A5RMGQH9ecpcaoLMemKB5XjtLOOOQ3vknQ== +"@digitalcredentials/vc-status-list@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc-status-list/-/vc-status-list-5.0.2.tgz#9de8b23b6d533668a354ff464a689ecc42f24445" + integrity sha512-PI0N7SM0tXpaNLelbCNsMAi34AjOeuhUzMSYTkHdeqRPX7oT2F3ukyOssgr4koEqDxw9shHtxHu3fSJzrzcPMQ== + dependencies: + "@digitalbazaar/vc-status-list-context" "^3.0.1" + "@digitalcredentials/bitstring" "^2.0.1" + "@digitalcredentials/vc" "^4.1.1" + credentials-context "^2.0.0" + +"@digitalcredentials/vc@^4.1.1": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc/-/vc-4.2.0.tgz#d2197b26547d670965d5969a9e49437f244b5944" + integrity sha512-8Rxpn77JghJN7noBQdcMuzm/tB8vhDwPoFepr3oGd5w+CyJxOk2RnBlgIGlAAGA+mALFWECPv1rANfXno+hdjA== dependencies: "@digitalcredentials/jsonld" "^5.2.1" "@digitalcredentials/jsonld-signatures" "^9.3.1" credentials-context "^2.0.0" +"@digitalcredentials/vc@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@digitalcredentials/vc/-/vc-6.0.1.tgz#e4bdbac37d677c5288f2ad8d9ea59c3b41e0fd78" + integrity sha512-TZgLoi00Jc9uv3b6jStH+G8+bCqpHIqFw9DYODz+fVjNh197ksvcYqSndUDHa2oi0HCcK+soI8j4ba3Sa4Pl4w== + dependencies: + "@digitalbazaar/vc-status-list" "^7.0.0" + "@digitalcredentials/ed25519-signature-2020" "^3.0.2" + "@digitalcredentials/jsonld" "^6.0.0" + "@digitalcredentials/jsonld-signatures" "^9.3.2" + "@digitalcredentials/open-badges-context" "^2.1.0" + "@digitalcredentials/vc-status-list" "^5.0.2" + credentials-context "^2.0.0" + fix-esm "^1.0.1" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1164,6 +1469,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== +"@fastify/busboy@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" + integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== + "@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -1205,59 +1515,59 @@ resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== -"@hyperledger/anoncreds-nodejs@^0.2.0-dev.4": - version "0.2.0-dev.4" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.4.tgz#ac125817beb631dedbe27cb8d4c21d2123104d5e" - integrity sha512-EH/jAH+aATH9KByWF1lk1p76BN6VIsRZhG7jyRT1LAaaUNnmpQnjX6d/Mfkofvk4xFIRbp0cDl/UjaKaKfLsww== +"@hyperledger/anoncreds-nodejs@^0.2.0-dev.5": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-nodejs/-/anoncreds-nodejs-0.2.0-dev.5.tgz#6464de1220d22b3a6db68286ba9c970f6f441adb" + integrity sha512-8Comk3hx1xqcsbmS3xRtm5XS8XKymAsNM7dQ3UQeirtBkiAl1AzexraTLq/tAer6Cnmo/UpnhbEjbnJCyp8V3g== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/anoncreds-shared" "0.2.0-dev.4" + "@hyperledger/anoncreds-shared" "0.2.0-dev.5" "@mapbox/node-pre-gyp" "^1.0.11" ref-array-di "1.2.2" ref-struct-di "1.1.1" -"@hyperledger/anoncreds-shared@0.2.0-dev.4", "@hyperledger/anoncreds-shared@^0.2.0-dev.4": - version "0.2.0-dev.4" - resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.4.tgz#8050647fcb153b594671270d4c51b3b423e575be" - integrity sha512-8hwXc9zab8MgXgwo0OL5bShxMAfDEiBAB1/r+8mbwgANefDZwHwNOkq0yQLwT2KfSsvH9la7N2ehrtUf5E2FKg== +"@hyperledger/anoncreds-shared@0.2.0-dev.5", "@hyperledger/anoncreds-shared@^0.2.0-dev.5": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/anoncreds-shared/-/anoncreds-shared-0.2.0-dev.5.tgz#1d6da9db5cc16ba8766fb4db1166dbe5af63e96e" + integrity sha512-YtVju8WBKj3tdZbPWGjwdx7jkE5ePPfspPCvbcjIia00CWPES7UUkfjn8NVk82rq/Gi7IoWR3Jdpfv8rPe0fEA== -"@hyperledger/aries-askar-nodejs@^0.2.0-dev.1": - version "0.2.0-dev.1" - resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-nodejs/-/aries-askar-nodejs-0.2.0-dev.1.tgz#5b0fffe88438108e7ae34ac2f1f59fd31b9f1bc0" - integrity sha512-Ie1lw/4GNI1sGwNZ5ak6yV2dnhRLs7tPf1Q3CLPTsq1NtjPofsAAcwTfwDE2pYIdxCTXalC2ecy3VvXgtpllFA== +"@hyperledger/aries-askar-nodejs@^0.2.0-dev.5": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-nodejs/-/aries-askar-nodejs-0.2.0-dev.5.tgz#e831648d75ebde8e3f583e531710a21b08252f8d" + integrity sha512-C/17MpOP5jZdIHEAUnkQ0DymiQAPFACiw1tmBFOVhHTF7PZDtSXzzp+orewaKsXcFL5Qc1FoEyves5ougftAbw== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/aries-askar-shared" "0.2.0-dev.1" + "@hyperledger/aries-askar-shared" "0.2.0-dev.5" "@mapbox/node-pre-gyp" "^1.0.10" node-cache "^5.1.2" ref-array-di "^1.2.2" ref-struct-di "^1.1.1" -"@hyperledger/aries-askar-shared@0.2.0-dev.1", "@hyperledger/aries-askar-shared@^0.2.0-dev.1": - version "0.2.0-dev.1" - resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-shared/-/aries-askar-shared-0.2.0-dev.1.tgz#08c36f39355cc780cc3198e1cf5fc16d871ece91" - integrity sha512-I92Aflknb2HjDuT6UOcLqJjbZLQ6nP5E2Y4F9wreTIrk+nsN++UTvbuhuIz7PFddWfrT+mFtVG3E4cHaNU10pw== +"@hyperledger/aries-askar-shared@0.2.0-dev.5", "@hyperledger/aries-askar-shared@^0.2.0-dev.5": + version "0.2.0-dev.5" + resolved "https://registry.yarnpkg.com/@hyperledger/aries-askar-shared/-/aries-askar-shared-0.2.0-dev.5.tgz#81881eee427ee3f4ae2f56d248f83a6425ea79b8" + integrity sha512-H5yQEWDUL+G4rN85CyJe30dSeW7cSFHnFXaC1g9xkTXCom7eT4XxT8TpY5D/QBr3KWf26KECc/I1roZOTJQQJQ== dependencies: buffer "^6.0.3" -"@hyperledger/indy-vdr-nodejs@^0.2.0-dev.5": - version "0.2.0-dev.5" - resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.2.0-dev.5.tgz#ea40095116e0abdd4c28214122f01669367f1db5" - integrity sha512-TeeapuZBRS7+Tbn8QQ3RoMpGaI/QHjeU7TlDU5UHNoEFuZcBdDcdH6V9QAoJ1RNxc6k7tiUYKFir8LMQ+hXrXQ== +"@hyperledger/indy-vdr-nodejs@^0.2.0-dev.6": + version "0.2.0-dev.6" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-nodejs/-/indy-vdr-nodejs-0.2.0-dev.6.tgz#c21916600e17cf6ee46fc78a054cb904f9156594" + integrity sha512-yOmfOqJJJapJRWdKSJQG7q/frKGUrntoae4fiYnwdQEWy4rdRiyZPo0ht9R6uuZ/AQwxtNMMRylvQZBfHA+vKA== dependencies: "@2060.io/ffi-napi" "4.0.8" "@2060.io/ref-napi" "3.0.6" - "@hyperledger/indy-vdr-shared" "0.2.0-dev.5" + "@hyperledger/indy-vdr-shared" "0.2.0-dev.6" "@mapbox/node-pre-gyp" "^1.0.10" ref-array-di "^1.2.2" ref-struct-di "^1.1.1" -"@hyperledger/indy-vdr-shared@0.2.0-dev.5", "@hyperledger/indy-vdr-shared@^0.2.0-dev.5": - version "0.2.0-dev.5" - resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-shared/-/indy-vdr-shared-0.2.0-dev.5.tgz#ab33feda90dcbf457f3ff59da266ab32968ab48c" - integrity sha512-oPvNG5ePvtuz3H+KxWdCdxWXeo3Jxs8AFAAuG8qLPSlicEHpWchbT1amun8ccp1lk7pIBx9J0aLf08yrM5H8iw== +"@hyperledger/indy-vdr-shared@0.2.0-dev.6", "@hyperledger/indy-vdr-shared@^0.2.0-dev.6": + version "0.2.0-dev.6" + resolved "https://registry.yarnpkg.com/@hyperledger/indy-vdr-shared/-/indy-vdr-shared-0.2.0-dev.6.tgz#4954ee06fa8a2e4545b35cd525b7b86e0f10b6fe" + integrity sha512-pNLq0zkqv5rFCpU9tzyJ5DPvED5YE+UFP8iKwVD7fe+mAD6/VpweOunYNKgIBT4K1DYI21q7bs3SzxQZ0hLlKw== "@isaacs/string-locale-compare@^1.1.0": version "1.1.0" @@ -2572,6 +2882,32 @@ debug "^4.3.4" uint8arrays "^3.1.1" +"@sphereon/pex-models@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.2.tgz#e1a0ce16ccc6b32128fc8c2da79d65fc35f6d10f" + integrity sha512-Ec1qZl8tuPd+s6E+ZM7v+HkGkSOjGDMLNN1kqaxAfWpITBYtTLb+d5YvwjvBZ1P2upZ7zwNER97FfW5n/30y2w== + +"@sphereon/pex@^2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.2.tgz#3df9ed75281b46f0899256774060ed2ff982fade" + integrity sha512-NkR8iDTC2PSnYsOHlG2M2iOpFTTbzszs2/pL3iK3Dlv9QYLqX7NtPAlmeSwaoVP1NB1ewcs6U1DtemQAD+90yQ== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sphereon/pex-models" "^2.1.2" + "@sphereon/ssi-types" "^0.17.5" + ajv "^8.12.0" + ajv-formats "^2.1.1" + jwt-decode "^3.1.2" + nanoid "^3.3.6" + string.prototype.matchall "^4.0.8" + +"@sphereon/ssi-types@^0.17.5": + version "0.17.5" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.17.5.tgz#7b4de0326e7c2993ab816caeef6deaea41a5f65f" + integrity sha512-hoQOkeOtshvIzNAG+HTqcKxeGssLVfwX7oILHJgs6VMb1GhR6QlqjMAxflDxZ/8Aq2R0I6fEPWmf73zAXY2X2Q== + dependencies: + jwt-decode "^3.1.2" + "@sphereon/ssi-types@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.9.0.tgz#d140eb6abd77381926d0da7ac51b3c4b96a31b4b" @@ -2621,7 +2957,7 @@ resolved "https://registry.yarnpkg.com/@stablelib/constant-time/-/constant-time-1.0.1.tgz#bde361465e1cf7b9753061b77e376b0ca4c77e35" integrity sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg== -"@stablelib/ed25519@^1.0.2", "@stablelib/ed25519@^1.0.3": +"@stablelib/ed25519@^1.0.1", "@stablelib/ed25519@^1.0.2", "@stablelib/ed25519@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@stablelib/ed25519/-/ed25519-1.0.3.tgz#f8fdeb6f77114897c887bb6a3138d659d3f35996" integrity sha512-puIMWaX9QlRsbhxfDc5i+mNPMY+0TmQEskunY1rZEBPi1acBCVQAhnsk/1Hk50DGPtVsZtAWQg4NHGlVaO9Hqg== @@ -2929,6 +3265,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonpath@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.4.tgz#065be59981c1420832835af656377622271154be" + integrity sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA== + "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -3057,9 +3398,9 @@ "@types/node" "*" "@types/ws@^8.5.4": - version "8.5.5" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" - integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg== + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== dependencies: "@types/node" "*" @@ -3290,6 +3631,13 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -3300,6 +3648,16 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.0.0, ajv@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + anser@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" @@ -3534,6 +3892,19 @@ array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -3796,6 +4167,25 @@ base64-js@*, base64-js@^1.1.2, base64-js@^1.3.0, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64url-universal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/base64url-universal/-/base64url-universal-1.1.0.tgz#94da6356c1d43ead55b1d91c045c0a5b09ec8181" + integrity sha512-WyftvZqye29YQ10ZnuiBeEj0lk8SN8xHU9hOznkLc85wS1cLTp6RpzlMrHxMPD9nH7S55gsBqMqgGyz93rqmkA== + dependencies: + base64url "^3.0.0" + +base64url-universal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64url-universal/-/base64url-universal-2.0.0.tgz#6023785c0e349a90de1cf396e8a4519750a4e67b" + integrity sha512-6Hpg7EBf3t148C3+fMzjf+CHnADVDafWzlJUXAqqqbm4MKNXbsoPdOkWeRTjNlkYG7TpyjIpRO1Gk0SnsFD1rw== + dependencies: + base64url "^3.0.1" + +base64url@^3.0.0, base64url@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" + integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -3954,6 +4344,16 @@ browserslist@^4.21.3, browserslist@^4.21.5: node-releases "^2.0.8" update-browserslist-db "^1.0.10" +browserslist@^4.22.2: + version "4.22.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6" + integrity sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A== + dependencies: + caniuse-lite "^1.0.30001580" + electron-to-chromium "^1.4.648" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -4120,6 +4520,15 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.4, call-bind@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -4168,6 +4577,11 @@ caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz#f58a717afe92f9e69d0e35ff64df596bfad93912" integrity sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ== +caniuse-lite@^1.0.30001580: + version "1.0.30001581" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz#0dfd4db9e94edbdca67d57348ebc070dece279f4" + integrity sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ== + canonicalize@^1.0.1: version "1.0.8" resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.8.tgz#24d1f1a00ed202faafd9bf8e63352cd4450c6df1" @@ -4814,6 +5228,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto-ld@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/crypto-ld/-/crypto-ld-6.0.0.tgz#cf8dcf566cb3020bdb27f0279e6cc9b46d031cd7" + integrity sha512-XWL1LslqggNoaCI/m3I7HcvaSt9b2tYzdrXO+jHLUj9G1BvRfvV7ZTFDVY5nifYuIGAPdAGu7unPxLRustw3VA== + crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -4842,6 +5261,11 @@ data-uri-to-buffer@^3.0.1: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + dateformat@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -4859,7 +5283,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4906,7 +5330,7 @@ deep-extend@^0.6.0, deep-extend@~0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@^0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -4928,6 +5352,15 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -5119,6 +5552,16 @@ duplexer@^0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +ed25519-signature-2018-context@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ed25519-signature-2018-context/-/ed25519-signature-2018-context-1.1.0.tgz#68002ea7497c32e8170667cfd67468dedf7d220e" + integrity sha512-ppDWYMNwwp9bploq0fS4l048vHIq41nWsAbPq6H4mNVx9G/GxW3fwg4Ln0mqctP13MoEpREK7Biz8TbVVdYXqA== + +ed25519-signature-2020-context@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ed25519-signature-2020-context/-/ed25519-signature-2020-context-1.1.0.tgz#b2f724f07db154ddf0fd6605410d88736e56fd07" + integrity sha512-dBGSmoUIK6h2vadDctrDnhhTO01PR2hJk0mRNEfrRDPCjaIwrfy4J+eziEQ9Q1m8By4f/CSRgKM1h53ydKfdNg== + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -5136,6 +5579,11 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.371.tgz#393983ef087268a20c926a89be30e9f0bfc803b0" integrity sha512-jlBzY4tFcJaiUjzhRTCWAqRvTO/fWzjA3Bls0mykzGZ7zvcMP7h05W6UcgzfT9Ca1SW2xyKDOFRyI0pQeRNZGw== +electron-to-chromium@^1.4.648: + version "1.4.648" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz#c7b46c9010752c37bb4322739d6d2dd82354fbe4" + integrity sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg== + elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -5270,6 +5718,51 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: unbox-primitive "^1.0.2" which-typed-array "^1.1.9" +es-abstract@^1.22.1: + version "1.22.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.3.tgz#48e79f5573198de6dee3589195727f4f74bc4f32" + integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.2" + available-typed-arrays "^1.0.5" + call-bind "^1.0.5" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.2" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.12" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.13" + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -5346,6 +5839,18 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escodegen@^1.8.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-config-prettier@^8.3.0: version "8.8.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" @@ -5492,7 +5997,12 @@ espree@^9.5.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.0" -esprima@^4.0.0, esprima@~4.0.0: +esprima@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b" + integrity sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A== + +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -5511,7 +6021,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -5771,7 +6281,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-sta resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -5807,6 +6317,14 @@ fetch-blob@^2.1.1: resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c" integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow== +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + figlet@^1.5.2: version "1.6.0" resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.6.0.tgz#812050fa9f01043b4d44ddeb11f20fb268fa4b93" @@ -5946,6 +6464,15 @@ find-workspaces@^0.1.0: type-fest "^3.2.0" yaml "^2.1.3" +fix-esm@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fix-esm/-/fix-esm-1.0.1.tgz#e0e2199d841e43ff7db9b5f5ba7496bc45130ebb" + integrity sha512-EZtb7wPXZS54GaGxaWxMlhd1DUDCnAg5srlYdu/1ZVeW+7wwR3Tp59nu52dXByFs3MBRq+SByx1wDOJpRvLEXw== + dependencies: + "@babel/core" "^7.14.6" + "@babel/plugin-proposal-export-namespace-from" "^7.14.5" + "@babel/plugin-transform-modules-commonjs" "^7.14.5" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -5975,9 +6502,9 @@ flow-parser@^0.185.0: integrity sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ== follow-redirects@^1.14.0, follow-redirects@^1.15.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + version "1.15.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" + integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== for-each@^0.3.3: version "0.3.3" @@ -6000,6 +6527,13 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -6086,6 +6620,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -6096,6 +6635,16 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -6177,6 +6726,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -6540,6 +7099,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hermes-estree@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.8.0.tgz#530be27243ca49f008381c1f3e8b18fb26bf9ec0" @@ -7183,6 +7749,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" +is-typed-array@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -7217,6 +7790,11 @@ isarray@1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -7908,6 +8486,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -7963,11 +8546,39 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonld-signatures@^11.0.0: + version "11.2.1" + resolved "https://registry.yarnpkg.com/jsonld-signatures/-/jsonld-signatures-11.2.1.tgz#e2ff23ac7476fcdb92e5fecd9a1734ceaf904bb0" + integrity sha512-RNaHTEeRrX0jWeidPCwxMq/E/Ze94zFyEZz/v267ObbCHQlXhPO7GtkY6N5PSHQfQhZPXa8NlMBg5LiDF4dNbA== + dependencies: + "@digitalbazaar/security-context" "^1.0.0" + jsonld "^8.0.0" + serialize-error "^8.1.0" + +jsonld@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-8.3.2.tgz#7033f8994aed346b536e9046025f7f1fe9669934" + integrity sha512-MwBbq95szLwt8eVQ1Bcfwmgju/Y5P2GdtlHE2ncyfuYjIdEhluUVyj1eudacf1mOkWIoS9GpDBTECqhmq7EOaA== + dependencies: + "@digitalbazaar/http-client" "^3.4.1" + canonicalize "^1.0.1" + lru-cache "^6.0.0" + rdf-canonize "^3.4.0" + jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jsonpath@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" + integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== + dependencies: + esprima "1.2.2" + static-eval "2.0.2" + underscore "1.12.1" + just-diff-apply@^5.2.0: version "5.5.0" resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.5.0.tgz#771c2ca9fa69f3d2b54e7c3f5c1dfcbcc47f9f0f" @@ -8019,6 +8630,14 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +ky-universal@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.11.0.tgz#f5edf857865aaaea416a1968222148ad7d9e4017" + integrity sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw== + dependencies: + abort-controller "^3.0.0" + node-fetch "^3.2.10" + ky-universal@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/ky-universal/-/ky-universal-0.8.2.tgz#edc398d54cf495d7d6830aa1ab69559a3cc7f824" @@ -8032,6 +8651,11 @@ ky@^0.25.1: resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc" integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA== +ky@^0.33.3: + version "0.33.3" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" + integrity sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw== + lerna@^6.5.1: version "6.6.1" resolved "https://registry.yarnpkg.com/lerna/-/lerna-6.6.1.tgz#4897171aed64e244a2d0f9000eef5c5b228f9332" @@ -8127,6 +8751,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + libnpmaccess@6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-6.0.3.tgz#473cc3e4aadb2bc713419d92e45d23b070d8cded" @@ -9110,6 +9742,11 @@ nan@^2.11.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== +nanoid@^3.3.6: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -9240,6 +9877,11 @@ node-dir@^0.1.17: dependencies: minimatch "^3.0.2" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -9269,6 +9911,15 @@ node-fetch@^2.6.12: dependencies: whatwg-url "^5.0.0" +node-fetch@^3.2.10: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-gyp-build@^4.2.1, node-gyp-build@^4.3.0: version "4.6.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" @@ -9327,6 +9978,11 @@ node-pre-gyp@0.17.0: semver "^5.7.1" tar "^4.4.13" +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + node-releases@^2.0.8: version "2.0.10" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" @@ -9724,6 +10380,11 @@ object-inspect@^1.10.3, object-inspect@^1.12.3, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -9811,6 +10472,18 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -10015,6 +10688,11 @@ pacote@^15.0.0, pacote@^15.0.8: ssri "^10.0.0" tar "^6.1.11" +pako@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -10195,6 +10873,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -10470,6 +11153,13 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +rdf-canonize@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/rdf-canonize/-/rdf-canonize-3.4.0.tgz#87f88342b173cc371d812a07de350f0c1aa9f058" + integrity sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA== + dependencies: + setimmediate "^1.0.5" + react-devtools-core@^4.26.1: version "4.28.5" resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.28.5.tgz#c8442b91f068cdf0c899c543907f7f27d79c2508" @@ -10801,6 +11491,15 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.2.0" functions-have-names "^1.2.3" +regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" @@ -10835,6 +11534,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -10968,6 +11672,16 @@ rxjs@^7.2.0, rxjs@^7.5.5, rxjs@^7.8.0: dependencies: tslib "^2.1.0" +safe-array-concat@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -11037,7 +11751,7 @@ semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^ dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -11066,7 +11780,7 @@ serialize-error@^2.1.0: resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw== -serialize-error@^8.0.1: +serialize-error@^8.0.1, serialize-error@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== @@ -11088,6 +11802,25 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +set-function-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -11098,6 +11831,11 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -11398,6 +12136,13 @@ stacktrace-parser@^0.1.3: dependencies: type-fest "^0.7.1" +static-eval@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.2.tgz#2d1759306b1befa688938454c546b7871f806a42" + integrity sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== + dependencies: + escodegen "^1.8.1" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -11457,6 +12202,21 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string.prototype.matchall@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" + integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + regexp.prototype.flags "^1.5.0" + set-function-name "^2.0.0" + side-channel "^1.0.4" + string.prototype.trim@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" @@ -11466,6 +12226,15 @@ string.prototype.trim@^1.2.7: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" @@ -11475,6 +12244,15 @@ string.prototype.trimend@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string.prototype.trimstart@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" @@ -11484,6 +12262,15 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -11957,6 +12744,13 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -12025,6 +12819,36 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" @@ -12084,11 +12908,23 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +underscore@1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" + integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici@^5.21.2: + version "5.28.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.2.tgz#fea200eac65fc7ecaff80a023d1a0543423b4c91" + integrity sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w== + dependencies: + "@fastify/busboy" "^2.0.0" + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -12212,6 +13048,14 @@ update-browserslist-db@^1.0.10: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -12354,6 +13198,11 @@ web-did-resolver@^2.0.21: cross-fetch "^4.0.0" did-resolver "^4.0.0" +web-streams-polyfill@^3.0.3: + version "3.3.2" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz#32e26522e05128203a7de59519be3c648004343b" + integrity sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ== + webcrypto-core@^1.7.7: version "1.7.7" resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.7.tgz#06f24b3498463e570fed64d7cab149e5437b162c" @@ -12404,6 +13253,17 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== +which-typed-array@^1.1.11, which-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.4" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" @@ -12449,6 +13309,11 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -12552,9 +13417,9 @@ ws@^7, ws@^7.5.1: integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== ws@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== xstream@^11.14.0: version "11.14.0"