diff --git a/.devcontainer/AATH-container/devcontainer.json b/.devcontainer/AATH-container/devcontainer.json index 678fd1ee..a527dad5 100644 --- a/.devcontainer/AATH-container/devcontainer.json +++ b/.devcontainer/AATH-container/devcontainer.json @@ -9,6 +9,8 @@ "dockerfile": "../../aries-test-harness/Dockerfile.dev-harness" }, + "initializeCommand": "docker network inspect aath_network >/dev/null 2>&1 || docker network create aath_network", + "workspaceMount": "source=${localWorkspaceFolder}/aries-test-harness,target=/aries-test-harness,type=bind", "workspaceFolder": "/aries-test-harness", diff --git a/.devcontainer/ACA-Py-container/devcontainer.json b/.devcontainer/ACA-Py-container/devcontainer.json index d7279057..351a0e2f 100644 --- a/.devcontainer/ACA-Py-container/devcontainer.json +++ b/.devcontainer/ACA-Py-container/devcontainer.json @@ -10,6 +10,8 @@ "dockerfile": "../../aries-backchannels/acapy/Dockerfile.dev-acapy-main" }, + "initializeCommand": "docker network inspect aath_network >/dev/null 2>&1 || docker network create aath_network", + "workspaceMount": "source=${localWorkspaceFolder}/aries-backchannels/acapy,target=/aries-backchannels/acapy,type=bind", "mounts": [ diff --git a/.devcontainer/AFJ-container/devcontainer.json b/.devcontainer/AFJ-container/devcontainer.json index 49e59dff..bfa99d8d 100644 --- a/.devcontainer/AFJ-container/devcontainer.json +++ b/.devcontainer/AFJ-container/devcontainer.json @@ -9,6 +9,8 @@ // "dockerfile": "../../aries-backchannels/javascript/Dockerfile.dev-javascript" //}, + "initializeCommand": "docker network inspect aath_network >/dev/null 2>&1 || docker network create aath_network", + "workspaceMount": "source=${localWorkspaceFolder}/aries-backchannels/javascript,target=/aries-backchannels/javascript,type=bind", "workspaceFolder": "/aries-backchannels/javascript", @@ -31,10 +33,11 @@ apt-get update -y && \ apt-get install -y software-properties-common apt-transport-https curl build-essential && \ apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 && \ - add-apt-repository \"deb https://repo.sovrin.org/sdk/deb bionic stable\" && \ + add-apt-repository -y \"deb https://repo.sovrin.org/sdk/deb bionic stable\" && \ + apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3B4FE6ACC0B21F32 && \ + add-apt-repository -y \"deb http://security.ubuntu.com/ubuntu bionic-security main\" && \ apt-get install -y --allow-unauthenticated libindy", - "runArgs": [ "--network=aath_network", "--name=acme_agent" @@ -46,7 +49,7 @@ "vscode": { "settings": { "extensions.verifySignature": false, // Disable signature verification otherwise installtion takes too long or fails - "typescript.tsdk": "node_modules/typescript/lib" // Use workspace version of TypeScript + "typescript.tsdk": "server/node_modules/typescript/lib" // Use workspace version of TypeScript }, "extensions": [ "dbaeumer.vscode-eslint", // ESLint extension diff --git a/.devcontainer/Credo-ts-container/devcontainer.json b/.devcontainer/Credo-ts-container/devcontainer.json new file mode 100644 index 00000000..a4bab959 --- /dev/null +++ b/.devcontainer/Credo-ts-container/devcontainer.json @@ -0,0 +1,74 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu +{ + "image": "node:18.18-bookworm", + "name": "Credo Backchannel Dev", + // Use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + //"build": { + // "context": "../../aries-backchannels", + // "dockerfile": "../../aries-backchannels/credo-ts/Dockerfile.dev-credo-ts" + //}, + + "initializeCommand": "docker network inspect aath_network >/dev/null 2>&1 || docker network create aath_network", + + "workspaceMount": "source=${localWorkspaceFolder}/aries-backchannels/credo-ts,target=/aries-backchannels/credo-ts,type=bind", + + "workspaceFolder": "/aries-backchannels/credo-ts", + + "mounts": [ + { "source": "${localWorkspaceFolder}/aries-backchannels/data", "target": "/aries-backchannels/data", "type": "bind" } + ], + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "docker-from-docker": "latest" + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [9020], + + "runArgs": [ + "--network=aath_network", + "--name=acme_agent_dev" + ], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + "postCreateCommand": "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \ + export NVM_DIR=\"$HOME/.nvm\" && \ + [ -s \"$NVM_DIR/nvm.sh\" ] && \\. \"$NVM_DIR/nvm.sh\" && \ + [ -s \"$NVM_DIR/bash_completion\" ] && \\. \"$NVM_DIR/bash_completion\" && \ + nvm install 20 && \ + nvm use 20 && \ + npm install -g typescript && \ + yarn install && \ + cd server && \ + npm install && \ + apt-get update -y && \ + apt-get install -y software-properties-common apt-transport-https curl build-essential && \ + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 && \ + add-apt-repository -y \"deb https://repo.sovrin.org/sdk/deb bionic stable\" && \ + apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3B4FE6ACC0B21F32 && \ + add-apt-repository -y \"deb http://security.ubuntu.com/ubuntu bionic-security main\" && \ + apt-get install -y --allow-unauthenticated libindy", + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + "settings": { + "extensions.verifySignature": false, // Disable signature verification otherwise installtion takes too long or fails + "typescript.tsdk": "server/node_modules/typescript/lib" // Use workspace version of TypeScript + }, + "extensions": [ + "dbaeumer.vscode-eslint", // ESLint extension + "esbenp.prettier-vscode", // Prettier extension + "dzannotti.vscode-babel-coloring" // Babel Coloring extension + ] + } + }, + + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "root" +} diff --git a/.github/workflows/test-harness-acapy-afj.yml b/.github/workflows/skip-test-harness-acapy-afj.yml similarity index 100% rename from .github/workflows/test-harness-acapy-afj.yml rename to .github/workflows/skip-test-harness-acapy-afj.yml diff --git a/.github/workflows/test-harness-afj-acapy.yml b/.github/workflows/skip-test-harness-afj-acapy.yml similarity index 100% rename from .github/workflows/test-harness-afj-acapy.yml rename to .github/workflows/skip-test-harness-afj-acapy.yml diff --git a/.github/workflows/test-harness-afj.yml b/.github/workflows/skip-test-harness-afj.yml similarity index 100% rename from .github/workflows/test-harness-afj.yml rename to .github/workflows/skip-test-harness-afj.yml diff --git a/.github/workflows/test-harness-ariesvcx-javascript.yml b/.github/workflows/skip-test-harness-ariesvcx-javascript.yml similarity index 100% rename from .github/workflows/test-harness-ariesvcx-javascript.yml rename to .github/workflows/skip-test-harness-ariesvcx-javascript.yml diff --git a/.github/workflows/test-harness-javascript-ariesvcx.yml b/.github/workflows/skip-test-harness-javascript-ariesvcx.yml similarity index 100% rename from .github/workflows/test-harness-javascript-ariesvcx.yml rename to .github/workflows/skip-test-harness-javascript-ariesvcx.yml diff --git a/.github/workflows/test-harness-acapy-credo.yml b/.github/workflows/test-harness-acapy-credo.yml new file mode 100644 index 00000000..2370008e --- /dev/null +++ b/.github/workflows/test-harness-acapy-credo.yml @@ -0,0 +1,54 @@ +name: test-harness-acapy-credo +# RUNSET_NAME: "ACA-PY to Credo" +# Scope: AIP 1.0 +# Exceptions: Revocation +# +# Summary +# +# This runset uses the current main branch of ACA-Py for all of the agents except Bob (holder), +# which uses the master branch of Credo TS. The runset covers all of the AIP 1.0 tests +# except those that are known **not** to work with the Credo TS as the holder, +# notably those that involve revocation. +# +# Current +# +# Most of the tests are running. The tests not passing are being investigated. +# +# *Status Note Updated: 2024.09.06* +# +# End +on: + workflow_dispatch: +defaults: + run: + shell: bash +jobs: + test: + runs-on: ubuntu-latest + env: + LEDGER_URL_CONFIG: "http://localhost:9000" + TAILS_SERVER_URL_CONFIG: "http://localhost:6543" + START_TIMEOUT: 120 + steps: + - name: checkout-test-harness + uses: actions/checkout@v4 + with: + path: test-harness + - name: run-von-network + uses: ./test-harness/actions/run-von-network + - name: run-indy-tails-server + uses: ./test-harness/actions/run-indy-tails-server + - name: run-test-harness-wo-reports + uses: ./test-harness/actions/run-test-harness-wo-reports + with: + BUILD_AGENTS: "-a acapy-main -a credo" + TEST_AGENTS: "-d acapy-main -b credo" + TEST_SCOPE: "-t @AcceptanceTest -t @AIP10,@RFC0441,@RFC0211,@T001-RFC0453 -t ~@wip -t ~@DIDExchangeConnection -t ~@T004-RFC0211 -t ~@QualifiedDIDs" + REPORT_PROJECT: acapy-b-credo + - name: run-send-gen-test-results-secure + if: ${{ always() }} + uses: ./test-harness/actions/run-send-gen-test-results-secure + with: + REPORT_PROJECT: acapy-b-credo + ADMIN_USER: ${{ secrets.AllureAdminUser }} + ADMIN_PW: ${{ secrets.AllureAdminPW }} diff --git a/.github/workflows/test-harness-ariesvcx-credo.yml b/.github/workflows/test-harness-ariesvcx-credo.yml new file mode 100644 index 00000000..185b5b15 --- /dev/null +++ b/.github/workflows/test-harness-ariesvcx-credo.yml @@ -0,0 +1,51 @@ +name: test-harness-ariesvcx-credo +# RUNSET_NAME: "aries-vcx to credo" +# Scope: AIP 1.0 +# Exceptions: Public Dids +# +# Passing tests: @RFC0036,@RFC0037,@RFC0160,@revocation +# +# *Status Note Updated: 2024.09.06* +# +# End +on: + workflow_dispatch: +defaults: + run: + shell: bash +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: checkout-test-harness + uses: actions/checkout@v4 + with: + path: test-harness + - name: Docker Login + uses: azure/docker-login@v2 + with: + login-server: docker.pkg.github.com + username: $GITHUB_ACTOR + password: ${{ secrets.GITHUB_TOKEN }} + - name: run-von-network + uses: ./test-harness/actions/run-von-network + - name: run-indy-tails-server + uses: ./test-harness/actions/run-indy-tails-server + - name: run-test-harness-wo-reports + uses: ./test-harness/actions/run-test-harness-wo-reports + with: + BUILD_AGENTS: "-a aries-vcx -a credo" + TEST_AGENTS: "-d aries-vcx -b credo" + TEST_SCOPE: "-t @RFC0036,@RFC0037,@RFC0160,@revocation -t ~@T005-HIPE0011 -t ~@T006.1-HIPE0011 -t ~@RFC0025 -t ~@RFC0183 -t ~@RFC0211 -t ~@RFC0434 -t ~@RFC0453 -t ~@wip -t ~@DIDExchangeConnection -t ~@QualifiedDIDs" + REPORT_PROJECT: aries-vcx-b-credo + continue-on-error: true + - uses: actions/upload-artifact@v4 + with: + name: agent-logs + path: ./test-harness/.logs/ + - name: run-send-gen-test-results-secure + uses: ./test-harness/actions/run-send-gen-test-results-secure + with: + REPORT_PROJECT: aries-vcx-b-credo + ADMIN_USER: ${{ secrets.AllureAdminUser }} + ADMIN_PW: ${{ secrets.AllureAdminPW }} diff --git a/.github/workflows/test-harness-credo-acapy.yml b/.github/workflows/test-harness-credo-acapy.yml new file mode 100644 index 00000000..7a133d47 --- /dev/null +++ b/.github/workflows/test-harness-credo-acapy.yml @@ -0,0 +1,52 @@ +name: test-harness-credo-acapy +# RUNSET_NAME: "Credo to ACA-PY" +# Scope: AIP 1.0 +# Exceptions: Proof Proposal +# +# Summary +# +# This runset uses the current master branch of Credo TS for all of the agents except Bob (holder), +# which uses the main branch of ACA-Py. The runset covers all of the AIP 1.0 tests +# except those that are known **not** to work with the Credo TS +# +# Current +# +# All AIP10 tests are currently running. +# +# *Status Note Updated: 2024.09.06* +# +# End +on: + workflow_dispatch: +defaults: + run: + shell: bash +jobs: + test: + runs-on: ubuntu-latest + env: + LEDGER_URL_CONFIG: "http://localhost:9000" + TAILS_SERVER_URL_CONFIG: "http://localhost:6543" + steps: + - name: checkout-test-harness + uses: actions/checkout@v4 + with: + path: test-harness + - name: run-von-network + uses: ./test-harness/actions/run-von-network + - name: run-indy-tails-server + uses: ./test-harness/actions/run-indy-tails-server + - name: run-test-harness-wo-reports + uses: ./test-harness/actions/run-test-harness-wo-reports + with: + BUILD_AGENTS: "-a credo -a acapy-main" + TEST_AGENTS: "-d credo -b acapy-main" + TEST_SCOPE: "-t @AcceptanceTest -t ~@wip -t @AIP10,@RFC0211,@T001-RFC0453 -t ~@Transport_NoHttpOutbound -t ~@DIDExchangeConnection -t ~@QualifiedDIDs" + REPORT_PROJECT: credo-b-acapy + - name: run-send-gen-test-results-secure + if: ${{ always() }} + uses: ./test-harness/actions/run-send-gen-test-results-secure + with: + REPORT_PROJECT: credo-b-acapy + ADMIN_USER: ${{ secrets.AllureAdminUser }} + ADMIN_PW: ${{ secrets.AllureAdminPW }} diff --git a/.github/workflows/test-harness-credo-ariesvcx.yml b/.github/workflows/test-harness-credo-ariesvcx.yml new file mode 100644 index 00000000..a36d356f --- /dev/null +++ b/.github/workflows/test-harness-credo-ariesvcx.yml @@ -0,0 +1,51 @@ +name: test-harness-credo-ariesvcx +# RUNSET_NAME: "credo to aries-vcx" +# Scope: AIP 1.0 +# Exceptions: Public Dids, Revocations +# +# Passing tests: @RFC0036,@RFC0037,@RFC0160,@revocation +# +# *Status Note Updated: 2024.09.06* +# +# End +on: + workflow_dispatch: +defaults: + run: + shell: bash +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: checkout-test-harness + uses: actions/checkout@v4 + with: + path: test-harness + - name: Docker Login + uses: azure/docker-login@v2 + with: + login-server: docker.pkg.github.com + username: $GITHUB_ACTOR + password: ${{ secrets.GITHUB_TOKEN }} + - name: run-von-network + uses: ./test-harness/actions/run-von-network + - name: run-indy-tails-server + uses: ./test-harness/actions/run-indy-tails-server + - name: run-test-harness-wo-reports + uses: ./test-harness/actions/run-test-harness-wo-reports + with: + BUILD_AGENTS: "-a aries-vcx -a credo" + TEST_AGENTS: "-d credo -b aries-vcx" + TEST_SCOPE: "-t @RFC0036,@RFC0037,@RFC0160 -t ~@T003-RFC0160 -t ~@T004-RFC0160 -t ~@revocation -t ~@RFC0025 -t ~@RFC0183 -t ~@RFC0211 -t ~@RFC0434 -t ~@RFC0453 -t ~@wip -t ~@DIDExchangeConnection -t ~@QualifiedDIDs" + REPORT_PROJECT: credo-b-aries-vcx + continue-on-error: true + - uses: actions/upload-artifact@v4 + with: + name: agent-logs + path: ./test-harness/.logs/ + - name: run-send-gen-test-results-secure + uses: ./test-harness/actions/run-send-gen-test-results-secure + with: + REPORT_PROJECT: credo-b-aries-vcx + ADMIN_USER: ${{ secrets.AllureAdminUser }} + ADMIN_PW: ${{ secrets.AllureAdminPW }} diff --git a/.github/workflows/test-harness-credo.yml b/.github/workflows/test-harness-credo.yml new file mode 100644 index 00000000..b867de1c --- /dev/null +++ b/.github/workflows/test-harness-credo.yml @@ -0,0 +1,51 @@ +name: test-harness-credo-credo +# RUNSET_NAME: "Credo to Credo" +# Scope: AIP 1.0 +# Exceptions: Revocation +# +# Summary +# +# This runset uses the current main branch of Aries Framework - Credo-ts for all of the agents. The runset runs all of the tests in the suite +# that are expected to pass given the current state of the framework's support for AIP 1. Tests related to revocation (Indy HIPE 0011) are not included. +# +# Current +# +# All of the tests being executed in this runset are passing. +# +# *Status Note Updated: 2024.07.29* +# +# End +on: + workflow_dispatch: +defaults: + run: + shell: bash +jobs: + test: + runs-on: ubuntu-latest + env: + LEDGER_URL_CONFIG: "http://localhost:9000" + TAILS_SERVER_URL_CONFIG: "http://localhost:6543" + steps: + - name: checkout-test-harness + uses: actions/checkout@v4 + with: + path: test-harness + - name: run-von-network + uses: ./test-harness/actions/run-von-network + - name: run-indy-tails-server + uses: ./test-harness/actions/run-indy-tails-server + - name: run-test-harness-wo-reports + uses: ./test-harness/actions/run-test-harness-wo-reports + with: + BUILD_AGENTS: "-a credo" + TEST_AGENTS: "-d credo" + TEST_SCOPE: "-t @AcceptanceTest -t ~@wip -t @AIP10,@RFC0211 -t ~@DIDExchangeConnection -t ~@QualifiedDIDs" + REPORT_PROJECT: credo + - name: run-send-gen-test-results-secure + if: ${{ always() }} + uses: ./test-harness/actions/run-send-gen-test-results-secure + with: + REPORT_PROJECT: credo + ADMIN_USER: ${{ secrets.AllureAdminUser }} + ADMIN_PW: ${{ secrets.AllureAdminPW }} diff --git a/aries-backchannels/credo-ts/.vscode/launch.json b/aries-backchannels/credo-ts/.vscode/launch.json new file mode 100644 index 00000000..04c19353 --- /dev/null +++ b/aries-backchannels/credo-ts/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "tsc Launch & Debug Credo Agent", + "program": "${workspaceFolder}/src/index.ts", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": ["${workspaceFolder}/out/**/*.js"] + }, + { + "type": "node", + "request": "launch", + "name": "yarn Launch & Debug Credo Agent", + "runtimeExecutable": "yarn", + "env": { + "LEDGER_URL": "http://test.bcovrin.vonx.io", + "TAILS_SERVER_URL": "http://tails.bcovrin.vonx.io", + "DOCKERHOST": "acme_agent_dev" + }, + "args": [ + "ts-node", + "src/index.ts", + "9020" + ], + "cwd": "${workspaceFolder}/server", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/aries-backchannels/credo-ts/.vscode/tasks.json b/aries-backchannels/credo-ts/.vscode/tasks.json new file mode 100644 index 00000000..9a010f0c --- /dev/null +++ b/aries-backchannels/credo-ts/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "label": "tsc: build - tsconfig.json", + "command": "tsc", + "args": ["-p", "server/tsconfig.json"], + "problemMatcher": ["$tsc"] + } + ] +} \ No newline at end of file diff --git a/aries-backchannels/credo-ts/Dockerfile.credo b/aries-backchannels/credo-ts/Dockerfile.credo new file mode 100644 index 00000000..b93978b7 --- /dev/null +++ b/aries-backchannels/credo-ts/Dockerfile.credo @@ -0,0 +1,49 @@ +FROM ubuntu:22.04 AS base + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -y && apt-get install -y \ + software-properties-common \ + apt-transport-https \ + curl \ + # Only needed to build indy-sdk + build-essential + +# libindy +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 +RUN add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" + +# nodejs +RUN curl -sL https://deb.nodesource.com/setup_20.x | bash +RUN apt-get install -y nodejs + +# yarn +# RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ +# echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list +RUN npm install -g yarn + +# install depdencies +# RUN apt-get update -y && apt-get install -y --allow-unauthenticated \ +# nodejs + #libindy \ + + +# Install yarn seperately due to `no-install-recommends` to skip nodejs install +#RUN apt-get install -y --no-install-recommends yarn + +RUN apt-get upgrade -y libstdc++6 + + +WORKDIR /src +ENV RUN_MODE="docker" + +COPY credo-ts/server . +COPY credo-ts/ngrok-wait.sh ./ngrok-wait.sh + +# Run install after copying only depdendency file +# to make use of docker layer caching +RUN yarn install + +# For now we use ts-node. Compiling with typescript +# doesn't work because indy-sdk types are not exported +ENTRYPOINT [ "bash", "ngrok-wait.sh"] \ No newline at end of file diff --git a/aries-backchannels/credo-ts/Dockerfile.credo-arm b/aries-backchannels/credo-ts/Dockerfile.credo-arm new file mode 100644 index 00000000..42b94b1a --- /dev/null +++ b/aries-backchannels/credo-ts/Dockerfile.credo-arm @@ -0,0 +1,41 @@ +FROM snel/von-image:node-1.12-4-arm64 + +ENV DEBIAN_FRONTEND noninteractive + +USER root + +RUN apt-get update -y && apt-get install -y \ + software-properties-common \ + apt-transport-https \ + curl \ + # Only needed to build indy-sdk + build-essential + +# nodejs +RUN curl -sL https://deb.nodesource.com/setup_12.x | bash + +# yarn +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list + +# install depdencies +RUN apt-get update -y && apt-get install -y --allow-unauthenticated \ + nodejs + +# Install yarn seperately due to `no-install-recommends` to skip nodejs install +RUN apt-get install -y --no-install-recommends yarn + +WORKDIR /src +ENV RUN_MODE="docker" + +COPY javascript/server/package.json package.json + +# Run install after copying only depdendency file +# to make use of docker layer caching +RUN yarn install + +# Copy other depedencies +COPY javascript/server . +COPY javascript/ngrok-wait.sh ./ngrok-wait.sh + +ENTRYPOINT [ "bash", "ngrok-wait.sh"] \ No newline at end of file diff --git a/aries-backchannels/credo-ts/Dockerfile.credo-branch b/aries-backchannels/credo-ts/Dockerfile.credo-branch new file mode 100644 index 00000000..10285195 --- /dev/null +++ b/aries-backchannels/credo-ts/Dockerfile.credo-branch @@ -0,0 +1,56 @@ +FROM ubuntu:18.04 as base + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update -y && apt-get install -y \ + software-properties-common \ + apt-transport-https \ + curl \ + git \ + # Only needed to build indy-sdk + build-essential + +# libindy +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 +RUN add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" + +# nodejs +RUN curl -sL https://deb.nodesource.com/setup_12.x | bash + +# yarn +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list + +# install depdencies +RUN apt-get update -y && apt-get install -y --allow-unauthenticated \ + libindy \ + nodejs + +# Install yarn seperately due to `no-install-recommends` to skip nodejs install +RUN apt-get install -y --no-install-recommends yarn + +FROM base as final + +WORKDIR /dependencies + +RUN git clone https://github.com/TimoGlastra/credo-ts && cd credo-ts && git checkout fix/credential-preview-type && yarn install && yarn build + +WORKDIR /src +ENV RUN_MODE="docker" + +COPY javascript/server/package.json package.json + +RUN yarn add file:/dependencies/credo-ts/packages/core +RUN yarn add file:/dependencies/credo-ts/packages/node + +# Run install after copying only depdendency file +# to make use of docker layer caching +RUN yarn install + +# Copy other depedencies +COPY credo/server . +COPY credo/ngrok-wait.sh ./ngrok-wait.sh + +# For now we use ts-node. Compiling with typescript +# doesn't work because indy-sdk types are not exported +ENTRYPOINT [ "bash", "ngrok-wait.sh"] \ No newline at end of file diff --git a/aries-backchannels/credo-ts/Dockerfile.dev-credo b/aries-backchannels/credo-ts/Dockerfile.dev-credo new file mode 100644 index 00000000..b6f6b895 --- /dev/null +++ b/aries-backchannels/credo-ts/Dockerfile.dev-credo @@ -0,0 +1,48 @@ +FROM ubuntu:22.04 as base + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update -y && apt-get install -y \ + software-properties-common \ + apt-transport-https \ + curl \ + # Only needed to build indy-sdk + build-essential + +# libindy +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 +RUN add-apt-repository "deb https://repo.sovrin.org/sdk/deb bionic stable" + +# nodejs +RUN curl -sL https://deb.nodesource.com/setup_18.x | bash + +# yarn +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list + +# install depdencies +RUN apt-get update -y && apt-get install -y --allow-unauthenticated \ + libindy \ + nodejs + +# Install yarn seperately due to `no-install-recommends` to skip nodejs install +RUN apt-get install -y --no-install-recommends yarn + +FROM base as final + +WORKDIR /src +ENV RUN_MODE="docker" + +#COPY javascript/server/package.json package.json + +# Run install after copying only depdendency file +# to make use of docker layer caching +RUN yarn install + +# Copy other depedencies +# COPY javascript/server . +# COPY javascript/ngrok-wait.sh ./ngrok-wait.sh + +# For now we use ts-node. Compiling with typescript +# doesn't work because indy-sdk types are not exported +#ENTRYPOINT [ "bash", "ngrok-wait.sh"] \ No newline at end of file diff --git a/aries-backchannels/credo-ts/ngrok-wait.sh b/aries-backchannels/credo-ts/ngrok-wait.sh new file mode 100755 index 00000000..80aeccfa --- /dev/null +++ b/aries-backchannels/credo-ts/ngrok-wait.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# based on code developed by Sovrin: https://github.com/hyperledger/aries-acapy-plugin-toolbox + +if [[ ! "${NGROK_NAME}" = "" ]]; then + echo "using ngrok end point [$NGROK_NAME]" + + NGROK_ENDPOINT=null + while [ -z "$NGROK_ENDPOINT" ] || [ "$NGROK_ENDPOINT" = "null" ] + do + # original get endpoint + #NGROK_ENDPOINT=$(curl -s $NGROK_NAME:4040/api/tunnels/command_line | grep -o "https:\/\/.*\.ngrok\.io") + echo "Fetching end point from ngrok service [$NGROK_NAME] for [$CONTAINER_NAME] ..." + NGROK_ENDPOINT=$(curl --silent $NGROK_NAME:4040/api/tunnels | ./jq -r --arg CONTAINER_NAME "$CONTAINER_NAME" 'first(.tunnels[] | select(.proto=="https" and (.name | index($CONTAINER_NAME))) | .public_url)') + + if [ -z "$NGROK_ENDPOINT" ] || [ "$NGROK_ENDPOINT" = "null" ]; then + echo "ngrok not ready, sleeping 5 seconds...." + sleep 5 + fi + done + + export AGENT_PUBLIC_ENDPOINT=$NGROK_ENDPOINT + echo "fetched end point [$AGENT_PUBLIC_ENDPOINT]" +fi + +echo "Starting Credo agent ..." + +yarn ts-node src/index.ts "$@" diff --git a/aries-backchannels/credo-ts/server/.gitignore b/aries-backchannels/credo-ts/server/.gitignore new file mode 100644 index 00000000..f77e021e --- /dev/null +++ b/aries-backchannels/credo-ts/server/.gitignore @@ -0,0 +1,56 @@ +### Node template +.DS_Store +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules +.npmrc +*.log + +# Typings +typings/ + +# Typescript +src/**/*.js +src/**/*.js.map +test/**/*.js +test/**/*.js.map + +# Test +/.tmp +/.nyc_output + +# IDE +.vscode +.idea + +# Project +/public +/dist + +# We want to fetch the latest aries framework on each run +yarn.lock \ No newline at end of file diff --git a/aries-backchannels/credo-ts/server/.prettierrc b/aries-backchannels/credo-ts/server/.prettierrc new file mode 100644 index 00000000..cbe842ac --- /dev/null +++ b/aries-backchannels/credo-ts/server/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "semi": false, + "singleQuote": true +} diff --git a/aries-backchannels/credo-ts/server/package.json b/aries-backchannels/credo-ts/server/package.json new file mode 100644 index 00000000..6804b62b --- /dev/null +++ b/aries-backchannels/credo-ts/server/package.json @@ -0,0 +1,51 @@ +{ + "name": "credo-ts-backchannel", + "version": "1.0.0", + "license": "Apache-2.0", + "description": "Aries Agent Test Harness backchannel for Credo-ts", + "scripts": { + "build": "yarn tsc", + "tsc": "tsc --project tsconfig.compile.json", + "start": "nodemon --watch \"src/**/*.ts\" --ignore \"node_modules/**/*\" --exec ts-node src/index.ts", + "start:prod": "cross-env NODE_ENV=production node dist/index.js", + "format": "prettier --ignore-path .gitignore '**/*.+(js|json|ts|md|yml|yaml)' --write" + }, + "dependencies": { + "@credo-ts/anoncreds": "^0.5.3", + "@credo-ts/askar": "^0.5.3", + "@credo-ts/core": "^0.5.3", + "@credo-ts/indy-vdr": "^0.5.3", + "@credo-ts/node": "^0.5.3", + "@hyperledger/anoncreds-nodejs": "^0.2.1", + "@hyperledger/aries-askar-nodejs": "^0.2.1", + "@hyperledger/indy-vdr-nodejs": "^0.2.0", + "@tsed/common": "^6.75.4", + "@tsed/core": "^6.75.4", + "@tsed/di": "^6.75.4", + "@tsed/exceptions": "^6.75.4", + "@tsed/platform-express": "^6.75.4", + "@tsed/swagger": "^6.88.0", + "body-parser": "1.19.0", + "cross-env": "7.0.3", + "express": "^4.17.1", + "minimist": "^1.2.5", + "node-fetch": "^2.6.5", + "rxjs": "^7.4.0" + }, + "devDependencies": { + "@types/express": "4.17.13", + "@types/minimist": "^1.2.0", + "@types/node": "14.14.28", + "@types/node-fetch": "^2.6.3", + "@types/ref-array-di": "^1.2.8", + "@types/ref-struct-di": "^1.1.12", + "nodemon": "2.0.14", + "prettier": "^2.4.1", + "ts-node": "10.3.0", + "typescript": "^5.4.5" + }, + "resolutions": { + "ffi-napi": "npm:@2060.io/ffi-napi", + "ref-napi": "npm:@2060.io/ref-napi" + } +} diff --git a/aries-backchannels/credo-ts/server/src/BaseController.ts b/aries-backchannels/credo-ts/server/src/BaseController.ts new file mode 100644 index 00000000..4140d176 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/BaseController.ts @@ -0,0 +1,17 @@ +import { TestHarnessConfig } from './TestHarnessConfig' + +export abstract class BaseController { + protected testHarnessConfig: TestHarnessConfig + + public constructor(testHarnessConfig: TestHarnessConfig) { + this.testHarnessConfig = testHarnessConfig + + this.testHarnessConfig.addController(this) + } + + protected get agent() { + return this.testHarnessConfig.agent + } + + public onStartup(): Promise | void {} +} diff --git a/aries-backchannels/credo-ts/server/src/Server.ts b/aries-backchannels/credo-ts/server/src/Server.ts new file mode 100644 index 00000000..56e37394 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/Server.ts @@ -0,0 +1,34 @@ +import { Configuration, Inject } from '@tsed/di' +import { PlatformApplication } from '@tsed/common' +import '@tsed/platform-express' // /!\ keep this import +import '@tsed/swagger' // import swagger Ts.ED module +import * as bodyParser from 'body-parser' +export const rootDir = __dirname + +@Configuration({ + rootDir, + acceptMimes: ['application/json'], + httpsPort: false, + swagger: [ + { + path: '/docs', + specVersion: '3.0.3', + }, + ], + mount: { + '/': [`${rootDir}/controllers/**/*.ts`], + }, +}) +export class Server { + @Inject() + app!: PlatformApplication + + @Configuration() + settings!: Configuration + + $beforeRoutesInit() { + this.app.use(bodyParser.json()) + + return null + } +} diff --git a/aries-backchannels/credo-ts/server/src/TestAgent.ts b/aries-backchannels/credo-ts/server/src/TestAgent.ts new file mode 100644 index 00000000..efceac37 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/TestAgent.ts @@ -0,0 +1,123 @@ +import { $log } from '@tsed/common' +import { Agent, AgentEventTypes, AgentMessageProcessedEvent, AutoAcceptCredential, AutoAcceptProof, CredentialsModule, DidsModule, InitConfig, MediatorModule, ProofsModule, V2CredentialProtocol, V2ProofProtocol, KeyDidCreateOptions } from '@credo-ts/core' +import { agentDependencies } from '@credo-ts/node' +import { AskarModule } from '@credo-ts/askar' +import { AnonCredsModule, LegacyIndyCredentialFormatService, LegacyIndyProofFormatService, V1CredentialProtocol, V1ProofProtocol, AnonCredsCredentialFormatService, AnonCredsProofFormatService } from '@credo-ts/anoncreds' +//import { AnonCredsRsModule } from '@aries-framework/anoncreds-rs' +import { IndyVdrAnonCredsRegistry, IndyVdrModule, IndyVdrSovDidResolver, IndyVdrIndyDidResolver, IndyVdrPoolConfig, IndyVdrIndyDidRegistrar } from '@credo-ts/indy-vdr' +import { TsedLogger } from './TsedLogger' +import { TransportConfig } from './TestHarnessConfig' +import { anoncreds } from '@hyperledger/anoncreds-nodejs' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { indyVdr } from '@hyperledger/indy-vdr-nodejs' +import { HttpInboundTransport } from '@credo-ts/node' + +export type TestAgent = Agent> + +export async function createAgent({ + genesisPath, + agentName, + transport, +}: { + genesisPath: string + agentName: string + transport: TransportConfig + useLegacyIndySdk?: boolean +}) { + const agentConfig: InitConfig = { + label: agentName, + walletConfig: { + id: `aath-credo-${Date.now()}`, + key: '00000000000000000000000000000Test01', + }, + endpoints: transport.endpoints, + useDidSovPrefixWhereAllowed: true, + logger: new TsedLogger($log), + } + + const genesisTransactions = await new agentDependencies.FileSystem().read(genesisPath) + + const modules = getAskarAnonCredsIndyModules({ + //indyNamespace: 'main-pool', + indyNamespace: 'bcovrin:test', + isProduction: false, + genesisTransactions, + // connectOnStartup: true, //TODO Should we do this in the test agent? We never did but all sample code I see includeing the demo does this + }) + + const agent = new Agent({ config: agentConfig, dependencies: agentDependencies, + modules + }) + + for (const it of transport.inboundTransports) { + agent.registerInboundTransport(it) + } + + for (const ot of transport.outboundTransports) { + agent.registerOutboundTransport(ot) + } + + await agent.initialize() + + // If at least a link secret is found, we assume there is a default one + if ((await agent.modules.anoncreds.getLinkSecretIds()).length === 0) { + await agent.modules.anoncreds.createLinkSecret() + } + + + agent.events.on(AgentEventTypes.AgentMessageProcessed, (data: AgentMessageProcessedEvent) => { + agent.config.logger.debug(`Processed inbound message: ${JSON.stringify(data.payload.message.toJSON())}`) + }) + + return agent +} + +export function getAskarAnonCredsIndyModules(indyNetworkConfig: IndyVdrPoolConfig) { + const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() + const legacyIndyProofFormatService = new LegacyIndyProofFormatService() + + return { + mediator: new MediatorModule({ + // Needed to accept mediation requests: https://github.com/hyperledger/aries-framework-javascript/issues/668 + autoAcceptMediationRequests: true, + }), + credentials: new CredentialsModule({ + autoAcceptCredentials: AutoAcceptCredential.Never, + credentialProtocols: [ + new V1CredentialProtocol({ + indyCredentialFormat: legacyIndyCredentialFormatService, + }), + new V2CredentialProtocol({ + // Credo Update - added AnonCredsCredentialFormatService as shown here, https://credo.js.org/guides/tutorials/issue-an-anoncreds-credential-over-didcomm#issuer + credentialFormats: [legacyIndyCredentialFormatService, new AnonCredsCredentialFormatService()], + }), + ], + }), + proofs: new ProofsModule({ + autoAcceptProofs: AutoAcceptProof.Never, + proofProtocols: [ + new V1ProofProtocol({ + indyProofFormat: legacyIndyProofFormatService, + }), + new V2ProofProtocol({ + proofFormats: [legacyIndyProofFormatService, new AnonCredsProofFormatService], + }), + ], + }), + anoncreds: new AnonCredsModule({ + registries: [new IndyVdrAnonCredsRegistry()], + anoncreds, + }), + //anoncredsRs: new AnonCredsRsModule({ anoncreds }), + indyVdr: new IndyVdrModule({ + indyVdr, + networks: [indyNetworkConfig], + }), + dids: new DidsModule({ + // Credo Update - added from registrars: [new IndyVdrIndyDidRegistrar()], as shown here, https://credo.js.org/guides/tutorials/issue-an-anoncreds-credential-over-didcomm#issuer + registrars: [new IndyVdrIndyDidRegistrar()], + resolvers: [new IndyVdrSovDidResolver(), new IndyVdrIndyDidResolver()], + }), + askar: new AskarModule({ ariesAskar }), + } as const +} diff --git a/aries-backchannels/credo-ts/server/src/TestHarnessConfig.ts b/aries-backchannels/credo-ts/server/src/TestHarnessConfig.ts new file mode 100644 index 00000000..64d981e8 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/TestHarnessConfig.ts @@ -0,0 +1,180 @@ +import { + HttpOutboundTransport, + WsOutboundTransport, + InboundTransport, + OutboundTransport, + KeyType, + TypedArrayEncoder, +} from '@credo-ts/core' +import { HttpInboundTransport, WsInboundTransport } from '@credo-ts/node' +import { $log } from '@tsed/common' +import { BaseController } from './BaseController' +import { createAgent, TestAgent } from './TestAgent' +import { getGenesisPath, getRandomSeed, registerPublicDid } from './utils/ledgerUtils' +import { indyDidFromPublicKeyBase58 } from '@credo-ts/core/build/utils/did' + +export class TestHarnessConfig { + private _createAgentArgs?: CreateAgentArguments + private agentPorts: AgentPorts + + public readonly backchannelPort: number + + private externalHost: string + private dockerHost: string + private runMode?: string + + private _agent?: TestAgent + + private _controllers: BaseController[] = [] + + public constructor({ backchannelPort }: { backchannelPort: number }) { + this.agentPorts = { + http: backchannelPort + 1, + ws: backchannelPort + 2, + } + + this.backchannelPort = backchannelPort + + this.dockerHost = process.env.DOCKERHOST ?? 'host.docker.internal' + this.runMode = process.env.RUN_MODE + this.externalHost = this.runMode === 'docker' ? this.dockerHost : 'localhost' + } + + public addController(controller: BaseController): void { + $log.info('Register controller') + this._controllers.push(controller) + } + + public get agent() { + if (!this._agent) { + throw new Error('Agent not initialized') + } + + return this._agent + } + + public async startAgent(options: { inboundTransports: Transport[]; outboundTransports: Transport[] }) { + const agentArgs = await this.getAgentArgs(options) + + this._agent = await createAgent(agentArgs) + + const key = await this._agent.context.wallet.createKey({keyType: KeyType.Ed25519, privateKey: TypedArrayEncoder.fromString(agentArgs.publicDidSeed)}) + + const didInfo = { + did: indyDidFromPublicKeyBase58(key.publicKeyBase58), + verkey: key.publicKeyBase58 + } + await this.agent.genericRecords.save({ content: { didInfo }, id: 'PUBLIC_DID_INFO' }) + } + + public async agentStartup() { + // Call handlers + for (const controller of this._controllers) { + await controller.onStartup.call(controller) + } + } + + public async stopAgent() { + await this.agent.shutdown() + } + + private async getAgentArgs(options: { inboundTransports: Transport[]; outboundTransports: Transport[] }) { + let agentArgs = this._createAgentArgs + if (!agentArgs) { + const agentName = process.env.AGENT_NAME ? `Credo ${process.env.AGENT_NAME}` : `Credo Agent (${this.agentPorts.http})` + + // There are multiple ways to retrieve the genesis file + // we account for all of them + const genesisFile = process.env.GENESIS_FILE + const genesisUrl = process.env.GENESIS_URL + const ledgerUrl = process.env.LEDGER_URL ?? `http://${this.externalHost}:9000` + const genesisPath = await getGenesisPath(genesisFile, genesisUrl, ledgerUrl, this.dockerHost) + + // Register public did + const publicDidSeed = getRandomSeed() + + await registerPublicDid(ledgerUrl, publicDidSeed) + + agentArgs = { + agentName, + publicDidSeed, + genesisPath, + } + + this._createAgentArgs = agentArgs + } + + return { ...agentArgs, transport: this.getTransportConfig(options) } + } + + private getTransportConfig(options: { + inboundTransports: Transport[] + outboundTransports: Transport[] + }): TransportConfig { + const inbound: InboundTransport[] = [] + const outbound: OutboundTransport[] = [] + const endpoints: string[] = [] + + for (const inboundTransport of options.inboundTransports) { + const InboundTransport = inboundTransportMapping[inboundTransport] + inbound.push(new InboundTransport({ port: this.agentPorts[inboundTransport] })) + + endpoints.push(this.getAgentEndpoint(inboundTransport)) + } + + for (const outboundTransport of options.outboundTransports) { + const OutboundTransport = outboundTransportMapping[outboundTransport] + outbound.push(new OutboundTransport()) + } + + return { + inboundTransports: inbound, + outboundTransports: outbound, + endpoints, + } + } + + private getAgentEndpoint(transport: Transport) { + const port = this.agentPorts[transport].toString() + + // may be set via ngrok. Not supported for WS + const agentEndpoint = process.env.AGENT_PUBLIC_ENDPOINT + if (transport == 'http' && agentEndpoint) { + return agentEndpoint + } else if (process.env.RUN_MODE == 'pwd') { + return `${transport}://${this.externalHost}`.replace('{PORT}', port) + } + + return `${transport}://${this.externalHost}:${port}` + } +} + +export interface CreateAgentArguments { + agentName: string + publicDidSeed: string + genesisPath: string + useLegacyIndySdk?: boolean +} + +export interface AgentPorts { + http: number + ws: number +} + +export type Transport = 'http' | 'ws' + +const inboundTransportMapping = { + http: HttpInboundTransport, + ws: WsInboundTransport, +} as const + +const outboundTransportMapping = { + http: HttpOutboundTransport, + ws: WsOutboundTransport, +} as const + +export interface TransportConfig { + inboundTransports: InboundTransport[] + outboundTransports: OutboundTransport[] + endpoints: string[] +} diff --git a/aries-backchannels/credo-ts/server/src/TsedLogger.ts b/aries-backchannels/credo-ts/server/src/TsedLogger.ts new file mode 100644 index 00000000..1ab928f4 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/TsedLogger.ts @@ -0,0 +1,61 @@ +import { Logger } from '@tsed/common' +import { LogLevel, BaseLogger } from '@credo-ts/core' +import util from 'util' + +export class TsedLogger extends BaseLogger { + private logger: Logger + + // Map our log levels to tslog levels + private tsedLogLevelMap = { + [LogLevel.test]: 'debug', + [LogLevel.trace]: 'debug', + [LogLevel.debug]: 'debug', + [LogLevel.info]: 'info', + [LogLevel.warn]: 'warn', + [LogLevel.error]: 'error', + [LogLevel.fatal]: 'fatal', + } as const + + public constructor(logger: Logger) { + super(LogLevel.debug) + this.logger = logger + } + + private log(level: Exclude, message: string, data?: Record): void { + const tsedLogLevel = this.tsedLogLevelMap[level] + + if (data) { + this.logger[tsedLogLevel](message, util.inspect(data, { showHidden: false, depth: null })) + } else { + this.logger[tsedLogLevel](message) + } + } + + public test(message: string, data?: Record): void { + this.log(LogLevel.test, message, data) + } + + public trace(message: string, data?: Record): void { + this.log(LogLevel.trace, message, data) + } + + public debug(message: string, data?: Record): void { + this.log(LogLevel.debug, message, data) + } + + public info(message: string, data?: Record): void { + this.log(LogLevel.info, message, data) + } + + public warn(message: string, data?: Record): void { + this.log(LogLevel.warn, message, data) + } + + public error(message: string, data?: Record): void { + this.log(LogLevel.error, message, data) + } + + public fatal(message: string, data?: Record): void { + this.log(LogLevel.fatal, message, data) + } +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/AgentStatusController.ts b/aries-backchannels/credo-ts/server/src/controllers/AgentStatusController.ts new file mode 100644 index 00000000..f7e2b495 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/AgentStatusController.ts @@ -0,0 +1,56 @@ +import { $log, BodyParams, Controller, Get, Post, Res } from '@tsed/common' +import packageJson from '@credo-ts/core/package.json' +import { TestHarnessConfig, Transport } from '../TestHarnessConfig' +import { BaseController } from '../BaseController' + +@Controller('/agent/command') +export class AgentStatusController extends BaseController { + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + @Get('/status') + getStatus(@Res() response: Res) { + // NOTE: Because the agent runs inside the backchannel (not separate processes) + // The agent is active as long as the backchannel is active + const active = true + + if (!active) { + response.status(418) + return { + status: 'inactive', + } + } + + return { + status: 'active', + } + } + + @Get('/version') + getVersion(@Res() response: Res) { + $log.info('Get Version') + const [version] = packageJson.version.split('+') + return version + } + + @Post('/agent/start') + async restartAgent(@BodyParams('data') data: StartAgentData) { + await this.testHarnessConfig.stopAgent() + + await this.testHarnessConfig.startAgent({ + inboundTransports: data.parameters.inbound_transports ?? ['http'], + outboundTransports: data.parameters.outbound_transports ?? ['http'], + }) + + await this.testHarnessConfig.agentStartup() + } +} + +interface StartAgentData { + parameters: { + 'mime-type'?: string + inbound_transports?: Transport[] + outbound_transports?: Transport[] + } +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/ConnectionController.ts b/aries-backchannels/credo-ts/server/src/controllers/ConnectionController.ts new file mode 100644 index 00000000..8c989ccf --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/ConnectionController.ts @@ -0,0 +1,165 @@ +import { Controller, Get, PathParams, Post, BodyParams } from '@tsed/common' +import { NotFound } from '@tsed/exceptions' +import { + ConnectionRecord, + ConnectionInvitationMessage, + JsonTransformer, + AgentConfig, + ConnectionEventTypes, + ConnectionStateChangedEvent, + ConnectionState, + CredoError, + DidExchangeState, +} from '@credo-ts/core' + +import { convertToNewInvitation } from '@credo-ts/core/build/modules/oob/helpers' +import { filter, firstValueFrom, ReplaySubject, tap, timeout } from 'rxjs' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' +import { ConnectionUtils } from '../utils/ConnectionUtils' + +@Controller('/agent/command/connection') +export class ConnectionController extends BaseController { + private subject = new ReplaySubject() + + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + onStartup = () => { + this.subject = new ReplaySubject() + // Catch all events in replay subject for later use + this.agent.events + .observable(ConnectionEventTypes.ConnectionStateChanged) + .subscribe(this.subject) + } + + @Get('/:id') + async getConnectionById(@PathParams('id') id: string) { + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, id) + + if (!connection) throw new NotFound(`Connection with id ${id} not found`) + + return this.mapConnection(connection) + } + + @Get('/') + async getAllConnections() { + const connections = await this.agent.connections.getAll() + + return connections.map((conn) => this.mapConnection(conn)) + } + + @Post('/create-invitation') + async createInvitation(@BodyParams('data') data?: { mediator_connection_id?: string }) { + const mediatorId = await this.mediatorIdFromMediatorConnectionId(data?.mediator_connection_id) + + const routing = await this.agent.mediationRecipient.getRouting({ + mediatorId, + }) + + const { invitation, outOfBandRecord } = await this.agent.oob.createLegacyInvitation({ + routing, + // Needed to complete connection: https://github.com/hyperledger/aries-framework-javascript/issues/668 + autoAcceptConnection: true, + }) + + const config = this.agent.dependencyManager.resolve(AgentConfig) + const invitationJson = invitation.toJSON({ useDidSovPrefixWhereAllowed: config.useDidSovPrefixWhereAllowed }) + + return { + state: ConnectionState.Invited, + // This should be the connection id. However, the connection id is not available until a request is received. + // We can just use the oob invitation I guess. + connection_id: outOfBandRecord.id, + invitation: invitationJson, + } + } + + @Post('/receive-invitation') + async receiveInvitation(@BodyParams('data') data: Record & { mediator_connection_id?: string }) { + const { mediator_connection_id, ...invitationJson } = data + + const mediatorId = await this.mediatorIdFromMediatorConnectionId(mediator_connection_id) + + const routing = await this.agent.mediationRecipient.getRouting({ + mediatorId, + }) + + const oobInvitation = convertToNewInvitation(JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage)) + const { connectionRecord } = await this.agent.oob.receiveInvitation(oobInvitation, { + routing, + // Needed to complete connection: https://github.com/hyperledger/aries-framework-javascript/issues/668 + autoAcceptConnection: true, + autoAcceptInvitation: true, + }) + + this.agent.config.logger.debug('ConnectionController.receiveInvitation: connectionRecord.id: ', connectionRecord) + + if (!connectionRecord) { + throw new CredoError('Processing invitation did not result in a connection record') + } + + return this.mapConnection(connectionRecord) + } + + @Post('/accept-invitation') + async acceptInvitation(@BodyParams('id') id: string) { + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, id) + + return this.mapConnection(connection) + } + + @Post('/accept-request') + async acceptRequest(@BodyParams('id') id: string) { + // possible here that the request hasn't finished processing yet + await this.waitForState(id, DidExchangeState.RequestReceived) + + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, id) + + return this.mapConnection(connection) + } + + @Post('/send-ping') + async sendPing( + @BodyParams('id') id: string, + // For now we ignore data. This could contain a comment property, + // AFJ doesn't support passing it for now + @BodyParams('data') data: any + ) { + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, id) + + if (!connection) throw new NotFound(`Connection with id ${id} not found`) + + return this.mapConnection(connection) + } + + private async mediatorIdFromMediatorConnectionId(mediatorConnectionId?: string): Promise { + if (!mediatorConnectionId) return undefined + + // Find mediator id if mediator connection id is provided + const mediator = await this.agent.mediationRecipient.findByConnectionId(mediatorConnectionId) + + return mediator?.id + } + + private async waitForState(id: string, state: DidExchangeState) { + return await firstValueFrom( + this.subject.pipe( + filter((c) => c.payload.connectionRecord.id === id || c.payload.connectionRecord.outOfBandId === id), + filter((c) => c.payload.connectionRecord.state === state), + timeout(20000) + ) + ) + } + + private mapConnection(connection: ConnectionRecord) { + return { + // If we use auto accept, we can't include the state as we will move quicker than the calls in the test harness. This will + // make verification fail. The test harness recognizes the 'N/A' state. + state: connection.state === DidExchangeState.Completed ? connection.rfc0160State : 'N/A', + connection_id: connection.id, + connection, + } + } +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/CredentialController.ts b/aries-backchannels/credo-ts/server/src/controllers/CredentialController.ts new file mode 100644 index 00000000..d3015899 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/CredentialController.ts @@ -0,0 +1,16 @@ +import { Controller, Get, PathParams } from '@tsed/common' +import { CredentialUtils } from '../utils/CredentialUtils' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' + +@Controller('/agent/command/credential') +export class CredentialController extends BaseController { + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + @Get('/:credentialId') + async getCredentialById(@PathParams('credentialId') credentialId: string) { + return await CredentialUtils.getAnonCredsCredentialById(this.agent, credentialId) + } +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/CredentialDefinitionController.ts b/aries-backchannels/credo-ts/server/src/controllers/CredentialDefinitionController.ts new file mode 100644 index 00000000..91234314 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/CredentialDefinitionController.ts @@ -0,0 +1,105 @@ +import { Controller, Get, PathParams, Post, BodyParams } from '@tsed/common' +import { InternalServerError, NotFound } from '@tsed/exceptions' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' +import { AnonCredsApi, AnonCredsCredentialDefinition, AnonCredsCredentialDefinitionRepository } from '@credo-ts/anoncreds' +import { DidInfo } from '../types' + +@Controller('/agent/command/credential-definition') +export class CredentialDefinitionController extends BaseController { + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + @Get('/:credentialDefinitionId') + async getCredentialDefinitionById( + @PathParams('credentialDefinitionId') credentialDefinitionId: string + ): Promise { + try { + const { credentialDefinition } = await this.agent.modules.anoncreds.getCredentialDefinition(credentialDefinitionId) + + if (!credentialDefinition) { + throw new NotFound(`credential definition with credentialDefinitionId "${credentialDefinitionId}" not found.`) + } + return { ...credentialDefinition, id: credentialDefinitionId } + } catch (error) { + // Credential definition does not exist on ledger + if (error instanceof NotFound) { + throw error + } + + // All other errors + throw new InternalServerError( + `Error while retrieving credential definition with id ${credentialDefinitionId}`, + error + ) + } + } + + @Post() + async createCredentialDefinition( + @BodyParams('data') + data: { + tag: string + support_revocation: boolean + schema_id: string + } + ): Promise<{ + credential_definition_id: string + credential_definition: ReturnedCredentialDefinition + }> { + + // Check locally if credential definition already exists + const credentialDefinitionRepository = this.agent.dependencyManager.resolve(AnonCredsCredentialDefinitionRepository) + const [credentialDefinitionRecord] = await credentialDefinitionRepository.findByQuery(this.agent.context, { schemaId: data.schema_id, tag: data.tag }) + if (credentialDefinitionRecord) { + + return { + credential_definition_id: credentialDefinitionRecord.credentialDefinitionId, + credential_definition: { ...credentialDefinitionRecord.credentialDefinition, id: credentialDefinitionRecord.credentialDefinitionId }, + } + } + + // TODO: handle schema not found exception + try { + + const anoncredsApi = this.agent.dependencyManager.resolve(AnonCredsApi) + + const schema = await anoncredsApi.getSchema(data.schema_id) + + const publicDidInfoRecord = await this.agent.genericRecords.findById('PUBLIC_DID_INFO') + + if (!publicDidInfoRecord) { + throw new Error('Agent does not have any public did') + } + + //const issuerId = (publicDidInfoRecord.content.didInfo as unknown as DidInfo).did + const prefix = 'did:indy:bcovrin:test:'; //TODO compile the prefix from variables. + const id = (publicDidInfoRecord.content.didInfo as unknown as DidInfo).did + const issuerId = `${prefix}${id}`; + const { credentialDefinitionState } = await anoncredsApi.registerCredentialDefinition({ + credentialDefinition: { + issuerId, + schemaId: schema.schemaId, + tag: data.tag, + }, + //options: { supportRevocation: false, didIndyNamespace: 'main-pool'}}) + options: { supportRevocation: false, indyNamespace: 'bcovrin:test'}}) + + if (!credentialDefinitionState.credentialDefinition || !credentialDefinitionState.credentialDefinitionId) { + throw new Error() + } + + return { + credential_definition_id: credentialDefinitionState.credentialDefinitionId, + credential_definition: { ...credentialDefinitionState.credentialDefinition, id: credentialDefinitionState.credentialDefinitionId }, + } + } catch (error: any) { + throw new InternalServerError(`Error registering credential definition: ${error.message}`, error) + } + } +} + +interface ReturnedCredentialDefinition extends AnonCredsCredentialDefinition { + id: string +} \ No newline at end of file diff --git a/aries-backchannels/credo-ts/server/src/controllers/DidController.ts b/aries-backchannels/credo-ts/server/src/controllers/DidController.ts new file mode 100644 index 00000000..b45fff5e --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/DidController.ts @@ -0,0 +1,16 @@ +import { Controller, Get } from '@tsed/common' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' + +@Controller('/agent/command/did') +export class DidController extends BaseController { + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + @Get() + async getPublicDid() { + const publicDidInfoRecord = await this.agent.genericRecords.findById('PUBLIC_DID_INFO') + return publicDidInfoRecord ? publicDidInfoRecord.content.didInfo : {} + } +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/IssueCredentialController.ts b/aries-backchannels/credo-ts/server/src/controllers/IssueCredentialController.ts new file mode 100644 index 00000000..cc458c58 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/IssueCredentialController.ts @@ -0,0 +1,174 @@ +import { BodyParams, Controller, Get, PathParams, Post } from '@tsed/common' +import { BadRequest, NotFound } from '@tsed/exceptions' +import { + CredentialEventTypes, + CredentialExchangeRecord, + CredentialState, + CredentialStateChangedEvent, + JsonTransformer, +} from '@credo-ts/core' +import { CredentialUtils } from '../utils/CredentialUtils' +import { filter, firstValueFrom, ReplaySubject, timeout } from 'rxjs' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' +import { ConnectionUtils } from '../utils/ConnectionUtils' +import { V1CredentialPreview } from '@credo-ts/anoncreds' + +@Controller('/agent/command/issue-credential') +export class IssueCredentialController extends BaseController { + private subject = new ReplaySubject() + + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + public onStartup() { + this.subject = new ReplaySubject() + // Catch all events in replay subject for later use + this.agent.events + .observable(CredentialEventTypes.CredentialStateChanged) + .subscribe(this.subject) + } + + @Get('/:threadId') + async getCredentialByThreadId(@PathParams('threadId') threadId: string) { + const credential = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + if (!credential) { + throw new NotFound(`credential for thead id "${threadId}" not found.`) + } + return this.mapCredential(credential) + } + + @Get('/') + async getAllCredentials() { + const credentials = await this.agent.credentials.getAll() + + return credentials.map((cred) => this.mapCredential(cred)) + } + + @Post('/send-proposal') + async sendProposal( + @BodyParams('data') + data: { + connection_id: string + schema_issuer_did?: string + issuer_did?: string + schema_name?: string + cred_def_id?: string + schema_version?: string + schema_id?: string + credential_proposal: any + } + ) { + const preview = JsonTransformer.fromJSON(data.credential_proposal, V1CredentialPreview) + + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, data.connection_id) + const credentialRecord = await this.agent.credentials.proposeCredential({ + connectionId: connection.id, + protocolVersion: 'v1', + credentialFormats: { + indy: { + attributes: preview.attributes, + schemaIssuerDid: data.schema_issuer_did, + issuerDid: data.issuer_did, + schemaName: data.schema_name, + credentialDefinitionId: data.cred_def_id, + schemaVersion: data.schema_version, + schemaId: data.schema_id, + }, + }, + }) + + return this.mapCredential(credentialRecord) + } + + @Post('/send-offer') + async sendOffer( + @BodyParams('id') threadId?: string, + @BodyParams('data') + data?: { + connection_id: string + cred_def_id: string + credential_preview: any + } + ) { + let credentialRecord: CredentialExchangeRecord + + if (threadId) { + await this.waitForState(threadId, CredentialState.ProposalReceived) + const { id } = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + credentialRecord = await this.agent.credentials.acceptProposal({ + credentialRecordId: id, + }) + return this.mapCredential(credentialRecord) + } else if (data) { + const preview = JsonTransformer.fromJSON(data.credential_preview, V1CredentialPreview) + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, data.connection_id) + credentialRecord = await this.agent.credentials.offerCredential({ + connectionId: connection.id, + protocolVersion: 'v1', + credentialFormats: { + indy: { + attributes: preview.attributes, + credentialDefinitionId: data.cred_def_id, + }, + }, + }) + return this.mapCredential(credentialRecord) + } else { + throw new BadRequest(`Missing both id and data properties`) + } + } + + @Post('/send-request') + async sendRequest(@BodyParams('id') threadId: string) { + await this.waitForState(threadId, CredentialState.OfferReceived) + let { id } = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + + const credentialRecord = await this.agent.credentials.acceptOffer({ + credentialRecordId: id, + }) + return this.mapCredential(credentialRecord) + } + + @Post('/issue') + async acceptRequest(@BodyParams('id') threadId: string, @BodyParams('data') data?: { comment?: string }) { + await this.waitForState(threadId, CredentialState.RequestReceived) + const { id } = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + + const credentialRecord = await this.agent.credentials.acceptRequest({ + credentialRecordId: id, + comment: data?.comment, + }) + return this.mapCredential(credentialRecord) + } + + @Post('/store') + async storeCredential(@BodyParams('id') threadId: string) { + await this.waitForState(threadId, CredentialState.CredentialReceived) + let { id } = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + const credentialRecord = await this.agent.credentials.acceptCredential({ credentialRecordId: id }) + + return this.mapCredential(credentialRecord) + } + + private async waitForState(threadId: string, state: CredentialState) { + return await firstValueFrom( + this.subject.pipe( + filter((c) => c.payload.credentialRecord.threadId === threadId), + filter((c) => c.payload.credentialRecord.state === state), + timeout(20000) + ) + ) + } + + private mapCredential(credentialRecord: CredentialExchangeRecord) { + const credentialId = credentialRecord.credentials[0]?.credentialRecordId + + return { + state: credentialRecord.state, + credential_id: credentialId, + thread_id: credentialRecord.threadId, + } + } +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/IssueCredentialV2Controller.ts b/aries-backchannels/credo-ts/server/src/controllers/IssueCredentialV2Controller.ts new file mode 100644 index 00000000..8673e064 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/IssueCredentialV2Controller.ts @@ -0,0 +1,221 @@ +import { BodyParams, Controller, Get, PathParams, Post } from '@tsed/common' +import { BadRequest, NotFound } from '@tsed/exceptions' +import { + CredentialEventTypes, + CredentialExchangeRecord, + CredentialState, + CredentialStateChangedEvent, + JsonTransformer, + V2CredentialPreview, +} from '@credo-ts/core' +import { CredentialUtils } from '../utils/CredentialUtils' +import { filter, firstValueFrom, ReplaySubject, timeout } from 'rxjs' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' +import { ConnectionUtils } from '../utils/ConnectionUtils' + +const afjFormatToAathFormatMapping: Record = { + indy: 'indy', + anoncreds: 'indy', +} + +@Controller('/agent/command/issue-credential-v2') +export class IssueCredentialV2Controller extends BaseController { + private subject = new ReplaySubject() + + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + public onStartup() { + this.subject = new ReplaySubject() + // Catch all events in replay subject for later use + this.agent.events + .observable(CredentialEventTypes.CredentialStateChanged) + .subscribe(this.subject) + } + + @Get('/:threadId') + async getCredentialByThreadId(@PathParams('threadId') threadId: string) { + const credential = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + if (!credential) { + throw new NotFound(`credential for thead id "${threadId}" not found.`) + } + return this.mapCredential(credential) + } + + @Get('/') + async getAllCredentials() { + const credentials = await this.agent.credentials.getAll() + + return credentials.map((cred) => this.mapCredential(cred)) + } + + @Post('/send-proposal') + async sendProposal( + @BodyParams('data') + data: { + connection_id: string + filter: { + indy: { + schema_issuer_did?: string + issuer_did?: string + schema_name?: string + cred_def_id?: string + schema_version?: string + schema_id?: string + } + } + credential_preview: any + } + ) { + // Disable validation. the @type sent by AATH is 'issue-credential/2.0/credential-preview' + // which is not a valid message type. + const preview = JsonTransformer.fromJSON(data.credential_preview, V2CredentialPreview, { validate: false }) + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, data.connection_id) + const credentialRecord = await this.agent.credentials.proposeCredential({ + connectionId: connection.id, + protocolVersion: 'v2', + credentialFormats: { + indy: { + attributes: preview.attributes, + schemaIssuerDid: data.filter.indy.schema_issuer_did, + issuerDid: data.filter.indy.issuer_did, + schemaName: data.filter.indy.schema_name, + credentialDefinitionId: data.filter.indy.cred_def_id, + schemaVersion: data.filter.indy.schema_version, + schemaId: data.filter.indy.schema_id, + }, + }, + }) + + return this.mapCredential(credentialRecord) + } + + @Post('/send-offer') + async sendOffer( + @BodyParams('id') threadId?: string, + @BodyParams('data') + data?: { + connection_id: string + filter: { + indy: { + schema_issuer_did?: string + issuer_did?: string + schema_name?: string + cred_def_id?: string + schema_version?: string + schema_id?: string + } + } + credential_preview: any + } + ) { + let credentialRecord: CredentialExchangeRecord + + this.agent.config.logger.info('Sending credential offer', data) + + if (threadId) { + await this.waitForState(threadId, CredentialState.ProposalReceived) + let credentialRecord = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + + this.agent.config.logger.info('Replying to credential proposal') + + credentialRecord = await this.agent.credentials.acceptProposal({ + credentialRecordId: credentialRecord.id, + }) + return this.mapCredential(credentialRecord) + } else if (data) { + // Disable validation. the @type sent by AATH is 'issue-credential/2.0/credential-preview' + // which is not a valid message type. + const preview = JsonTransformer.fromJSON(data.credential_preview, V2CredentialPreview, { validate: false }) + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, data.connection_id) + + if (!data.filter.indy.cred_def_id) { + throw new BadRequest('data.filter.indy.cred_def_id is required') + } + + credentialRecord = await this.agent.credentials.offerCredential({ + connectionId: connection.id, + protocolVersion: 'v2', + credentialFormats: { + indy: { + attributes: preview.attributes, + credentialDefinitionId: data.filter.indy.cred_def_id, + }, + // Credo Upgrade - added based on https://credo.js.org/guides/tutorials/issue-an-anoncreds-credential-over-didcomm#4-issuing-a-credential + anoncreds: { + attributes: preview.attributes, + // TODO Change the filters here from indy to anoncreds + credentialDefinitionId: data.filter.indy.cred_def_id, + }, + }, + }) + return this.mapCredential(credentialRecord) + } else { + throw new BadRequest(`Missing both id and data properties`) + } + } + + @Post('/send-request') + async sendRequest(@BodyParams('id') threadId: string) { + await this.waitForState(threadId, CredentialState.OfferReceived) + let { id } = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + + const credentialRecord = await this.agent.credentials.acceptOffer({ + credentialRecordId: id, + }) + return this.mapCredential(credentialRecord) + } + + @Post('/issue') + async acceptRequest(@BodyParams('id') threadId: string, @BodyParams('data') data?: { comment?: string }) { + await this.waitForState(threadId, CredentialState.RequestReceived) + const { id } = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + + const credentialRecord = await this.agent.credentials.acceptRequest({ + credentialRecordId: id, + comment: data?.comment, + }) + return this.mapCredential(credentialRecord) + } + + @Post('/store') + async storeCredential(@BodyParams('id') threadId: string) { + await this.waitForState(threadId, CredentialState.CredentialReceived) + let { id } = await CredentialUtils.getCredentialByThreadId(this.agent, threadId) + const credentialRecord = await this.agent.credentials.acceptCredential({ credentialRecordId: id }) + + return this.mapCredential(credentialRecord) + } + + private async waitForState(threadId: string, state: CredentialState) { + return await firstValueFrom( + this.subject.pipe( + filter((c) => c.payload.credentialRecord.threadId === threadId), + filter((c) => c.payload.credentialRecord.state === state), + timeout(20000) + ) + ) + } + + private mapCredential(credentialRecord: CredentialExchangeRecord) { + let returnModel = { + state: credentialRecord.state, + thread_id: credentialRecord.threadId, + } + + for (const credential of credentialRecord.credentials) { + const key = afjFormatToAathFormatMapping[credential.credentialRecordType] + + returnModel = { + ...returnModel, + [key]: { + credential_id: credential.credentialRecordId, + }, + } + } + + return returnModel + } +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/MediationController.ts b/aries-backchannels/credo-ts/server/src/controllers/MediationController.ts new file mode 100644 index 00000000..6541dcb5 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/MediationController.ts @@ -0,0 +1,107 @@ +import { Controller, Get, PathParams, Post, BodyParams } from '@tsed/common' +import { NotFound, NotImplemented } from '@tsed/exceptions' +import { + MediationRecord, + MediationState, + MediationRole, + RoutingEventTypes, + MediationStateChangedEvent, +} from '@credo-ts/core' +import { TestHarnessConfig } from '../TestHarnessConfig' +import { BaseController } from '../BaseController' +import { filter, firstValueFrom, ReplaySubject, timeout } from 'rxjs' +import { ConnectionUtils } from '../utils/ConnectionUtils' + +@Controller('/agent/command/mediation') +export class MediationController extends BaseController { + private subject = new ReplaySubject() + + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + public onStartup(): void | Promise { + const observable = this.agent.events.observable(RoutingEventTypes.MediationStateChanged) + + // Catch all events in replay subject for later use + this.subject = new ReplaySubject() + observable.subscribe(this.subject) + + // Initiate message pickup whenever mediation is completed + observable.subscribe(async (event) => { + if ( + event.payload.mediationRecord.role === MediationRole.Recipient && + event.payload.mediationRecord.state === MediationState.Granted + ) { + await this.agent.mediationRecipient.initiateMessagePickup(event.payload.mediationRecord) + } + }) + } + + @Get('/:connectionId') + async getMediationRecordByConnectionId(@PathParams('connectionId') connectionId: string) { + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, connectionId) + const mediationRecord = await this.agent.mediationRecipient.findByConnectionId(connection.id) + + if (!mediationRecord) { + throw new NotFound(`mediation record for connectionId "${connectionId}" not found.`) + } + + return this.mapMediationRecord(mediationRecord) + } + + @Post('/send-request') + async sendMediationRequest(@BodyParams('id') connectionId: string) { + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, connectionId) + + const mediationRecord = await this.agent.mediationRecipient.requestMediation(connection) + + return this.mapMediationRecord(mediationRecord) + } + + @Post('/send-grant') + async sendMediationGrant(@BodyParams('id') connectionId: string) { + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, connectionId) + let { + payload: { mediationRecord }, + } = await this.waitForState(connection.id, MediationState.Granted) + + // Auto accept is enabled, so we're not granting the mediation explicitly + return this.mapMediationRecord(mediationRecord) + } + + @Post('/send-deny') + async sendMediationDeny(@BodyParams('id') connectionId: string) { + throw new NotImplemented('Sending mediation deny message not supported in AFJ') + } + + private async waitForState(connectionId: string, state: MediationState) { + return await firstValueFrom( + this.subject.pipe( + filter((e) => e.payload.mediationRecord.connectionId === connectionId), + filter((e) => e.payload.mediationRecord.state === state), + timeout(20000) + ) + ) + } + + private mapMediationRecord(mediationRecord: MediationRecord) { + return { + state: mediatorStateMapping[mediationRecord.role][mediationRecord.state], + connection_id: mediationRecord.connectionId, + } + } +} + +const mediatorStateMapping = { + [MediationRole.Mediator]: { + [MediationState.Requested]: 'request-received', + [MediationState.Granted]: 'grant-sent', + [MediationState.Denied]: 'deny-sent', + }, + [MediationRole.Recipient]: { + [MediationState.Requested]: 'request-sent', + [MediationState.Granted]: 'grant-received', + [MediationState.Denied]: 'deny-received', + }, +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/OutOfBandController.ts b/aries-backchannels/credo-ts/server/src/controllers/OutOfBandController.ts new file mode 100644 index 00000000..d8b9402c --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/OutOfBandController.ts @@ -0,0 +1,172 @@ +/* import { Controller, Get, PathParams, Post, BodyParams } from '@tsed/common' +import { NotFound } from '@tsed/exceptions' +import { + OutOfBandRecord, + OutOfBandInvitation, + ConnectionInvitationMessage, + ConnectionRecord, + JsonTransformer, + AgentConfig, + OutOfBandEventTypes, + OutOfBandStateChangedEvent, + OutOfBandState, + CredoError, + DidExchangeState, +} from '@credo-ts/core' + +import { convertToNewInvitation } from '@credo-ts/core/build/modules/oob/helpers' +import { filter, firstValueFrom, ReplaySubject, tap, timeout } from 'rxjs' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' +import { ConnectionUtils } from '../utils/ConnectionUtils' + +@Controller('/agent/command/oob') +export class OutOfBandController extends BaseController { + private subject = new ReplaySubject() + + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + onStartup = () => { + this.subject = new ReplaySubject() + // Catch all events in replay subject for later use + this.agent.events + .observable(OutOfBandEventTypes.OutOfBandStateChanged) + .subscribe(this.subject) + } + + @Get('/:id') + async getConnectionById(@PathParams('id') id: string) { + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, id) + + if (!connection) throw new NotFound(`Connection with id ${id} not found`) + + return this.mapConnection(connection) + } + + @Get('/') + async getAllConnections() { + const connections = await this.agent.connections.getAll() + + return connections.map((conn) => this.mapConnection(conn)) + } + + @Post('/create-invitation') + async createInvitation(@BodyParams('data') data?: { mediator_connection_id?: string }) { + const mediatorId = await this.mediatorIdFromMediatorConnectionId(data?.mediator_connection_id) + + const routing = await this.agent.mediationRecipient.getRouting({ + mediatorId, + }) + + const outOfBandRecord = await this.agent.oob.createInvitation({ + routing, + // Needed to complete connection: https://github.com/hyperledger/aries-framework-javascript/issues/668 + // TODO probably don't need this anymore. + autoAcceptConnection: true, + }) + + const config = this.agent.dependencyManager.resolve(AgentConfig) + const invitation = outOfBandRecord.outOfBandInvitation + const invitationJson = invitation.toJSON({ useDidSovPrefixWhereAllowed: config.useDidSovPrefixWhereAllowed }) + + return { + state: OutOfBandState.Initial, + // This should be the connection id. However, the connection id is not available until a request is received. + // We can just use the oob invitation I guess. + connection_id: outOfBandRecord.id, + invitation: invitationJson, + } + } + + @Post('/receive-invitation') + async receiveInvitation(@BodyParams('data') data: Record & { mediator_connection_id?: string }) { + const { mediator_connection_id, ...invitationJson } = data + + const mediatorId = await this.mediatorIdFromMediatorConnectionId(mediator_connection_id) + + const routing = await this.agent.mediationRecipient.getRouting({ + mediatorId, + }) + + const oobInvitation = convertToNewInvitation(JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage)) + const { outOfBandRecord, connectionRecord } = await this.agent.oob.receiveInvitation(oobInvitation, { + routing, + // Needed to complete connection: https://github.com/hyperledger/aries-framework-javascript/issues/668 + autoAcceptConnection: true, + autoAcceptInvitation: true, + }) + + this.agent.config.logger.debug('OutOfBandController.receiveInvitation: outOfBandRecord.id: ', outOfBandRecord) + + if (!connectionRecord) { + throw new CredoError('Processing invitation did not result in a out of band record') + } + + // return this.mapConnection(outOfBandRecord) + return this.mapConnection(connectionRecord) + + } + + @Post('/accept-invitation') + async acceptInvitation(@BodyParams('id') id: string) { + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, id) + + return this.mapConnection(connection) + } + + @Post('/accept-request') + async acceptRequest(@BodyParams('id') id: string) { + // possible here that the request hasn't finished processing yet + await this.waitForState(id, DidExchangeState.RequestReceived) + + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, id) + + return this.mapConnection(connection) + } + + @Post('/send-ping') + async sendPing( + @BodyParams('id') id: string, + // For now we ignore data. This could contain a comment property, + // AFJ doesn't support passing it for now + @BodyParams('data') data: any + ) { + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, id) + + if (!connection) throw new NotFound(`Connection with id ${id} not found`) + + return this.mapConnection(connection) + } + + private async mediatorIdFromMediatorConnectionId(mediatorConnectionId?: string): Promise { + if (!mediatorConnectionId) return undefined + + // Find mediator id if mediator connection id is provided + const mediator = await this.agent.mediationRecipient.findByConnectionId(mediatorConnectionId) + + return mediator?.id + } + + private async waitForState(id: string, state: DidExchangeState) { + return await firstValueFrom( + this.subject.pipe( + filter((c) => c.payload.connectionRecord.id === id ), + filter((c) => c.payload.connectionRecord.state === state), + timeout(20000) + ) + ) + } + + private mapConnection(connection: ConnectionRecord) { + return { + // If we use auto accept, we can't include the state as we will move quicker than the calls in the test harness. This will + // make verification fail. The test harness recognizes the 'N/A' state. + state: connection.state === DidExchangeState.Completed ? connection.rfc0160State : 'N/A', + connection_id: connection.id, + connection, + } + } +} + */ \ No newline at end of file diff --git a/aries-backchannels/credo-ts/server/src/controllers/PresentProofController.ts b/aries-backchannels/credo-ts/server/src/controllers/PresentProofController.ts new file mode 100644 index 00000000..fb261ce4 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/PresentProofController.ts @@ -0,0 +1,211 @@ +import { Controller, Get, PathParams, Post, BodyParams } from '@tsed/common' +import { NotFound } from '@tsed/exceptions' +import { + JsonTransformer, + ProofExchangeRecord, + AgentConfig, + Logger, + ProofEventTypes, + ProofStateChangedEvent, + ProofState, +} from '@credo-ts/core' +import { ProofUtils } from '../utils/ProofUtils' +import { filter, firstValueFrom, ReplaySubject, timeout } from 'rxjs' +import util from 'util' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' +import { ConnectionUtils } from '../utils/ConnectionUtils' +import { V1PresentationPreview, AnonCredsProofRequest, AnonCredsRequestedAttributeMatch, AnonCredsRequestedPredicateMatch } from '@credo-ts/anoncreds' +@Controller('/agent/command/proof') +export class PresentProofController extends BaseController { + private logger: Logger + private subject = new ReplaySubject() + + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + + this.logger = this.agent.dependencyManager.resolve(AgentConfig).logger + } + + public onStartup() { + this.subject = new ReplaySubject() + // Catch all events in replay subject for later use + this.agent.events.observable(ProofEventTypes.ProofStateChanged).subscribe(this.subject) + } + + @Get('/:threadId') + async getProofByThreadId(@PathParams('threadId') threadId: string) { + const proofRecord = await ProofUtils.getProofByThreadId(this.agent, threadId) + + if (!proofRecord) { + throw new NotFound(`proof record for thead id "${threadId}" not found.`) + } + + return this.mapProofExchangeRecord(proofRecord) + } + + @Get('/') + async getAllProofs() { + const proofs = await this.agent.proofs.getAll() + + return proofs.map((proof) => this.mapProofExchangeRecord(proof)) + } + + @Post('/send-proposal') + async sendProposal( + @BodyParams('data') + data: { + connection_id: string + presentation_proposal: { + comment?: string + attributes: any + predicates: any + } + } + ) { + + const presentationProposal = JsonTransformer.fromJSON(data.presentation_proposal, V1PresentationPreview) + const { attributes, predicates } = presentationProposal + + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, data.connection_id) + + const proofRecord = await this.agent.proofs.proposeProof({ + connectionId: connection.id, + protocolVersion: 'v1', + proofFormats: { + indy: { + attributes, + predicates, + }, + }, + comment: data.presentation_proposal.comment + }) + + return this.mapProofExchangeRecord(proofRecord) + } + + @Post('/send-request') + async sendRequest( + @BodyParams('id') threadId: string, + @BodyParams('data') + data: { + connection_id: string + presentation_request: { + comment?: string + proof_request: { + data: unknown + } + } + } + ) { + + this.logger.info('Sending request', { + proofRequest: util.inspect(data.presentation_request.proof_request, { showHidden: false, depth: null }), + }) + // Do not validate, we only need a few properties from the proof request + const proofRequest = data.presentation_request.proof_request.data as AnonCredsProofRequest + + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, data.connection_id) + + // TODO: AFJ doesn't support to negotiate proposal yet + // if thread id is present + const proofRecord = await this.agent.proofs.requestProof({ + connectionId: connection.id, + protocolVersion: 'v1', + proofFormats: { + indy: { + name: proofRequest.name ?? 'proof-request', + version: proofRequest.version ?? '1.0', + non_revoked: proofRequest.non_revoked, + requested_attributes: proofRequest.requested_attributes, + requested_predicates: proofRequest.requested_predicates + } + }, + comment: data.presentation_request.comment, + }) + + return this.mapProofExchangeRecord(proofRecord) + } + + @Post('/send-presentation') + async sendPresentation( + @BodyParams('id') threadId: string, + @BodyParams('data') + data: { + self_attested_attributes: Record + requested_attributes: Record + requested_predicates: Record + comment: string + } + ) { + await this.waitForState(threadId, ProofState.RequestReceived) + let proofRecord = await ProofUtils.getProofByThreadId(this.agent, threadId) + + const retrievedCredentials = await this.agent.proofs.getCredentialsForRequest({ + proofRecordId: proofRecord.id, + // Some tests include presenting a revoked credential, expecting the verification to fail + // So not excluding those from the retrieved credentials. + proofFormats: {indy: {filterByNonRevocationRequirements: false }} + }) + + let attributes: Record = {} + let predicates: Record = {} + + if (data.requested_attributes) { + Object.keys(data.requested_attributes).forEach((key) => { + attributes[key] = retrievedCredentials.proofFormats.indy?.attributes[key]?.find( + (a) => a.credentialId === data.requested_attributes[key].cred_id + ) as AnonCredsRequestedAttributeMatch + }) + } + if (data.requested_predicates) { + Object.keys(data.requested_predicates).forEach((key) => { + predicates[key] = retrievedCredentials.proofFormats.indy?.predicates[key].find( + (p) => p.credentialId === data.requested_predicates[key].cred_id + ) as AnonCredsRequestedPredicateMatch + }) + } + + this.logger.info('Created proof request', { + attributes: util.inspect(attributes, { showHidden: false, depth: null }), + predicates: util.inspect(predicates, { showHidden: false, depth: null }), + retrievedCredentials: util.inspect(retrievedCredentials, { showHidden: false, depth: null }), + }) + + proofRecord = await this.agent.proofs.acceptRequest({ + proofRecordId: proofRecord.id, + proofFormats: { indy: { attributes, predicates, selfAttestedAttributes: {} } }, + comment: data.comment, + }) + + return this.mapProofExchangeRecord(proofRecord) + } + + @Post('/verify-presentation') + async verifyPresentation(@BodyParams('id') threadId: string) { + await this.waitForState(threadId, ProofState.PresentationReceived) + + let proofRecord = await ProofUtils.getProofByThreadId(this.agent, threadId) + if (proofRecord) { + return this.mapProofExchangeRecord(await this.agent.proofs.acceptPresentation({ proofRecordId: proofRecord.id })) + } + } + + private async waitForState(threadId: string, state: ProofState) { + return await firstValueFrom( + this.subject.pipe( + filter((c) => c.payload.proofRecord.threadId === threadId), + filter((c) => c.payload.proofRecord.state === state), + timeout(20000) + ) + ) + } + + private mapProofExchangeRecord(proofExchangeRecord: ProofExchangeRecord) { + return { + state: proofExchangeRecord.state, + thread_id: proofExchangeRecord.threadId, + verified: proofExchangeRecord.isVerified, + } + } +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/PresentProofV2Controller.ts b/aries-backchannels/credo-ts/server/src/controllers/PresentProofV2Controller.ts new file mode 100644 index 00000000..a8941b87 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/PresentProofV2Controller.ts @@ -0,0 +1,211 @@ +import { Controller, Get, PathParams, Post, BodyParams } from '@tsed/common' +import { NotFound } from '@tsed/exceptions' +import { + JsonTransformer, + ProofExchangeRecord, + AgentConfig, + Logger, + ProofEventTypes, + ProofStateChangedEvent, + ProofState, +} from '@credo-ts/core' +import { ProofUtils } from '../utils/ProofUtils' +import { filter, firstValueFrom, ReplaySubject, timeout } from 'rxjs' +import util from 'util' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' +import { ConnectionUtils } from '../utils/ConnectionUtils' +// TODO: have to change that V1PresentationPreview to something else +import { V1PresentationPreview, AnonCredsProofRequest, AnonCredsRequestedAttributeMatch, AnonCredsRequestedPredicateMatch } from '@credo-ts/anoncreds' +@Controller('/agent/command/proof-v2') +export class PresentProofController extends BaseController { + private logger: Logger + private subject = new ReplaySubject() + + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + + this.logger = this.agent.dependencyManager.resolve(AgentConfig).logger + } + + public onStartup() { + this.subject = new ReplaySubject() + // Catch all events in replay subject for later use + this.agent.events.observable(ProofEventTypes.ProofStateChanged).subscribe(this.subject) + } + + @Get('/:threadId') + async getProofByThreadId(@PathParams('threadId') threadId: string) { + const proofRecord = await ProofUtils.getProofByThreadId(this.agent, threadId) + + if (!proofRecord) { + throw new NotFound(`proof record for thead id "${threadId}" not found.`) + } + + return this.mapProofExchangeRecord(proofRecord) + } + + @Get('/') + async getAllProofs() { + const proofs = await this.agent.proofs.getAll() + + return proofs.map((proof) => this.mapProofExchangeRecord(proof)) + } + + @Post('/send-proposal') + async sendProposal( + @BodyParams('data') + data: { + connection_id: string + presentation_proposal: { + comment?: string + attributes: any + predicates: any + } + } + ) { + + const presentationProposal = JsonTransformer.fromJSON(data.presentation_proposal, V1PresentationPreview) + const { attributes, predicates } = presentationProposal + + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, data.connection_id) + + const proofRecord = await this.agent.proofs.proposeProof({ + connectionId: connection.id, + protocolVersion: 'v2', + proofFormats: { + indy: { + attributes, + predicates, + }, + }, + comment: data.presentation_proposal.comment + }) + + return this.mapProofExchangeRecord(proofRecord) + } + + @Post('/send-request') + async sendRequest( + @BodyParams('id') threadId: string, + @BodyParams('data') + data: { + connection_id: string + presentation_request: { + comment?: string + proof_request: { + data: unknown + } + } + } + ) { + + this.logger.info('Sending request', { + proofRequest: util.inspect(data.presentation_request.proof_request, { showHidden: false, depth: null }), + }) + // Do not validate, we only need a few properties from the proof request + const proofRequest = data.presentation_request.proof_request.data as AnonCredsProofRequest + + const connection = await ConnectionUtils.getConnectionByConnectionIdOrOutOfBandId(this.agent, data.connection_id) + + // TODO: AFJ doesn't support to negotiate proposal yet + // if thread id is present + const proofRecord = await this.agent.proofs.requestProof({ + connectionId: connection.id, + protocolVersion: 'v2', + proofFormats: { + indy: { + name: proofRequest.name ?? 'proof-request', + version: proofRequest.version ?? '2.0', + non_revoked: proofRequest.non_revoked, + requested_attributes: proofRequest.requested_attributes, + requested_predicates: proofRequest.requested_predicates + } + }, + comment: data.presentation_request.comment, + }) + + return this.mapProofExchangeRecord(proofRecord) + } + + @Post('/send-presentation') + async sendPresentation( + @BodyParams('id') threadId: string, + @BodyParams('data') + data: { + self_attested_attributes: Record + requested_attributes: Record + requested_predicates: Record + comment: string + } + ) { + await this.waitForState(threadId, ProofState.RequestReceived) + let proofRecord = await ProofUtils.getProofByThreadId(this.agent, threadId) + + const retrievedCredentials = await this.agent.proofs.getCredentialsForRequest({ + proofRecordId: proofRecord.id, + // Some tests include presenting a revoked credential, expecting the verification to fail + // So not excluding those from the retrieved credentials. + proofFormats: {indy: {filterByNonRevocationRequirements: false }} + }) + + let attributes: Record = {} + let predicates: Record = {} + + if (data.requested_attributes) { + Object.keys(data.requested_attributes).forEach((key) => { + attributes[key] = retrievedCredentials.proofFormats.indy?.attributes[key]?.find( + (a) => a.credentialId === data.requested_attributes[key].cred_id + ) as AnonCredsRequestedAttributeMatch + }) + } + if (data.requested_predicates) { + Object.keys(data.requested_predicates).forEach((key) => { + predicates[key] = retrievedCredentials.proofFormats.indy?.predicates[key].find( + (p) => p.credentialId === data.requested_predicates[key].cred_id + ) as AnonCredsRequestedPredicateMatch + }) + } + + this.logger.info('Created proof request', { + attributes: util.inspect(attributes, { showHidden: false, depth: null }), + predicates: util.inspect(predicates, { showHidden: false, depth: null }), + retrievedCredentials: util.inspect(retrievedCredentials, { showHidden: false, depth: null }), + }) + + proofRecord = await this.agent.proofs.acceptRequest({ + proofRecordId: proofRecord.id, + proofFormats: { indy: { attributes, predicates, selfAttestedAttributes: {} } }, + comment: data.comment, + }) + + return this.mapProofExchangeRecord(proofRecord) + } + + @Post('/verify-presentation') + async verifyPresentation(@BodyParams('id') threadId: string) { + await this.waitForState(threadId, ProofState.PresentationReceived) + + let proofRecord = await ProofUtils.getProofByThreadId(this.agent, threadId) + if (proofRecord) { + return this.mapProofExchangeRecord(await this.agent.proofs.acceptPresentation({ proofRecordId: proofRecord.id })) + } + } + + private async waitForState(threadId: string, state: ProofState) { + return await firstValueFrom( + this.subject.pipe( + filter((c) => c.payload.proofRecord.threadId === threadId), + filter((c) => c.payload.proofRecord.state === state), + timeout(20000) + ) + ) + } + + private mapProofExchangeRecord(proofExchangeRecord: ProofExchangeRecord) { + return { + state: proofExchangeRecord.state, + thread_id: proofExchangeRecord.threadId, + } + } +} diff --git a/aries-backchannels/credo-ts/server/src/controllers/SchemaController.ts b/aries-backchannels/credo-ts/server/src/controllers/SchemaController.ts new file mode 100644 index 00000000..17c133b3 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/controllers/SchemaController.ts @@ -0,0 +1,90 @@ +import { AnonCredsSchema, AnonCredsSchemaRepository } from '@credo-ts/anoncreds' +import { DidInfo } from '../types' +import { Controller, Get, PathParams, Post, BodyParams } from '@tsed/common' +import { InternalServerError, NotFound } from '@tsed/exceptions' +import { BaseController } from '../BaseController' +import { TestHarnessConfig } from '../TestHarnessConfig' + +@Controller('/agent/command/schema') +export class SchemaController extends BaseController { + private createdSchemas: { + [schemaName: string]: AnonCredsSchema + } = {} + + public constructor(testHarnessConfig: TestHarnessConfig) { + super(testHarnessConfig) + } + + @Get('/:schemaId') + async getSchemaById(@PathParams('schemaId') schemaId: string): Promise { + try { + + const { schema } = await this.agent.modules.anoncreds.getSchema(schemaId) + + if (!schema) { + throw new NotFound(`schema with schemaId "${schemaId}" not found.`) + } + return { ...schema, id: schemaId } + } catch (error: any) { + // Schema does not exist on ledger + if (error instanceof NotFound) { + throw error + } + + // All other errors + throw new InternalServerError(`Error while retrieving schema with id ${schemaId}`, error) + } + } + + @Post() + async createSchema(@BodyParams('data') data: any): Promise<{schema_id: string, schema: ReturnedSchema}> { + + // Check if the schema exists already, if so return the details. + const schemaRepository = this.agent.dependencyManager.resolve(AnonCredsSchemaRepository) + const [schemaRecord] = await schemaRepository.findByQuery(this.agent.context, { schemaName: data.schema_name, + schemaVersion: data.schema_version }) + if (schemaRecord) { + + return { + schema_id: schemaRecord.schemaId, + schema: { ...schemaRecord.schema, id: schemaRecord.schemaId }, + } + } + + // Schema does not exist in the repository so create it. + const publicDidInfoRecord = await this.agent.genericRecords.findById('PUBLIC_DID_INFO') + + if (!publicDidInfoRecord) { + throw new Error('Agent does not have any public did') + } + + + const prefix = 'did:indy:bcovrin:test:'; //TODO compile the prefix from variables. + const id = (publicDidInfoRecord.content.didInfo as unknown as DidInfo).did + const issuerId = `${prefix}${id}`; + const schema = await this.agent.modules.anoncreds.registerSchema({ + schema: { + attrNames: data.attributes, + name: data.schema_name, + version: data.schema_version, + issuerId, + }, + options: {}, + //options: { didIndyNamespace: 'main-pool'} + //options: { indyNamespace: 'bcovrin:test'} + }) + + if (!schema.schemaState.schema || !schema.schemaState.schemaId) { + throw new Error(`Schema could not be registered: ${JSON.stringify(schema.schemaState)}}`) // TODO + } + + return { + schema_id: schema.schemaState.schemaId, + schema: { ...schema.schemaState.schema, id: schema.schemaState.schemaId }, + } + } +} + +interface ReturnedSchema extends AnonCredsSchema { + id: string +} diff --git a/aries-backchannels/credo-ts/server/src/index.ts b/aries-backchannels/credo-ts/server/src/index.ts new file mode 100644 index 00000000..1d3f3ad2 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/index.ts @@ -0,0 +1,50 @@ +import { $log, Logger, registerProvider } from '@tsed/common' +import minimist from 'minimist' + +import { TestHarnessConfig } from './TestHarnessConfig' +import { PlatformExpress } from '@tsed/platform-express' +import { Server } from './Server' + +async function startup() { + const cliArguments = minimist(process.argv.slice(2), { + alias: { + port: 'p', + }, + default: { + port: 9020, + }, + }) + + const testHarnessConfig = new TestHarnessConfig({ backchannelPort: Number(cliArguments.port) }) + + registerProvider({ + provide: TestHarnessConfig, + useValue: testHarnessConfig, + }) + + $log.level = 'debug' + + // TODO: Set up native logger for anoncreds, askar and indy-vdr + + await testHarnessConfig.startAgent({ inboundTransports: ['http'], outboundTransports: ['http'] }) + + try { + $log.debug('Start server...') + + const platform = await PlatformExpress.bootstrap(Server, { + httpPort: testHarnessConfig.backchannelPort, + }); + + await testHarnessConfig.agentStartup() + + await platform.listen(true) + + $log.level = 'debug' + } catch (er) { + $log.error(er) + + throw er + } +} + +startup() diff --git a/aries-backchannels/credo-ts/server/src/types.ts b/aries-backchannels/credo-ts/server/src/types.ts new file mode 100644 index 00000000..763f017c --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/types.ts @@ -0,0 +1,4 @@ +export type DidInfo = { + did: string + verkey: string +} \ No newline at end of file diff --git a/aries-backchannels/credo-ts/server/src/utils/ConnectionUtils.ts b/aries-backchannels/credo-ts/server/src/utils/ConnectionUtils.ts new file mode 100644 index 00000000..1b901be8 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/utils/ConnectionUtils.ts @@ -0,0 +1,20 @@ +import { Agent, ConnectionRecord } from '@credo-ts/core' +import { NotFound } from '@tsed/exceptions' + +export class ConnectionUtils { + /** + * The connection id is not available yet when creating a connection invitation. Instead, we return the out of band id + * and use this method to find the associated connection for a given out of band id. + */ + public static async getConnectionByConnectionIdOrOutOfBandId(agent: Agent, id: string): Promise { + const connection = await agent.connections.findById(id) + + if (connection) return connection + + const connections = await agent.connections.findAllByOutOfBandId(id) + + if (connections.length === 0) throw new NotFound(`Connection with id ${id} not found`) + + return connections[0] + } +} diff --git a/aries-backchannels/credo-ts/server/src/utils/CredentialUtils.ts b/aries-backchannels/credo-ts/server/src/utils/CredentialUtils.ts new file mode 100644 index 00000000..bd9aa3c7 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/utils/CredentialUtils.ts @@ -0,0 +1,23 @@ +import { Agent, CredentialRepository } from '@credo-ts/core' +import { AnonCredsCredentialInfo, AnonCredsCredentialRepository, AnonCredsHolderService, AnonCredsHolderServiceSymbol } from '@credo-ts/anoncreds' + +export class CredentialUtils { + public static async getCredentialByThreadId(agent: Agent, threadId: string) { + const credentialRepository = agent.dependencyManager.resolve(CredentialRepository) + return credentialRepository.getSingleByQuery(agent.context, { threadId }) + } + + public static async getAnonCredsCredentialById(agent: Agent, credentialId: string): Promise> { + const holderService = agent.dependencyManager.resolve(AnonCredsHolderServiceSymbol) + const credentialInfo = await holderService.getCredential(agent.context, { id: credentialId }) + + return { + attrs: credentialInfo.attributes, + cred_def_id: credentialInfo.credentialDefinitionId, + referent: credentialInfo.credentialId, + schema_id: credentialInfo.schemaId, + cred_rev_id: credentialInfo.credentialRevocationId, + rev_reg_id: credentialInfo.revocationRegistryId, + } + } +} diff --git a/aries-backchannels/credo-ts/server/src/utils/ProofUtils.ts b/aries-backchannels/credo-ts/server/src/utils/ProofUtils.ts new file mode 100644 index 00000000..8306ebf8 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/utils/ProofUtils.ts @@ -0,0 +1,8 @@ +import { Agent, ProofRepository } from '@credo-ts/core' + +export class ProofUtils { + public static async getProofByThreadId(agent: Agent, threadId: string) { + const proofRepository = agent.dependencyManager.resolve(ProofRepository) + return proofRepository.getSingleByQuery(agent.context, { threadId }) + } +} diff --git a/aries-backchannels/credo-ts/server/src/utils/httpUtils.ts b/aries-backchannels/credo-ts/server/src/utils/httpUtils.ts new file mode 100644 index 00000000..c12bc4f1 --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/utils/httpUtils.ts @@ -0,0 +1,17 @@ +import fetch, { RequestInit } from 'node-fetch' + +export async function postJson( + url: string, + data: Record, + options?: Exclude +) { + return await fetch(url, { + ...options, + method: 'POST', + body: JSON.stringify(data), + }) +} + +export async function get(url: string) { + return await fetch(url) +} diff --git a/aries-backchannels/credo-ts/server/src/utils/ledgerUtils.ts b/aries-backchannels/credo-ts/server/src/utils/ledgerUtils.ts new file mode 100644 index 00000000..39be6eda --- /dev/null +++ b/aries-backchannels/credo-ts/server/src/utils/ledgerUtils.ts @@ -0,0 +1,46 @@ +import { postJson, get } from './httpUtils' +import os from 'os' +import path from 'path' +import { promises as fs } from 'fs' + +export async function registerPublicDid(ledgerUrl: string, seed: string, alias?: string) { + const data = { + alias, + seed, + role: 'TRUST_ANCHOR', + } + + await postJson(`${ledgerUrl}/register`, data) +} + +export function getRandomSeed() { + const min = 100000 + const max = 999999 + + const randomNum = Math.floor(Math.random() * (max - min) + min) + + return `my_seed_000000000000000000${randomNum}` +} + +export async function getGenesisPath( + genesisFile?: string, + genesisUrl?: string, + ledgerUrl?: string, + dockerHost?: string +) { + // If the genesis file is present, we already have the path + if (genesisFile) return genesisFile + + let genesisTransactionUrl: string + + if (genesisUrl) genesisTransactionUrl = genesisUrl + else if (ledgerUrl) genesisTransactionUrl = `${ledgerUrl}/genesis` + else genesisTransactionUrl = `http://${dockerHost}:9000/genesis` + + const result = await get(genesisTransactionUrl) + const genesis = await result.text() + const genesisPath = path.join(os.tmpdir(), 'genesis.txn') + await fs.writeFile(genesisPath, genesis) + + return genesisPath +} diff --git a/aries-backchannels/credo-ts/server/tsconfig.compile.json b/aries-backchannels/credo-ts/server/tsconfig.compile.json new file mode 100644 index 00000000..a1524178 --- /dev/null +++ b/aries-backchannels/credo-ts/server/tsconfig.compile.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "./dist", + "moduleResolution": "node", + "declaration": true, + "noResolve": false, + "preserveConstEnums": true, + "sourceMap": true, + "noEmit": false, + "inlineSources": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/aries-backchannels/credo-ts/server/tsconfig.json b/aries-backchannels/credo-ts/server/tsconfig.json new file mode 100644 index 00000000..3214ca61 --- /dev/null +++ b/aries-backchannels/credo-ts/server/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2016", + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "moduleResolution": "node", + "isolatedModules": false, + "suppressImplicitAnyIndexErrors": false, + "noImplicitAny": true, + "strict": true, + "strictNullChecks": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "allowSyntheticDefaultImports": true, + "importHelpers": true, + "newLine": "LF", + "noEmit": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["es7", "dom", "esnext.asynciterable", "ES2019.Object"], + "typeRoots": ["./node_modules/@types"] + }, + "include": ["./src/**/*.ts"], + "exclude": ["node_modules", "./public", "dist", "test"], + "linterOptions": { + "exclude": [] + } +} diff --git a/aries-backchannels/javascript/.vscode/launch.json b/aries-backchannels/javascript/.vscode/launch.json index e9f98640..2d49dab1 100644 --- a/aries-backchannels/javascript/.vscode/launch.json +++ b/aries-backchannels/javascript/.vscode/launch.json @@ -7,10 +7,23 @@ { "type": "node", "request": "launch", - "name": "Launch & Debug Credo Agent", + "name": "Launch & Debug AFJ Agent", "program": "${workspaceFolder}/src/index.ts", "preLaunchTask": "tsc: build - tsconfig.json", "outFiles": ["${workspaceFolder}/out/**/*.js"] }, + { + "type": "node", + "request": "launch", + "name": "yarn Launch & Debug AFJ Agent", + "runtimeExecutable": "yarn", + "args": [ + "ts-node", + "src/index.ts", + "9020" + ], + "cwd": "${workspaceFolder}/server", + "console": "integratedTerminal" + } ] } \ No newline at end of file diff --git a/aries-backchannels/javascript/server/package.json b/aries-backchannels/javascript/server/package.json index c1b7421d..5ea4d924 100644 --- a/aries-backchannels/javascript/server/package.json +++ b/aries-backchannels/javascript/server/package.json @@ -11,21 +11,21 @@ "format": "prettier --ignore-path .gitignore '**/*.+(js|json|ts|md|yml|yaml)' --write" }, "dependencies": { - "@credo-ts/anoncreds": "alpha", - "@credo-ts/askar": "alpha", - "@credo-ts/core": "alpha", - "@credo-ts/indy-vdr": "alpha", - "@credo-ts/node": "alpha", + "@aries-framework/anoncreds": "alpha", "@aries-framework/anoncreds-rs": "alpha", - "@hyperledger/anoncreds-nodejs": "0.2.2", - "@hyperledger/aries-askar-nodejs": "0.2.1", - "@hyperledger/indy-vdr-nodejs": "0.2.2", - "@tsed/common": "7.69.4", - "@tsed/core": "7.69.4", - "@tsed/di": "7.69.4", - "@tsed/exceptions": "7.69.4", - "@tsed/platform-express": "7.69.4", - "@tsed/swagger": "7.69.4", + "@aries-framework/askar": "alpha", + "@aries-framework/core": "alpha", + "@aries-framework/indy-vdr": "alpha", + "@aries-framework/node": "alpha", + "@hyperledger/anoncreds-nodejs": "^0.1.0-dev.11", + "@hyperledger/aries-askar-nodejs": "^0.1.0-dev.6", + "@hyperledger/indy-vdr-nodejs": "^0.1.0-dev.12", + "@tsed/common": "6.75.4", + "@tsed/core": "6.75.4", + "@tsed/di": "6.75.4", + "@tsed/exceptions": "6.75.4", + "@tsed/platform-express": "6.75.4", + "@tsed/swagger": "^6.88.0", "body-parser": "1.19.0", "cross-env": "7.0.3", "express": "^4.17.1", @@ -41,10 +41,10 @@ "nodemon": "2.0.14", "prettier": "^2.4.1", "ts-node": "10.3.0", - "typescript": "^5.4.5" + "typescript": "~4.3.0" }, "resolutions": { "ffi-napi": "npm:@2060.io/ffi-napi", "ref-napi": "npm:@2060.io/ref-napi" } -} +} \ No newline at end of file diff --git a/aries-test-harness/agent_backchannel_client.py b/aries-test-harness/agent_backchannel_client.py index efc3d918..cdb9e8f4 100644 --- a/aries-test-harness/agent_backchannel_client.py +++ b/aries-test-harness/agent_backchannel_client.py @@ -2,6 +2,7 @@ import json import os.path from time import sleep +import urllib.parse from aiohttp import ClientSession @@ -72,6 +73,10 @@ def agent_backchannel_GET(url, topic, operation=None, id=None, anoncreds=False) if operation: agent_url = agent_url + operation + "/" if id: + # Some ids like a schema id contain / and can be qualified. This causes problems finding the proper route in the backchannel. + # if id contains one or more /, encode the id. + if '/' in id: + id = urllib.parse.quote(id, safe='') agent_url = agent_url + id if (anoncreds): params["anoncreds"] = 'True' diff --git a/aries-test-harness/features/steps/0036-issue-credential.py b/aries-test-harness/features/steps/0036-issue-credential.py index d12a2bc8..3ac14aee 100644 --- a/aries-test-harness/features/steps/0036-issue-credential.py +++ b/aries-test-harness/features/steps/0036-issue-credential.py @@ -99,10 +99,13 @@ def step_impl(context, issuer): resp_json = json.loads(resp_text) - - context.issuer_schema_id_dict[get_schema_name(context)] = resp_json[ - "schema_id" - ] + # if the schema id contains indy then convert it to a legacy id. + if "indy" in resp_json["schema_id"]: + context.issuer_schema_id_dict[get_schema_name(context)] = convert_fully_qualified_indy_schema_id_to_legacy(resp_json["schema_id"]) + else: + context.issuer_schema_id_dict[get_schema_name(context)] = resp_json[ + "schema_id" + ] @when('"{issuer}" creates a new credential definition') @@ -136,10 +139,11 @@ def step_impl(context, issuer): # context.cred_rev_creation_time = resp_json["created"] context.cred_rev_creation_time = time.time() - context.credential_definition_id_dict[get_schema_name(context)] = resp_json[ - "credential_definition_id" - ] - + # if the credential definition id contains indy then convert it to a legacy id. + if "indy" in resp_json["credential_definition_id"]: + context.credential_definition_id_dict[get_schema_name(context)] = convert_fully_qualified_indy_cred_def_id_to_legacy(resp_json["credential_definition_id"]) + else: + context.credential_definition_id_dict[get_schema_name(context)] = resp_json["credential_definition_id"] @then('"{issuer}" has an existing schema') def step_impl(context, issuer): @@ -265,6 +269,22 @@ def step_impl(context, holder, issuer): # Get the thread ID from the response text. context.cred_thread_id = resp_json["thread_id"] +# Some agents will have qualifed ids like did:indy:bcovrin:test:PDTK22oZnNyDSEn3XedwMq/anoncreds/v0/SCHEMA/test_schema.Acme/1.0.0 +# This function will strip it down to the id only, so remove the prefix and the suffix that has : or / in it and match the regex /^([a-zA-Z0-9]{21,22}):2:(.+):([0-9.]+)$/ +def convert_fully_qualified_indy_cred_def_id_to_legacy(id): + parts = id.split(":") + id_part = parts[-1].split("/")[0] + schema_name = parts[-1].split("/")[4] + schema_id = parts[-1].split("/")[5] + cred_def_id = "CL" + return f"{id_part}:3:{cred_def_id}:{schema_name}:{schema_id}" + +def convert_fully_qualified_indy_schema_id_to_legacy(id): + parts = id.split(":") + id_part = parts[-1].split("/")[0] + schema_name = parts[-1].split("/")[4] + version = parts[-1].split("/")[5] + return f"{id_part}:2:{schema_name}:{version}" @given('"{issuer}" offers a credential') @when('"{issuer}" offers a credential') @@ -488,12 +508,18 @@ def step_impl(context, holder): assert resp_json["referent"] == credentials[-1] # Some agents don't return or have a schema id or cred_def_id, so only check it it exists. if "schema_id" in resp_json: - assert resp_json["schema_id"] == context.issuer_schema_id_dict[schema_name] + if "indy" in resp_json["schema_id"]: + assert convert_fully_qualified_indy_schema_id_to_legacy(resp_json["schema_id"]) == context.issuer_schema_id_dict[schema_name] + else: + assert resp_json["schema_id"] == context.issuer_schema_id_dict[schema_name] if "cred_def_id" in resp_json: - assert ( - resp_json["cred_def_id"] - == context.credential_definition_id_dict[schema_name] - ) + if "indy" in resp_json["cred_def_id"]: + assert convert_fully_qualified_indy_cred_def_id_to_legacy(resp_json["cred_def_id"]) == context.credential_definition_id_dict[schema_name] + else: + assert ( + resp_json["cred_def_id"] + == context.credential_definition_id_dict[schema_name] + ) # Make sure the issuer is not holding the credential # get the credential from the holders wallet diff --git a/gen-interop.sh b/gen-interop.sh index 5edc3758..4ac03b78 100755 --- a/gen-interop.sh +++ b/gen-interop.sh @@ -1,7 +1,7 @@ #! /bin/bash # Configuration Data -- order matters in these arrays. A new entry requires an entry in ALL "ta_" arrays -ta_tlas=("acapy" "javascript" "aries-vcx") +ta_tlas=("acapy" "credo" "aries-vcx") ta_names=("Aries Cloud Agent Python" "Credo-TS" "Aries VCX") ta_shortnames=("ACA-Py" "Credo" "VCX") ta_scopes=("AIP 1, 2" "AIP 1" "AIP 1") @@ -16,7 +16,7 @@ aath_guide="docs/guide" # We don't want these files checked into the website if [[ "$1" == "clean" ]]; then - rm -rf ${aath_guide} docs/acapy.md docs/aries-vcx.md docs/javascript.md docs/README.md + rm -rf ${aath_guide} docs/acapy.md docs/aries-vcx.md docs/credo.md docs/README.md exit 0 fi