├── .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 |
8 |
9 | Aries Framework JavaScript REST API
10 |
11 |
17 |
22 |
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 |
--------------------------------------------------------------------------------