├── .env.demo ├── .env.sample ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── BUG-REPORT.md │ ├── DMP_2024.yml │ └── FEATURE-REQUEST.md └── workflows │ ├── continuous-delivery.yml │ └── continuous-integration.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── afj-rest.js ├── compass.yml ├── docker-compose.yml ├── jest.config.base.ts ├── jest.config.ts ├── package.json ├── patches ├── @credo-ts+anoncreds+0.5.3+001+fix: Extensible model confict in Anoncreds and Did.patch ├── @credo-ts+core+0.5.0.patch ├── @credo-ts+core+0.5.1+001+initial.patch ├── @credo-ts+core+0.5.3+002+fix-process-problem-report.patch ├── @credo-ts+core+0.5.3+004+added-prettyVc-in-JsonCredential-interface.patch ├── @credo-ts+core+0.5.3+005+commenting validationPresentation to avoid abandoned issue.patch ├── @credo-ts+core+0.5.3+006+w3c-issuance-without-holder-did-negotiaton.patch └── @credo-ts+tenants+0.5.3+001+cache-tenant-record-patch.patch ├── samples ├── cliConfig.json ├── sample.ts └── sampleWithApp.ts ├── scripts └── taskdef │ ├── credo-ecs-taskdef.json │ └── credo-fargate-taskdef.json ├── src ├── authentication.ts ├── cli.ts ├── cliAgent.ts ├── controllers │ ├── agent │ │ └── AgentController.ts │ ├── basic-messages │ │ └── BasicMessageController.ts │ ├── connections │ │ └── ConnectionController.ts │ ├── credentials │ │ ├── CredentialController.ts │ │ ├── CredentialDefinitionController.ts │ │ └── SchemaController.ts │ ├── did │ │ └── DidController.ts │ ├── endorser-transaction │ │ └── EndorserTransactionController.ts │ ├── examples.ts │ ├── multi-tenancy │ │ └── MultiTenancyController.ts │ ├── outofband │ │ └── OutOfBandController.ts │ ├── polygon │ │ └── PolygonController.ts │ ├── proofs │ │ └── ProofController.ts │ ├── question-answer │ │ └── QuestionAnswerController.ts │ └── types.ts ├── enums │ └── enum.ts ├── errorHandlingService.ts ├── errorMessages.ts ├── errors │ ├── ApiError.ts │ ├── errors.ts │ └── index.ts ├── events │ ├── BasicMessageEvents.ts │ ├── ConnectionEvents.ts │ ├── CredentialEvents.ts │ ├── ProofEvents.ts │ ├── QuestionAnswerEvents.ts │ ├── ReuseConnectionEvents.ts │ ├── WebSocketEvents.ts │ └── WebhookEvent.ts ├── index.ts ├── routes │ ├── routes.ts │ └── swagger.json ├── securityMiddleware.ts ├── server.ts └── utils │ ├── ServerConfig.ts │ ├── TsyringeAdapter.ts │ ├── agent.ts │ ├── errorConverter.ts │ ├── helpers.ts │ ├── logger.ts │ ├── tsyringeTsoaIocContainer.ts │ └── webhook.ts ├── tsconfig.build.json ├── tsconfig.eslint.json ├── tsconfig.json ├── tsoa.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | ignorePatterns: ['**/tests/*'], 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:import/recommended', 7 | 'plugin:import/typescript', 8 | 'plugin:@typescript-eslint/recommended', 9 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 10 | ], 11 | parserOptions: { 12 | tsconfigRootDir: __dirname, 13 | project: ['./tsconfig.eslint.json'], 14 | }, 15 | settings: { 16 | 'import/extensions': ['.js', '.ts'], 17 | 'import/parsers': { 18 | '@typescript-eslint/parser': ['.ts', '.tsx'], 19 | }, 20 | 'import/resolver': { 21 | typescript: { 22 | project: './tsconfig.json', 23 | alwaysTryTypes: true, 24 | }, 25 | }, 26 | }, 27 | rules: { 28 | 'no-constant-condition': 'warn', 29 | '@typescript-eslint/no-explicit-any': 'warn', 30 | '@typescript-eslint/explicit-function-return-type': 'off', 31 | '@typescript-eslint/explicit-module-boundary-types': 'off', 32 | '@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: false, variables: true }], 33 | '@typescript-eslint/explicit-member-accessibility': 'error', 34 | 'no-console': 'error', 35 | '@typescript-eslint/ban-ts-comment': 'warn', 36 | '@typescript-eslint/consistent-type-imports': 'error', 37 | 'import/no-cycle': 'error', 38 | 'import/order': [ 39 | 'error', 40 | { 41 | groups: ['type', ['builtin', 'external'], 'parent', 'sibling', 'index'], 42 | alphabetize: { 43 | order: 'asc', 44 | }, 45 | 'newlines-between': 'always', 46 | }, 47 | ], 48 | 'import/no-extraneous-dependencies': [ 49 | 'error', 50 | { 51 | devDependencies: false, 52 | }, 53 | ], 54 | }, 55 | overrides: [ 56 | { 57 | files: ['jest.config.ts', '.eslintrc.js'], 58 | env: { 59 | node: true, 60 | }, 61 | }, 62 | { 63 | files: ['*.test.ts', '**/__tests__/**', '**/tests/**', 'jest.*.ts', '**/samples/**'], 64 | env: { 65 | jest: true, 66 | node: false, 67 | }, 68 | rules: { 69 | 'import/no-extraneous-dependencies': [ 70 | 'error', 71 | { 72 | devDependencies: true, 73 | }, 74 | ], 75 | }, 76 | }, 77 | ], 78 | } 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG-REPORT.md: -------------------------------------------------------------------------------- 1 | ## 🧾 Preliminary Checks 2 | 3 | - [ ] I have searched [existing issues](https://github.com/credebl/credo-controller/issues) and [pull requests](https://github.com/credebl/credo-controller/pulls) for duplicates. 4 | - [ ] I'm willing to create a PR fixing this issue. (if applicable). 5 | 6 | --- 7 | 8 | ## 🐞 Bug Description 9 | 10 | _A clear and concise description of what the bug is._ 11 | 12 | When I try to [...], I get this unexpected behavior [...] 13 | 14 | --- 15 | 16 | ## 🧪 Steps to Reproduce 17 | 18 | _Provide clear steps to reproduce the bug._ 19 | 20 | 1. Go to '...' 21 | 2. Click on '...' 22 | 3. Scroll down to '...' 23 | 4. See error 24 | 25 | --- 26 | 27 | ## ✅ Expected Behavior 28 | 29 | _What did you expect to happen?_ 30 | 31 | --- 32 | 33 | ## ❌ Actual Behavior 34 | 35 | _What actually happened instead?_ 36 | 37 | --- 38 | 39 | ## 📌 Affected Version/Commit 40 | 41 | _Version number, branch name, or commit hash where the bug occurs._ 42 | 43 | --- 44 | 45 | ## 💻 Environment 46 | 47 | _Where did the issue occur?_ 48 | 49 | - [ ] Local development 50 | - [ ] Production 51 | - [ ] CI/CD 52 | - [ ] Other 53 | 54 | --- 55 | 56 | ## 🧾 Relevant Logs, Screenshots, or Stack Traces 57 | 58 | _Paste any error messages or screenshots that can help diagnose the issue._ 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/DMP_2024.yml: -------------------------------------------------------------------------------- 1 | name: DMP 2024 Project Template 2 | description: List a new project for Dedicated Mentoring Program (DMP) 2024 3 | title: '[DMP 2024]: ' 4 | labels: ['DMP 2024'] 5 | body: 6 | - type: textarea 7 | id: ticket-description 8 | validations: 9 | required: true 10 | attributes: 11 | label: Ticket Contents 12 | value: | 13 | ## Description 14 | [Provide a brief description of the feature, including why it is needed and what it will accomplish.] 15 | 16 | - type: textarea 17 | id: ticket-goals 18 | validations: 19 | required: true 20 | attributes: 21 | label: Goals & Mid-Point Milestone 22 | description: List the goals of the feature. Please add the goals that must be achieved by Mid-point check-in i.e 1.5 months into the coding period. 23 | value: | 24 | ## Goals 25 | - [ ] [Goal 1] 26 | - [ ] [Goal 2] 27 | - [ ] [Goal 3] 28 | - [ ] [Goal 4] 29 | - [ ] [Goals Achieved By Mid-point Milestone] 30 | 31 | - type: textarea 32 | id: ticket-setup 33 | attributes: 34 | label: Setup/Installation 35 | description: Please list or link setup or installation guide (if any) 36 | 37 | - type: textarea 38 | id: ticket-expected-outcome 39 | attributes: 40 | label: Expected Outcome 41 | description: Describe in detail what the final product or result should look like and how it should behave. 42 | 43 | - type: textarea 44 | id: ticket-acceptance-criteria 45 | attributes: 46 | label: Acceptance Criteria 47 | description: List the acceptance criteria for this feature. 48 | 49 | - type: textarea 50 | id: ticket-implementation-details 51 | validations: 52 | required: true 53 | attributes: 54 | label: Implementation Details 55 | description: List any technical details about the proposed implementation, including any specific technologies that will be used. 56 | 57 | - type: textarea 58 | id: ticket-mockups 59 | attributes: 60 | label: Mockups/Wireframes 61 | description: Include links to any visual aids, mockups, wireframes, or diagrams that help illustrate what the final product should look like. This is not always necessary, but can be very helpful in many cases. 62 | 63 | - type: input 64 | id: ticket-product 65 | attributes: 66 | label: Product Name 67 | placeholder: Enter Product Name 68 | validations: 69 | required: true 70 | 71 | - type: dropdown 72 | id: ticket-organisation 73 | attributes: 74 | label: Organisation Name 75 | description: Enter Organisation Name 76 | multiple: false 77 | options: 78 | - Bandhu 79 | - Blockster Labs (CREDEBL) 80 | - Civis 81 | - Dhwani 82 | - Dhiway 83 | - EGov 84 | - EkShop Marketplace 85 | - FIDE 86 | - If Me 87 | - Key Education Foundation 88 | - Norwegian Meteorological Institute 89 | - Planet Read 90 | - Project Second Chance 91 | - Reap Benefit 92 | - SamagraX 93 | - ShikshaLokam 94 | - Tech4Dev 95 | - Tekdi 96 | - The Mifos Initiative 97 | - Tibil 98 | - Ushahidi 99 | - Arghyam 100 | - Piramal Swasthya Management Research Institute 101 | validations: 102 | required: true 103 | 104 | - type: dropdown 105 | id: ticket-governance-domain 106 | attributes: 107 | label: Domain 108 | options: 109 | - ⁠Healthcare 110 | - ⁠Education 111 | - Financial Inclusion 112 | - ⁠Livelihoods 113 | - ⁠Skilling 114 | - ⁠Learning & Development 115 | - ⁠Agriculture 116 | - ⁠Service Delivery 117 | - Open Source Library 118 | - Water 119 | - Identity & Digital Credentialing 120 | validations: 121 | required: true 122 | 123 | - type: dropdown 124 | id: ticket-technical-skills-required 125 | attributes: 126 | label: Tech Skills Needed 127 | description: Select the technologies needed for this ticket (use Ctrl or Command to select multiple) 128 | multiple: true 129 | options: 130 | - .NET 131 | - Angular 132 | - Artificial Intelligence 133 | - ASP.NET 134 | - Astro.js 135 | - AWS 136 | - Babel 137 | - Bootstrap 138 | - C# 139 | - Chart.js 140 | - CI/CD 141 | - Computer Vision 142 | - CORS 143 | - cURL 144 | - Cypress 145 | - D3.js 146 | - Database 147 | - Debugging 148 | - Deno 149 | - Design 150 | - DevOps 151 | - Django 152 | - Docker 153 | - Electron 154 | - ESLint 155 | - Express.js 156 | - Feature 157 | - Flask 158 | - Go 159 | - GraphQL 160 | - HTML 161 | - Ionic 162 | - Jest 163 | - Java 164 | - JavaScript 165 | - Jenkins 166 | - JWT 167 | - Kubernetes 168 | - Laravel 169 | - Machine Learning 170 | - Maintenance 171 | - Markdown 172 | - Material-UI 173 | - Microservices 174 | - MongoDB 175 | - Mobile 176 | - Mockups 177 | - Mocha 178 | - Natural Language Processing 179 | - NATS Messaging 180 | - NestJS 181 | - Next.js 182 | - Node.js 183 | - NUnit 184 | - OAuth 185 | - Performance Improvement 186 | - Prettier 187 | - Python 188 | - Question 189 | - React 190 | - React Native 191 | - Redux 192 | - RESTful APIs 193 | - Ruby 194 | - Ruby on Rails 195 | - Rust 196 | - Scala 197 | - Security 198 | - Selenium 199 | - SEO 200 | - Serverless 201 | - Solidity 202 | - Spring Boot 203 | - SQL 204 | - Swagger 205 | - Tailwind CSS 206 | - Test 207 | - Testing Library 208 | - Three.js 209 | - TypeScript 210 | - UI/UX/Design 211 | - Virtual Reality 212 | - Vue.js 213 | - WebSockets 214 | - Webpack 215 | - Other 216 | validations: 217 | required: true 218 | 219 | - type: textarea 220 | id: ticket-mentors 221 | attributes: 222 | label: Mentor(s) 223 | description: Please tag relevant mentors for the ticket 224 | validations: 225 | required: true 226 | 227 | - type: dropdown 228 | id: ticket-category 229 | attributes: 230 | label: Category 231 | description: Choose the categories that best describe your ticket 232 | multiple: true 233 | options: 234 | - API 235 | - Analytics 236 | - Accessibility 237 | - Backend 238 | - Breaking Change 239 | - Beginner Friendly 240 | - Configuration 241 | - CI/CD 242 | - Database 243 | - Data Science 244 | - Deprecation 245 | - Documentation 246 | - Deployment 247 | - Frontend 248 | - Internationalization 249 | - Localization 250 | - Machine Learning 251 | - Maintenance 252 | - Mobile 253 | - Performance Improvement 254 | - Question 255 | - Refactoring 256 | - Research 257 | - Needs Reproduction 258 | - SEO 259 | - Security 260 | - Testing 261 | - AI 262 | - Other 263 | validations: 264 | required: true 265 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md: -------------------------------------------------------------------------------- 1 | ## ✅ Preliminary Checks 2 | 3 | - [ ] I have searched [existing issues](https://github.com/credebl/credo-controller/issues) and [pull requests](https://github.com/credebl/credo-controller/pulls) to avoid duplicates. 4 | - [ ] I'm willing to create a PR for this feature. (if applicable). 5 | 6 | --- 7 | 8 | ## 🧩 Problem Statement 9 | 10 | _Is your feature request related to a problem? Please describe it clearly._ 11 | 12 | > Ex: I'm always frustrated when [...] 13 | 14 | --- 15 | 16 | ## 💡 Proposed Solution 17 | 18 | _A clear and concise description of what you want to happen._ 19 | 20 | > Ex: It would be great if [...] 21 | 22 | --- 23 | 24 | ## 🔄 Alternatives Considered 25 | 26 | _Have you considered any alternative solutions or features?_ 27 | 28 | > Ex: I also thought about [...], but [...] 29 | 30 | --- 31 | 32 | ## 📎 Additional Context 33 | 34 | _Add any other context, references, mockups, or screenshots here._ 35 | 36 | --- 37 | 38 | ## ✅ Acceptance Criteria 39 | 40 | _List specific tasks or outcomes that define when this request is complete._ 41 | 42 | - A new endpoint `/v1/...` is added 43 | - Docs updated 44 | - Tests written and passing 45 | -------------------------------------------------------------------------------- /.github/workflows/continuous-delivery.yml: -------------------------------------------------------------------------------- 1 | name: Continous Delivery 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | SERVICE: credo-controller 11 | 12 | jobs: 13 | build-and-push: 14 | name: Push Docker image to GitHub 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: Checkout Repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Extract Git Tag 26 | id: get_tag 27 | run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 28 | 29 | - name: Log in to GitHub Container Registry 30 | uses: docker/login-action@v3 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.actor }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Build and Push Docker Image ${{ env.SERVICE }} 37 | uses: docker/build-push-action@v6 38 | with: 39 | context: . 40 | file: Dockerfile 41 | push: true 42 | tags: | 43 | ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.SERVICE }}:${{ env.TAG }} 44 | ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.SERVICE }}:latest 45 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | 9 | concurrency: 10 | # Cancel previous runs that are not completed yet 11 | group: afj-controller-${{ github.ref }}-${{ github.repository }}-${{ github.event_name }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | validate: 16 | runs-on: ubuntu-20.04 17 | name: Validate 18 | steps: 19 | - name: Checkout afj-controller 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup NodeJS 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 18.19.0 26 | cache: 'yarn' 27 | 28 | - name: Install dependencies 29 | run: yarn install 30 | 31 | - name: Linting 32 | run: yarn lint 33 | 34 | - name: Prettier 35 | run: yarn check-format 36 | 37 | - name: Compile 38 | run: yarn check-types 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .vscode 4 | yarn-error.log 5 | .idea 6 | coverage 7 | .DS_Store 8 | logs.txt 9 | *.tgz 10 | 11 | # dotenv environment variable files 12 | .env 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | .env.local 17 | 18 | # parcel-bundler cache (https://parceljs.org/) 19 | .cache 20 | .parcel-cache 21 | 22 | # Next.js build output 23 | .next 24 | out 25 | 26 | # Nuxt.js build / generate output 27 | .nuxt 28 | dist 29 | 30 | # Gatsby files 31 | .cache/ 32 | # Comment in the public line in if your project uses Gatsby and not Next.js 33 | # https://nextjs.org/blog/next-9-1#public-directory-support 34 | # public 35 | 36 | # vuepress build output 37 | .vuepress/dist 38 | 39 | # vuepress v2.x temp and cache directory 40 | .temp 41 | .cache 42 | 43 | # Docusaurus cache and generated files 44 | .docusaurus 45 | 46 | # Serverless directories 47 | .serverless/ 48 | 49 | # FuseBox cache 50 | .fusebox/ 51 | 52 | # DynamoDB Local files 53 | .dynamodb/ 54 | 55 | # TernJS port file 56 | .tern-port 57 | 58 | # Stores VSCode versions used for testing VSCode extensions 59 | .vscode-test 60 | 61 | # yarn v2 62 | .yarn/cache 63 | .yarn/unplugged 64 | .yarn/build-state.yml 65 | .yarn/install-state.gz 66 | .pnp.* 67 | build 68 | logs.txt 69 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .vscode 4 | .idea 5 | routes -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### [0.9.4](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.9.3...rest-v0.9.4) (2022-10-07) 4 | 5 | ### Features 6 | 7 | - **rest:** added did resolver endpoint and tests ([#172](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/172)) ([9a1a24e](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/9a1a24ee2c958d09fff13075e0f56e0d3ed9ce7c)) 8 | 9 | ### [0.9.3](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.9.2...rest-v0.9.3) (2022-09-21) 10 | 11 | ### Features 12 | 13 | - **rest:** added create-offer endpoint and tests ([#169](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/169)) ([5458e9e](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/5458e9ee06c8a8d61fb6a812ea04f4d1a59b21dc)) 14 | - **rest:** added filters to getAllCredentials ([#166](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/166)) ([af7ec19](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/af7ec197b317b16cb5d2083d880006f29d0272c6)) 15 | - **rest:** added WebSocket event server ([#170](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/170)) ([e190821](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/e190821b3f71c97e03cc92222fedceeadb514aab)) 16 | 17 | ### [0.9.2](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.9.1...rest-v0.9.2) (2022-09-07) 18 | 19 | ### Bug Fixes 20 | 21 | - **rest:** accept proof properties now optional ([#162](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/162)) ([f927fdc](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/f927fdcd4a6142a6bc086d82d3b6e6ed1317108d)) 22 | 23 | ### [0.9.1](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.9.0...rest-v0.9.1) (2022-09-05) 24 | 25 | ### Bug Fixes 26 | 27 | - **rest:** moved route generation to compile ([#160](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/160)) ([8c70864](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/8c70864cacaada486b0ca7a7f9ba0ca2395f9efd)) 28 | 29 | ## [0.9.0](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.8.1...rest-v0.9.0) (2022-09-02) 30 | 31 | ### ⚠ BREAKING CHANGES 32 | 33 | - **rest:** update to AFJ 0.2.0 (#148) 34 | 35 | ### Features 36 | 37 | - **rest:** update to AFJ 0.2.0 ([#148](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/148)) ([8ec4dc4](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/8ec4dc4548305d5cc8180b657f5865002eb3ee4a)) 38 | 39 | ### [0.8.1](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.8.0...rest-v0.8.1) (2022-06-28) 40 | 41 | ### Features 42 | 43 | - **rest:** added multi use param to create invitation ([#100](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/100)) ([d00f11d](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/d00f11d78e9f65de3907bd6bf94dd6c38e2ddc3b)) 44 | - **rest:** improved class validation ([#108](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/108)) ([cb48752](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/cb48752f0e222080f46c0699528e901de1226211)) 45 | 46 | ### Bug Fixes 47 | 48 | - **rest:** changed webhook event topic to type ([#117](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/117)) ([fed645e](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/fed645ec4ba77313e092bce097444a96aa66cf6e)) 49 | - **rest:** ledger not found error ([2374b42](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/2374b4232a0b11738fb57e23dd2a3ac1b81ad073)) 50 | 51 | ## [0.8.0](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.7.0...rest-v0.8.0) (2022-02-26) 52 | 53 | ### Features 54 | 55 | - **rest:** add cli and docker image publishing ([#96](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/96)) ([87d0205](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/87d02058e4b7d1fba1039265f5d595880f862097)) 56 | - **rest:** add webhooks ([#93](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/93)) ([9fc020d](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/9fc020d7db0f002894e520766987eec327a2ed69)) 57 | - **rest:** added basic messages and receive invitation by url ([#97](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/97)) ([956c928](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/956c928e3599925c65d8f99852bf06cebc06dba7)) 58 | 59 | ## [0.7.0](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.6.1...rest-v0.7.0) (2022-01-04) 60 | 61 | ### ⚠ BREAKING CHANGES 62 | 63 | - update aries framework javascript version to 0.1.0 (#86) 64 | 65 | ### Miscellaneous Chores 66 | 67 | - update aries framework javascript version to 0.1.0 ([#86](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/86)) ([ebaa11a](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/ebaa11a8f1c4588b020e870abd092a5813ec28ef)) 68 | 69 | ### [0.6.1](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.6.0...rest-v0.6.1) (2021-12-07) 70 | 71 | ### Bug Fixes 72 | 73 | - **rest:** made nonce optional on proofrequest ([#84](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/84)) ([c1efe58](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/c1efe58055639e1c3df0429df6a0efe8fcdeb850)) 74 | 75 | ## [0.6.0](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.5.0...rest-v0.6.0) (2021-12-06) 76 | 77 | ### ⚠ BREAKING CHANGES 78 | 79 | - **rest:** proof request indy fields are now snake_case as used by indy instead of camelCase as used by AFJ. 80 | 81 | ### Bug Fixes 82 | 83 | - **deps:** update dependencies ([#78](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/78)) ([ca38eba](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/ca38eba50dbb524269865d4fbfcb2d33720d0b48)) 84 | - **rest:** remove record transformer ([#77](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/77)) ([cda30f5](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/cda30f56b557a11645e9201ecf3e615ce8c890f5)) 85 | 86 | ## [0.5.0](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.4.0...rest-v0.5.0) (2021-11-15) 87 | 88 | ### ⚠ BREAKING CHANGES 89 | 90 | - **rest:** the 'extraControllers' config property has been removed in favor of a custom 'app' property. This allows for a more flexible wat to customize the express app. See the sample for an example. 91 | 92 | ### Features 93 | 94 | - **rest:** allow app instance for custom configuration ([#73](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/73)) ([35400df](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/35400df5bdf1f621109e38aca4fa6644664612c8)) 95 | 96 | ## [0.4.0](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.3.0...rest-v0.4.0) (2021-11-07) 97 | 98 | ### ⚠ BREAKING CHANGES 99 | 100 | - **rest:** changed oob proof parameter from c_i to d_m (#67) 101 | 102 | ### Features 103 | 104 | - **rest:** added outofband offer to credentialController ([#70](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/70)) ([d514688](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/d514688e2ca2c36312ef27b4d4a59ee3059e33de)) 105 | - **rest:** added support for custom label and custom imageUrl ([#71](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/71)) ([686bddd](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/686bddd58d0947ab4dda1b1d4a49ce721c6b464b)) 106 | 107 | ### Code Refactoring 108 | 109 | - **rest:** changed oob proof parameter from c_i to d_m ([#67](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/67)) ([5f9b1ae](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/5f9b1aeabcd81b5d3a084f69b280ceff84298b7e)) 110 | 111 | ## [0.3.0](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.2.0...rest-v0.3.0) (2021-11-01) 112 | 113 | ### ⚠ BREAKING CHANGES 114 | 115 | - **rest:** The credentential-definitions endpoint topic contained a typo (credential-defintions instead of credential-definitions) 116 | - **rest:** The connection id is moved from the path to the request body for credential and proof endpoints 117 | 118 | ### Bug Fixes 119 | 120 | - **rest:** typo in credential definition endpoint ([b4d345e](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/b4d345ed2af112679389ad4d8ed76760e442cc26)) 121 | 122 | ### Code Refactoring 123 | 124 | - **rest:** moved connectionId from path to requestbody ([#59](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/59)) ([1d37f0b](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/1d37f0bdde96742fc947213f8b934353872c570c)) 125 | 126 | ## [0.2.0](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.1.2...rest-v0.2.0) (2021-10-05) 127 | 128 | ### ⚠ BREAKING CHANGES 129 | 130 | - **rest:** The port property has been moved into a new configuration object. 131 | 132 | ### Features 133 | 134 | - **rest:** added support for custom controllers ([#39](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/39)) ([8362e30](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/8362e30d8a4c9ef24779769f81b6e74f7f5978cc)) 135 | 136 | ### [0.1.2](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.1.1...rest-v0.1.2) (2021-09-17) 137 | 138 | ### Bug Fixes 139 | 140 | - **rest:** routing fix and moved cors to dependencies ([#31](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/31)) ([0999658](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/09996580a0015004ca18d36487276588460d0dfd)) 141 | 142 | ### [0.1.1](https://www.github.com/hyperledger/aries-framework-javascript-ext/compare/rest-v0.1.0...rest-v0.1.1) (2021-09-16) 143 | 144 | ### Bug Fixes 145 | 146 | - **rest:** require package.json to avoid error ([43e683a](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/43e683a11f4eed1d848f612c6e32e82d62141769)) 147 | 148 | ## 0.1.0 (2021-09-16) 149 | 150 | ### Features 151 | 152 | - add rest package ([#10](https://www.github.com/hyperledger/aries-framework-javascript-ext/issues/10)) ([e761767](https://www.github.com/hyperledger/aries-framework-javascript-ext/commit/e7617670c3cc05ee63e827cc5a5c5079a5e8eea5)) 153 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Builder stage 2 | FROM node:18.19.0 AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy package.json and yarn.lock files 7 | COPY package.json yarn.lock ./ 8 | 9 | # Copy the rest of the application code 10 | COPY . . 11 | 12 | # Install dependencies 13 | RUN rm -rf node_modules 14 | RUN yarn install --frozen-lockfile 15 | 16 | RUN yarn global add patch-package 17 | 18 | # Build the application 19 | RUN yarn build 20 | 21 | # Stage 2: Production stage 22 | FROM node:18.19.0-slim 23 | 24 | WORKDIR /app 25 | 26 | # Copy built files and node_modules from the builder stage 27 | COPY --from=builder /app/build ./build 28 | COPY --from=builder /app/bin ./bin 29 | COPY --from=builder /app/package.json ./ 30 | COPY --from=builder /app/node_modules ./node_modules 31 | COPY --from=builder /app/patches ./patches 32 | 33 | # Set entry point 34 | ENTRYPOINT ["node", "./bin/afj-rest.js", "start"] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 AYANWORKS Technology Solutions Pvt. Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Hyperledger Aries logo 8 |

9 |

Aries Framework JavaScript REST API

10 |

11 | License 17 | typescript 22 | @aries-framework/rest version 27 | 28 |

29 |
30 | 31 | The Aries Framework JavaScript REST API is the most convenient way for self-sovereign identity (SSI) developers to interact with SSI agents. 32 | 33 | - ⭐ **Endpoints** to create connections, issue credentials, and request proofs. 34 | - 💻 **CLI** that makes it super easy to start an instance of the REST API. 35 | - 🌐 **Interoperable** with all major Aries implementations. 36 | 37 | ### Quick start 38 | 39 | The REST API provides an OpenAPI schema that can easily be viewed using the SwaggerUI that is provided with the server. The docs can be viewed on the `/docs` endpoint (e.g. http://localhost:3000/docs). 40 | 41 | > The OpenAPI spec is generated from the model classes used by Aries Framework JavaScript. Due to limitations in the inspection of these classes, the generated schema does not always exactly match the expected format. Keep this in mind when using this package. If you encounter any issues, feel free to open an issue. 42 | 43 | #### Using the CLI 44 | 45 | Using the CLI is the easiest way to get started with the REST API. 46 | 47 | **With Docker (easiest)** 48 | 49 | Make sure you have [Docker](https://docs.docker.com/get-docker/) installed. To get a minimal version of the agent running the following command is sufficient: 50 | 51 | ```sh 52 | docker run -p 5000:5000 -p 3000:3000 ghcr.io/hyperledger/afj-rest \ 53 | --label "AFJ Rest" \ 54 | --wallet-id "walletId" \ 55 | --wallet-key "walletKey" \ 56 | --endpoint http://localhost:5000 \ 57 | --admin-port 3000 \ 58 | --outbound-transport http \ 59 | --inbound-transport http 5000 60 | ``` 61 | 62 | See the [docker-compose.yml](https://github.com/hyperledger/aries-framework-javascript-ext/tree/main/docker-compose.yml) file for an example of using the afj-rest image with Docker Compose. 63 | 64 | > ⚠️ The Docker image is not optimized for ARM architectures and won't work on Apple Silicon Macs. See the **Directly on Computer** below on how to run it directly on your computer without Docker. 65 | 66 | **Directly on Computer** 67 | 68 | To run AFJ REST API directly on your computer you need to have the indy-sdk installed. Follow the Indy [installation steps](https://github.com/hyperledger/aries-framework-javascript/tree/main/docs/libindy) for your platform and verify Indy is installed. 69 | 70 | Once you have installed Indy, you can start the REST server using the following command: 71 | 72 | ```sh 73 | npx -p @aries-framework/rest afj-rest start \ 74 | --label "AFJ Rest" \ 75 | --wallet-id "walletId" \ 76 | --wallet-key "walletKey" \ 77 | --endpoint http://localhost:5000 \ 78 | --admin-port 3000 \ 79 | --outbound-transport http \ 80 | --inbound-transport http 5000 81 | ``` 82 | 83 | **Configuration** 84 | 85 | To find out all available configuration options from the CLI, you can run the CLI command with `--help`. This will print a full list of all available options. 86 | 87 | ```sh 88 | # With docker 89 | docker run ghcr.io/hyperledger/afj-rest --help 90 | 91 | # Directly on computer 92 | npx -p @aries-framework/rest afj-rest start --help 93 | ``` 94 | 95 | It is also possible to configure the REST API using a json config. When providing a lot of configuration options, this is definitely the easiest way to use configure the agent. All properties should use camelCase for the key names. See the example [CLI Config](https://github.com/hyperledger/aries-framework-javascript-ext/tree/main/packages/rest/samples/cliConfig.json) for an detailed example. 96 | 97 | ```json 98 | { 99 | "label": "AFJ Rest Agent", 100 | "walletId": "walletId", 101 | "walletKey": "walletKey" 102 | // ... other config options ... // 103 | } 104 | ``` 105 | 106 | As a final option it is possible to configure the agent using environment variables. All properties are prefixed by `AFJ_REST` transformed to UPPER_SNAKE_CASE. 107 | 108 | ```sh 109 | # With docker 110 | docker run -e AFJ_REST_WALLET_KEY=my-secret-key ghcr.io/hyperledger/afj-rest ... 111 | 112 | # Directly on computer 113 | AFJ_REST_WALLET_KEY="my-secret-key" npx -p @aries-framework/rest afj-rest start ... 114 | ``` 115 | 116 | #### Starting Own Server 117 | 118 | Starting your own server is more involved than using the CLI, but allows more fine-grained control over the settings and allows you to extend the REST API with custom endpoints. 119 | 120 | You can create an agent instance and import the `startServer` method from the `rest` package. That's all you have to do. 121 | 122 | ```ts 123 | import { startServer } from '@aries-framework/rest' 124 | import { Agent } from '@aries-framework/core' 125 | import { agentDependencies } from '@aries-framework/node' 126 | 127 | // The startServer function requires an initialized agent and a port. 128 | // An example of how to setup an agent is located in the `samples` directory. 129 | const run = async () => { 130 | const agent = new Agent( 131 | { 132 | // ... AFJ Config ... // 133 | }, 134 | agentDependencies 135 | ) 136 | await startServer(agent, { port: 3000 }) 137 | } 138 | 139 | // A Swagger (OpenAPI) definition is exposed on http://localhost:3000/docs 140 | run() 141 | ``` 142 | 143 | ### WebSocket & webhooks 144 | 145 | The REST API provides the option to connect as a client and receive events emitted from your agent using WebSocket and webhooks. 146 | 147 | You can hook into the events listener using webhooks, or connect a WebSocket client directly to the default server. 148 | 149 | The currently supported events are: 150 | 151 | - `Basic messages` 152 | - `Connections` 153 | - `Credentials` 154 | - `Proofs` 155 | 156 | When using the CLI, a webhook url can be specified using the `--webhook-url` config option. 157 | 158 | When using the REST server as an library, the WebSocket server and webhook url can be configured in the `startServer` and `setupServer` methods. 159 | 160 | ```ts 161 | // You can either call startServer() or setupServer() and pass the ServerConfig interface with a webhookUrl and/or a WebSocket server 162 | 163 | const run = async (agent: Agent) => { 164 | const config = { 165 | port: 3000, 166 | webhookUrl: 'http://test.com', 167 | socketServer: new Server({ port: 8080 }), 168 | } 169 | await startServer(agent, config) 170 | } 171 | run() 172 | ``` 173 | 174 | The `startServer` method will create and start a WebSocket server on the default http port if no socketServer is provided, and will use the provided socketServer if available. 175 | 176 | However, the `setupServer` method does not automatically create a socketServer, if one is not provided in the config options. 177 | 178 | In case of an event, we will send the event to the webhookUrl with the topic of the event added to the url (http://test.com/{topic}). 179 | 180 | So in this case when a connection event is triggered, it will be sent to: http://test.com/connections 181 | 182 | The payload of the webhook contains the serialized record related to the topic of the event. For the `connections` topic this will be a `ConnectionRecord`, for the `credentials` topic it will be a `CredentialRecord`, and so on. 183 | 184 | For the WebSocket clients, the events are sent as JSON stringified objects 185 | -------------------------------------------------------------------------------- /bin/afj-rest.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable @typescript-eslint/no-var-requires, no-undef */ 3 | 4 | const { runCliServer } = require('../build/cli') 5 | 6 | runCliServer() 7 | -------------------------------------------------------------------------------- /compass.yml: -------------------------------------------------------------------------------- 1 | name: credo-controller 2 | id: ari:cloud:compass:6095931c-8137-4ae1-822a-2d7411835fc7:component/9029b47a-062e-4e25-a761-a2c0fc9ebd98/b6016bde-79c2-4d3f-84cf-b25f27bc50c7 3 | description: Controller App for Aries Framework JavaScript REST Extension 4 | configVersion: 1 5 | typeId: SERVICE 6 | ownerId: ari:cloud:identity::team/6fe50e36-8efb-47a6-aab5-ea47bd10ec4e 7 | fields: 8 | tier: 4 9 | links: 10 | - name: null 11 | type: REPOSITORY 12 | url: https://github.com/credebl/credo-controller 13 | relationships: 14 | DEPENDS_ON: [] 15 | labels: 16 | - aries 17 | - aries-framework-javascript 18 | - decentralized-identity 19 | - language:typescript 20 | - self-sovereign-identity 21 | - source:github 22 | customFields: null 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | rest-sample: 5 | build: . 6 | restart: always 7 | environment: 8 | # possible to set values using env variables 9 | AFJ_REST_LOG_LEVEL: 1 10 | volumes: 11 | # also possible to set values using json 12 | - ./samples/cliConfig.json:/config.json 13 | ports: 14 | - '4001:4001' 15 | - '4002:4002' 16 | - '3001:3001' 17 | # platform: linux/amd64 18 | # or via command line arguments 19 | command: --auto-accept-connections --config /config.json 20 | -------------------------------------------------------------------------------- /jest.config.base.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types' 2 | 3 | const config: Config.InitialOptions = { 4 | testTimeout: 120000, 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | coveragePathIgnorePatterns: ['/build/', '/node_modules/', '/__tests__/', 'tests'], 8 | coverageDirectory: '/coverage/', 9 | verbose: true, 10 | testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], 11 | globals: { 12 | 'ts-jest': { 13 | isolatedModules: true, 14 | }, 15 | }, 16 | } 17 | 18 | export default config 19 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types' 2 | 3 | import base from './jest.config.base' 4 | 5 | const config: Config.InitialOptions = { 6 | ...base, 7 | name: 'credo-controller', 8 | displayName: 'credo-controller', 9 | testTimeout: 120000, 10 | } 11 | 12 | export default config 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "credo-controller", 3 | "main": "build/index", 4 | "types": "build/index", 5 | "version": "2.0.0", 6 | "files": [ 7 | "build" 8 | ], 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "license": "Apache-2.0", 13 | "description": "Rest endpoint wrapper for using your agent over HTTP", 14 | "homepage": "https://github.com/hyperledger/aries-framework-javascript-ext/tree/main/packages/rest", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/hyperledger/aries-framework-javascript-ext", 18 | "directory": "packages/rest" 19 | }, 20 | "bin": { 21 | "afj-rest": "bin/afj-rest.js" 22 | }, 23 | "scripts": { 24 | "check-types": "tsc --noEmit -p tsconfig.build.json", 25 | "prettier": "prettier '**/*.+(js|json|ts|md|yml|yaml)'", 26 | "format": "yarn prettier --write", 27 | "check-format": "yarn prettier --list-different", 28 | "tsoa": "tsoa spec-and-routes", 29 | "dev": "tsoa spec-and-routes && tsnd --respawn samples/sampleWithApp.ts", 30 | "build": "yarn run clean && yarn run compile", 31 | "prestart:dev": "yarn run clean && yarn run compile", 32 | "start:dev": "./bin/afj-rest.js --config ./samples/cliConfig.json", 33 | "clean": "rimraf -rf ./build", 34 | "compile": "tsoa spec-and-routes && tsc -p tsconfig.build.json", 35 | "prepublishOnly": "yarn run build", 36 | "test": "jest", 37 | "postinstall": "patch-package", 38 | "lint": "eslint --ignore-path .gitignore .", 39 | "validate": "yarn lint && yarn check-types && yarn check-format" 40 | }, 41 | "dependencies": { 42 | "@ayanworks/credo-polygon-w3c-module": "1.0.1-alpha.1", 43 | "@credo-ts/anoncreds": "0.5.3", 44 | "@credo-ts/askar": "0.5.3", 45 | "@credo-ts/core": "0.5.3", 46 | "@credo-ts/indy-vdr": "0.5.3", 47 | "@credo-ts/node": "0.5.3", 48 | "@credo-ts/push-notifications": "^0.7.0", 49 | "@credo-ts/question-answer": "0.5.3", 50 | "@credo-ts/tenants": "0.5.3", 51 | "@hyperledger/anoncreds-nodejs": "0.2.2", 52 | "@hyperledger/aries-askar-nodejs": "0.2.1", 53 | "@hyperledger/indy-vdr-nodejs": "0.2.2", 54 | "@tsoa/runtime": "^6.0.0", 55 | "@types/node-fetch": "^2.6.4", 56 | "@types/ref-struct-di": "^1.1.9", 57 | "@types/uuid": "^8.3.4", 58 | "@types/ws": "^8.5.4", 59 | "axios": "^1.4.0", 60 | "body-parser": "^1.20.0", 61 | "cors": "^2.8.5", 62 | "dotenv": "^16.4.5", 63 | "express": "^4.18.1", 64 | "express-rate-limit": "^7.1.5", 65 | "joi": "^17.12.3", 66 | "jsonwebtoken": "^9.0.2", 67 | "node-fetch": "^2.6.7", 68 | "patch-package": "^8.0.0", 69 | "postinstall-postinstall": "^2.1.0", 70 | "reflect-metadata": "^0.1.13", 71 | "swagger-ui-express": "^4.4.0", 72 | "tslog": "^3.3.3", 73 | "tsoa": "^6.0.1", 74 | "tsyringe": "^4.8.0", 75 | "yargs": "^17.3.1" 76 | }, 77 | "devDependencies": { 78 | "@types/body-parser": "^1.19.2", 79 | "@types/cors": "^2.8.12", 80 | "@types/eslint": "^8.40.2", 81 | "@types/express": "^4.17.13", 82 | "@types/jest": "^27.0.3", 83 | "@types/jsonwebtoken": "^9.0.5", 84 | "@types/multer": "^1.4.7", 85 | "@types/node": "^18.18.8", 86 | "@types/ref-array-di": "^1.2.8", 87 | "@types/ref-struct-di": "^1.1.9", 88 | "@types/supertest": "^2.0.12", 89 | "@types/swagger-ui-express": "^4.1.3", 90 | "@typescript-eslint/eslint-plugin": "^6.19.1", 91 | "@typescript-eslint/parser": "^6.19.1", 92 | "eslint": "^7.32.0", 93 | "eslint-config-prettier": "^8.8.0", 94 | "eslint-import-resolver-typescript": "^3.5.5", 95 | "eslint-plugin-import": "^2.27.5", 96 | "eslint-plugin-prettier": "^4.2.1", 97 | "jest": "^29.7.0", 98 | "ngrok": "^4.3.1", 99 | "prettier": "^2.8.8", 100 | "supertest": "^6.2.3", 101 | "ts-jest": "^29.1.2", 102 | "ts-node-dev": "^2.0.0", 103 | "typescript": "^5.3.3" 104 | }, 105 | "engines": { 106 | "node": "18.19.0" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /patches/@credo-ts+core+0.5.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@credo-ts/core/build/agent/EnvelopeService.js b/node_modules/@credo-ts/core/build/agent/EnvelopeService.js 2 | index 12261a9..0238d59 100644 3 | --- a/node_modules/@credo-ts/core/build/agent/EnvelopeService.js 4 | +++ b/node_modules/@credo-ts/core/build/agent/EnvelopeService.js 5 | @@ -32,12 +32,14 @@ let EnvelopeService = class EnvelopeService { 6 | let encryptedMessage = await agentContext.wallet.pack(message, recipientKeysBase58, senderKeyBase58 !== null && senderKeyBase58 !== void 0 ? senderKeyBase58 : undefined); 7 | // If the message has routing keys (mediator) pack for each mediator 8 | for (const routingKeyBase58 of routingKeysBase58) { 9 | + console.log(`message['@type']`, JSON.stringify(message['@type'])) 10 | const forwardMessage = new messages_1.ForwardMessage({ 11 | // Forward to first recipient key 12 | to: recipientKeysBase58[0], 13 | message: encryptedMessage, 14 | }); 15 | recipientKeysBase58 = [routingKeyBase58]; 16 | + forwardMessage["messageType"] = message['@type']; 17 | this.logger.debug('Forward message created', forwardMessage); 18 | const forwardJson = forwardMessage.toJSON({ 19 | useDidSovPrefixWhereAllowed: agentContext.config.useDidSovPrefixWhereAllowed, 20 | diff --git a/node_modules/@credo-ts/core/build/modules/routing/messages/ForwardMessage.d.ts b/node_modules/@credo-ts/core/build/modules/routing/messages/ForwardMessage.d.ts 21 | index 4f8577b..396f78a 100644 22 | --- a/node_modules/@credo-ts/core/build/modules/routing/messages/ForwardMessage.d.ts 23 | +++ b/node_modules/@credo-ts/core/build/modules/routing/messages/ForwardMessage.d.ts 24 | @@ -3,6 +3,7 @@ import { EncryptedMessage } from '../../../types'; 25 | export interface ForwardMessageOptions { 26 | id?: string; 27 | to: string; 28 | + messageType: string; 29 | message: EncryptedMessage; 30 | } 31 | /** 32 | @@ -19,5 +20,6 @@ export declare class ForwardMessage extends AgentMessage { 33 | readonly type: string; 34 | static readonly type: import("../../../utils/messageType").ParsedMessageType; 35 | to: string; 36 | + messageType: string; 37 | message: EncryptedMessage; 38 | } 39 | diff --git a/node_modules/@credo-ts/core/build/types.d.ts b/node_modules/@credo-ts/core/build/types.d.ts 40 | index e0384d9..0a669fb 100644 41 | --- a/node_modules/@credo-ts/core/build/types.d.ts 42 | +++ b/node_modules/@credo-ts/core/build/types.d.ts 43 | @@ -81,6 +81,7 @@ export interface PlaintextMessage { 44 | thid?: string; 45 | pthid?: string; 46 | }; 47 | + messageType: string; 48 | [key: string]: unknown; 49 | } 50 | export interface OutboundPackage { 51 | -------------------------------------------------------------------------------- /patches/@credo-ts+core+0.5.1+001+initial.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@credo-ts/core/build/agent/EnvelopeService.js b/node_modules/@credo-ts/core/build/agent/EnvelopeService.js 2 | index 12261a9..0238d59 100644 3 | --- a/node_modules/@credo-ts/core/build/agent/EnvelopeService.js 4 | +++ b/node_modules/@credo-ts/core/build/agent/EnvelopeService.js 5 | @@ -32,12 +32,14 @@ let EnvelopeService = class EnvelopeService { 6 | let encryptedMessage = await agentContext.wallet.pack(message, recipientKeysBase58, senderKeyBase58 !== null && senderKeyBase58 !== void 0 ? senderKeyBase58 : undefined); 7 | // If the message has routing keys (mediator) pack for each mediator 8 | for (const routingKeyBase58 of routingKeysBase58) { 9 | + console.log(`message['@type']`, JSON.stringify(message['@type'])) 10 | const forwardMessage = new messages_1.ForwardMessage({ 11 | // Forward to first recipient key 12 | to: recipientKeysBase58[0], 13 | message: encryptedMessage, 14 | }); 15 | recipientKeysBase58 = [routingKeyBase58]; 16 | + forwardMessage["messageType"] = message['@type']; 17 | this.logger.debug('Forward message created', forwardMessage); 18 | const forwardJson = forwardMessage.toJSON({ 19 | useDidSovPrefixWhereAllowed: agentContext.config.useDidSovPrefixWhereAllowed, 20 | diff --git a/node_modules/@credo-ts/core/build/modules/routing/messages/ForwardMessage.d.ts b/node_modules/@credo-ts/core/build/modules/routing/messages/ForwardMessage.d.ts 21 | index 4f8577b..396f78a 100644 22 | --- a/node_modules/@credo-ts/core/build/modules/routing/messages/ForwardMessage.d.ts 23 | +++ b/node_modules/@credo-ts/core/build/modules/routing/messages/ForwardMessage.d.ts 24 | @@ -3,6 +3,7 @@ import { EncryptedMessage } from '../../../types'; 25 | export interface ForwardMessageOptions { 26 | id?: string; 27 | to: string; 28 | + messageType: string; 29 | message: EncryptedMessage; 30 | } 31 | /** 32 | @@ -19,5 +20,6 @@ export declare class ForwardMessage extends AgentMessage { 33 | readonly type: string; 34 | static readonly type: import("../../../utils/messageType").ParsedMessageType; 35 | to: string; 36 | + messageType: string; 37 | message: EncryptedMessage; 38 | } 39 | diff --git a/node_modules/@credo-ts/core/build/types.d.ts b/node_modules/@credo-ts/core/build/types.d.ts 40 | index e0384d9..0a669fb 100644 41 | --- a/node_modules/@credo-ts/core/build/types.d.ts 42 | +++ b/node_modules/@credo-ts/core/build/types.d.ts 43 | @@ -81,6 +81,7 @@ export interface PlaintextMessage { 44 | thid?: string; 45 | pthid?: string; 46 | }; 47 | + messageType: string; 48 | [key: string]: unknown; 49 | } 50 | export interface OutboundPackage { -------------------------------------------------------------------------------- /patches/@credo-ts+core+0.5.3+002+fix-process-problem-report.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@credo-ts/core/build/modules/credentials/protocol/BaseCredentialProtocol.js b/node_modules/@credo-ts/core/build/modules/credentials/protocol/BaseCredentialProtocol.js 2 | index 30dbb7a..5b1b54c 100644 3 | --- a/node_modules/@credo-ts/core/build/modules/credentials/protocol/BaseCredentialProtocol.js 4 | +++ b/node_modules/@credo-ts/core/build/modules/credentials/protocol/BaseCredentialProtocol.js 5 | @@ -19,11 +19,9 @@ class BaseCredentialProtocol { 6 | */ 7 | async processProblemReport(messageContext) { 8 | const { message: credentialProblemReportMessage, agentContext } = messageContext; 9 | - const connection = messageContext.assertReadyConnection(); 10 | agentContext.config.logger.debug(`Processing problem report with message id ${credentialProblemReportMessage.id}`); 11 | const credentialRecord = await this.getByProperties(agentContext, { 12 | threadId: credentialProblemReportMessage.threadId, 13 | - connectionId: connection.id, 14 | }); 15 | // Update record 16 | credentialRecord.errorMessage = `${credentialProblemReportMessage.description.code}: ${credentialProblemReportMessage.description.en}`; 17 | diff --git a/node_modules/@credo-ts/core/build/modules/proofs/protocol/BaseProofProtocol.js b/node_modules/@credo-ts/core/build/modules/proofs/protocol/BaseProofProtocol.js 18 | index 25d2948..cf9e315 100644 19 | --- a/node_modules/@credo-ts/core/build/modules/proofs/protocol/BaseProofProtocol.js 20 | +++ b/node_modules/@credo-ts/core/build/modules/proofs/protocol/BaseProofProtocol.js 21 | @@ -8,11 +8,10 @@ const ProofState_1 = require("../models/ProofState"); 22 | const repository_1 = require("../repository"); 23 | class BaseProofProtocol { 24 | async processProblemReport(messageContext) { 25 | - const { message: proofProblemReportMessage, agentContext, connection } = messageContext; 26 | + const { message: proofProblemReportMessage, agentContext } = messageContext; 27 | agentContext.config.logger.debug(`Processing problem report with message id ${proofProblemReportMessage.id}`); 28 | const proofRecord = await this.getByProperties(agentContext, { 29 | threadId: proofProblemReportMessage.threadId, 30 | - connectionId: connection === null || connection === void 0 ? void 0 : connection.id, 31 | }); 32 | // Update record 33 | proofRecord.errorMessage = `${proofProblemReportMessage.description.code}: ${proofProblemReportMessage.description.en}`; 34 | -------------------------------------------------------------------------------- /patches/@credo-ts+core+0.5.3+004+added-prettyVc-in-JsonCredential-interface.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts 2 | index d12468b..ae70f36 100644 3 | --- a/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts 4 | +++ b/node_modules/@credo-ts/core/build/modules/credentials/formats/jsonld/JsonLdCredentialFormat.d.ts 5 | @@ -10,6 +10,8 @@ export interface JsonCredential { 6 | issuanceDate: string; 7 | expirationDate?: string; 8 | credentialSubject: SingleOrArray; 9 | + //TODO change type 10 | + prettyVc?: any; 11 | [key: string]: unknown; 12 | } 13 | /** -------------------------------------------------------------------------------- /patches/@credo-ts+core+0.5.3+005+commenting validationPresentation to avoid abandoned issue.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@credo-ts/core/build/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.js b/node_modules/@credo-ts/core/build/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.js 2 | index 006d870..da56801 100644 3 | --- a/node_modules/@credo-ts/core/build/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.js 4 | +++ b/node_modules/@credo-ts/core/build/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.js 5 | @@ -170,7 +170,8 @@ class DifPresentationExchangeProofFormatService { 6 | try { 7 | ps.validatePresentationDefinition(request.presentation_definition); 8 | ps.validatePresentationSubmission(jsonPresentation.presentation_submission); 9 | - ps.validatePresentation(request.presentation_definition, parsedPresentation); 10 | + // FIXME: Commenting validatePresentation() for now due to intermittent abandoned issue 11 | + //ps.validatePresentation(request.presentation_definition, parsedPresentation); 12 | let verificationResult; 13 | // FIXME: for some reason it won't accept the input if it doesn't know 14 | // whether it's a JWT or JSON-LD VP even though the input is the same. -------------------------------------------------------------------------------- /patches/@credo-ts+core+0.5.3+006+w3c-issuance-without-holder-did-negotiaton.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js b/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js 2 | index fb1fb9d..b519694 100644 3 | --- a/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js 4 | +++ b/node_modules/@credo-ts/core/build/modules/credentials/protocol/v2/V2CredentialProtocol.js 5 | @@ -97,7 +97,6 @@ class V2CredentialProtocol extends BaseCredentialProtocol_1.BaseCredentialProtoc 6 | let credentialRecord = await this.findByProperties(messageContext.agentContext, { 7 | threadId: proposalMessage.threadId, 8 | role: models_1.CredentialRole.Issuer, 9 | - connectionId: connection === null || connection === void 0 ? void 0 : connection.id, 10 | }); 11 | const formatServices = this.getFormatServicesFromMessage(proposalMessage.formats); 12 | if (formatServices.length === 0) { -------------------------------------------------------------------------------- /patches/@credo-ts+tenants+0.5.3+001+cache-tenant-record-patch.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@credo-ts/tenants/build/context/TenantAgentContextProvider.d.ts b/node_modules/@credo-ts/tenants/build/context/TenantAgentContextProvider.d.ts 2 | index 91bb8f4..b4dae61 100644 3 | --- a/node_modules/@credo-ts/tenants/build/context/TenantAgentContextProvider.d.ts 4 | +++ b/node_modules/@credo-ts/tenants/build/context/TenantAgentContextProvider.d.ts 5 | @@ -1,5 +1,5 @@ 6 | import type { TenantRecord } from '../repository'; 7 | -import type { AgentContextProvider, UpdateAssistantUpdateOptions } from '@credo-ts/core'; 8 | +import type { AgentContextProvider, UpdateAssistantUpdateOptions , CacheModule, InMemoryLruCache } from '@credo-ts/core'; 9 | import { AgentContext, EventEmitter, Logger } from '@credo-ts/core'; 10 | import { TenantRecordService } from '../services'; 11 | import { TenantSessionCoordinator } from './TenantSessionCoordinator'; 12 | @@ -9,7 +9,9 @@ export declare class TenantAgentContextProvider implements AgentContextProvider 13 | private eventEmitter; 14 | private logger; 15 | private tenantSessionCoordinator; 16 | - constructor(tenantRecordService: TenantRecordService, rootAgentContext: AgentContext, eventEmitter: EventEmitter, tenantSessionCoordinator: TenantSessionCoordinator, logger: Logger); 17 | + private cacheModule; 18 | + private inMemoryLruCache; 19 | + constructor(tenantRecordService: TenantRecordService, rootAgentContext: AgentContext, eventEmitter: EventEmitter, tenantSessionCoordinator: TenantSessionCoordinator, logger: Logger, cache: InMemoryLruCache); 20 | getAgentContextForContextCorrelationId(contextCorrelationId: string): Promise; 21 | getContextForInboundMessage(inboundMessage: unknown, options?: { 22 | contextCorrelationId?: string; 23 | diff --git a/node_modules/@credo-ts/tenants/build/context/TenantAgentContextProvider.js b/node_modules/@credo-ts/tenants/build/context/TenantAgentContextProvider.js 24 | index d491d4e..d60ec79 100644 25 | --- a/node_modules/@credo-ts/tenants/build/context/TenantAgentContextProvider.js 26 | +++ b/node_modules/@credo-ts/tenants/build/context/TenantAgentContextProvider.js 27 | @@ -24,16 +24,28 @@ let TenantAgentContextProvider = class TenantAgentContextProvider { 28 | this.eventEmitter = eventEmitter; 29 | this.tenantSessionCoordinator = tenantSessionCoordinator; 30 | this.logger = logger; 31 | + this.cache = new core_1.CacheModule({ 32 | + cache: new core_1.InMemoryLruCache({ limit: 100 }), 33 | + }); 34 | // Start listener for newly created routing keys, so we can register a mapping for each new key for the tenant 35 | this.listenForRoutingKeyCreatedEvents(); 36 | } 37 | async getAgentContextForContextCorrelationId(contextCorrelationId) { 38 | + this.logger.debug('debug ========= Inside getAgentContextForContextCorrelationId') 39 | // It could be that the root agent context is requested, in that case we return the root agent context 40 | if (contextCorrelationId === this.rootAgentContext.contextCorrelationId) { 41 | return this.rootAgentContext; 42 | } 43 | // TODO: maybe we can look at not having to retrieve the tenant record if there's already a context available. 44 | - const tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, contextCorrelationId); 45 | + this.logger.debug('debug ========= Get tenantRecord from cache') 46 | + let tenantRecord = await this.cache.config.cache.get(this.rootAgentContext, `contextCorrelationId-${contextCorrelationId}`) 47 | + if(!tenantRecord) { 48 | + // TODO: maybe we can look at not having to retrieve the tenant record if there's already a context available. 49 | + this.logger.debug('debug ========= TenantRecord not found in cache') 50 | + tenantRecord = await this.tenantRecordService.getTenantById(this.rootAgentContext, contextCorrelationId) 51 | + await this.cache.config.cache.set(this.rootAgentContext,`contextCorrelationId-${contextCorrelationId}`,tenantRecord) 52 | + this.logger.debug(`debug ========= Cached tenant agent context for tenant '${contextCorrelationId}'`) 53 | + } 54 | const shouldUpdate = !(0, core_1.isStorageUpToDate)(tenantRecord.storageVersion); 55 | // If the tenant storage is not up to date, and autoUpdate is disabled we throw an error 56 | if (shouldUpdate && !this.rootAgentContext.config.autoUpdateStorageOnStartup) { -------------------------------------------------------------------------------- /samples/cliConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "AFJ Rest Agent 1", 3 | "walletId": "sample", 4 | "walletKey": "sample", 5 | "walletType": "postgres", 6 | "walletUrl": "localhost:5432", 7 | "walletAccount": "postgres", 8 | "walletPassword": "postgres", 9 | "walletAdminAccount": "postgres", 10 | "walletAdminPassword": "postgres", 11 | "walletScheme": "ProfilePerWallet", 12 | "indyLedger": [ 13 | { 14 | "genesisTransactions": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_testnet_genesis", 15 | "indyNamespace": "indicio:testnet" 16 | }, 17 | { 18 | "genesisTransactions": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_demonet_genesis", 19 | "indyNamespace": "indicio:demonet" 20 | }, 21 | { 22 | "genesisTransactions": "https://raw.githubusercontent.com/bcgov/von-network/main/BCovrin/genesis_test", 23 | "indyNamespace": "bcovrin:testnet" 24 | } 25 | ], 26 | "endpoint": ["http://localhost:4002"], 27 | "autoAcceptConnections": true, 28 | "autoAcceptCredentials": "always", 29 | "autoAcceptProofs": "contentApproved", 30 | "logLevel": 2, 31 | "inboundTransport": [ 32 | { 33 | "transport": "http", 34 | "port": 4002 35 | } 36 | ], 37 | "outboundTransport": ["http"], 38 | "adminPort": 4001, 39 | "tenancy": true, 40 | "schemaFileServerURL": "https://schema.credebl.id/schemas/", 41 | "didRegistryContractAddress": "0xcB80F37eDD2bE3570c6C9D5B0888614E04E1e49E", 42 | "schemaManagerContractAddress": "0x4742d43C2dFCa5a1d4238240Afa8547Daf87Ee7a", 43 | "rpcUrl": "https://rpc-amoy.polygon.technology", 44 | "fileServerUrl": "https://schema.credebl.id", 45 | "fileServerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBeWFuV29ya3MiLCJpZCI6ImNhZDI3ZjhjLTMyNWYtNDRmZC04ZmZkLWExNGNhZTY3NTMyMSJ9.I3IR7abjWbfStnxzn1BhxhV0OEzt1x3mULjDdUcgWHk" 46 | } 47 | -------------------------------------------------------------------------------- /samples/sample.ts: -------------------------------------------------------------------------------- 1 | import type { ServerConfig } from '../src/utils/ServerConfig' 2 | 3 | import { connect } from 'ngrok' 4 | 5 | import { startServer } from '../src/index' 6 | import { setupAgent } from '../src/utils/agent' 7 | 8 | const run = async () => { 9 | const endpoint = await connect(3001) 10 | 11 | const agent = await setupAgent({ 12 | port: 3001, 13 | endpoints: [endpoint], 14 | name: 'Aries Test Agent', 15 | }) 16 | 17 | const conf: ServerConfig = { 18 | port: 3000, 19 | cors: true, 20 | } 21 | 22 | await startServer(agent, conf) 23 | } 24 | 25 | run() 26 | -------------------------------------------------------------------------------- /samples/sampleWithApp.ts: -------------------------------------------------------------------------------- 1 | import type { ServerConfig } from '../src/utils/ServerConfig' 2 | 3 | import { AgentConfig } from '@credo-ts/core' 4 | import bodyParser from 'body-parser' 5 | import express from 'express' 6 | import { connect } from 'ngrok' 7 | 8 | import { startServer } from '../src/index' 9 | import { setupAgent } from '../src/utils/agent' 10 | 11 | const run = async () => { 12 | const endpoint = await connect(3001) 13 | 14 | const agent = await setupAgent({ 15 | port: 3001, 16 | endpoints: [endpoint], 17 | name: 'Aries Test Agent', 18 | }) 19 | 20 | const app = express() 21 | const jsonParser = bodyParser.json() 22 | 23 | app.post('/greeting', jsonParser, (req, res) => { 24 | const config = agent.dependencyManager.resolve(AgentConfig) 25 | 26 | res.send(`Hello, ${config.label}!`) 27 | }) 28 | 29 | const conf: ServerConfig = { 30 | port: 3000, 31 | webhookUrl: 'http://localhost:5000/agent-events', 32 | app: app, 33 | } 34 | 35 | await startServer(agent, conf) 36 | } 37 | 38 | run() 39 | -------------------------------------------------------------------------------- /scripts/taskdef/credo-ecs-taskdef.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "${FAMILY}", 3 | "containerDefinitions": [ 4 | { 5 | "name": "Platform-admin", 6 | "image": "%REPOSITORY_URI%:CREDO_v_%BUILD_NUMBER%", 7 | "cpu": 154, 8 | "memory": 307, 9 | "portMappings": [ 10 | { 11 | "containerPort": 8001, 12 | "hostPort": 8001, 13 | "protocol": "tcp" 14 | }, 15 | { 16 | "containerPort": 9001, 17 | "hostPort": 9001, 18 | "protocol": "tcp" 19 | } 20 | ], 21 | "essential": true, 22 | "command": ["--auto-accept-connections", "--config", "/config.json"], 23 | "environment": [ 24 | { 25 | "name": "AFJ_REST_LOG_LEVEL", 26 | "value": "1" 27 | } 28 | ], 29 | "environmentFiles": [ 30 | { 31 | "value": "${S3_ARN}", 32 | "type": "s3" 33 | } 34 | ], 35 | "mountPoints": [ 36 | { 37 | "sourceVolume": "config", 38 | "containerPath": "/config.json", 39 | "readOnly": true 40 | } 41 | ], 42 | "volumesFrom": [], 43 | "ulimits": [] 44 | } 45 | ], 46 | "executionRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/ecsTaskExecutionRole", 47 | "placementConstraints": [], 48 | "requiresCompatibilities": ["EC2"], 49 | "cpu": "154", 50 | "memory": "307", 51 | "volumes": [ 52 | { 53 | "name": "config", 54 | "host": { 55 | "sourcePath": "${SourcePath}" 56 | } 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /scripts/taskdef/credo-fargate-taskdef.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "${FAMILY}", 3 | "containerDefinitions": [ 4 | { 5 | "name": "Platform-admin", 6 | "image": "%REPOSITORY_URI%:CREDO_v_%BUILD_NUMBER%", 7 | "cpu": 0, 8 | "portMappings": [ 9 | { 10 | "containerPort": 8004, 11 | "hostPort": 8004, 12 | "protocol": "tcp" 13 | }, 14 | { 15 | "containerPort": 9004, 16 | "hostPort": 9004, 17 | "protocol": "tcp" 18 | } 19 | ], 20 | "essential": true, 21 | "command": ["--auto-accept-connections", "--config", "/config/${CONFIG_FILE}"], 22 | "environment": [ 23 | { 24 | "name": "AFJ_REST_LOG_LEVEL", 25 | "value": "1" 26 | } 27 | ], 28 | "environmentFiles": [ 29 | { 30 | "value": "${S3_ARN}", 31 | "type": "s3" 32 | } 33 | ], 34 | "mountPoints": [ 35 | { 36 | "sourceVolume": "config", 37 | "containerPath": "/config", 38 | "readOnly": false 39 | } 40 | ], 41 | "volumesFrom": [], 42 | "ulimits": [], 43 | "logConfiguration": { 44 | "logDriver": "awslogs", 45 | "options": { 46 | "awslogs-group": "/ecs/${FAMILY}", 47 | "awslogs-create-group": "true", 48 | "awslogs-region": "ap-south-1", 49 | "awslogs-stream-prefix": "ecs" 50 | } 51 | } 52 | } 53 | ], 54 | "executionRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/ecsTaskExecutionRole", 55 | "networkMode": "awsvpc", 56 | "placementConstraints": [], 57 | "requiresCompatibilities": ["FARGATE"], 58 | "cpu": "1024", 59 | "memory": "2048", 60 | "volumes": [ 61 | { 62 | "name": "config", 63 | "efsVolumeConfiguration": { 64 | "fileSystemId": "${EFS}", 65 | "rootDirectory": "/" 66 | } 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /src/authentication.ts: -------------------------------------------------------------------------------- 1 | import type * as express from 'express' 2 | 3 | import { LogLevel } from '@credo-ts/core' 4 | 5 | import { TsLogger } from './utils/logger' 6 | 7 | let dynamicApiKey: string = 'api_key' // Initialize with a default value 8 | 9 | export async function expressAuthentication( 10 | request: express.Request, 11 | securityName: string, 12 | secMethod?: { [key: string]: any }, 13 | scopes?: string 14 | ) { 15 | const logger = new TsLogger(LogLevel.info) 16 | 17 | logger.info(`secMethod::: ${secMethod}`) 18 | logger.info(`scopes::: ${scopes}`) 19 | 20 | const apiKeyHeader = request.headers['authorization'] 21 | 22 | if (securityName === 'apiKey') { 23 | if (apiKeyHeader) { 24 | const providedApiKey = apiKeyHeader as string 25 | 26 | if (providedApiKey === dynamicApiKey) { 27 | return 'success' 28 | } 29 | } 30 | } 31 | } 32 | 33 | export function setDynamicApiKey(newApiKey: string) { 34 | dynamicApiKey = newApiKey 35 | } 36 | 37 | export function getDynamicApiKey() { 38 | return dynamicApiKey 39 | } 40 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import type { AriesRestConfig } from './cliAgent' 2 | 3 | import yargs from 'yargs' 4 | 5 | import { runRestAgent } from './cliAgent' 6 | 7 | interface IndyLedger { 8 | genesisTransactions: string 9 | indyNamespace: string 10 | } 11 | 12 | interface Parsed { 13 | label: string 14 | 'wallet-id': string 15 | 'wallet-key': string 16 | 'wallet-type': string 17 | 'wallet-url': string 18 | 'wallet-scheme': string 19 | 'wallet-account': string 20 | 'wallet-password': string 21 | 'wallet-admin-account': string 22 | 'wallet-admin-password': string 23 | 'indy-ledger': IndyLedger[] 24 | endpoint?: string[] 25 | 'log-level': number 26 | 'outbound-transport': ('http' | 'ws')[] 27 | 'inbound-transport'?: InboundTransport[] 28 | 'auto-accept-connections'?: boolean 29 | 'auto-accept-credentials'?: 'always' | 'never' | 'contentApproved' 30 | 'auto-accept-proofs'?: 'always' | 'never' | 'contentApproved' 31 | 'webhook-url'?: string 32 | 'admin-port': number 33 | tenancy: boolean 34 | 'did-registry-contract-address'?: string 35 | 'schema-manager-contract-address'?: string 36 | 'wallet-connect-timeout'?: number 37 | 'wallet-max-connections'?: number 38 | 'wallet-idle-timeout'?: number 39 | schemaFileServerURL?: string 40 | didRegistryContractAddress?: string 41 | schemaManagerContractAddress?: string 42 | rpcUrl?: string 43 | fileServerUrl?: string 44 | fileServerToken?: string 45 | } 46 | 47 | interface InboundTransport { 48 | transport: Transports 49 | port: number 50 | } 51 | 52 | type Transports = 'http' | 'ws' 53 | 54 | async function parseArguments(): Promise { 55 | return yargs 56 | .command('start', 'Start Credo Rest agent') 57 | .option('label', { 58 | alias: 'l', 59 | string: true, 60 | demandOption: true, 61 | }) 62 | .option('wallet-id', { 63 | string: true, 64 | demandOption: true, 65 | }) 66 | .option('wallet-key', { 67 | string: true, 68 | demandOption: true, 69 | }) 70 | .option('wallet-type', { 71 | string: true, 72 | demandOption: true, 73 | }) 74 | .option('wallet-url', { 75 | string: true, 76 | demandOption: true, 77 | }) 78 | .option('wallet-scheme', { 79 | string: true, 80 | demandOption: true, 81 | }) 82 | .option('wallet-account', { 83 | string: true, 84 | demandOption: true, 85 | }) 86 | .option('wallet-password', { 87 | string: true, 88 | demandOption: true, 89 | }) 90 | .option('wallet-admin-account', { 91 | string: true, 92 | demandOption: true, 93 | }) 94 | .option('wallet-admin-password', { 95 | string: true, 96 | demandOption: true, 97 | }) 98 | .option('indy-ledger', { 99 | array: true, 100 | default: [], 101 | coerce: (input) => { 102 | return input.map((item: { genesisTransactions: string; indyNamespace: string }) => ({ 103 | genesisTransactions: item.genesisTransactions, 104 | indyNamespace: item.indyNamespace, 105 | })) 106 | }, 107 | }) 108 | .option('endpoint', { 109 | array: true, 110 | coerce: (input) => { 111 | return input.map((item: string) => String(item)) 112 | }, 113 | }) 114 | .option('log-level', { 115 | number: true, 116 | default: 3, 117 | }) 118 | .option('outbound-transport', { 119 | array: true, 120 | coerce: (input) => { 121 | const validValues = ['http', 'ws'] 122 | return input.map((item: string) => { 123 | if (validValues.includes(item)) { 124 | return item as 'http' | 'ws' 125 | } else { 126 | throw new Error(`Invalid value for outbound-transport: ${item}. Valid values are 'http' or 'ws'.`) 127 | } 128 | }) 129 | }, 130 | }) 131 | .option('inbound-transport', { 132 | array: true, 133 | coerce: (input) => { 134 | const transports: InboundTransport[] = [] 135 | for (const item of input) { 136 | if ( 137 | typeof item === 'object' && 138 | 'transport' in item && 139 | typeof item.transport === 'string' && 140 | 'port' in item && 141 | typeof item.port === 'number' 142 | ) { 143 | const transport: Transports = item.transport as Transports 144 | const port: number = item.port 145 | transports.push({ transport, port }) 146 | } else { 147 | throw new Error( 148 | 'Inbound transport should be specified as an array of objects with transport and port properties.' 149 | ) 150 | } 151 | } 152 | return transports 153 | }, 154 | }) 155 | .option('auto-accept-connections', { 156 | boolean: true, 157 | default: false, 158 | }) 159 | .option('auto-accept-credentials', { 160 | choices: ['always', 'never', 'contentApproved'], 161 | coerce: (input: string) => { 162 | if (input === 'always' || input === 'never' || input === 'contentApproved') { 163 | return input as 'always' | 'never' | 'contentApproved' 164 | } else { 165 | throw new Error( 166 | 'Invalid value for auto-accept-credentials. Valid values are "always", "never", or "contentApproved".' 167 | ) 168 | } 169 | }, 170 | }) 171 | .option('auto-accept-proofs', { 172 | choices: ['always', 'never', 'contentApproved'], 173 | coerce: (input: string) => { 174 | if (input === 'always' || input === 'never' || input === 'contentApproved') { 175 | return input as 'always' | 'never' | 'contentApproved' 176 | } else { 177 | throw new Error( 178 | 'Invalid value for auto-accept-proofs. Valid values are "always", "never", or "contentApproved".' 179 | ) 180 | } 181 | }, 182 | }) 183 | .option('webhook-url', { 184 | string: true, 185 | }) 186 | .option('admin-port', { 187 | number: true, 188 | demandOption: true, 189 | }) 190 | .option('tenancy', { 191 | boolean: true, 192 | default: false, 193 | }) 194 | .option('did-registry-contract-address', { 195 | string: true, 196 | }) 197 | .option('schema-manager-contract-address', { 198 | string: true, 199 | }) 200 | .option('wallet-connect-timeout', { 201 | number: true, 202 | }) 203 | .option('wallet-max-connections', { 204 | number: true, 205 | }) 206 | .option('wallet-idle-timeout', { 207 | number: true, 208 | }) 209 | .config() 210 | .env('AFJ_REST') 211 | .parseAsync() as Promise 212 | } 213 | 214 | export async function runCliServer() { 215 | const parsed = await parseArguments() 216 | 217 | await runRestAgent({ 218 | label: parsed.label, 219 | walletConfig: { 220 | id: parsed['wallet-id'], 221 | key: parsed['wallet-key'], 222 | storage: { 223 | type: parsed['wallet-type'], 224 | config: { 225 | host: parsed['wallet-url'], 226 | connectTimeout: parsed['wallet-connect-timeout'] || Number(process.env.CONNECT_TIMEOUT), 227 | maxConnections: parsed['wallet-max-connections'] || Number(process.env.MAX_CONNECTIONS), 228 | idleTimeout: parsed['wallet-idle-timeout'] || Number(process.env.IDLE_TIMEOUT), 229 | }, 230 | credentials: { 231 | account: parsed['wallet-account'], 232 | password: parsed['wallet-password'], 233 | adminAccount: parsed['wallet-admin-account'], 234 | adminPassword: parsed['wallet-admin-password'], 235 | }, 236 | }, 237 | }, 238 | indyLedger: parsed['indy-ledger'], 239 | endpoints: parsed.endpoint, 240 | autoAcceptConnections: parsed['auto-accept-connections'], 241 | autoAcceptCredentials: parsed['auto-accept-credentials'], 242 | autoAcceptProofs: parsed['auto-accept-proofs'], 243 | logLevel: parsed['log-level'], 244 | inboundTransports: parsed['inbound-transport'], 245 | outboundTransports: parsed['outbound-transport'], 246 | webhookUrl: parsed['webhook-url'], 247 | adminPort: parsed['admin-port'], 248 | tenancy: parsed.tenancy, 249 | schemaFileServerURL: parsed.schemaFileServerURL, 250 | didRegistryContractAddress: parsed.didRegistryContractAddress, 251 | schemaManagerContractAddress: parsed.schemaManagerContractAddress, 252 | rpcUrl: parsed.rpcUrl, 253 | fileServerUrl: parsed.fileServerUrl, 254 | fileServerToken: parsed.fileServerToken, 255 | } as AriesRestConfig) 256 | } 257 | -------------------------------------------------------------------------------- /src/controllers/agent/AgentController.ts: -------------------------------------------------------------------------------- 1 | import type { RestAgentModules } from '../../cliAgent' 2 | import type { AgentInfo } from '../types' 3 | 4 | import { Agent } from '@credo-ts/core' 5 | import { injectable } from 'tsyringe' 6 | 7 | import ErrorHandlingService from '../../errorHandlingService' 8 | 9 | import { Controller, Delete, Get, Route, Tags, Security } from 'tsoa' 10 | 11 | @Tags('Agent') 12 | @Route('/agent') 13 | @injectable() 14 | export class AgentController extends Controller { 15 | private agent: Agent 16 | 17 | public constructor(agent: Agent) { 18 | super() 19 | this.agent = agent 20 | } 21 | 22 | /** 23 | * Retrieve basic agent information 24 | */ 25 | @Get('/') 26 | public async getAgentInfo(): Promise { 27 | try { 28 | return { 29 | label: this.agent.config.label, 30 | endpoints: this.agent.config.endpoints, 31 | isInitialized: this.agent.isInitialized, 32 | publicDid: undefined, 33 | } 34 | } catch (error) { 35 | throw ErrorHandlingService.handle(error) 36 | } 37 | } 38 | 39 | /** 40 | * Delete wallet 41 | */ 42 | @Security('apiKey') 43 | @Delete('/wallet') 44 | public async deleteWallet() { 45 | try { 46 | const deleteWallet = await this.agent.wallet.delete() 47 | return deleteWallet 48 | } catch (error) { 49 | throw ErrorHandlingService.handle(error) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/controllers/basic-messages/BasicMessageController.ts: -------------------------------------------------------------------------------- 1 | import type { RestAgentModules } from '../../cliAgent' 2 | import type { BasicMessageRecord, BasicMessageStorageProps } from '@credo-ts/core' 3 | 4 | import { Agent } from '@credo-ts/core' 5 | import { injectable } from 'tsyringe' 6 | 7 | import ErrorHandlingService from '../../errorHandlingService' 8 | import { BasicMessageRecordExample, RecordId } from '../examples' 9 | 10 | import { Body, Controller, Example, Get, Path, Post, Route, Tags, Security } from 'tsoa' 11 | 12 | @Tags('Basic Messages') 13 | @Route('/basic-messages') 14 | @Security('apiKey') 15 | @injectable() 16 | export class BasicMessageController extends Controller { 17 | private agent: Agent 18 | 19 | public constructor(agent: Agent) { 20 | super() 21 | this.agent = agent 22 | } 23 | 24 | /** 25 | * Retrieve basic messages by connection id 26 | * 27 | * @param connectionId Connection identifier 28 | * @returns BasicMessageRecord[] 29 | */ 30 | @Example([BasicMessageRecordExample]) 31 | @Get('/:connectionId') 32 | public async getBasicMessages(@Path('connectionId') connectionId: RecordId): Promise { 33 | try { 34 | const basicMessageRecords = await this.agent.basicMessages.findAllByQuery({ connectionId }) 35 | this.setStatus(200) 36 | return basicMessageRecords 37 | } catch (error) { 38 | throw ErrorHandlingService.handle(error) 39 | } 40 | } 41 | 42 | /** 43 | * Send a basic message to a connection 44 | * 45 | * @param connectionId Connection identifier 46 | * @param content The content of the message 47 | */ 48 | @Example(BasicMessageRecordExample) 49 | @Post('/:connectionId') 50 | public async sendMessage(@Path('connectionId') connectionId: RecordId, @Body() request: Record<'content', string>) { 51 | try { 52 | const basicMessageRecord = await this.agent.basicMessages.sendMessage(connectionId, request.content) 53 | this.setStatus(204) 54 | return basicMessageRecord 55 | } catch (error) { 56 | throw ErrorHandlingService.handle(error) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/controllers/connections/ConnectionController.ts: -------------------------------------------------------------------------------- 1 | import type { RestAgentModules } from '../../cliAgent' 2 | import type { ConnectionRecordProps } from '@credo-ts/core' 3 | 4 | import { DidExchangeState, Agent } from '@credo-ts/core' 5 | import { injectable } from 'tsyringe' 6 | 7 | import ErrorHandlingService from '../../errorHandlingService' 8 | import { NotFoundError } from '../../errors' 9 | import { ConnectionRecordExample, RecordId } from '../examples' 10 | 11 | import { Controller, Delete, Example, Get, Path, Post, Query, Route, Tags, Security } from 'tsoa' 12 | 13 | @Tags('Connections') 14 | @Route() 15 | @injectable() 16 | export class ConnectionController extends Controller { 17 | private agent: Agent 18 | 19 | public constructor(agent: Agent) { 20 | super() 21 | this.agent = agent 22 | } 23 | 24 | /** 25 | * Retrieve all connections records 26 | * @param alias Alias 27 | * @param state Connection state 28 | * @param myDid My DID 29 | * @param theirDid Their DID 30 | * @param theirLabel Their label 31 | * @returns ConnectionRecord[] 32 | */ 33 | @Example([ConnectionRecordExample]) 34 | @Security('apiKey') 35 | @Get('/connections') 36 | public async getAllConnections( 37 | @Query('outOfBandId') outOfBandId?: string, 38 | @Query('alias') alias?: string, 39 | @Query('state') state?: DidExchangeState, 40 | @Query('myDid') myDid?: string, 41 | @Query('theirDid') theirDid?: string, 42 | @Query('theirLabel') theirLabel?: string 43 | ) { 44 | try { 45 | const connections = await this.agent.connections.findAllByQuery({ 46 | outOfBandId, 47 | alias, 48 | myDid, 49 | theirDid, 50 | theirLabel, 51 | state, 52 | }) 53 | 54 | return connections.map((c) => c.toJSON()) 55 | } catch (error) { 56 | throw ErrorHandlingService.handle(error) 57 | } 58 | } 59 | 60 | /** 61 | * Retrieve connection record by connection id 62 | * @param connectionId Connection identifier 63 | * @returns ConnectionRecord 64 | */ 65 | @Example(ConnectionRecordExample) 66 | @Security('apiKey') 67 | @Get('/connections/:connectionId') 68 | public async getConnectionById(@Path('connectionId') connectionId: RecordId) { 69 | try { 70 | const connection = await this.agent.connections.findById(connectionId) 71 | 72 | if (!connection) throw new NotFoundError(`Connection with connection id "${connectionId}" not found.`) 73 | 74 | return connection.toJSON() 75 | } catch (error) { 76 | throw ErrorHandlingService.handle(error) 77 | } 78 | } 79 | 80 | /** 81 | * Deletes a connection record from the connection repository. 82 | * 83 | * @param connectionId Connection identifier 84 | */ 85 | @Delete('/connections/:connectionId') 86 | @Security('apiKey') 87 | public async deleteConnection(@Path('connectionId') connectionId: RecordId) { 88 | try { 89 | this.setStatus(204) 90 | await this.agent.connections.deleteById(connectionId) 91 | } catch (error) { 92 | throw ErrorHandlingService.handle(error) 93 | } 94 | } 95 | 96 | /** 97 | * Accept a connection request as inviter by sending a connection response message 98 | * for the connection with the specified connection id. 99 | * 100 | * This is not needed when auto accepting of connection is enabled. 101 | * 102 | * @param connectionId Connection identifier 103 | * @returns ConnectionRecord 104 | */ 105 | @Example(ConnectionRecordExample) 106 | @Security('apiKey') 107 | @Post('/connections/:connectionId/accept-request') 108 | public async acceptRequest(@Path('connectionId') connectionId: RecordId) { 109 | try { 110 | const connection = await this.agent.connections.acceptRequest(connectionId) 111 | return connection.toJSON() 112 | } catch (error) { 113 | throw ErrorHandlingService.handle(error) 114 | } 115 | } 116 | 117 | /** 118 | * Accept a connection response as invitee by sending a trust ping message 119 | * for the connection with the specified connection id. 120 | * 121 | * This is not needed when auto accepting of connection is enabled. 122 | * 123 | * @param connectionId Connection identifier 124 | * @returns ConnectionRecord 125 | */ 126 | @Example(ConnectionRecordExample) 127 | @Security('apiKey') 128 | @Post('/connections/:connectionId/accept-response') 129 | public async acceptResponse(@Path('connectionId') connectionId: RecordId) { 130 | try { 131 | const connection = await this.agent.connections.acceptResponse(connectionId) 132 | return connection.toJSON() 133 | } catch (error) { 134 | throw ErrorHandlingService.handle(error) 135 | } 136 | } 137 | 138 | @Get('/url/:invitationId') 139 | public async getInvitation(@Path('invitationId') invitationId: string) { 140 | try { 141 | const outOfBandRecord = await this.agent.oob.findByCreatedInvitationId(invitationId) 142 | 143 | if (!outOfBandRecord || outOfBandRecord.state !== 'await-response') 144 | throw new NotFoundError(`connection with invitationId "${invitationId}" not found.`) 145 | 146 | const invitationJson = outOfBandRecord.outOfBandInvitation.toJSON({ useDidSovPrefixWhereAllowed: true }) 147 | return invitationJson 148 | } catch (error) { 149 | throw ErrorHandlingService.handle(error) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/controllers/credentials/CredentialController.ts: -------------------------------------------------------------------------------- 1 | import type { RestAgentModules } from '../../cliAgent' 2 | import type { 3 | CredentialExchangeRecordProps, 4 | CredentialProtocolVersionType, 5 | PeerDidNumAlgo2CreateOptions, 6 | Routing, 7 | } from '@credo-ts/core' 8 | 9 | import { 10 | CredentialState, 11 | Agent, 12 | W3cCredentialService, 13 | CredentialRole, 14 | createPeerDidDocumentFromServices, 15 | PeerDidNumAlgo, 16 | } from '@credo-ts/core' 17 | import { injectable } from 'tsyringe' 18 | 19 | import ErrorHandlingService from '../../errorHandlingService' 20 | import { CredentialExchangeRecordExample, RecordId } from '../examples' 21 | import { OutOfBandController } from '../outofband/OutOfBandController' 22 | import { 23 | AcceptCredentialRequestOptions, 24 | ProposeCredentialOptions, 25 | AcceptCredentialProposalOptions, 26 | CredentialOfferOptions, 27 | CreateOfferOptions, 28 | AcceptCredential, 29 | CreateOfferOobOptions, 30 | ThreadId, 31 | } from '../types' 32 | 33 | import { Body, Controller, Get, Path, Post, Route, Tags, Example, Query, Security } from 'tsoa' 34 | 35 | @Tags('Credentials') 36 | @Security('apiKey') 37 | @Route('/credentials') 38 | @injectable() 39 | export class CredentialController extends Controller { 40 | private agent: Agent 41 | private outOfBandController: OutOfBandController 42 | 43 | public constructor(agent: Agent, outOfBandController: OutOfBandController) { 44 | super() 45 | this.agent = agent 46 | this.outOfBandController = outOfBandController 47 | } 48 | 49 | /** 50 | * Retrieve all credential exchange records 51 | * 52 | * @returns CredentialExchangeRecord[] 53 | */ 54 | @Example([CredentialExchangeRecordExample]) 55 | @Get('/') 56 | public async getAllCredentials( 57 | @Query('threadId') threadId?: ThreadId, 58 | @Query('parentThreadId') parentThreadId?: ThreadId, 59 | @Query('connectionId') connectionId?: RecordId, 60 | @Query('state') state?: CredentialState, 61 | @Query('role') role?: CredentialRole 62 | ) { 63 | try { 64 | const credentials = await this.agent.credentials.findAllByQuery({ 65 | connectionId, 66 | threadId, 67 | state, 68 | parentThreadId, 69 | role, 70 | }) 71 | 72 | return credentials.map((c) => c.toJSON()) 73 | } catch (error) { 74 | throw ErrorHandlingService.handle(error) 75 | } 76 | } 77 | 78 | // TODO: Fix W3cCredentialRecordExample from example 79 | // @Example([W3cCredentialRecordExample]) 80 | @Get('/w3c') 81 | public async getAllW3c() { 82 | try { 83 | const w3cCredentialService = await this.agent.dependencyManager.resolve(W3cCredentialService) 84 | const w3cCredentialRecords = await w3cCredentialService.getAllCredentialRecords(this.agent.context) 85 | return w3cCredentialRecords 86 | } catch (error) { 87 | throw ErrorHandlingService.handle(error) 88 | } 89 | } 90 | 91 | // TODO: Fix W3cCredentialRecordExample from example 92 | // @Example([W3cCredentialRecordExample]) 93 | @Get('/w3c/:id') 94 | public async getW3cById(@Path('id') id: string) { 95 | try { 96 | const w3cCredentialService = await this.agent.dependencyManager.resolve(W3cCredentialService) 97 | const w3cRecord = await w3cCredentialService.getCredentialRecordById(this.agent.context, id) 98 | return w3cRecord 99 | } catch (error) { 100 | throw ErrorHandlingService.handle(error) 101 | } 102 | } 103 | 104 | /** 105 | * Retrieve credential exchange record by credential record id 106 | * 107 | * @param credentialRecordId 108 | * @returns CredentialExchangeRecord 109 | */ 110 | @Example(CredentialExchangeRecordExample) 111 | @Get('/:credentialRecordId') 112 | public async getCredentialById(@Path('credentialRecordId') credentialRecordId: RecordId) { 113 | try { 114 | const credential = await this.agent.credentials.getById(credentialRecordId) 115 | return credential.toJSON() 116 | } catch (error) { 117 | throw ErrorHandlingService.handle(error) 118 | } 119 | } 120 | 121 | /** 122 | * Initiate a new credential exchange as holder by sending a propose credential message 123 | * to the connection with a specified connection id. 124 | * 125 | * @param options 126 | * @returns CredentialExchangeRecord 127 | */ 128 | @Example(CredentialExchangeRecordExample) 129 | @Post('/propose-credential') 130 | public async proposeCredential(@Body() proposeCredentialOptions: ProposeCredentialOptions) { 131 | try { 132 | const credential = await this.agent.credentials.proposeCredential(proposeCredentialOptions) 133 | return credential 134 | } catch (error) { 135 | throw ErrorHandlingService.handle(error) 136 | } 137 | } 138 | 139 | /** 140 | * Accept a credential proposal as issuer by sending an accept proposal message 141 | * to the connection associated with the credential exchange record. 142 | * 143 | * @param credentialRecordId credential identifier 144 | * @param options 145 | * @returns CredentialExchangeRecord 146 | */ 147 | @Example(CredentialExchangeRecordExample) 148 | @Post('/accept-proposal') 149 | public async acceptProposal(@Body() acceptCredentialProposal: AcceptCredentialProposalOptions) { 150 | try { 151 | const credential = await this.agent.credentials.acceptProposal(acceptCredentialProposal) 152 | 153 | return credential 154 | } catch (error) { 155 | throw ErrorHandlingService.handle(error) 156 | } 157 | } 158 | 159 | /** 160 | * Initiate a new credential exchange as issuer by creating a credential offer 161 | * without specifying a connection id 162 | * 163 | * @param options 164 | * @returns AgentMessage, CredentialExchangeRecord 165 | */ 166 | @Example(CredentialExchangeRecordExample) 167 | @Post('/create-offer') 168 | public async createOffer(@Body() createOfferOptions: CreateOfferOptions) { 169 | try { 170 | const offer = await this.agent.credentials.offerCredential(createOfferOptions) 171 | return offer 172 | } catch (error) { 173 | throw ErrorHandlingService.handle(error) 174 | } 175 | } 176 | 177 | @Post('/create-offer-oob') 178 | public async createOfferOob(@Body() outOfBandOption: CreateOfferOobOptions) { 179 | try { 180 | let invitationDid: string | undefined 181 | let routing: Routing 182 | const linkSecretIds = await this.agent.modules.anoncreds.getLinkSecretIds() 183 | if (linkSecretIds.length === 0) { 184 | await this.agent.modules.anoncreds.createLinkSecret() 185 | } 186 | 187 | if (outOfBandOption?.invitationDid) { 188 | invitationDid = outOfBandOption?.invitationDid 189 | } else { 190 | routing = await this.agent.mediationRecipient.getRouting({}) 191 | const didDocument = createPeerDidDocumentFromServices([ 192 | { 193 | id: 'didcomm', 194 | recipientKeys: [routing.recipientKey], 195 | routingKeys: routing.routingKeys, 196 | serviceEndpoint: routing.endpoints[0], 197 | }, 198 | ]) 199 | const did = await this.agent.dids.create({ 200 | didDocument, 201 | method: 'peer', 202 | options: { 203 | numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, 204 | }, 205 | }) 206 | invitationDid = did.didState.did 207 | } 208 | 209 | const offerOob = await this.agent.credentials.createOffer({ 210 | protocolVersion: outOfBandOption.protocolVersion as CredentialProtocolVersionType<[]>, 211 | credentialFormats: outOfBandOption.credentialFormats, 212 | autoAcceptCredential: outOfBandOption.autoAcceptCredential, 213 | comment: outOfBandOption.comment, 214 | }) 215 | 216 | const credentialMessage = offerOob.message 217 | const outOfBandRecord = await this.agent.oob.createInvitation({ 218 | label: outOfBandOption.label, 219 | messages: [credentialMessage], 220 | autoAcceptConnection: true, 221 | imageUrl: outOfBandOption?.imageUrl, 222 | goalCode: outOfBandOption?.goalCode, 223 | invitationDid, 224 | }) 225 | return { 226 | invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ 227 | domain: this.agent.config.endpoints[0], 228 | }), 229 | invitation: outOfBandRecord.outOfBandInvitation.toJSON({ 230 | useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, 231 | }), 232 | outOfBandRecord: outOfBandRecord.toJSON(), 233 | outOfBandRecordId: outOfBandRecord.id, 234 | credentialRequestThId: offerOob.credentialRecord.threadId, 235 | invitationDid: outOfBandOption?.invitationDid ? '' : invitationDid, 236 | } 237 | } catch (error) { 238 | throw ErrorHandlingService.handle(error) 239 | } 240 | } 241 | 242 | /** 243 | * Accept a credential offer as holder by sending an accept offer message 244 | * to the connection associated with the credential exchange record. 245 | * 246 | * @param credentialRecordId credential identifier 247 | * @param options 248 | * @returns CredentialExchangeRecord 249 | */ 250 | @Example(CredentialExchangeRecordExample) 251 | @Post('/accept-offer') 252 | public async acceptOffer(@Body() acceptCredentialOfferOptions: CredentialOfferOptions) { 253 | try { 254 | const linkSecretIds = await this.agent.modules.anoncreds.getLinkSecretIds() 255 | if (linkSecretIds.length === 0) { 256 | await this.agent.modules.anoncreds.createLinkSecret() 257 | } 258 | const acceptOffer = await this.agent.credentials.acceptOffer(acceptCredentialOfferOptions) 259 | return acceptOffer 260 | } catch (error) { 261 | throw ErrorHandlingService.handle(error) 262 | } 263 | } 264 | 265 | /** 266 | * Accept a credential request as issuer by sending an accept request message 267 | * to the connection associated with the credential exchange record. 268 | * 269 | * @param credentialRecordId credential identifier 270 | * @param options 271 | * @returns CredentialExchangeRecord 272 | */ 273 | @Example(CredentialExchangeRecordExample) 274 | @Post('/accept-request') 275 | public async acceptRequest(@Body() acceptCredentialRequestOptions: AcceptCredentialRequestOptions) { 276 | try { 277 | const credential = await this.agent.credentials.acceptRequest(acceptCredentialRequestOptions) 278 | return credential 279 | } catch (error) { 280 | throw ErrorHandlingService.handle(error) 281 | } 282 | } 283 | 284 | /** 285 | * Accept a credential as holder by sending an accept credential message 286 | * to the connection associated with the credential exchange record. 287 | * 288 | * @param options 289 | * @returns CredentialExchangeRecord 290 | */ 291 | @Example(CredentialExchangeRecordExample) 292 | @Post('/accept-credential') 293 | public async acceptCredential(@Body() acceptCredential: AcceptCredential) { 294 | try { 295 | const credential = await this.agent.credentials.acceptCredential(acceptCredential) 296 | return credential 297 | } catch (error) { 298 | throw ErrorHandlingService.handle(error) 299 | } 300 | } 301 | 302 | /** 303 | * Return credentialRecord 304 | * 305 | * @param credentialRecordId 306 | * @returns credentialRecord 307 | */ 308 | @Get('/:credentialRecordId/form-data') 309 | public async credentialFormData(@Path('credentialRecordId') credentialRecordId: string) { 310 | try { 311 | const credentialDetails = await this.agent.credentials.getFormatData(credentialRecordId) 312 | return credentialDetails 313 | } catch (error) { 314 | throw ErrorHandlingService.handle(error) 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/controllers/credentials/CredentialDefinitionController.ts: -------------------------------------------------------------------------------- 1 | import type { RestAgentModules } from '../../cliAgent' 2 | import type { SchemaId } from '../examples' 3 | 4 | import { getUnqualifiedCredentialDefinitionId, parseIndyCredentialDefinitionId } from '@credo-ts/anoncreds' 5 | import { Agent } from '@credo-ts/core' 6 | import { injectable } from 'tsyringe' 7 | 8 | import { CredentialEnum, EndorserMode } from '../../enums/enum' 9 | import ErrorHandlingService from '../../errorHandlingService' 10 | import { ENDORSER_DID_NOT_PRESENT } from '../../errorMessages' 11 | import { BadRequestError, InternalServerError, NotFoundError } from '../../errors/errors' 12 | import { CredentialDefinitionExample, CredentialDefinitionId } from '../examples' 13 | 14 | import { Body, Controller, Example, Get, Path, Post, Route, Tags, Security, Response } from 'tsoa' 15 | 16 | @Tags('Credential Definitions') 17 | @Route('/credential-definitions') 18 | @Security('apiKey') 19 | @injectable() 20 | export class CredentialDefinitionController extends Controller { 21 | // TODO: Currently this only works if Extensible from credo-ts is renamed to something else, since there are two references to Extensible 22 | private agent: Agent 23 | public constructor(agent: Agent) { 24 | super() 25 | this.agent = agent 26 | } 27 | 28 | /** 29 | * Retrieve credential definition by credential definition id 30 | * 31 | * @param credentialDefinitionId 32 | * @returns CredDef 33 | */ 34 | @Example(CredentialDefinitionExample) 35 | @Get('/:credentialDefinitionId') 36 | public async getCredentialDefinitionById( 37 | @Path('credentialDefinitionId') credentialDefinitionId: CredentialDefinitionId 38 | ) { 39 | try { 40 | const credentialDefinitionResult = await this.agent.modules.anoncreds.getCredentialDefinition( 41 | credentialDefinitionId 42 | ) 43 | 44 | if (credentialDefinitionResult.resolutionMetadata?.error === 'notFound') { 45 | throw new NotFoundError(credentialDefinitionResult.resolutionMetadata.message) 46 | } 47 | const error = credentialDefinitionResult.resolutionMetadata?.error 48 | 49 | if (error === 'invalid' || error === 'unsupportedAnonCredsMethod') { 50 | throw new BadRequestError(credentialDefinitionResult.resolutionMetadata.message) 51 | } 52 | 53 | if (error !== undefined || credentialDefinitionResult.credentialDefinition === undefined) { 54 | throw new InternalServerError(credentialDefinitionResult.resolutionMetadata.message) 55 | } 56 | 57 | return credentialDefinitionResult 58 | } catch (error) { 59 | throw ErrorHandlingService.handle(error) 60 | } 61 | } 62 | 63 | /** 64 | * Creates a new credential definition. 65 | * 66 | * @param credentialDefinitionRequest 67 | * @returns CredDef 68 | */ 69 | @Example(CredentialDefinitionExample) 70 | @Response(200, 'Action required') 71 | @Response(202, 'Wait for action to complete') 72 | @Post('/') 73 | public async createCredentialDefinition( 74 | @Body() 75 | credentialDefinitionRequest: { 76 | issuerId: string 77 | schemaId: SchemaId 78 | tag: string 79 | endorse?: boolean 80 | endorserDid?: string 81 | } 82 | ) { 83 | try { 84 | const { issuerId, schemaId, tag, endorse, endorserDid } = credentialDefinitionRequest 85 | const credDef = { 86 | issuerId, 87 | schemaId, 88 | tag, 89 | type: 'CL', 90 | } 91 | const credentialDefinitionPayload = { 92 | credentialDefinition: credDef, 93 | options: { 94 | endorserMode: '', 95 | endorserDid: '', 96 | supportRevocation: false, 97 | }, 98 | } 99 | if (!endorse) { 100 | credentialDefinitionPayload.options.endorserMode = EndorserMode.Internal 101 | credentialDefinitionPayload.options.endorserDid = issuerId 102 | } else { 103 | if (!endorserDid) { 104 | throw new BadRequestError(ENDORSER_DID_NOT_PRESENT) 105 | } 106 | credentialDefinitionPayload.options.endorserMode = EndorserMode.External 107 | credentialDefinitionPayload.options.endorserDid = endorserDid ? endorserDid : '' 108 | } 109 | 110 | const registerCredentialDefinitionResult = await this.agent.modules.anoncreds.registerCredentialDefinition( 111 | credentialDefinitionPayload 112 | ) 113 | 114 | if (registerCredentialDefinitionResult.credentialDefinitionState.state === CredentialEnum.Failed) { 115 | throw new InternalServerError('Falied to register credef on ledger') 116 | } 117 | 118 | if (registerCredentialDefinitionResult.credentialDefinitionState.state === CredentialEnum.Wait) { 119 | // The request has been accepted for processing, but the processing has not been completed. 120 | this.setStatus(202) 121 | return registerCredentialDefinitionResult 122 | } 123 | 124 | if (registerCredentialDefinitionResult.credentialDefinitionState.state === CredentialEnum.Action) { 125 | return registerCredentialDefinitionResult 126 | } 127 | 128 | // TODO: Return uniform response for both Internally and Externally endorsed Schemas 129 | if (!endorse) { 130 | const indyCredDefId = parseIndyCredentialDefinitionId( 131 | registerCredentialDefinitionResult.credentialDefinitionState.credentialDefinitionId as string 132 | ) 133 | const getCredentialDefinitionId = await getUnqualifiedCredentialDefinitionId( 134 | indyCredDefId.namespaceIdentifier, 135 | indyCredDefId.schemaSeqNo, 136 | indyCredDefId.tag 137 | ) 138 | registerCredentialDefinitionResult.credentialDefinitionState.credentialDefinitionId = getCredentialDefinitionId 139 | return registerCredentialDefinitionResult.credentialDefinitionState 140 | } 141 | return registerCredentialDefinitionResult 142 | } catch (error) { 143 | throw ErrorHandlingService.handle(error) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/controllers/credentials/SchemaController.ts: -------------------------------------------------------------------------------- 1 | import type { RestAgentModules } from '../../cliAgent' 2 | 3 | import { getUnqualifiedSchemaId, parseIndySchemaId } from '@credo-ts/anoncreds' 4 | import { Agent } from '@credo-ts/core' 5 | import { injectable } from 'tsyringe' 6 | 7 | import { CredentialEnum, EndorserMode, SchemaError } from '../../enums/enum' 8 | import ErrorHandlingService from '../../errorHandlingService' 9 | import { ENDORSER_DID_NOT_PRESENT } from '../../errorMessages' 10 | import { BadRequestError, InternalServerError, NotFoundError } from '../../errors/errors' 11 | import { CreateSchemaSuccessful, SchemaExample } from '../examples' 12 | import { CreateSchemaInput } from '../types' 13 | 14 | import { Example, Get, Post, Route, Tags, Security, Path, Body, Controller } from 'tsoa' 15 | @Tags('Schemas') 16 | @Route('/schemas') 17 | @Security('apiKey') 18 | @injectable() 19 | export class SchemaController extends Controller { 20 | private agent: Agent 21 | 22 | public constructor(agent: Agent) { 23 | super() 24 | this.agent = agent 25 | } 26 | 27 | /** 28 | * Get schema by schemaId 29 | * @param schemaId 30 | * @param notFoundErrormessage 31 | * @param forbiddenError 32 | * @param badRequestError 33 | * @param internalServerError 34 | * @returns get schema by Id 35 | */ 36 | @Example(SchemaExample) 37 | @Get('/:schemaId') 38 | public async getSchemaById(@Path('schemaId') schemaId: string) { 39 | try { 40 | const schemBySchemaId = await this.agent.modules.anoncreds.getSchema(schemaId) 41 | 42 | if ( 43 | (schemBySchemaId && 44 | schemBySchemaId?.resolutionMetadata && 45 | schemBySchemaId?.resolutionMetadata?.error === SchemaError.NotFound) || 46 | schemBySchemaId?.resolutionMetadata?.error === SchemaError.UnSupportedAnonCredsMethod 47 | ) { 48 | throw new NotFoundError(schemBySchemaId?.resolutionMetadata?.message) 49 | } 50 | 51 | return schemBySchemaId 52 | } catch (error) { 53 | throw ErrorHandlingService.handle(error) 54 | } 55 | } 56 | 57 | /** 58 | * Create schema 59 | * @param schema 60 | * @param notFoundError 61 | * @param forbiddenError 62 | * @param badRequestError 63 | * @param internalServerError 64 | * @returns get schema 65 | */ 66 | @Post('/') 67 | @Example(CreateSchemaSuccessful) 68 | public async createSchema(@Body() schema: CreateSchemaInput) { 69 | try { 70 | const { issuerId, name, version, attributes } = schema 71 | 72 | const schemaPayload = { 73 | issuerId, 74 | name, 75 | version, 76 | attrNames: attributes, 77 | } 78 | const createSchemaPayload = { 79 | schema: schemaPayload, 80 | options: { 81 | endorserMode: '', 82 | endorserDid: '', 83 | }, 84 | } 85 | 86 | if (!schema.endorse) { 87 | createSchemaPayload.options.endorserMode = EndorserMode.Internal 88 | createSchemaPayload.options.endorserDid = issuerId 89 | } else { 90 | if (!schema.endorserDid) { 91 | throw new BadRequestError(ENDORSER_DID_NOT_PRESENT) 92 | } 93 | createSchemaPayload.options.endorserMode = EndorserMode.External 94 | createSchemaPayload.options.endorserDid = schema.endorserDid 95 | } 96 | 97 | const createSchemaTxResult = await this.agent.modules.anoncreds.registerSchema(createSchemaPayload) 98 | 99 | if (createSchemaTxResult.schemaState.state === CredentialEnum.Failed) { 100 | throw new InternalServerError(`Schema creation failed. Reason: ${createSchemaTxResult.schemaState.reason}`) 101 | } 102 | 103 | if (createSchemaTxResult.schemaState.state === CredentialEnum.Wait) { 104 | this.setStatus(202) 105 | return createSchemaTxResult 106 | } 107 | 108 | if (createSchemaTxResult.schemaState.state === CredentialEnum.Action) { 109 | return createSchemaTxResult 110 | } 111 | 112 | if (createSchemaTxResult.schemaState.state === CredentialEnum.Finished) { 113 | // TODO: Return uniform response for both Internally and Externally endorsed Schemas 114 | if (!schema.endorse) { 115 | const indySchemaId = parseIndySchemaId(createSchemaTxResult.schemaState.schemaId as string) 116 | 117 | const getSchemaUnqualifiedId = await getUnqualifiedSchemaId( 118 | indySchemaId.namespaceIdentifier, 119 | indySchemaId.schemaName, 120 | indySchemaId.schemaVersion 121 | ) 122 | 123 | createSchemaTxResult.schemaState.schemaId = getSchemaUnqualifiedId 124 | return createSchemaTxResult.schemaState 125 | } 126 | return createSchemaTxResult 127 | } 128 | } catch (error) { 129 | throw ErrorHandlingService.handle(error) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/controllers/endorser-transaction/EndorserTransactionController.ts: -------------------------------------------------------------------------------- 1 | import type { Version } from '../examples' 2 | import type { IndyVdrDidCreateOptions } from '@credo-ts/indy-vdr' 3 | 4 | import { 5 | getUnqualifiedCredentialDefinitionId, 6 | getUnqualifiedSchemaId, 7 | parseIndyCredentialDefinitionId, 8 | parseIndySchemaId, 9 | } from '@credo-ts/anoncreds' 10 | import { Agent } from '@credo-ts/core' 11 | import { injectable } from 'tsyringe' 12 | 13 | import { CredentialEnum, EndorserMode } from '../../enums/enum' 14 | import ErrorHandlingService from '../../errorHandlingService' 15 | import { BadRequestError } from '../../errors' 16 | import { DidNymTransaction, EndorserTransaction, WriteTransaction } from '../types' 17 | 18 | import { Body, Controller, Post, Route, Tags, Security } from 'tsoa' 19 | 20 | @Tags('EndorserTransaction') 21 | @Route('/transactions') 22 | @Security('apiKey') 23 | @injectable() 24 | export class EndorserTransactionController extends Controller { 25 | private agent: Agent 26 | 27 | public constructor(agent: Agent) { 28 | super() 29 | this.agent = agent 30 | } 31 | 32 | @Post('/endorse') 33 | public async endorserTransaction(@Body() endorserTransaction: EndorserTransaction) { 34 | try { 35 | if (!endorserTransaction.transaction) { 36 | throw new BadRequestError('Transaction is required') 37 | } 38 | if (!endorserTransaction.endorserDid) { 39 | throw new BadRequestError('EndorserDid is required') 40 | } 41 | const signedTransaction = await this.agent.modules.indyVdr.endorseTransaction( 42 | endorserTransaction.transaction, 43 | endorserTransaction.endorserDid 44 | ) 45 | 46 | return { signedTransaction } 47 | } catch (error) { 48 | throw ErrorHandlingService.handle(error) 49 | } 50 | } 51 | 52 | @Post('/set-endorser-role') 53 | public async didNymTransaction(@Body() didNymTransaction: DidNymTransaction) { 54 | try { 55 | const didCreateSubmitResult = await this.agent.dids.create({ 56 | did: didNymTransaction.did, 57 | options: { 58 | endorserMode: EndorserMode.External, 59 | endorsedTransaction: { 60 | nymRequest: didNymTransaction.nymRequest, 61 | }, 62 | }, 63 | }) 64 | 65 | return didCreateSubmitResult 66 | } catch (error) { 67 | throw ErrorHandlingService.handle(error) 68 | } 69 | } 70 | 71 | @Post('/write') 72 | public async writeSchemaAndCredDefOnLedger( 73 | @Body() 74 | writeTransaction: WriteTransaction 75 | ) { 76 | try { 77 | if (writeTransaction.schema) { 78 | const writeSchema = await this.submitSchemaOnLedger( 79 | writeTransaction.schema, 80 | writeTransaction.endorsedTransaction 81 | ) 82 | return writeSchema 83 | } else if (writeTransaction.credentialDefinition) { 84 | const writeCredDef = await this.submitCredDefOnLedger( 85 | writeTransaction.credentialDefinition, 86 | writeTransaction.endorsedTransaction 87 | ) 88 | return writeCredDef 89 | } else { 90 | throw new Error('Please provide valid schema or credential-def!') 91 | } 92 | } catch (error) { 93 | throw ErrorHandlingService.handle(error) 94 | } 95 | } 96 | 97 | public async submitSchemaOnLedger( 98 | schema: { 99 | issuerId: string 100 | name: string 101 | version: Version 102 | attributes: string[] 103 | }, 104 | endorsedTransaction?: string 105 | ) { 106 | if (!schema.issuerId) { 107 | throw new BadRequestError('IssuerId is required') 108 | } 109 | if (!schema.name) { 110 | throw new BadRequestError('Name is required') 111 | } 112 | if (!schema.version) { 113 | throw new BadRequestError('Version is required') 114 | } 115 | if (!schema.attributes) { 116 | throw new BadRequestError('Attributes is required') 117 | } 118 | const { issuerId, name, version, attributes } = schema 119 | const { schemaState } = await this.agent.modules.anoncreds.registerSchema({ 120 | options: { 121 | endorserMode: EndorserMode.External, 122 | endorsedTransaction, 123 | }, 124 | schema: { 125 | attrNames: attributes, 126 | issuerId: issuerId, 127 | name: name, 128 | version: version, 129 | }, 130 | }) 131 | 132 | const indySchemaId = parseIndySchemaId(schemaState.schemaId) 133 | const getSchemaUnqualifiedId = await getUnqualifiedSchemaId( 134 | indySchemaId.namespaceIdentifier, 135 | indySchemaId.schemaName, 136 | indySchemaId.schemaVersion 137 | ) 138 | if (schemaState.state === CredentialEnum.Finished || schemaState.state === CredentialEnum.Action) { 139 | schemaState.schemaId = getSchemaUnqualifiedId 140 | } 141 | return schemaState 142 | } 143 | 144 | public async submitCredDefOnLedger( 145 | credentialDefinition: { 146 | schemaId: string 147 | issuerId: string 148 | tag: string 149 | value: unknown 150 | type: string 151 | }, 152 | endorsedTransaction?: string 153 | ) { 154 | if (!credentialDefinition.schemaId) { 155 | throw new BadRequestError('SchemaId is required') 156 | } 157 | if (!credentialDefinition.issuerId) { 158 | throw new BadRequestError('IssuerId is required') 159 | } 160 | if (!credentialDefinition.tag) { 161 | throw new BadRequestError('Tag is required') 162 | } 163 | if (!credentialDefinition.value) { 164 | throw new BadRequestError('Value is required') 165 | } 166 | if (!credentialDefinition.type) { 167 | throw new BadRequestError('Type is required') 168 | } 169 | const { credentialDefinitionState } = await this.agent.modules.anoncreds.registerCredentialDefinition({ 170 | credentialDefinition, 171 | options: { 172 | endorserMode: EndorserMode.External, 173 | endorsedTransaction: endorsedTransaction, 174 | }, 175 | }) 176 | 177 | const indyCredDefId = parseIndyCredentialDefinitionId(credentialDefinitionState.credentialDefinitionId) 178 | const getCredentialDefinitionId = await getUnqualifiedCredentialDefinitionId( 179 | indyCredDefId.namespaceIdentifier, 180 | indyCredDefId.schemaSeqNo, 181 | indyCredDefId.tag 182 | ) 183 | if ( 184 | credentialDefinitionState.state === CredentialEnum.Finished || 185 | credentialDefinitionState.state === CredentialEnum.Action 186 | ) { 187 | credentialDefinitionState.credentialDefinitionId = getCredentialDefinitionId 188 | } 189 | return credentialDefinitionState 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/controllers/examples.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ProofRole, 3 | AutoAcceptProof, 4 | BasicMessageRole, 5 | CredentialState, 6 | DidExchangeRole, 7 | DidExchangeState, 8 | OutOfBandInvitationOptions, 9 | OutOfBandRecordProps, 10 | ProofExchangeRecordProps, 11 | ProofState, 12 | OutOfBandRole, 13 | OutOfBandState, 14 | CredentialRole, 15 | } from '@credo-ts/core' 16 | 17 | /** 18 | * @example "821f9b26-ad04-4f56-89b6-e2ef9c72b36e" 19 | */ 20 | export type RecordId = string 21 | 22 | /** 23 | * @example "did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL" 24 | */ 25 | export type Did = string 26 | 27 | /** 28 | * @example "1.0.0" 29 | */ 30 | export type Version = string 31 | 32 | /** 33 | * @example "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag" 34 | */ 35 | export type CredentialDefinitionId = string 36 | 37 | /** 38 | * @example "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0" 39 | */ 40 | export type SchemaId = string 41 | 42 | export const BasicMessageRecordExample = { 43 | _tags: { 44 | role: 'sender', 45 | connectionId: '2aecf74c-3073-4f98-9acb-92415d096834', 46 | }, 47 | metadata: {}, 48 | id: '74bcf865-1fdc-45b4-b517-9def02dfd25f', 49 | createdAt: new Date('2022-08-18T08:38:40.216Z'), 50 | content: 'string', 51 | sentTime: '2022-08-18T08:38:40.216Z', 52 | connectionId: '2aecf74c-3073-4f98-9acb-92415d096834', 53 | role: 'sender' as BasicMessageRole, 54 | } 55 | 56 | export const ConnectionRecordExample = { 57 | _tags: { 58 | invitationDid: 59 | 'did:peer:2.SeyJzIjoiaHR0cHM6Ly9kYTIzLTg5LTIwLTE2Mi0xNDYubmdyb2suaW8iLCJ0IjoiZGlkLWNvbW11bmljYXRpb24iLCJwcmlvcml0eSI6MCwicmVjaXBpZW50S2V5cyI6WyJkaWQ6a2V5Ono2TWtualg3U1lXRmdHMThCYkNEZHJnemhuQnA0UlhyOGVITHZxQ3FvRXllckxiTiN6Nk1rbmpYN1NZV0ZnRzE4QmJDRGRyZ3pobkJwNFJYcjhlSEx2cUNxb0V5ZXJMYk4iXSwiciI6W119', 60 | did: 'did:peer:1zQmfQh1T3rSqarP2FZ37uKjdQHPKFdVyo2mGiAPHZ8Ep7hv', 61 | state: 'invitation-sent' as DidExchangeState, 62 | invitationKey: '9HG4rJFpLiWf56MWxHj9rgdpErFzim2zEpHuxy1dw7oz', 63 | outOfBandId: 'edbc89fe-785f-4774-a288-46012486881d', 64 | verkey: '9HG4rJFpLiWf56MWxHj9rgdpErFzim2zEpHuxy1dw7oz', 65 | role: 'responder' as DidExchangeRole, 66 | }, 67 | metadata: {}, 68 | id: '821f9b26-ad04-4f56-89b6-e2ef9c72b36e', 69 | createdAt: new Date('2022-01-01T00:00:00.000Z'), 70 | did: 'did:peer:1zQmfQh1T3rSqarP2FZ37uKjdQHPKFdVyo2mGiAPHZ8Ep7hv', 71 | state: 'invitation-sent' as DidExchangeState, 72 | role: 'responder' as DidExchangeRole, 73 | invitationDid: 74 | 'did:peer:2.SeyJzIjoiaHR0cHM6Ly9kYTIzLTg5LTIwLTE2Mi0xNDYubmdyb2suaW8iLCJ0IjoiZGlkLWNvbW11bmljYXRpb24iLCJwcmlvcml0eSI6MCwicmVjaXBpZW50S2V5cyI6WyJkaWQ6a2V5Ono2TWtualg3U1lXRmdHMThCYkNEZHJnemhuQnA0UlhyOGVITHZxQ3FvRXllckxiTiN6Nk1rbmpYN1NZV0ZnRzE4QmJDRGRyZ3pobkJwNFJYcjhlSEx2cUNxb0V5ZXJMYk4iXSwiciI6W119', 75 | outOfBandId: 'edbc89fe-785f-4774-a288-46012486881d', 76 | } 77 | 78 | export const DidRecordExample = { 79 | didDocument: { 80 | '@context': [ 81 | 'https://w3id.org/did/v1', 82 | 'https://w3id.org/security/suites/ed25519-2018/v1', 83 | 'https://w3id.org/security/suites/x25519-2019/v1', 84 | ], 85 | id: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', 86 | alsoKnownAs: undefined, 87 | controller: undefined, 88 | verificationMethod: [ 89 | { 90 | id: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', 91 | type: 'Ed25519VerificationKey2018', 92 | controller: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', 93 | publicKeyBase58: '6fioC1zcDPyPEL19pXRS2E4iJ46zH7xP6uSgAaPdwDrx', 94 | }, 95 | ], 96 | authentication: [ 97 | 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', 98 | ], 99 | assertionMethod: [ 100 | 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', 101 | ], 102 | capabilityInvocation: [ 103 | 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', 104 | ], 105 | capabilityDelegation: [ 106 | 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', 107 | ], 108 | keyAgreement: [ 109 | { 110 | id: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL#z6LSrdqo4M24WRDJj1h2hXxgtDTyzjjKCiyapYVgrhwZAySn', 111 | type: 'X25519KeyAgreementKey2019', 112 | controller: 'did:key:z6Mkk7yqnGF3YwTrLpqrW6PGsKci7dNqh1CjnvMbzrMerSeL', 113 | publicKeyBase58: 'FxfdY3DCQxVZddKGAtSjZdFW9bCCW7oRwZn1NFJ2Tbg2', 114 | }, 115 | ], 116 | service: undefined, 117 | }, 118 | didDocumentMetadata: {}, 119 | didResolutionMetadata: { 120 | contentType: 'application/did+ld+json', 121 | }, 122 | } 123 | 124 | type OutOfBandRecordProperties = Omit 125 | export type OutOfBandInvitationProps = Omit< 126 | OutOfBandInvitationOptions, 127 | 'handshakeProtocols' | 'services' | 'appendedAttachments' 128 | > 129 | 130 | export interface OutOfBandRecordWithInvitationProps extends OutOfBandRecordProperties { 131 | outOfBandInvitation: OutOfBandInvitationProps 132 | } 133 | 134 | export const outOfBandInvitationExample = { 135 | '@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.1/invitation', 136 | '@id': 'd6472943-e5d0-4d95-8b48-790ed5a41931', 137 | label: 'Aries Test Agent', 138 | accept: ['didcomm/aip1', 'didcomm/aip2;env=rfc19'], 139 | handshake_protocols: ['https://didcomm.org/didexchange/1.0', 'https://didcomm.org/connections/1.0'], 140 | services: [ 141 | { 142 | id: '#inline-0', 143 | serviceEndpoint: 'https://6b77-89-20-162-146.ngrok.io', 144 | type: 'did-communication', 145 | recipientKeys: ['did:key:z6MkmTBHTWrvLPN8pBmUj7Ye5ww9GiacXCYMNVvpScSpf1DM'], 146 | routingKeys: [], 147 | }, 148 | ], 149 | } 150 | 151 | export const outOfBandRecordExample = { 152 | _tags: { 153 | invitationId: '1cbd22e4-1906-41e9-8807-83d84437f978', 154 | state: 'await-response', 155 | role: 'sender', 156 | recipientKeyFingerprints: ['z6MktUCPZjfRJXD4GMcYuXiqX2qZ8vBw6UAYpDFiHEUfwuLj'], 157 | }, 158 | outOfBandInvitation: outOfBandInvitationExample, 159 | metadata: {}, 160 | id: '42a95528-0e30-4f86-a462-0efb02178b53', 161 | createdAt: new Date('2022-01-01T00:00:00.000Z'), 162 | role: 'sender' as OutOfBandRole, 163 | state: 'await-response' as OutOfBandState, 164 | reusable: false, 165 | } 166 | 167 | // TODO: Fix example to satisfy W3cCredentialRecordOptions 168 | export const W3cCredentialRecordExample = { 169 | credential: { 170 | // Populate with the required properties for a W3cVerifiableCredential 171 | // Example: 172 | '@context': ['https://www.w3.org/2018/credentials/v1'], 173 | type: ['VerifiableCredential'], 174 | issuer: 'https://example.com', 175 | issuanceDate: '2023-01-01T00:00:00Z', 176 | credentialSubject: { 177 | id: 'did:example:1234567890', 178 | name: 'John Doe', 179 | }, 180 | proof: { 181 | type: 'Ed25519Signature2018', 182 | created: '2023-01-01T00:00:00Z', 183 | proofPurpose: 'assertionMethod', 184 | verificationMethod: 'https://example.com/keys/1', 185 | jws: '...', 186 | }, 187 | }, 188 | tags: { 189 | // Populate with the required properties for CustomW3cCredentialTags 190 | // Example: 191 | tag1: 'value1', 192 | tag2: 'value2', 193 | }, 194 | } 195 | 196 | export const CredentialExchangeRecordExample = { 197 | _tags: { 198 | state: 'offer-sent', 199 | threadId: '82701488-b43c-4d7b-9244-4bb204a7ae26', 200 | connectionId: 'ac6d0fdd-0db8-4f52-8a3d-de7ff8ddc14b', 201 | }, 202 | metadata: { 203 | '_internal/indyCredential': { 204 | credentialDefinitionId: 'q7ATwTYbQDgiigVijUAej:3:CL:318187:latest', 205 | schemaId: 'q7ATwTYbQDgiigVijUAej:2:Employee Badge:1.0', 206 | }, 207 | }, 208 | credentials: [], 209 | id: '821f9b26-ad04-4f56-89b6-e2ef9c72b36e', 210 | createdAt: new Date('2022-01-01T00:00:00.000Z'), 211 | state: 'offer-sent' as CredentialState, 212 | connectionId: 'ac6d0fdd-0db8-4f52-8a3d-de7ff8ddc14b', 213 | threadId: '82701488-b43c-4d7b-9244-4bb204a7ae26', 214 | credentialAttributes: [], 215 | protocolVersion: 'v1', 216 | role: 'issuer' as CredentialRole.Issuer, 217 | } 218 | 219 | export const ProofRecordExample = { 220 | _tags: { 221 | state: 'proposal-sent' as ProofState, 222 | threadId: '0019d466-5eea-4269-8c40-031b4896c5b7', 223 | connectionId: '2aecf74c-3073-4f98-9acb-92415d096834', 224 | } as ProofExchangeRecordProps, 225 | metadata: {}, 226 | id: '821f9b26-ad04-4f56-89b6-e2ef9c72b36e', 227 | createdAt: new Date('2022-01-01T00:00:00.000Z'), 228 | state: 'proposal-sent' as ProofState, 229 | connectionId: '2aecf74c-3073-4f98-9acb-92415d096834', 230 | threadId: '0019d466-5eea-4269-8c40-031b4896c5b7', 231 | autoAcceptProof: 'always' as AutoAcceptProof, 232 | protocolVersion: 'v1', 233 | role: 'verifier' as ProofRole.Verifier, 234 | } 235 | 236 | export const SchemaExample = { 237 | ver: '1.0', 238 | id: 'WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0', 239 | name: 'schema', 240 | version: '1.0', 241 | attrNames: ['string'], 242 | seqNo: 351936, 243 | } 244 | 245 | export const CreateSchemaSuccessful = { 246 | state: 'finished', 247 | schema: { 248 | issuerId: 'did:indy:bcovrin:testnet:LRCUFcizUL74AGgLqdJHK7', 249 | name: 'Test Schema', 250 | version: '1.0.0', 251 | attrNames: ['Name', 'Age'], 252 | }, 253 | schemaId: 'LRCUFcizUL74AGgLqdJHK7:2:Test Schema:1.0.0', 254 | } 255 | 256 | export const CreateDidResponse = { 257 | did: 'did:indy:bcovrin:testnet:LRCUFcizUL74AGgLqdJHK7', 258 | didDocument: { 259 | '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], 260 | id: 'did:indy:bcovrin:testnet:LRCUFcizUL74AGgLqdJHK7', 261 | verificationMethod: [ 262 | { 263 | id: 'did:indy:bcovrin:testnet:LRCUFcizUL74AGgLqdJHK7#verkey', 264 | type: 'Ed25519VerificationKey2018', 265 | controller: 'did:indy:bcovrin:testnet:LRCUFcizUL74AGgLqdJHK7', 266 | publicKeyBase58: 'BapLDK4dEY88vWcQgNbpAPVVP4r3CHs4MvShmmhqkxXM', 267 | }, 268 | ], 269 | authentication: ['did:indy:bcovrin:testnet:LRCUFcizUL74AGgLqdJHK7#verkey'], 270 | }, 271 | } 272 | 273 | export const CredentialDefinitionExample = { 274 | ver: '1.0', 275 | id: 'WgWxqztrNooG92RXvxSTWv:3:CL:20:tag', 276 | schemaId: '351936', 277 | type: 'CL', 278 | tag: 'definition', 279 | value: { 280 | primary: { 281 | n: 'string', 282 | s: 'string', 283 | r: { 284 | master_secret: 'string', 285 | string: 'string', 286 | }, 287 | rctxt: 'string', 288 | z: 'string', 289 | }, 290 | revocation: { 291 | g: '1 string', 292 | g_dash: 'string', 293 | h: 'string', 294 | h0: 'string', 295 | h1: 'string', 296 | h2: 'string', 297 | htilde: 'string', 298 | h_cap: 'string', 299 | u: 'string', 300 | pk: 'string', 301 | y: 'string', 302 | }, 303 | }, 304 | } 305 | -------------------------------------------------------------------------------- /src/controllers/outofband/OutOfBandController.ts: -------------------------------------------------------------------------------- 1 | import type { RestAgentModules } from '../../cliAgent' 2 | import type { OutOfBandInvitationProps, OutOfBandRecordWithInvitationProps } from '../examples' 3 | import type { AgentMessageType, RecipientKeyOption, CreateInvitationOptions } from '../types' 4 | import type { 5 | ConnectionRecordProps, 6 | CreateLegacyInvitationConfig, 7 | PeerDidNumAlgo2CreateOptions, 8 | Routing, 9 | } from '@credo-ts/core' 10 | 11 | import { 12 | AgentMessage, 13 | JsonTransformer, 14 | OutOfBandInvitation, 15 | Agent, 16 | Key, 17 | KeyType, 18 | createPeerDidDocumentFromServices, 19 | PeerDidNumAlgo, 20 | } from '@credo-ts/core' 21 | import { injectable } from 'tsyringe' 22 | 23 | import ErrorHandlingService from '../../errorHandlingService' 24 | import { NotFoundError } from '../../errors' 25 | import { ConnectionRecordExample, outOfBandInvitationExample, outOfBandRecordExample, RecordId } from '../examples' 26 | import { AcceptInvitationConfig, ReceiveInvitationByUrlProps, ReceiveInvitationProps } from '../types' 27 | 28 | import { Body, Controller, Delete, Example, Get, Path, Post, Query, Route, Tags, Security } from 'tsoa' 29 | 30 | @Tags('Out Of Band') 31 | @Security('apiKey') 32 | @Route('/oob') 33 | @injectable() 34 | export class OutOfBandController extends Controller { 35 | private agent: Agent 36 | 37 | public constructor(agent: Agent) { 38 | super() 39 | this.agent = agent 40 | } 41 | 42 | /** 43 | * Retrieve all out of band records 44 | * @param invitationId invitation identifier 45 | * @returns OutOfBandRecord[] 46 | */ 47 | @Example([outOfBandRecordExample]) 48 | @Get() 49 | public async getAllOutOfBandRecords(@Query('invitationId') invitationId?: RecordId) { 50 | try { 51 | let outOfBandRecords = await this.agent.oob.getAll() 52 | 53 | if (invitationId) outOfBandRecords = outOfBandRecords.filter((o) => o.outOfBandInvitation.id === invitationId) 54 | 55 | return outOfBandRecords.map((c) => c.toJSON()) 56 | } catch (error) { 57 | throw ErrorHandlingService.handle(error) 58 | } 59 | } 60 | 61 | /** 62 | * Retrieve an out of band record by id 63 | * @param recordId record identifier 64 | * @returns OutOfBandRecord 65 | */ 66 | @Example(outOfBandRecordExample) 67 | @Get('/:outOfBandId') 68 | public async getOutOfBandRecordById(@Path('outOfBandId') outOfBandId: RecordId) { 69 | try { 70 | const outOfBandRecord = await this.agent.oob.findById(outOfBandId) 71 | 72 | if (!outOfBandRecord) throw new NotFoundError(`Out of band record with id "${outOfBandId}" not found.`) 73 | 74 | return outOfBandRecord.toJSON() 75 | } catch (error) { 76 | throw ErrorHandlingService.handle(error) 77 | } 78 | } 79 | 80 | /** 81 | * Creates an outbound out-of-band record containing out-of-band invitation message defined in 82 | * Aries RFC 0434: Out-of-Band Protocol 1.1. 83 | * @param config configuration of how out-of-band invitation should be created 84 | * @returns Out of band record 85 | */ 86 | @Example<{ 87 | invitationUrl: string 88 | invitation: OutOfBandInvitationProps 89 | outOfBandRecord: OutOfBandRecordWithInvitationProps 90 | }>({ 91 | invitationUrl: 'string', 92 | invitation: outOfBandInvitationExample, 93 | outOfBandRecord: outOfBandRecordExample, 94 | }) 95 | @Post('/create-invitation') 96 | public async createInvitation( 97 | @Body() config: CreateInvitationOptions & RecipientKeyOption // props removed because of issues with serialization 98 | ) { 99 | try { 100 | let invitationDid: string | undefined 101 | if (config?.invitationDid) { 102 | invitationDid = config?.invitationDid 103 | } else { 104 | const didRouting = await this.agent.mediationRecipient.getRouting({}) 105 | const didDocument = createPeerDidDocumentFromServices([ 106 | { 107 | id: 'didcomm', 108 | recipientKeys: [didRouting.recipientKey], 109 | routingKeys: didRouting.routingKeys, 110 | serviceEndpoint: didRouting.endpoints[0], 111 | }, 112 | ]) 113 | const did = await this.agent.dids.create({ 114 | didDocument, 115 | method: 'peer', 116 | options: { 117 | numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, 118 | }, 119 | }) 120 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 121 | invitationDid = did.didState.did 122 | } 123 | 124 | const outOfBandRecord = await this.agent.oob.createInvitation({ ...config, invitationDid }) 125 | return { 126 | invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ 127 | domain: this.agent.config.endpoints[0], 128 | }), 129 | invitation: outOfBandRecord.outOfBandInvitation.toJSON({ 130 | useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, 131 | }), 132 | outOfBandRecord: outOfBandRecord.toJSON(), 133 | invitationDid: config?.invitationDid ? '' : invitationDid, 134 | } 135 | } catch (error) { 136 | throw ErrorHandlingService.handle(error) 137 | } 138 | } 139 | 140 | /** 141 | * Creates an outbound out-of-band record in the same way how `createInvitation` method does it, 142 | * but it also converts out-of-band invitation message to an "legacy" invitation message defined 143 | * in RFC 0160: Connection Protocol and returns it together with out-of-band record. 144 | * 145 | * @param config configuration of how a invitation should be created 146 | * @returns out-of-band record and invitation 147 | */ 148 | @Example<{ invitation: OutOfBandInvitationProps; outOfBandRecord: OutOfBandRecordWithInvitationProps }>({ 149 | invitation: outOfBandInvitationExample, 150 | outOfBandRecord: outOfBandRecordExample, 151 | }) 152 | @Post('/create-legacy-invitation') 153 | public async createLegacyInvitation( 154 | @Body() config?: Omit & RecipientKeyOption 155 | ) { 156 | try { 157 | let routing: Routing 158 | if (config?.recipientKey) { 159 | routing = { 160 | endpoints: this.agent.config.endpoints, 161 | routingKeys: [], 162 | recipientKey: Key.fromPublicKeyBase58(config.recipientKey, KeyType.Ed25519), 163 | mediatorId: undefined, 164 | } 165 | } else { 166 | routing = await this.agent.mediationRecipient.getRouting({}) 167 | } 168 | const { outOfBandRecord, invitation } = await this.agent.oob.createLegacyInvitation({ 169 | ...config, 170 | routing, 171 | }) 172 | return { 173 | invitationUrl: invitation.toUrl({ 174 | domain: this.agent.config.endpoints[0], 175 | useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, 176 | }), 177 | invitation: invitation.toJSON({ 178 | useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, 179 | }), 180 | outOfBandRecord: outOfBandRecord.toJSON(), 181 | ...(config?.recipientKey ? {} : { recipientKey: routing.recipientKey.publicKeyBase58 }), 182 | } 183 | } catch (error) { 184 | throw ErrorHandlingService.handle(error) 185 | } 186 | } 187 | 188 | /** 189 | * Creates a new connectionless legacy invitation. 190 | * 191 | * @param config configuration of how a connection invitation should be created 192 | * @returns a message and a invitationUrl 193 | */ 194 | @Example<{ message: AgentMessageType; invitationUrl: string }>({ 195 | message: { 196 | '@id': 'eac4ff4e-b4fb-4c1d-aef3-b29c89d1cc00', 197 | '@type': 'https://didcomm.org/connections/1.0/invitation', 198 | }, 199 | invitationUrl: 'http://example.com/invitation_url', 200 | }) 201 | @Post('/create-legacy-connectionless-invitation') 202 | public async createLegacyConnectionlessInvitation( 203 | @Body() 204 | config: { 205 | recordId: string 206 | message: AgentMessageType 207 | domain: string 208 | } 209 | ) { 210 | try { 211 | const agentMessage = JsonTransformer.fromJSON(config.message, AgentMessage) 212 | 213 | return await this.agent.oob.createLegacyConnectionlessInvitation({ 214 | ...config, 215 | message: agentMessage, 216 | }) 217 | } catch (error) { 218 | throw ErrorHandlingService.handle(error) 219 | } 220 | } 221 | 222 | /** 223 | * Creates inbound out-of-band record and assigns out-of-band invitation message to it if the 224 | * message is valid. 225 | * 226 | * @param invitation either OutOfBandInvitation or ConnectionInvitationMessage 227 | * @param config config for handling of invitation 228 | * @returns out-of-band record and connection record if one has been created. 229 | */ 230 | @Example<{ outOfBandRecord: OutOfBandRecordWithInvitationProps; connectionRecord: ConnectionRecordProps }>({ 231 | outOfBandRecord: outOfBandRecordExample, 232 | connectionRecord: ConnectionRecordExample, 233 | }) 234 | @Post('/receive-invitation') 235 | public async receiveInvitation(@Body() invitationRequest: ReceiveInvitationProps) { 236 | const { invitation, ...config } = invitationRequest 237 | 238 | try { 239 | const invite = new OutOfBandInvitation({ ...invitation, handshakeProtocols: invitation.handshake_protocols }) 240 | const { outOfBandRecord, connectionRecord } = await this.agent.oob.receiveInvitation(invite, config) 241 | 242 | return { 243 | outOfBandRecord: outOfBandRecord.toJSON(), 244 | connectionRecord: connectionRecord?.toJSON(), 245 | } 246 | } catch (error) { 247 | throw ErrorHandlingService.handle(error) 248 | } 249 | } 250 | 251 | /** 252 | * Creates inbound out-of-band record and assigns out-of-band invitation message to it if the 253 | * message is valid. 254 | * 255 | * @param invitationUrl invitation url 256 | * @param config config for handling of invitation 257 | * @returns out-of-band record and connection record if one has been created. 258 | */ 259 | @Example<{ outOfBandRecord: OutOfBandRecordWithInvitationProps; connectionRecord: ConnectionRecordProps }>({ 260 | outOfBandRecord: outOfBandRecordExample, 261 | connectionRecord: ConnectionRecordExample, 262 | }) 263 | @Post('/receive-invitation-url') 264 | public async receiveInvitationFromUrl(@Body() invitationRequest: ReceiveInvitationByUrlProps) { 265 | const { invitationUrl, ...config } = invitationRequest 266 | 267 | try { 268 | const linkSecretIds = await this.agent.modules.anoncreds.getLinkSecretIds() 269 | if (linkSecretIds.length === 0) { 270 | await this.agent.modules.anoncreds.createLinkSecret() 271 | } 272 | const { outOfBandRecord, connectionRecord } = await this.agent.oob.receiveInvitationFromUrl(invitationUrl, config) 273 | return { 274 | outOfBandRecord: outOfBandRecord.toJSON(), 275 | connectionRecord: connectionRecord?.toJSON(), 276 | } 277 | } catch (error) { 278 | throw ErrorHandlingService.handle(error) 279 | } 280 | } 281 | 282 | /** 283 | * Accept a connection invitation as invitee (by sending a connection request message) for the connection with the specified connection id. 284 | * This is not needed when auto accepting of connections is enabled. 285 | */ 286 | @Example<{ outOfBandRecord: OutOfBandRecordWithInvitationProps; connectionRecord: ConnectionRecordProps }>({ 287 | outOfBandRecord: outOfBandRecordExample, 288 | connectionRecord: ConnectionRecordExample, 289 | }) 290 | @Post('/:outOfBandId/accept-invitation') 291 | public async acceptInvitation( 292 | @Path('outOfBandId') outOfBandId: RecordId, 293 | @Body() acceptInvitationConfig: AcceptInvitationConfig 294 | ) { 295 | try { 296 | const { outOfBandRecord, connectionRecord } = await this.agent.oob.acceptInvitation( 297 | outOfBandId, 298 | acceptInvitationConfig 299 | ) 300 | 301 | return { 302 | outOfBandRecord: outOfBandRecord.toJSON(), 303 | connectionRecord: connectionRecord?.toJSON(), 304 | } 305 | } catch (error) { 306 | throw ErrorHandlingService.handle(error) 307 | } 308 | } 309 | 310 | /** 311 | * Deletes an out of band record from the repository. 312 | * 313 | * @param outOfBandId Record identifier 314 | */ 315 | @Delete('/:outOfBandId') 316 | public async deleteOutOfBandRecord(@Path('outOfBandId') outOfBandId: RecordId) { 317 | try { 318 | this.setStatus(204) 319 | await this.agent.oob.deleteById(outOfBandId) 320 | } catch (error) { 321 | throw ErrorHandlingService.handle(error) 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/controllers/polygon/PolygonController.ts: -------------------------------------------------------------------------------- 1 | import type { RestAgentModules } from '../../cliAgent' 2 | import type { SchemaMetadata } from '../types' 3 | 4 | import { generateSecp256k1KeyPair } from '@ayanworks/credo-polygon-w3c-module' 5 | import { DidOperation } from '@ayanworks/credo-polygon-w3c-module/build/ledger' 6 | import { Agent } from '@credo-ts/core' 7 | import * as fs from 'fs' 8 | import { injectable } from 'tsyringe' 9 | 10 | import ErrorHandlingService from '../../errorHandlingService' 11 | import { BadRequestError, UnprocessableEntityError } from '../../errors' 12 | 13 | import { Route, Tags, Security, Controller, Post, Body, Get, Path } from 'tsoa' 14 | 15 | @Tags('Polygon') 16 | @Security('apiKey') 17 | @Route('/polygon') 18 | @injectable() 19 | export class Polygon extends Controller { 20 | private agent: Agent 21 | 22 | public constructor(agent: Agent) { 23 | super() 24 | this.agent = agent 25 | } 26 | 27 | /** 28 | * Create Secp256k1 key pair for polygon DID 29 | * 30 | * @returns Secp256k1KeyPair 31 | */ 32 | @Post('create-keys') 33 | public async createKeyPair(): Promise<{ 34 | privateKey: string 35 | publicKeyBase58: string 36 | address: string 37 | }> { 38 | try { 39 | return await generateSecp256k1KeyPair() 40 | } catch (error) { 41 | // Handle the error here 42 | throw ErrorHandlingService.handle(error) 43 | } 44 | } 45 | 46 | /** 47 | * Create polygon based W3C schema 48 | * 49 | * @returns Schema JSON 50 | */ 51 | @Post('create-schema') 52 | public async createSchema( 53 | @Body() 54 | createSchemaRequest: { 55 | did: string 56 | schemaName: string 57 | schema: { [key: string]: any } 58 | } 59 | ): Promise { 60 | try { 61 | const { did, schemaName, schema } = createSchemaRequest 62 | if (!did || !schemaName || !schema) { 63 | throw new BadRequestError('One or more parameters are empty or undefined.') 64 | } 65 | 66 | const schemaResponse = await this.agent.modules.polygon.createSchema({ 67 | did, 68 | schemaName, 69 | schema, 70 | }) 71 | if (schemaResponse.schemaState?.state === 'failed') { 72 | const reason = schemaResponse.schemaState?.reason?.toLowerCase() 73 | if (reason && reason.includes('insufficient') && reason.includes('funds')) { 74 | throw new UnprocessableEntityError( 75 | 'Insufficient funds to the address, Please add funds to perform this operation' 76 | ) 77 | } else { 78 | throw new Error(schemaResponse.schemaState?.reason) 79 | } 80 | } 81 | const schemaServerConfig = fs.readFileSync('config.json', 'utf-8') 82 | const configJson = JSON.parse(schemaServerConfig) 83 | if (!configJson.schemaFileServerURL) { 84 | throw new Error('Please provide valid schema file server URL') 85 | } 86 | 87 | if (!schemaResponse?.schemaId) { 88 | throw new BadRequestError('Invalid schema response') 89 | } 90 | const schemaPayload: SchemaMetadata = { 91 | schemaUrl: configJson.schemaFileServerURL + schemaResponse?.schemaId, 92 | did: schemaResponse?.did, 93 | schemaId: schemaResponse?.schemaId, 94 | schemaTxnHash: schemaResponse?.resourceTxnHash, 95 | } 96 | return schemaPayload 97 | } catch (error) { 98 | throw ErrorHandlingService.handle(error) 99 | } 100 | } 101 | 102 | /** 103 | * Estimate transaction 104 | * 105 | * @returns Transaction Object 106 | */ 107 | @Post('estimate-transaction') 108 | public async estimateTransaction( 109 | @Body() 110 | estimateTransactionRequest: { 111 | operation: any 112 | transaction: any 113 | } 114 | ): Promise { 115 | try { 116 | const { operation } = estimateTransactionRequest 117 | 118 | if (!(operation in DidOperation)) { 119 | throw new BadRequestError('Invalid method parameter!') 120 | } 121 | if (operation === DidOperation.Create) { 122 | return this.agent.modules.polygon.estimateFeeForDidOperation({ operation }) 123 | } else if (operation === DidOperation.Update) { 124 | return this.agent.modules.polygon.estimateFeeForDidOperation({ operation }) 125 | } 126 | } catch (error) { 127 | throw ErrorHandlingService.handle(error) 128 | } 129 | } 130 | 131 | /** 132 | * Fetch schema details 133 | * 134 | * @returns Schema Object 135 | */ 136 | @Get(':did/:schemaId') 137 | public async getSchemaById(@Path('did') did: string, @Path('schemaId') schemaId: string): Promise { 138 | try { 139 | return this.agent.modules.polygon.getSchemaById(did, schemaId) 140 | } catch (error) { 141 | throw ErrorHandlingService.handle(error) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/controllers/proofs/ProofController.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AcceptProofRequestOptions, 3 | PeerDidNumAlgo2CreateOptions, 4 | ProofExchangeRecordProps, 5 | ProofsProtocolVersionType, 6 | Routing, 7 | } from '@credo-ts/core' 8 | 9 | import { Agent, PeerDidNumAlgo, createPeerDidDocumentFromServices } from '@credo-ts/core' 10 | import { injectable } from 'tsyringe' 11 | 12 | import ErrorHandlingService from '../../errorHandlingService' 13 | import { ProofRecordExample, RecordId } from '../examples' 14 | import { 15 | AcceptProofProposal, 16 | CreateProofRequestOobOptions, 17 | RequestProofOptions, 18 | RequestProofProposalOptions, 19 | } from '../types' 20 | 21 | import { Body, Controller, Example, Get, Path, Post, Query, Route, Tags, Security } from 'tsoa' 22 | 23 | @Tags('Proofs') 24 | @Route('/proofs') 25 | @Security('apiKey') 26 | @injectable() 27 | export class ProofController extends Controller { 28 | private agent: Agent 29 | public constructor(agent: Agent) { 30 | super() 31 | this.agent = agent 32 | } 33 | 34 | /** 35 | * Retrieve all proof records 36 | * 37 | * @param threadId 38 | * @returns ProofRecord[] 39 | */ 40 | @Example([ProofRecordExample]) 41 | @Get('/') 42 | public async getAllProofs(@Query('threadId') threadId?: string) { 43 | try { 44 | let proofs = await this.agent.proofs.getAll() 45 | 46 | if (threadId) proofs = proofs.filter((p) => p.threadId === threadId) 47 | 48 | return proofs.map((proof) => proof.toJSON()) 49 | } catch (error) { 50 | throw ErrorHandlingService.handle(error) 51 | } 52 | } 53 | 54 | /** 55 | * Retrieve proof record by proof record id 56 | * 57 | * @param proofRecordId 58 | * @returns ProofRecord 59 | */ 60 | @Get('/:proofRecordId') 61 | @Example(ProofRecordExample) 62 | public async getProofById(@Path('proofRecordId') proofRecordId: RecordId) { 63 | try { 64 | const proof = await this.agent.proofs.getById(proofRecordId) 65 | 66 | return proof.toJSON() 67 | } catch (error) { 68 | throw ErrorHandlingService.handle(error) 69 | } 70 | } 71 | 72 | /** 73 | * Initiate a new presentation exchange as prover by sending a presentation proposal request 74 | * to the connection with the specified connection id. 75 | * 76 | * @param proposal 77 | * @returns ProofRecord 78 | */ 79 | @Post('/propose-proof') 80 | @Example(ProofRecordExample) 81 | public async proposeProof(@Body() requestProofProposalOptions: RequestProofProposalOptions) { 82 | try { 83 | const proof = await this.agent.proofs.proposeProof({ 84 | connectionId: requestProofProposalOptions.connectionId, 85 | protocolVersion: 'v1' as ProofsProtocolVersionType<[]>, 86 | proofFormats: requestProofProposalOptions.proofFormats, 87 | comment: requestProofProposalOptions.comment, 88 | autoAcceptProof: requestProofProposalOptions.autoAcceptProof, 89 | goalCode: requestProofProposalOptions.goalCode, 90 | parentThreadId: requestProofProposalOptions.parentThreadId, 91 | }) 92 | 93 | return proof 94 | } catch (error) { 95 | throw ErrorHandlingService.handle(error) 96 | } 97 | } 98 | 99 | /** 100 | * Accept a presentation proposal as verifier by sending an accept proposal message 101 | * to the connection associated with the proof record. 102 | * 103 | * @param proofRecordId 104 | * @param proposal 105 | * @returns ProofRecord 106 | */ 107 | @Post('/:proofRecordId/accept-proposal') 108 | @Example(ProofRecordExample) 109 | public async acceptProposal(@Body() acceptProposal: AcceptProofProposal) { 110 | try { 111 | const proof = await this.agent.proofs.acceptProposal(acceptProposal) 112 | 113 | return proof 114 | } catch (error) { 115 | throw ErrorHandlingService.handle(error) 116 | } 117 | } 118 | 119 | /** 120 | * Creates a presentation request bound to existing connection 121 | */ 122 | @Post('/request-proof') 123 | @Example(ProofRecordExample) 124 | public async requestProof(@Body() requestProofOptions: RequestProofOptions) { 125 | try { 126 | const requestProofPayload = { 127 | connectionId: requestProofOptions.connectionId, 128 | protocolVersion: requestProofOptions.protocolVersion as ProofsProtocolVersionType<[]>, 129 | comment: requestProofOptions.comment, 130 | proofFormats: requestProofOptions.proofFormats, 131 | autoAcceptProof: requestProofOptions.autoAcceptProof, 132 | goalCode: requestProofOptions.goalCode, 133 | parentThreadId: requestProofOptions.parentThreadId, 134 | willConfirm: requestProofOptions.willConfirm, 135 | } 136 | const proof = await this.agent.proofs.requestProof(requestProofPayload) 137 | 138 | return proof 139 | } catch (error) { 140 | throw ErrorHandlingService.handle(error) 141 | } 142 | } 143 | 144 | /** 145 | * Creates a presentation request not bound to any proposal or existing connection 146 | */ 147 | @Post('create-request-oob') 148 | @Example(ProofRecordExample) 149 | public async createRequest(@Body() createRequestOptions: CreateProofRequestOobOptions) { 150 | try { 151 | let routing: Routing 152 | let invitationDid: string | undefined 153 | 154 | if (createRequestOptions?.invitationDid) { 155 | invitationDid = createRequestOptions?.invitationDid 156 | } else { 157 | routing = await this.agent.mediationRecipient.getRouting({}) 158 | const didDocument = createPeerDidDocumentFromServices([ 159 | { 160 | id: 'didcomm', 161 | recipientKeys: [routing.recipientKey], 162 | routingKeys: routing.routingKeys, 163 | serviceEndpoint: routing.endpoints[0], 164 | }, 165 | ]) 166 | const did = await this.agent.dids.create({ 167 | didDocument, 168 | method: 'peer', 169 | options: { 170 | numAlgo: PeerDidNumAlgo.MultipleInceptionKeyWithoutDoc, 171 | }, 172 | }) 173 | invitationDid = did.didState.did 174 | } 175 | 176 | const proof = await this.agent.proofs.createRequest({ 177 | protocolVersion: createRequestOptions.protocolVersion as ProofsProtocolVersionType<[]>, 178 | proofFormats: createRequestOptions.proofFormats, 179 | goalCode: createRequestOptions.goalCode, 180 | willConfirm: createRequestOptions.willConfirm, 181 | parentThreadId: createRequestOptions.parentThreadId, 182 | autoAcceptProof: createRequestOptions.autoAcceptProof, 183 | comment: createRequestOptions.comment, 184 | }) 185 | const proofMessage = proof.message 186 | const outOfBandRecord = await this.agent.oob.createInvitation({ 187 | label: createRequestOptions.label, 188 | messages: [proofMessage], 189 | autoAcceptConnection: true, 190 | imageUrl: createRequestOptions?.imageUrl, 191 | goalCode: createRequestOptions?.goalCode, 192 | invitationDid, 193 | }) 194 | 195 | return { 196 | invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ 197 | domain: this.agent.config.endpoints[0], 198 | }), 199 | invitation: outOfBandRecord.outOfBandInvitation.toJSON({ 200 | useDidSovPrefixWhereAllowed: this.agent.config.useDidSovPrefixWhereAllowed, 201 | }), 202 | outOfBandRecord: outOfBandRecord.toJSON(), 203 | invitationDid: createRequestOptions?.invitationDid ? '' : invitationDid, 204 | proofRecordThId: proof.proofRecord.threadId, 205 | } 206 | } catch (error) { 207 | throw ErrorHandlingService.handle(error) 208 | } 209 | } 210 | 211 | /** 212 | * Accept a presentation request as prover by sending an accept request message 213 | * to the connection associated with the proof record. 214 | * 215 | * @param proofRecordId 216 | * @param request 217 | * @returns ProofRecord 218 | */ 219 | @Post('/:proofRecordId/accept-request') 220 | @Example(ProofRecordExample) 221 | public async acceptRequest( 222 | @Path('proofRecordId') proofRecordId: string, 223 | @Body() 224 | request: { 225 | filterByPresentationPreview?: boolean 226 | filterByNonRevocationRequirements?: boolean 227 | comment?: string 228 | } 229 | ) { 230 | try { 231 | const requestedCredentials = await this.agent.proofs.selectCredentialsForRequest({ 232 | proofRecordId, 233 | }) 234 | 235 | const acceptProofRequest: AcceptProofRequestOptions = { 236 | proofRecordId, 237 | comment: request.comment, 238 | proofFormats: requestedCredentials.proofFormats, 239 | } 240 | 241 | const proof = await this.agent.proofs.acceptRequest(acceptProofRequest) 242 | 243 | return proof.toJSON() 244 | } catch (error) { 245 | throw ErrorHandlingService.handle(error) 246 | } 247 | } 248 | 249 | /** 250 | * Accept a presentation as prover by sending an accept presentation message 251 | * to the connection associated with the proof record. 252 | * 253 | * @param proofRecordId 254 | * @returns ProofRecord 255 | */ 256 | @Post('/:proofRecordId/accept-presentation') 257 | @Example(ProofRecordExample) 258 | public async acceptPresentation(@Path('proofRecordId') proofRecordId: string) { 259 | try { 260 | const proof = await this.agent.proofs.acceptPresentation({ proofRecordId }) 261 | return proof 262 | } catch (error) { 263 | throw ErrorHandlingService.handle(error) 264 | } 265 | } 266 | 267 | /** 268 | * Return proofRecord 269 | * 270 | * @param proofRecordId 271 | * @returns ProofRecord 272 | */ 273 | @Get('/:proofRecordId/form-data') 274 | @Example(ProofRecordExample) 275 | // TODO: Add return type 276 | public async proofFormData(@Path('proofRecordId') proofRecordId: string): Promise { 277 | try { 278 | const proof = await this.agent.proofs.getFormatData(proofRecordId) 279 | return proof 280 | } catch (error) { 281 | throw ErrorHandlingService.handle(error) 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/controllers/question-answer/QuestionAnswerController.ts: -------------------------------------------------------------------------------- 1 | import type { RestAgentModules } from '../../cliAgent' 2 | import type { ValidResponse } from '@credo-ts/question-answer' 3 | 4 | import { Agent } from '@credo-ts/core' 5 | import { QuestionAnswerRecord, QuestionAnswerRole, QuestionAnswerState } from '@credo-ts/question-answer' 6 | import { injectable } from 'tsyringe' 7 | 8 | import ErrorHandlingService from '../../errorHandlingService' 9 | import { NotFoundError } from '../../errors' 10 | import { RecordId } from '../examples' 11 | 12 | import { Body, Controller, Get, Path, Post, Route, Tags, Query, Security, Example } from 'tsoa' 13 | 14 | @Tags('Question Answer') 15 | @Route('/question-answer') 16 | @Security('apiKey') 17 | @injectable() 18 | export class QuestionAnswerController extends Controller { 19 | private agent: Agent 20 | 21 | public constructor(agent: Agent) { 22 | super() 23 | this.agent = agent 24 | } 25 | 26 | /** 27 | * Retrieve question and answer records by query 28 | * 29 | * @param connectionId Connection identifier 30 | * @param role Role of the question 31 | * @param state State of the question 32 | * @param threadId Thread identifier 33 | * @returns QuestionAnswerRecord[] 34 | */ 35 | @Get('/') 36 | public async getQuestionAnswerRecords( 37 | @Query('connectionId') connectionId?: string, 38 | @Query('role') role?: QuestionAnswerRole, 39 | @Query('state') state?: QuestionAnswerState, 40 | @Query('threadId') threadId?: string 41 | ) { 42 | try { 43 | const questionAnswerRecords = await this.agent.modules.questionAnswer.findAllByQuery({ 44 | connectionId, 45 | role, 46 | state, 47 | threadId, 48 | }) 49 | return questionAnswerRecords.map((record) => record.toJSON()) 50 | } catch (error) { 51 | throw ErrorHandlingService.handle(error) 52 | } 53 | } 54 | 55 | /** 56 | * Send a question to a connection 57 | * 58 | * @param connectionId Connection identifier 59 | * @param content The content of the message 60 | */ 61 | @Example(QuestionAnswerRecord) 62 | @Post('question/:connectionId') 63 | public async sendQuestion( 64 | @Path('connectionId') connectionId: RecordId, 65 | @Body() 66 | config: { 67 | question: string 68 | validResponses: ValidResponse[] 69 | detail?: string 70 | } 71 | ) { 72 | try { 73 | const { question, validResponses, detail } = config 74 | 75 | const record = await this.agent.modules.questionAnswer.sendQuestion(connectionId, { 76 | question, 77 | validResponses, 78 | detail, 79 | }) 80 | 81 | return record.toJSON() 82 | } catch (error) { 83 | throw ErrorHandlingService.handle(error) 84 | } 85 | } 86 | 87 | /** 88 | * Send a answer to question 89 | * 90 | * @param id The id of the question answer record 91 | * @param response The response of the question 92 | */ 93 | @Post('answer/:id') 94 | public async sendAnswer(@Path('id') id: RecordId, @Body() request: Record<'response', string>) { 95 | try { 96 | const record = await this.agent.modules.questionAnswer.sendAnswer(id, request.response) 97 | return record.toJSON() 98 | } catch (error) { 99 | throw ErrorHandlingService.handle(error) 100 | } 101 | } 102 | 103 | /** 104 | * Retrieve question answer record by id 105 | * @param connectionId Connection identifier 106 | * @returns ConnectionRecord 107 | */ 108 | @Get('/:id') 109 | public async getQuestionAnswerRecordById(@Path('id') id: RecordId) { 110 | try { 111 | const record = await this.agent.modules.questionAnswer.findById(id) 112 | 113 | if (!record) throw new NotFoundError(`Question Answer Record with id "${id}" not found.`) 114 | 115 | return record.toJSON() 116 | } catch (error) { 117 | throw ErrorHandlingService.handle(error) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/controllers/types.ts: -------------------------------------------------------------------------------- 1 | import type { RecordId, Version } from './examples' 2 | import type { CustomHandshakeProtocol } from '../enums/enum' 3 | import type { AnonCredsCredentialFormat, LegacyIndyCredentialFormat } from '@credo-ts/anoncreds' 4 | import type { 5 | AutoAcceptCredential, 6 | AutoAcceptProof, 7 | CredentialFormatPayload, 8 | HandshakeProtocol, 9 | ReceiveOutOfBandInvitationConfig, 10 | OutOfBandDidCommService, 11 | DidResolutionMetadata, 12 | DidDocumentMetadata, 13 | ProofExchangeRecord, 14 | ProofFormat, 15 | DidRegistrationExtraOptions, 16 | DidDocument, 17 | DidRegistrationSecretOptions, 18 | InitConfig, 19 | WalletConfig, 20 | CredentialExchangeRecord, 21 | DidResolutionOptions, 22 | JsonCredential, 23 | AgentMessage, 24 | Routing, 25 | Attachment, 26 | KeyType, 27 | JsonLdCredentialFormat, 28 | } from '@credo-ts/core' 29 | import type { DIDDocument } from 'did-resolver' 30 | 31 | export type TenantConfig = Pick & { 32 | walletConfig: Pick 33 | } 34 | 35 | export interface AgentInfo { 36 | label: string 37 | endpoints: string[] 38 | isInitialized: boolean 39 | publicDid: void 40 | // publicDid?: { 41 | // did: string 42 | // verkey: string 43 | // } 44 | } 45 | 46 | export interface AgentMessageType { 47 | '@id': string 48 | '@type': string 49 | [key: string]: unknown 50 | } 51 | 52 | export interface DidResolutionResultProps { 53 | didResolutionMetadata: DidResolutionMetadata 54 | didDocument: DIDDocument | null 55 | didDocumentMetadata: DidDocumentMetadata 56 | } 57 | 58 | export interface ProofRequestMessageResponse { 59 | message: string 60 | proofRecord: ProofExchangeRecord 61 | } 62 | 63 | // type CredentialFormats = [CredentialFormat] 64 | type CredentialFormats = [LegacyIndyCredentialFormat, AnonCredsCredentialFormat, JsonLdCredentialFormat] 65 | 66 | enum ProtocolVersion { 67 | v1 = 'v1', 68 | v2 = 'v2', 69 | } 70 | export interface ProposeCredentialOptions { 71 | protocolVersion: ProtocolVersion 72 | credentialFormats: CredentialFormatPayload 73 | autoAcceptCredential?: AutoAcceptCredential 74 | comment?: string 75 | connectionId: RecordId 76 | } 77 | 78 | // export interface ProposeCredentialOptions extends BaseOptions { 79 | // connectionId: string 80 | // protocolVersion: CredentialProtocolVersionType 81 | // credentialFormats: CredentialFormatPayload, 'createProposal'> 82 | // } 83 | 84 | export interface AcceptCredentialProposalOptions { 85 | credentialRecordId: string 86 | credentialFormats?: CredentialFormatPayload 87 | autoAcceptCredential?: AutoAcceptCredential 88 | comment?: string 89 | } 90 | 91 | export interface CreateOfferOptions { 92 | protocolVersion: ProtocolVersion 93 | connectionId: RecordId 94 | credentialFormats: CredentialFormatPayload 95 | autoAcceptCredential?: AutoAcceptCredential 96 | comment?: string 97 | } 98 | 99 | type CredentialFormatType = LegacyIndyCredentialFormat | JsonLdCredentialFormat | AnonCredsCredentialFormat 100 | 101 | export interface CreateOfferOobOptions { 102 | protocolVersion: string 103 | credentialFormats: CredentialFormatPayload 104 | autoAcceptCredential?: AutoAcceptCredential 105 | comment?: string 106 | goalCode?: string 107 | parentThreadId?: string 108 | willConfirm?: boolean 109 | label?: string 110 | imageUrl?: string 111 | recipientKey?: string 112 | invitationDid?: string 113 | } 114 | export interface CredentialCreateOfferOptions { 115 | credentialRecord: CredentialExchangeRecord 116 | credentialFormats: JsonCredential 117 | options: any 118 | attachmentId?: string 119 | } 120 | 121 | export interface CreateProofRequestOobOptions { 122 | protocolVersion: string 123 | proofFormats: any 124 | goalCode?: string 125 | parentThreadId?: string 126 | willConfirm?: boolean 127 | autoAcceptProof?: AutoAcceptProof 128 | comment?: string 129 | label?: string 130 | imageUrl?: string 131 | recipientKey?: string 132 | invitationDid?: string 133 | } 134 | 135 | export interface OfferCredentialOptions { 136 | credentialFormats: { 137 | indy: { 138 | credentialDefinitionId: string 139 | attributes: { 140 | name: string 141 | value: string 142 | }[] 143 | } 144 | } 145 | autoAcceptCredential?: AutoAcceptCredential 146 | comment?: string 147 | connectionId: string 148 | } 149 | 150 | export interface V2OfferCredentialOptions { 151 | protocolVersion: string 152 | connectionId: string 153 | credentialFormats: { 154 | indy: { 155 | credentialDefinitionId: string 156 | attributes: { 157 | name: string 158 | value: string 159 | }[] 160 | } 161 | } 162 | autoAcceptCredential: string 163 | } 164 | 165 | export interface AcceptCredential { 166 | credentialRecordId: RecordId 167 | } 168 | 169 | export interface CredentialOfferOptions { 170 | credentialRecordId: RecordId 171 | credentialFormats?: CredentialFormatPayload 172 | autoAcceptCredential?: AutoAcceptCredential 173 | comment?: string 174 | } 175 | 176 | export interface AcceptCredentialRequestOptions { 177 | credentialRecordId: RecordId 178 | credentialFormats?: CredentialFormatPayload 179 | autoAcceptCredential?: AutoAcceptCredential 180 | comment?: string 181 | } 182 | 183 | type ReceiveOutOfBandInvitationProps = Omit 184 | 185 | export interface ReceiveInvitationProps extends ReceiveOutOfBandInvitationProps { 186 | invitation: OutOfBandInvitationSchema 187 | } 188 | 189 | export interface ReceiveInvitationByUrlProps extends ReceiveOutOfBandInvitationProps { 190 | invitationUrl: string 191 | } 192 | 193 | export interface AcceptInvitationConfig { 194 | autoAcceptConnection?: boolean 195 | reuseConnection?: boolean 196 | label?: string 197 | alias?: string 198 | imageUrl?: string 199 | mediatorId?: string 200 | } 201 | 202 | export interface OutOfBandInvitationSchema { 203 | '@id'?: string 204 | '@type': string 205 | label: string 206 | goalCode?: string 207 | goal?: string 208 | accept?: string[] 209 | handshake_protocols?: CustomHandshakeProtocol[] 210 | services: Array 211 | imageUrl?: string 212 | } 213 | 214 | export interface ConnectionInvitationSchema { 215 | id?: string 216 | '@type': string 217 | label: string 218 | did?: string 219 | recipientKeys?: string[] 220 | serviceEndpoint?: string 221 | routingKeys?: string[] 222 | imageUrl?: string 223 | } 224 | 225 | // TODO: added type in protocolVersion 226 | // export interface RequestProofOptions { 227 | // protocolVersion: 'v1' | 'v2' 228 | // connectionId: string 229 | // // TODO: added indy proof formate 230 | // proofFormats: ProofFormatPayload<[IndyProofFormat], 'createRequest'> 231 | // comment: string 232 | // autoAcceptProof?: AutoAcceptProof 233 | // parentThreadId?: string 234 | // } 235 | 236 | export interface RequestProofOptions { 237 | connectionId: string 238 | protocolVersion: string 239 | proofFormats: any 240 | comment: string 241 | autoAcceptProof: AutoAcceptProof 242 | goalCode?: string 243 | parentThreadId?: string 244 | willConfirm?: boolean 245 | } 246 | 247 | // TODO: added type in protocolVersion 248 | export interface RequestProofProposalOptions { 249 | connectionId: string 250 | proofFormats: { 251 | formats: ProofFormat[] 252 | action: 'createProposal' 253 | } 254 | goalCode?: string 255 | parentThreadId?: string 256 | autoAcceptProof?: AutoAcceptProof 257 | comment?: string 258 | } 259 | 260 | export interface AcceptProofProposal { 261 | proofRecordId: string 262 | proofFormats: { 263 | formats: ProofFormat[] 264 | action: 'acceptProposal' 265 | } 266 | comment?: string 267 | autoAcceptProof?: AutoAcceptProof 268 | goalCode?: string 269 | willConfirm?: boolean 270 | } 271 | 272 | export interface GetTenantAgentOptions { 273 | tenantId: string 274 | } 275 | 276 | export interface DidCreateOptions { 277 | method?: string 278 | did?: string 279 | options?: DidRegistrationExtraOptions 280 | secret?: DidRegistrationSecretOptions 281 | didDocument?: DidDocument 282 | seed?: any 283 | } 284 | 285 | export interface ResolvedDid { 286 | didUrl: string 287 | options?: DidResolutionOptions 288 | } 289 | 290 | export interface DidCreate { 291 | keyType?: KeyType 292 | seed?: string 293 | domain?: string 294 | method: string 295 | network?: string 296 | did?: string 297 | role?: string 298 | endorserDid?: string 299 | didDocument?: DidDocument 300 | privatekey?: string 301 | endpoint?: string 302 | } 303 | 304 | export interface CreateTenantOptions { 305 | config: Omit 306 | } 307 | 308 | // export type WithTenantAgentCallback = ( 309 | // tenantAgent: TenantAgent 310 | // ) => Promise 311 | 312 | export interface WithTenantAgentOptions { 313 | tenantId: string 314 | method: string 315 | payload?: any 316 | } 317 | 318 | export interface ReceiveConnectionsForTenants { 319 | tenantId: string 320 | invitationId?: string 321 | } 322 | 323 | export interface CreateInvitationOptions { 324 | label?: string 325 | alias?: string 326 | imageUrl?: string 327 | goalCode?: string 328 | goal?: string 329 | handshake?: boolean 330 | handshakeProtocols?: HandshakeProtocol[] 331 | messages?: AgentMessage[] 332 | multiUseInvitation?: boolean 333 | autoAcceptConnection?: boolean 334 | routing?: Routing 335 | appendedAttachments?: Attachment[] 336 | invitationDid?: string 337 | } 338 | 339 | //todo:Add transaction type 340 | export interface EndorserTransaction { 341 | transaction: string | Record 342 | endorserDid: string 343 | } 344 | 345 | export interface DidNymTransaction { 346 | did: string 347 | nymRequest: string 348 | } 349 | 350 | //todo:Add endorsedTransaction type 351 | export interface WriteTransaction { 352 | endorsedTransaction: string 353 | endorserDid?: string 354 | schema?: { 355 | issuerId: string 356 | name: string 357 | version: Version 358 | attributes: string[] 359 | } 360 | credentialDefinition?: { 361 | schemaId: string 362 | issuerId: string 363 | tag: string 364 | value: unknown 365 | type: string 366 | } 367 | } 368 | export interface RecipientKeyOption { 369 | recipientKey?: string 370 | } 371 | 372 | export interface CreateSchemaInput { 373 | issuerId: string 374 | name: string 375 | version: Version 376 | attributes: string[] 377 | endorse?: boolean 378 | endorserDid?: string 379 | } 380 | 381 | export interface SchemaMetadata { 382 | did: string 383 | schemaId: string 384 | schemaTxnHash?: string 385 | schemaUrl?: string 386 | } 387 | /** 388 | * @example "ea4e5e69-fc04-465a-90d2-9f8ff78aa71d" 389 | */ 390 | export type ThreadId = string 391 | -------------------------------------------------------------------------------- /src/enums/enum.ts: -------------------------------------------------------------------------------- 1 | export enum CredentialEnum { 2 | Finished = 'finished', 3 | Action = 'action', 4 | Failed = 'failed', 5 | Wait = 'wait', 6 | } 7 | 8 | export enum Role { 9 | Author = 'author', 10 | Endorser = 'endorser', 11 | } 12 | 13 | export enum DidMethod { 14 | Indy = 'indy', 15 | Key = 'key', 16 | Web = 'web', 17 | Polygon = 'polygon', 18 | Peer = 'peer', 19 | } 20 | 21 | export enum NetworkName { 22 | Bcovrin = 'bcovrin', 23 | Indicio = 'indicio', 24 | } 25 | 26 | export enum IndicioTransactionAuthorAgreement { 27 | Indicio_Testnet_Mainnet_Version = '1.0', 28 | // To do: now testnet has also moved to version 1.3 of TAA 29 | Indicio_Demonet_Version = '1.3', 30 | } 31 | 32 | export enum Network { 33 | Bcovrin_Testnet = 'bcovrin:testnet', 34 | Indicio_Testnet = 'indicio:testnet', 35 | Indicio_Demonet = 'indicio:demonet', 36 | Indicio_Mainnet = 'indicio:mainnet', 37 | } 38 | 39 | export enum NetworkTypes { 40 | Testnet = 'testnet', 41 | Demonet = 'demonet', 42 | Mainnet = 'mainnet', 43 | } 44 | 45 | export enum IndicioAcceptanceMechanism { 46 | Wallet_Agreement = 'wallet_agreement', 47 | Accept = 'accept', 48 | } 49 | 50 | export enum EndorserMode { 51 | Internal = 'internal', 52 | External = 'external', 53 | } 54 | 55 | export enum SchemaError { 56 | NotFound = 'notFound', 57 | UnSupportedAnonCredsMethod = 'unsupportedAnonCredsMethod', 58 | } 59 | 60 | export enum HttpStatusCode { 61 | OK = 200, 62 | Created = 201, 63 | BadRequest = 400, 64 | Unauthorized = 401, 65 | Forbidden = 403, 66 | NotFound = 404, 67 | InternalServerError = 500, 68 | } 69 | 70 | export declare enum CustomHandshakeProtocol { 71 | DidExchange = 'https://didcomm.org/didexchange/1.1', 72 | Connections = 'https://didcomm.org/connections/1.0', 73 | } 74 | -------------------------------------------------------------------------------- /src/errorHandlingService.ts: -------------------------------------------------------------------------------- 1 | import type { BaseError } from './errors/errors' 2 | 3 | import { AnonCredsError, AnonCredsRsError, AnonCredsStoreRecordError } from '@credo-ts/anoncreds' 4 | import { 5 | CredoError, 6 | RecordNotFoundError, 7 | RecordDuplicateError, 8 | ClassValidationError, 9 | MessageSendingError, 10 | } from '@credo-ts/core' 11 | import { IndyVdrError } from '@hyperledger/indy-vdr-nodejs' 12 | 13 | import { RecordDuplicateError as CustomRecordDuplicateError, NotFoundError, InternalServerError } from './errors/errors' 14 | import convertError from './utils/errorConverter' 15 | 16 | class ErrorHandlingService { 17 | public static handle(error: unknown) { 18 | if (error instanceof RecordDuplicateError) { 19 | throw this.handleRecordDuplicateError(error) 20 | } else if (error instanceof ClassValidationError) { 21 | throw this.handleClassValidationError(error) 22 | } else if (error instanceof MessageSendingError) { 23 | throw this.handleMessageSendingError(error) 24 | } else if (error instanceof RecordNotFoundError) { 25 | throw this.handleRecordNotFoundError(error) 26 | } else if (error instanceof AnonCredsRsError) { 27 | throw this.handleAnonCredsRsError(error) 28 | } else if (error instanceof AnonCredsStoreRecordError) { 29 | throw this.handleAnonCredsStoreRecordError(error) 30 | } else if (error instanceof IndyVdrError) { 31 | throw this.handleIndyVdrError(error) 32 | } else if (error instanceof AnonCredsError) { 33 | throw this.handleAnonCredsError(error) 34 | } else if (error instanceof CredoError) { 35 | throw this.handleCredoError(error) 36 | } else if (error instanceof Error) { 37 | throw convertError(error.constructor.name, error.message) 38 | } else { 39 | throw new InternalServerError(`An unknown error occurred ${error}`) 40 | } 41 | } 42 | private static handleIndyVdrError(error: IndyVdrError) { 43 | throw new InternalServerError(`IndyVdrError: ${error.message}`) 44 | } 45 | 46 | private static handleAnonCredsError(error: AnonCredsError): BaseError { 47 | throw new InternalServerError(`AnonCredsError: ${error.message}`) 48 | } 49 | 50 | private static handleAnonCredsRsError(error: AnonCredsRsError): BaseError { 51 | throw new InternalServerError(`AnonCredsRsError: ${error.message}`) 52 | } 53 | 54 | private static handleAnonCredsStoreRecordError(error: AnonCredsStoreRecordError): BaseError { 55 | throw new InternalServerError(`AnonCredsStoreRecordError: ${error.message}`) 56 | } 57 | 58 | private static handleCredoError(error: CredoError): BaseError { 59 | throw new InternalServerError(`CredoError: ${error.message}`) 60 | } 61 | 62 | private static handleRecordNotFoundError(error: RecordNotFoundError): BaseError { 63 | throw new NotFoundError(error.message) 64 | } 65 | 66 | private static handleRecordDuplicateError(error: RecordDuplicateError): BaseError { 67 | throw new CustomRecordDuplicateError(error.message) 68 | } 69 | 70 | private static handleClassValidationError(error: ClassValidationError): BaseError { 71 | throw new InternalServerError(`ClassValidationError: ${error.message}`) 72 | } 73 | 74 | private static handleMessageSendingError(error: MessageSendingError): BaseError { 75 | throw new InternalServerError(`MessageSendingError: ${error.message}`) 76 | } 77 | } 78 | 79 | export default ErrorHandlingService 80 | -------------------------------------------------------------------------------- /src/errorMessages.ts: -------------------------------------------------------------------------------- 1 | export const LEDGER_NOT_FOUND = 'ledger not found' 2 | export const LEDGER_INVALID_TRANSACTION = 'transaction with an invalid schema id' 3 | export const COMMON_INVALID_STRUCTURE = 'schemaId has invalid structure' 4 | export const ENDORSER_DID_NOT_PRESENT = 'Please provide the endorser DID' 5 | -------------------------------------------------------------------------------- /src/errors/ApiError.ts: -------------------------------------------------------------------------------- 1 | export interface ApiError { 2 | message: string 3 | details?: unknown 4 | } 5 | -------------------------------------------------------------------------------- /src/errors/errors.ts: -------------------------------------------------------------------------------- 1 | class BaseError extends Error { 2 | public statusCode: number 3 | 4 | public constructor(message: string, statusCode: number) { 5 | super(message) 6 | this.name = this.constructor.name 7 | this.statusCode = statusCode 8 | Error.captureStackTrace(this, this.constructor) 9 | } 10 | } 11 | 12 | class InternalServerError extends BaseError { 13 | public constructor(message: string = 'Internal Server Error') { 14 | super(message, 500) 15 | } 16 | } 17 | 18 | class NotFoundError extends BaseError { 19 | public constructor(message: string = 'Not Found') { 20 | super(message, 404) 21 | } 22 | } 23 | 24 | class BadRequestError extends BaseError { 25 | public constructor(message: string = 'Bad Request') { 26 | super(message, 400) 27 | } 28 | } 29 | 30 | class UnauthorizedError extends BaseError { 31 | public constructor(message: string = 'Unauthorized') { 32 | super(message, 401) 33 | } 34 | } 35 | 36 | class PaymentRequiredError extends BaseError { 37 | public constructor(message: string = 'Payment Required') { 38 | super(message, 402) 39 | } 40 | } 41 | 42 | class ForbiddenError extends BaseError { 43 | public constructor(message: string = 'Forbidden') { 44 | super(message, 403) 45 | } 46 | } 47 | 48 | class ConflictError extends BaseError { 49 | public constructor(message: string = 'Conflict') { 50 | super(message, 409) 51 | } 52 | } 53 | 54 | class UnprocessableEntityError extends BaseError { 55 | public constructor(message: string = 'Unprocessable Entity') { 56 | super(message, 422) 57 | } 58 | } 59 | 60 | class LedgerNotFoundError extends NotFoundError { 61 | public constructor(message: string = 'Ledger Not Found') { 62 | super(message) 63 | } 64 | } 65 | 66 | class LedgerInvalidTransactionError extends BadRequestError { 67 | public constructor(message: string = 'Ledger Invalid Transaction') { 68 | super(message) 69 | } 70 | } 71 | 72 | class CommonInvalidStructureError extends BadRequestError { 73 | public constructor(message: string = 'Common Invalid Structure') { 74 | super(message) 75 | } 76 | } 77 | 78 | class RecordDuplicateError extends ConflictError { 79 | public constructor(message: string = 'RecordDuplicateError') { 80 | super(message) 81 | } 82 | } 83 | 84 | const errorMap: Record BaseError> = { 85 | InternalServerError, 86 | NotFoundError, 87 | BadRequestError, 88 | LedgerNotFoundError, 89 | LedgerInvalidTransactionError, 90 | CommonInvalidStructureError, 91 | UnauthorizedError, 92 | PaymentRequiredError, 93 | ForbiddenError, 94 | ConflictError, 95 | RecordDuplicateError, 96 | UnprocessableEntityError, 97 | } 98 | 99 | export { 100 | InternalServerError, 101 | NotFoundError, 102 | BadRequestError, 103 | LedgerNotFoundError, 104 | LedgerInvalidTransactionError, 105 | CommonInvalidStructureError, 106 | UnauthorizedError, 107 | PaymentRequiredError, 108 | ForbiddenError, 109 | ConflictError, 110 | BaseError, 111 | RecordDuplicateError, 112 | UnprocessableEntityError, 113 | errorMap, 114 | } 115 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors' 2 | -------------------------------------------------------------------------------- /src/events/BasicMessageEvents.ts: -------------------------------------------------------------------------------- 1 | import type { ServerConfig } from '../utils/ServerConfig' 2 | import type { Agent, BasicMessageStateChangedEvent } from '@credo-ts/core' 3 | 4 | import { BasicMessageEventTypes } from '@credo-ts/core' 5 | 6 | import { sendWebSocketEvent } from './WebSocketEvents' 7 | import { sendWebhookEvent } from './WebhookEvent' 8 | 9 | export const basicMessageEvents = async (agent: Agent, config: ServerConfig) => { 10 | agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, async (event: BasicMessageStateChangedEvent) => { 11 | const record = event.payload.basicMessageRecord 12 | const body = record.toJSON() 13 | 14 | // Only send webhook if webhook url is configured 15 | if (config.webhookUrl) { 16 | await sendWebhookEvent(config.webhookUrl + '/basic-messages', body, agent.config.logger) 17 | } 18 | 19 | if (config.socketServer) { 20 | // Always emit websocket event to clients (could be 0) 21 | sendWebSocketEvent(config.socketServer, { 22 | ...event, 23 | payload: { 24 | message: event.payload.message.toJSON(), 25 | basicMessageRecord: body, 26 | }, 27 | }) 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/events/ConnectionEvents.ts: -------------------------------------------------------------------------------- 1 | import type { ServerConfig } from '../utils/ServerConfig' 2 | import type { Agent, ConnectionStateChangedEvent } from '@credo-ts/core' 3 | 4 | import { ConnectionEventTypes } from '@credo-ts/core' 5 | 6 | import { sendWebSocketEvent } from './WebSocketEvents' 7 | import { sendWebhookEvent } from './WebhookEvent' 8 | 9 | export const connectionEvents = async (agent: Agent, config: ServerConfig) => { 10 | agent.events.on(ConnectionEventTypes.ConnectionStateChanged, async (event: ConnectionStateChangedEvent) => { 11 | const record = event.payload.connectionRecord 12 | const body = { ...record.toJSON(), ...event.metadata } 13 | 14 | // Only send webhook if webhook url is configured 15 | if (config.webhookUrl) { 16 | await sendWebhookEvent(config.webhookUrl + '/connections', body, agent.config.logger) 17 | } 18 | 19 | if (config.socketServer) { 20 | // Always emit websocket event to clients (could be 0) 21 | sendWebSocketEvent(config.socketServer, { 22 | ...event, 23 | payload: { 24 | ...event.payload, 25 | connectionRecord: body, 26 | }, 27 | }) 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/events/CredentialEvents.ts: -------------------------------------------------------------------------------- 1 | import type { RestMultiTenantAgentModules } from '../cliAgent' 2 | import type { ServerConfig } from '../utils/ServerConfig' 3 | import type { Agent, CredentialStateChangedEvent } from '@credo-ts/core' 4 | 5 | import { CredentialEventTypes } from '@credo-ts/core' 6 | 7 | import { sendWebSocketEvent } from './WebSocketEvents' 8 | import { sendWebhookEvent } from './WebhookEvent' 9 | 10 | export const credentialEvents = async (agent: Agent, config: ServerConfig) => { 11 | agent.events.on(CredentialEventTypes.CredentialStateChanged, async (event: CredentialStateChangedEvent) => { 12 | const record = event.payload.credentialRecord 13 | 14 | const body: Record = { 15 | ...record.toJSON(), 16 | ...event.metadata, 17 | outOfBandId: null, 18 | credentialData: null, 19 | } 20 | 21 | if (event.metadata.contextCorrelationId !== 'default') { 22 | await agent.modules.tenants.withTenantAgent( 23 | { tenantId: event.metadata.contextCorrelationId }, 24 | async (tenantAgent) => { 25 | if (record?.connectionId) { 26 | const connectionRecord = await tenantAgent.connections.findById(record.connectionId!) 27 | body.outOfBandId = connectionRecord?.outOfBandId 28 | } 29 | const data = await tenantAgent.credentials.getFormatData(record.id) 30 | body.credentialData = data 31 | } 32 | ) 33 | } 34 | 35 | if (event.metadata.contextCorrelationId === 'default') { 36 | if (record?.connectionId) { 37 | const connectionRecord = await agent.connections.findById(record.connectionId!) 38 | body.outOfBandId = connectionRecord?.outOfBandId 39 | } 40 | 41 | const data = await agent.credentials.getFormatData(record.id) 42 | body.credentialData = data 43 | } 44 | // Only send webhook if webhook url is configured 45 | if (config.webhookUrl) { 46 | await sendWebhookEvent(config.webhookUrl + '/credentials', body, agent.config.logger) 47 | } 48 | 49 | if (config.socketServer) { 50 | // Always emit websocket event to clients (could be 0) 51 | sendWebSocketEvent(config.socketServer, { 52 | ...event, 53 | payload: { 54 | ...event.payload, 55 | credentialRecord: body, 56 | }, 57 | }) 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /src/events/ProofEvents.ts: -------------------------------------------------------------------------------- 1 | import type { ServerConfig } from '../utils/ServerConfig' 2 | import type { Agent, ProofStateChangedEvent } from '@credo-ts/core' 3 | 4 | import { ProofEventTypes } from '@credo-ts/core' 5 | 6 | import { sendWebSocketEvent } from './WebSocketEvents' 7 | import { sendWebhookEvent } from './WebhookEvent' 8 | 9 | export const proofEvents = async (agent: Agent, config: ServerConfig) => { 10 | agent.events.on(ProofEventTypes.ProofStateChanged, async (event: ProofStateChangedEvent) => { 11 | const record = event.payload.proofRecord 12 | const body = { ...record.toJSON(), ...event.metadata } as { proofData?: any } 13 | if (event.metadata.contextCorrelationId !== 'default') { 14 | const tenantAgent = await agent.modules.tenants.getTenantAgent({ 15 | tenantId: event.metadata.contextCorrelationId, 16 | }) 17 | const data = await tenantAgent.proofs.getFormatData(record.id) 18 | body.proofData = data 19 | } 20 | 21 | //Emit webhook for dedicated agent 22 | if (event.metadata.contextCorrelationId === 'default') { 23 | const data = await agent.proofs.getFormatData(record.id) 24 | body.proofData = data 25 | } 26 | 27 | // Only send webhook if webhook url is configured 28 | if (config.webhookUrl) { 29 | await sendWebhookEvent(config.webhookUrl + '/proofs', body, agent.config.logger) 30 | } 31 | 32 | if (config.socketServer) { 33 | // Always emit websocket event to clients (could be 0) 34 | sendWebSocketEvent(config.socketServer, { 35 | ...event, 36 | payload: { 37 | ...event.payload, 38 | proofRecord: body, 39 | }, 40 | }) 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /src/events/QuestionAnswerEvents.ts: -------------------------------------------------------------------------------- 1 | import type { ServerConfig } from '../utils/ServerConfig' 2 | import type { Agent } from '@credo-ts/core' 3 | import type { QuestionAnswerStateChangedEvent } from '@credo-ts/question-answer' 4 | 5 | import { QuestionAnswerEventTypes } from '@credo-ts/question-answer' 6 | 7 | import { sendWebSocketEvent } from './WebSocketEvents' 8 | import { sendWebhookEvent } from './WebhookEvent' 9 | 10 | export const questionAnswerEvents = async (agent: Agent, config: ServerConfig) => { 11 | agent.events.on( 12 | QuestionAnswerEventTypes.QuestionAnswerStateChanged, 13 | async (event: QuestionAnswerStateChangedEvent) => { 14 | const record = event.payload.questionAnswerRecord 15 | const body = { ...record.toJSON(), ...event.metadata } 16 | 17 | // Only send webhook if webhook url is configured 18 | if (config.webhookUrl) { 19 | await sendWebhookEvent(config.webhookUrl + '/question-answer', body, agent.config.logger) 20 | } 21 | 22 | if (config.socketServer) { 23 | // Always emit websocket event to clients (could be 0) 24 | sendWebSocketEvent(config.socketServer, { 25 | ...event, 26 | payload: { 27 | message: event.payload.questionAnswerRecord.toJSON(), 28 | questionAnswerRecord: body, 29 | }, 30 | }) 31 | } 32 | } 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/events/ReuseConnectionEvents.ts: -------------------------------------------------------------------------------- 1 | import type { ServerConfig } from '../utils/ServerConfig' 2 | import type { Agent, HandshakeReusedEvent } from '@credo-ts/core' 3 | 4 | import { OutOfBandEventTypes } from '@credo-ts/core' 5 | 6 | import { sendWebSocketEvent } from './WebSocketEvents' 7 | import { sendWebhookEvent } from './WebhookEvent' 8 | 9 | export const reuseConnectionEvents = async (agent: Agent, config: ServerConfig) => { 10 | agent.events.on(OutOfBandEventTypes.HandshakeReused, async (event: HandshakeReusedEvent) => { 11 | const body = { 12 | ...event.payload.connectionRecord.toJSON(), 13 | outOfBandRecord: event.payload.outOfBandRecord.toJSON(), 14 | reuseThreadId: event.payload.reuseThreadId, 15 | ...event.metadata, 16 | } 17 | 18 | // Only send webhook if webhook url is configured 19 | if (config.webhookUrl) { 20 | await sendWebhookEvent(config.webhookUrl + '/connections', body, agent.config.logger) 21 | } 22 | 23 | if (config.socketServer) { 24 | // Always emit websocket event to clients (could be 0) 25 | sendWebSocketEvent(config.socketServer, { 26 | ...event, 27 | payload: { 28 | ...event.payload, 29 | connectionRecord: body, 30 | }, 31 | }) 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/events/WebSocketEvents.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | 3 | export const sendWebSocketEvent = async (server: WebSocket.Server, data: unknown) => { 4 | server.clients.forEach((client) => { 5 | if (client.readyState === WebSocket.OPEN) { 6 | typeof data === 'string' ? client.send(data) : client.send(JSON.stringify(data)) 7 | } 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/events/WebhookEvent.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from '@credo-ts/core' 2 | 3 | import fetch from 'node-fetch' 4 | 5 | export const sendWebhookEvent = async (webhookUrl: string, body: Record, logger: Logger) => { 6 | try { 7 | await fetch(webhookUrl, { 8 | method: 'POST', 9 | body: JSON.stringify(body), 10 | headers: { 'Content-Type': 'application/json' }, 11 | }) 12 | } catch (error) { 13 | logger.error(`Error sending ${body.type} webhook event to ${webhookUrl}`, { 14 | cause: error, 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | 3 | import type { ServerConfig } from './utils/ServerConfig' 4 | import type { Agent } from '@credo-ts/core' 5 | import type { Socket } from 'net' 6 | 7 | import { Server } from 'ws' 8 | 9 | import { setupServer } from './server' 10 | 11 | export const startServer = async (agent: Agent, config: ServerConfig) => { 12 | const socketServer = config.socketServer ?? new Server({ noServer: true }) 13 | const app = await setupServer(agent, { ...config, socketServer }) 14 | const server = app.listen(config.port) 15 | 16 | // If no socket server is provided, we will use the existing http server 17 | // to also host the websocket server 18 | if (!config.socketServer) { 19 | server.on('upgrade', (request, socket, head) => { 20 | // eslint-disable-next-line @typescript-eslint/no-empty-function 21 | socketServer.handleUpgrade(request, socket as Socket, head, () => {}) 22 | }) 23 | } 24 | 25 | return server 26 | } 27 | -------------------------------------------------------------------------------- /src/securityMiddleware.ts: -------------------------------------------------------------------------------- 1 | import type * as express from 'express' 2 | import type { NextFunction } from 'express' 3 | 4 | import { Middlewares } from '@tsoa/runtime' 5 | 6 | import { expressAuthentication } from './authentication' // Import your authentication function 7 | 8 | @Middlewares() 9 | export class SecurityMiddleware { 10 | public async use(request: express.Request, response: express.Response, next: NextFunction) { 11 | try { 12 | const securityName = 'apiKey' 13 | 14 | // Extract route path or controller name from the request 15 | const routePath = request.path 16 | const requestMethod = request.method 17 | 18 | // List of paths for which authentication should be skipped 19 | const pathsToSkipAuthentication = [ 20 | { path: '/url/', method: 'GET' }, 21 | { path: '/multi-tenancy/url/', method: 'GET' }, 22 | { path: '/agent', method: 'GET' }, 23 | ] 24 | 25 | // Check if authentication should be skipped for this route or controller 26 | const skipAuthentication = pathsToSkipAuthentication.some( 27 | ({ path, method }) => routePath.includes(path) && requestMethod === method 28 | ) 29 | 30 | if (skipAuthentication) { 31 | // Skip authentication for this route or controller 32 | next() 33 | } else if (securityName) { 34 | const result = await expressAuthentication(request, securityName) 35 | 36 | if (result === 'success') { 37 | next() 38 | } else { 39 | response.status(401).json({ message: 'Unauthorized' }) 40 | } 41 | } else { 42 | response.status(400).json({ message: 'Bad Request' }) 43 | } 44 | } catch (error) { 45 | next(error) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import type { ServerConfig } from './utils/ServerConfig' 3 | import type { Response as ExResponse, Request as ExRequest, NextFunction } from 'express' 4 | 5 | import { Agent } from '@credo-ts/core' 6 | import bodyParser from 'body-parser' 7 | import cors from 'cors' 8 | import dotenv from 'dotenv' 9 | import express from 'express' 10 | import { rateLimit } from 'express-rate-limit' 11 | import * as fs from 'fs' 12 | import { serve, generateHTML } from 'swagger-ui-express' 13 | import { container } from 'tsyringe' 14 | 15 | import { setDynamicApiKey } from './authentication' 16 | import { BaseError } from './errors/errors' 17 | import { basicMessageEvents } from './events/BasicMessageEvents' 18 | import { connectionEvents } from './events/ConnectionEvents' 19 | import { credentialEvents } from './events/CredentialEvents' 20 | import { proofEvents } from './events/ProofEvents' 21 | import { questionAnswerEvents } from './events/QuestionAnswerEvents' 22 | import { reuseConnectionEvents } from './events/ReuseConnectionEvents' 23 | import { RegisterRoutes } from './routes/routes' 24 | import { SecurityMiddleware } from './securityMiddleware' 25 | 26 | import { ValidateError } from 'tsoa' 27 | 28 | dotenv.config() 29 | 30 | export const setupServer = async (agent: Agent, config: ServerConfig, apiKey?: string) => { 31 | container.registerInstance(Agent, agent) 32 | fs.writeFileSync('config.json', JSON.stringify(config, null, 2)) 33 | const app = config.app ?? express() 34 | if (config.cors) app.use(cors()) 35 | 36 | if (config.socketServer || config.webhookUrl) { 37 | questionAnswerEvents(agent, config) 38 | basicMessageEvents(agent, config) 39 | connectionEvents(agent, config) 40 | credentialEvents(agent, config) 41 | proofEvents(agent, config) 42 | reuseConnectionEvents(agent, config) 43 | } 44 | 45 | // Use body parser to read sent json payloads 46 | app.use( 47 | bodyParser.urlencoded({ 48 | extended: true, 49 | limit: '50mb', 50 | }) 51 | ) 52 | 53 | setDynamicApiKey(apiKey ? apiKey : '') 54 | 55 | app.use(bodyParser.json({ limit: '50mb' })) 56 | app.use('/docs', serve, async (_req: ExRequest, res: ExResponse) => { 57 | return res.send(generateHTML(await import('./routes/swagger.json'))) 58 | }) 59 | 60 | const windowMs = Number(process.env.windowMs) 61 | const maxRateLimit = Number(process.env.maxRateLimit) 62 | const limiter = rateLimit({ 63 | windowMs, // 1 second 64 | max: maxRateLimit, // max 800 requests per second 65 | }) 66 | 67 | // apply rate limiter to all requests 68 | app.use(limiter) 69 | 70 | // Note: Having used it above, redirects accordingly 71 | app.use((req, res, next) => { 72 | if (req.url == '/') { 73 | res.redirect('/docs') 74 | return 75 | } 76 | next() 77 | }) 78 | 79 | const securityMiddleware = new SecurityMiddleware() 80 | app.use(securityMiddleware.use) 81 | RegisterRoutes(app) 82 | 83 | app.use(function errorHandler(err: unknown, req: ExRequest, res: ExResponse, next: NextFunction): ExResponse | void { 84 | if (err instanceof ValidateError) { 85 | agent.config.logger.warn(`Caught Validation Error for ${req.path}:`, err.fields) 86 | return res.status(422).json({ 87 | message: 'Validation Failed', 88 | details: err?.fields, 89 | }) 90 | } else if (err instanceof BaseError) { 91 | return res.status(err.statusCode).json({ 92 | message: err.message, 93 | }) 94 | } else if (err instanceof Error) { 95 | // Extend the Error type with custom properties 96 | const error = err as Error & { statusCode?: number; status?: number; stack?: string } 97 | const statusCode = error.statusCode || error.status || 500 98 | return res.status(statusCode).json({ 99 | message: error.message || 'Internal Server Error', 100 | }) 101 | } 102 | next() 103 | }) 104 | 105 | return app 106 | } 107 | -------------------------------------------------------------------------------- /src/utils/ServerConfig.ts: -------------------------------------------------------------------------------- 1 | import type { Express } from 'express' 2 | import type { Server } from 'ws' 3 | 4 | export interface ServerConfig { 5 | port: number 6 | cors?: boolean 7 | app?: Express 8 | webhookUrl?: string 9 | /* Socket server is used for sending events over websocket to clients */ 10 | socketServer?: Server 11 | schemaFileServerURL?: string 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/TsyringeAdapter.ts: -------------------------------------------------------------------------------- 1 | import type { IocContainer } from '@tsoa/runtime' 2 | 3 | import { container } from 'tsyringe' 4 | 5 | export const iocContainer: IocContainer = { 6 | get: (controller: { prototype: T }): T => { 7 | return container.resolve(controller as never) 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/agent.ts: -------------------------------------------------------------------------------- 1 | import type { InitConfig } from '@credo-ts/core' 2 | 3 | import { PolygonModule } from '@ayanworks/credo-polygon-w3c-module' 4 | import { 5 | AnonCredsModule, 6 | LegacyIndyCredentialFormatService, 7 | LegacyIndyProofFormatService, 8 | V1CredentialProtocol, 9 | V1ProofProtocol, 10 | AnonCredsCredentialFormatService, 11 | AnonCredsProofFormatService, 12 | } from '@credo-ts/anoncreds' 13 | import { AskarModule } from '@credo-ts/askar' 14 | import { 15 | AutoAcceptCredential, 16 | CredentialsModule, 17 | DidsModule, 18 | JsonLdCredentialFormatService, 19 | KeyDidRegistrar, 20 | KeyDidResolver, 21 | DifPresentationExchangeProofFormatService, 22 | ProofsModule, 23 | V2CredentialProtocol, 24 | V2ProofProtocol, 25 | WebDidResolver, 26 | Agent, 27 | ConnectionInvitationMessage, 28 | HttpOutboundTransport, 29 | LogLevel, 30 | } from '@credo-ts/core' 31 | import { IndyVdrAnonCredsRegistry, IndyVdrModule } from '@credo-ts/indy-vdr' 32 | import { agentDependencies, HttpInboundTransport } from '@credo-ts/node' 33 | import { TenantsModule } from '@credo-ts/tenants' 34 | import { anoncreds } from '@hyperledger/anoncreds-nodejs' 35 | import { ariesAskar } from '@hyperledger/aries-askar-nodejs' 36 | import { indyVdr } from '@hyperledger/indy-vdr-nodejs' 37 | 38 | import { TsLogger } from './logger' 39 | 40 | export const setupAgent = async ({ name, endpoints, port }: { name: string; endpoints: string[]; port: number }) => { 41 | const logger = new TsLogger(LogLevel.debug) 42 | 43 | const config: InitConfig = { 44 | label: name, 45 | endpoints: endpoints, 46 | walletConfig: { 47 | id: name, 48 | key: name, 49 | }, 50 | logger: logger, 51 | } 52 | 53 | const legacyIndyCredentialFormat = new LegacyIndyCredentialFormatService() 54 | const legacyIndyProofFormat = new LegacyIndyProofFormatService() 55 | const agent = new Agent({ 56 | config: config, 57 | modules: { 58 | indyVdr: new IndyVdrModule({ 59 | indyVdr, 60 | networks: [ 61 | { 62 | isProduction: false, 63 | indyNamespace: 'bcovrin:test', 64 | genesisTransactions: process.env.BCOVRIN_TEST_GENESIS as string, 65 | connectOnStartup: true, 66 | }, 67 | ], 68 | }), 69 | askar: new AskarModule({ 70 | ariesAskar, 71 | }), 72 | 73 | anoncreds: new AnonCredsModule({ 74 | registries: [new IndyVdrAnonCredsRegistry()], 75 | anoncreds, 76 | }), 77 | 78 | dids: new DidsModule({ 79 | registrars: [new KeyDidRegistrar()], 80 | resolvers: [new KeyDidResolver(), new WebDidResolver()], 81 | }), 82 | proofs: new ProofsModule({ 83 | proofProtocols: [ 84 | new V1ProofProtocol({ 85 | indyProofFormat: legacyIndyProofFormat, 86 | }), 87 | new V2ProofProtocol({ 88 | proofFormats: [ 89 | legacyIndyProofFormat, 90 | new AnonCredsProofFormatService(), 91 | new DifPresentationExchangeProofFormatService(), 92 | ], 93 | }), 94 | ], 95 | }), 96 | credentials: new CredentialsModule({ 97 | autoAcceptCredentials: AutoAcceptCredential.ContentApproved, 98 | credentialProtocols: [ 99 | new V1CredentialProtocol({ 100 | indyCredentialFormat: legacyIndyCredentialFormat, 101 | }), 102 | new V2CredentialProtocol({ 103 | credentialFormats: [ 104 | legacyIndyCredentialFormat, 105 | new JsonLdCredentialFormatService(), 106 | new AnonCredsCredentialFormatService(), 107 | ], 108 | }), 109 | ], 110 | }), 111 | tenants: new TenantsModule(), 112 | polygon: new PolygonModule({ 113 | didContractAddress: '', 114 | schemaManagerContractAddress: '', 115 | fileServerToken: '', 116 | rpcUrl: '', 117 | serverUrl: '', 118 | }), 119 | }, 120 | dependencies: agentDependencies, 121 | }) 122 | 123 | const httpInbound = new HttpInboundTransport({ 124 | port: port, 125 | }) 126 | 127 | agent.registerInboundTransport(httpInbound) 128 | 129 | agent.registerOutboundTransport(new HttpOutboundTransport()) 130 | 131 | httpInbound.app.get('/invitation', async (req, res) => { 132 | if (typeof req.query.d_m === 'string') { 133 | const invitation = await ConnectionInvitationMessage.fromUrl(req.url.replace('d_m=', 'c_i=')) 134 | res.send(invitation.toJSON()) 135 | } 136 | if (typeof req.query.c_i === 'string') { 137 | const invitation = await ConnectionInvitationMessage.fromUrl(req.url) 138 | res.send(invitation.toJSON()) 139 | } else { 140 | const { outOfBandInvitation } = await agent.oob.createInvitation() 141 | 142 | res.send(outOfBandInvitation.toUrl({ domain: endpoints + '/invitation' })) 143 | } 144 | }) 145 | 146 | await agent.initialize() 147 | 148 | return agent 149 | } 150 | -------------------------------------------------------------------------------- /src/utils/errorConverter.ts: -------------------------------------------------------------------------------- 1 | import type { BaseError } from '../errors/errors' 2 | 3 | import { errorMap } from '../errors/errors' 4 | 5 | function convertError(errorType: string, message: string = 'An error occurred'): BaseError { 6 | const ErrorClass = errorMap[errorType] || errorMap.InternalServerError 7 | throw new ErrorClass(message) 8 | } 9 | 10 | export default convertError 11 | -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { JsonTransformer } from '@credo-ts/core' 2 | import { JsonEncoder } from '@credo-ts/core/build/utils/JsonEncoder' 3 | 4 | export function objectToJson(result: T) { 5 | const serialized = JsonTransformer.serialize(result) 6 | return JsonEncoder.fromString(serialized) 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | import type { ILogObject } from 'tslog' 4 | 5 | import { LogLevel, BaseLogger } from '@credo-ts/core' 6 | import { appendFileSync } from 'fs' 7 | import { Logger } from 'tslog' 8 | 9 | function logToTransport(logObject: ILogObject) { 10 | appendFileSync('logs.txt', JSON.stringify(logObject) + '\n') 11 | } 12 | 13 | export class TsLogger extends BaseLogger { 14 | private logger: Logger 15 | 16 | // Map our log levels to tslog levels 17 | private tsLogLevelMap = { 18 | [LogLevel.test]: 'silly', 19 | [LogLevel.trace]: 'trace', 20 | [LogLevel.debug]: 'debug', 21 | [LogLevel.info]: 'info', 22 | [LogLevel.warn]: 'warn', 23 | [LogLevel.error]: 'error', 24 | [LogLevel.fatal]: 'fatal', 25 | } as const 26 | 27 | public constructor(logLevel: LogLevel, name?: string) { 28 | super(logLevel) 29 | 30 | this.logger = new Logger({ 31 | name, 32 | minLevel: this.logLevel == LogLevel.off ? undefined : this.tsLogLevelMap[this.logLevel], 33 | ignoreStackLevels: 5, 34 | attachedTransports: [ 35 | { 36 | transportLogger: { 37 | silly: logToTransport, 38 | debug: logToTransport, 39 | trace: logToTransport, 40 | info: logToTransport, 41 | warn: logToTransport, 42 | error: logToTransport, 43 | fatal: logToTransport, 44 | }, 45 | // always log to file 46 | minLevel: 'silly', 47 | }, 48 | ], 49 | }) 50 | } 51 | 52 | private log(level: Exclude, message: string, data?: Record): void { 53 | const tsLogLevel = this.tsLogLevelMap[level] 54 | 55 | if (data) { 56 | this.logger[tsLogLevel](message, data) 57 | } else { 58 | this.logger[tsLogLevel](message) 59 | } 60 | } 61 | 62 | public test(message: string, data?: Record): void { 63 | this.log(LogLevel.test, message, data) 64 | } 65 | 66 | public trace(message: string, data?: Record): void { 67 | this.log(LogLevel.trace, message, data) 68 | } 69 | 70 | public debug(message: string, data?: Record): void { 71 | this.log(LogLevel.debug, message, data) 72 | } 73 | 74 | public info(message: string, data?: Record): void { 75 | this.log(LogLevel.info, message, data) 76 | } 77 | 78 | public warn(message: string, data?: Record): void { 79 | this.log(LogLevel.warn, message, data) 80 | } 81 | 82 | public error(message: string, data?: Record): void { 83 | this.log(LogLevel.error, message, data) 84 | } 85 | 86 | public fatal(message: string, data?: Record): void { 87 | this.log(LogLevel.fatal, message, data) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/tsyringeTsoaIocContainer.ts: -------------------------------------------------------------------------------- 1 | import type { IocContainer } from '@tsoa/runtime' 2 | 3 | import { container } from 'tsyringe' 4 | 5 | export const iocContainer: IocContainer = { 6 | get: (controller: { prototype: T }): T => { 7 | return container.resolve(controller as never) 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/webhook.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingHttpHeaders } from 'http' 2 | 3 | import express, { json } from 'express' 4 | 5 | export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) 6 | 7 | export interface WebhookData { 8 | receivedAt: string 9 | headers: IncomingHttpHeaders 10 | body: { 11 | id: string 12 | state: string 13 | [key: string]: unknown 14 | } 15 | topic: string 16 | } 17 | 18 | export const webhookListener = async (port: number, webhooksReceived: WebhookData[]) => { 19 | const app = express() 20 | 21 | app.use(json()) 22 | 23 | app.post('/:topic', (req, res) => { 24 | const hookData: WebhookData = { receivedAt: Date(), headers: req.headers, body: req.body, topic: req.params.topic } 25 | webhooksReceived.push(hookData) 26 | res.sendStatus(200) 27 | }) 28 | return app.listen(port) 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2017", 5 | "declaration": true, 6 | "sourceMap": true, 7 | "strict": true, 8 | "noEmitOnError": true, 9 | "lib": ["ES2021"], 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "resolveJsonModule": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "types": ["jest"], 16 | "outDir": "./build" 17 | }, 18 | "include": ["src/**/*", "src/routes"], 19 | "exclude": [ 20 | "node_modules", 21 | "build", 22 | "**/*.test.ts", 23 | "**/__tests__/*.ts", 24 | "**/__mocks__/*.ts", 25 | "**/build/**", 26 | "scripts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "afj-controller/*": ["src"] 8 | } 9 | }, 10 | "include": ["./.eslintrc.js", "./jest.config.ts", "./jest.config.base.ts", "types", "tests", "samples", "src", "bin"], 11 | "exclude": ["node_modules", "build"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "ts-node": { 4 | "require": ["tsconfig-paths/register"], 5 | "files": true 6 | }, 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "credo-controller/*": ["*/src"] 11 | }, 12 | "lib": ["ES2021.Promise"], 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "types": ["jest", "node"] 16 | }, 17 | "exclude": ["node_modules", "build"] 18 | } 19 | -------------------------------------------------------------------------------- /tsoa.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryFile": "src/index.ts", 3 | "noImplicitAdditionalProperties": "throw-on-extras", 4 | "controllerPathGlobs": ["src/**/*Controller.ts"], 5 | "spec": { 6 | "outputDirectory": "src/routes", 7 | "specVersion": 3, 8 | "securityDefinitions": { 9 | "apiKey": { 10 | "type": "apiKey", 11 | "name": "Authorization", 12 | "in": "header" 13 | } 14 | } 15 | }, 16 | "routes": { 17 | "routesDir": "src/routes", 18 | "iocModule": "./src/utils/tsyringeTsoaIocContainer", 19 | "authenticationModule": "src/authentication" 20 | } 21 | } 22 | --------------------------------------------------------------------------------