├── .github
└── workflows
│ └── continuous_deployment.yaml
├── .gitignore
├── LICENSE
├── README.md
├── agent
├── .dockerignore
├── .env.development
├── .env.example
├── Dockerfile
├── package.json
├── src
│ ├── agent.ts
│ ├── constants.ts
│ ├── did
│ │ ├── cheqd.ts
│ │ ├── createKeys.ts
│ │ ├── index.ts
│ │ ├── jwk.ts
│ │ ├── key.ts
│ │ ├── setup.ts
│ │ ├── util.ts
│ │ └── web.ts
│ ├── endpoints.ts
│ ├── issuer.ts
│ ├── issuerMetadata.ts
│ ├── server.ts
│ ├── session.ts
│ └── verifier.ts
└── tsconfig.json
├── app
├── .env.example
├── .eslintrc.json
├── .gitignore
├── Dockerfile
├── app
│ ├── globals.css
│ ├── icon.svg
│ ├── layout.tsx
│ └── page.tsx
├── components.json
├── components
│ ├── IssueTab.tsx
│ ├── ReceiveTab.tsx
│ ├── VerifyTab.tsx
│ ├── highLight.module.css
│ ├── highLight.tsx
│ ├── main.tsx
│ └── ui
│ │ ├── alert.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── select.tsx
│ │ ├── tabs.tsx
│ │ ├── tooltip.tsx
│ │ └── typography.tsx
├── lib
│ ├── api.ts
│ ├── constants.ts
│ ├── hooks.ts
│ └── utils.ts
├── next.config.js
├── nginx.conf
├── package.json
├── postcss.config.js
├── public
│ └── logo.svg
├── tailwind.config.ts
└── tsconfig.json
├── assets
└── preview.png
├── docker-compose.yml
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── yarn.lock
/.github/workflows/continuous_deployment.yaml:
--------------------------------------------------------------------------------
1 | name: Continuous Deployment
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | build:
7 | default: true
8 | type: boolean
9 | required: false
10 | description: Build the website before deploying
11 | env:
12 | REGISTRY: ghcr.io
13 | IMAGE_NAME: ${{ github.repository }}
14 | APP: app
15 | AGENT: agent
16 |
17 | jobs:
18 | build-and-push-image-agent:
19 | runs-on: ubuntu-latest
20 | if: inputs.build == true && github.ref == 'refs/heads/main'
21 | permissions:
22 | contents: read
23 | packages: write
24 |
25 | defaults:
26 | run:
27 | working-directory: ${{ env.AGENT }}
28 |
29 | steps:
30 | - name: Checkout repository
31 | uses: actions/checkout@v2
32 |
33 | - name: Log in to the Container registry
34 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
35 | with:
36 | registry: ${{ env.REGISTRY }}
37 | username: ${{ github.actor }}
38 | password: ${{ secrets.GITHUB_TOKEN }}
39 |
40 | - name: Extract metadata (tags, labels) for Docker
41 | id: meta
42 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
43 | with:
44 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.AGENT }}
45 |
46 | - name: Build and push Docker image
47 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
48 | with:
49 | context: ./agent
50 | push: true
51 | tags: ${{ steps.meta.outputs.tags }}
52 | labels: ${{ steps.meta.outputs.labels }}
53 |
54 | build-and-push-image-app:
55 | runs-on: ubuntu-latest
56 | if: inputs.build == true && github.ref == 'refs/heads/main'
57 | permissions:
58 | contents: read
59 | packages: write
60 |
61 | defaults:
62 | run:
63 | working-directory: ${{ env.APP }}
64 |
65 | steps:
66 | - name: Checkout repository
67 | uses: actions/checkout@v2
68 |
69 | - uses: pnpm/action-setup@v2
70 | with:
71 | version: 8
72 |
73 | - name: Install dependencies
74 | run: pnpm install --no-frozen-lockfile
75 |
76 | - run: NEXT_PUBLIC_API_URL=https://openid4vc.animo.id pnpm build
77 |
78 | - name: Log in to the Container registry
79 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
80 | with:
81 | registry: ${{ env.REGISTRY }}
82 | username: ${{ github.actor }}
83 | password: ${{ secrets.GITHUB_TOKEN }}
84 |
85 | - name: Extract metadata (tags, labels) for Docker
86 | id: meta
87 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
88 | with:
89 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.APP }}
90 |
91 | - name: Build and push Docker image
92 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
93 | with:
94 | context: ./app
95 | push: true
96 | tags: ${{ steps.meta.outputs.tags }}
97 | labels: ${{ steps.meta.outputs.labels }}
98 |
99 | deploy:
100 | # Only run on main branch
101 | runs-on: ubuntu-latest
102 | needs: [build-and-push-image-agent, build-and-push-image-app]
103 | if: |
104 | always() &&
105 | (needs.build-and-push-image-agent.result == 'success' || needs.build-and-push-image-agent.result == 'skipped') &&
106 | (needs.build-and-push-image-app.result == 'success' || needs.build-and-push-image-app.result == 'skipped') &&
107 | github.ref == 'refs/heads/main'
108 | steps:
109 | - name: Checkout repository
110 | uses: actions/checkout@v2
111 |
112 | - name: Copy stack file to remote
113 | uses: garygrossgarten/github-action-scp@v0.7.3
114 | with:
115 | local: docker-compose.yml
116 | remote: openid4vc-playground/docker-compose.yml
117 | host: dashboard.dev.animo.id
118 | username: root
119 | privateKey: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
120 |
121 | - name: Deploy to Docker Swarm via SSH action
122 | uses: appleboy/ssh-action@v0.1.4
123 | env:
124 | AGENT_WALLET_KEY: ${{ secrets.AGENT_WALLET_KEY }}
125 | CHEQD_TESTNET_COSMOS_PAYER_SEED: ${{ secrets.CHEQD_TESTNET_COSMOS_PAYER_SEED }}
126 | DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED: ${{ secrets.DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED }}
127 | P256_SEED: ${{ secrets.P256_SEED }}
128 | ED25519_SEED: ${{ secrets.ED25519_SEED }}
129 | with:
130 | host: dashboard.dev.animo.id
131 | username: root
132 | key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
133 | envs: P256_SEED,ED25519_SEED,AGENT_WALLET_KEY,DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED,CHEQD_TESTNET_COSMOS_PAYER_SEED
134 | script: |
135 | ED25519_SEED=${ED25519_SEED} P256_SEED=${P256_SEED} AGENT_WALLET_KEY=${AGENT_WALLET_KEY} CHEQD_TESTNET_COSMOS_PAYER_SEED=${CHEQD_TESTNET_COSMOS_PAYER_SEED} DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED=${DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED} docker stack deploy --compose-file openid4vc-playground/docker-compose.yml openid4vc-playground --with-registry-auth
136 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | .env
5 | .env.local
--------------------------------------------------------------------------------
/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 2020-present Hyperledger Contributors.
190 | Copyright 2021 Queen’s Printer for Ontario. Mostafa Youssef (https://github.com/MosCD3), Amit Padmani (https://github.com/nbAmit), Prasad Katkar (https://github.com/NB-PrasadKatkar), Mike Richardson (https://github.com/NB-MikeRichardson)
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Animo OpenID4VC Playground
8 |
9 |
15 |
16 |
17 | > [!TIP]
18 | > Check out the demo at https://openid4vc.animo.id
19 |
20 | ## ✨ Hi there!
21 |
22 | Welcome to the repository of Animo's OpenID4VC Playground. This interactive playground demonstrates the use of OpenID4VC with different credential formats (such as SD-JWT VCs). This demo is built using [Aries Framework Javascript (AFJ)](https://github.com/hyperledger/aries-framework-javascript). AFJ is a framework written in TypeScript for building decentralized identity services.
23 |
24 | ## 🛠️ Usage
25 |
26 | ### Prerequisites
27 |
28 | - [NodeJS](https://nodejs.org/en/) v18.X.X - Other versions may work, not tested
29 | - [pnpm](https://pnpm.io/installation)
30 | - [Git](https://git-scm.com/downloads) - You probably already have this
31 |
32 | ### 🖥 App
33 |
34 | Copy the `.env.example` file to a `.env.local` file and set the environment variables. **This is not needed for development**.
35 |
36 | ```bash
37 | cd app
38 | cp .env.example .env
39 | ```
40 |
41 | | Variable | Description |
42 | | --------------------- | ----------------------------------------------------------------------------------------------------------------- |
43 | | `NEXT_PUBLIC_API_URL` | Used in the frontend application to connect with the backend. Default to `http://localhost:3001` for development. |
44 |
45 | ### 🎛️ Agent
46 |
47 | Copy the `.env.example` file to a `.env.local` file and set the environment variables. **This is not needed for development**.
48 |
49 | ```bash
50 | cd agent
51 | cp .env.example .env
52 | ```
53 |
54 | | Variable | Description |
55 | | ------------------ | --------------------------------------------------------------------------------------------- |
56 | | `AGENT_HOST` | Used in the backend application for the agent. The url at which the server will be available. |
57 | | `AGENT_WALLET_KEY` | Used in the backend application for the agent. Should be secure and kept private. |
58 |
59 | > [!IMPORTANT]
60 | > The issuer will use `did:web` for issuing credentials, but this requires `https` to be used. When developing locally it is recommend
61 | > to use `ngrok` (`npx ngrok http 3001`) and use that url as the `AGENT_HOST` variable. Make sure to also set the `NEXT_PUBLIC_API_URL` variable in the app to the ngrok.
62 | >
63 | > We may add issuance using did:key in development if the host url does not start with `https`.
64 |
65 | ### Install Dependencies
66 |
67 | ```bash
68 | pnpm install
69 | ```
70 |
71 | ### Development
72 |
73 | Open three terminal windows, and then run the following:
74 |
75 | ```bash
76 | npx ngrok http 3001
77 | ```
78 |
79 | Copy the https url from the ngrok command and set that as the `AGENT_HOST`
80 |
81 | ```bash
82 | cd agent
83 | AGENT_HOST=https://30f9-58-136-114-148.ngrok-free.app pnpm dev
84 | ```
85 |
86 | ```bash
87 | cd app
88 | pnpm dev
89 | ```
90 |
91 | ## 🖇️ How To Contribute
92 |
93 | You're welcome to contribute to this playground. Please make sure to open an issue first!
94 |
95 | This playground is open source and you're more than welcome to customize and use it to create your own OpenID4VC Playground. If you do, an attribution to [Animo](https://animo.id) would be very much appreciated!
96 |
--------------------------------------------------------------------------------
/agent/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .git
3 | .gitignore
4 | *.md
5 | dist
--------------------------------------------------------------------------------
/agent/.env.development:
--------------------------------------------------------------------------------
1 | AGENT_WALLET_KEY=secret-wallet-key
2 | DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED=2543786a945a27258087ccfe95ff62df
3 | CHEQD_TESTNET_COSMOS_PAYER_SEED=robust across amount corn curve panther opera wish toe ring bleak empower wreck party abstract glad average muffin picnic jar squeeze annual long aunt
4 | ED25519_SEED=5473a3e4c5ae3fd5fb3ad089563596e3
5 | P256_SEED=e5f18b10cd15cdb76818bc6ae8b71eb475e6eac76875ed085d3962239bbcf42f
--------------------------------------------------------------------------------
/agent/.env.example:
--------------------------------------------------------------------------------
1 | AGENT_HOST=http://localhost:3001
2 | AGENT_WALLET_KEY=secret-wallet-key
3 | DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED=2543786a945a27258087ccfe95ff62df
4 | CHEQD_TESTNET_COSMOS_PAYER_SEED=robust across amount corn curve panther opera wish toe ring bleak empower wreck party abstract glad average muffin picnic jar squeeze annual long aunt
5 | ED25519_SEED=5473a3e4c5ae3fd5fb3ad089563596e3
6 | P256_SEED=e5f18b10cd15cdb76818bc6ae8b71eb475e6eac76875ed085d3962239bbcf42f
--------------------------------------------------------------------------------
/agent/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18 as base
2 |
3 | COPY package.json /app/package.json
4 |
5 | ENV PNPM_HOME="/pnpm"
6 | ENV PATH="$PNPM_HOME:$PATH"
7 | RUN corepack enable
8 |
9 | WORKDIR /app
10 |
11 | FROM base AS prod-deps
12 | COPY tsconfig.json /app/tsconfig.json
13 |
14 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod
15 |
16 | FROM base AS build
17 | COPY tsconfig.json /app/tsconfig.json
18 |
19 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install
20 | COPY src src
21 | RUN pnpm run build
22 |
23 | FROM base
24 |
25 | COPY --from=prod-deps /app/node_modules /app/node_modules
26 | COPY --from=build /app/dist /app/dist
27 |
28 | EXPOSE 3000
29 | CMD [ "pnpm", "start" ]
--------------------------------------------------------------------------------
/agent/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "agent",
3 | "dependencies": {
4 | "@credo-ts/askar": "^0.5.10",
5 | "@credo-ts/cheqd": "^0.5.10",
6 | "@credo-ts/core": "^0.5.10",
7 | "@credo-ts/indy-vdr": "^0.5.10",
8 | "@credo-ts/node": "^0.5.10",
9 | "@credo-ts/openid4vc": "^0.5.10",
10 | "@hyperledger/aries-askar-nodejs": "^0.2.3",
11 | "cors": "^2.8.5",
12 | "express": "^4.18.2",
13 | "zod": "^3.22.4"
14 | },
15 | "devDependencies": {
16 | "@types/cors": "^2.8.17",
17 | "@types/express": "^4.17.21",
18 | "tsx": "^4.7.0",
19 | "typescript": "^5.3.3"
20 | },
21 | "scripts": {
22 | "build": "tsc -p tsconfig.json",
23 | "start": "node dist/server.js",
24 | "dev": "tsx watch -r dotenv/config src/server.ts dotenv_config_path=.env.development"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/agent/src/agent.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Agent,
3 | ConsoleLogger,
4 | DidsModule,
5 | JwkDidRegistrar,
6 | JwkDidResolver,
7 | KeyDidRegistrar,
8 | KeyDidResolver,
9 | LogLevel,
10 | WebDidResolver,
11 | joinUriParts,
12 | } from "@credo-ts/core";
13 | import { agentDependencies } from "@credo-ts/node";
14 | import { AskarModule } from "@credo-ts/askar";
15 | import {
16 | CheqdModule,
17 | CheqdDidRegistrar,
18 | CheqdDidResolver,
19 | } from "@credo-ts/cheqd";
20 | import { ariesAskar } from "@hyperledger/aries-askar-nodejs";
21 | import {
22 | OpenId4VcHolderModule,
23 | OpenId4VcIssuerModule,
24 | OpenId4VcVerifierModule,
25 | } from "@credo-ts/openid4vc";
26 | import {
27 | AGENT_HOST,
28 | AGENT_WALLET_KEY,
29 | CHEQD_TESTNET_COSMOS_PAYER_SEED,
30 | } from "./constants";
31 | import { Router } from "express";
32 | import { credentialRequestToCredentialMapper } from "./issuer";
33 |
34 | process.on("unhandledRejection", (reason) => {
35 | console.log("Unhandled rejection", reason);
36 | });
37 |
38 | export const openId4VciRouter = Router();
39 | export const openId4VpRouter = Router();
40 |
41 | export const agent = new Agent({
42 | dependencies: agentDependencies,
43 | config: {
44 | label: "OpenID4VC Playground",
45 | logger: new ConsoleLogger(LogLevel.trace),
46 | // TODO: add postgres storage
47 | walletConfig: {
48 | id: "openid4vc-playground",
49 | key: AGENT_WALLET_KEY,
50 | },
51 | },
52 | modules: {
53 | cheqd: new CheqdModule({
54 | networks: [
55 | {
56 | network: "testnet",
57 | cosmosPayerSeed: CHEQD_TESTNET_COSMOS_PAYER_SEED,
58 | },
59 | ],
60 | }),
61 | dids: new DidsModule({
62 | resolvers: [
63 | new KeyDidResolver(),
64 | new JwkDidResolver(),
65 | new WebDidResolver(),
66 | new CheqdDidResolver(),
67 | ],
68 | registrars: [
69 | new KeyDidRegistrar(),
70 | new JwkDidRegistrar(),
71 | new CheqdDidRegistrar(),
72 | ],
73 | }),
74 | askar: new AskarModule({
75 | ariesAskar,
76 | }),
77 | openId4VcIssuer: new OpenId4VcIssuerModule({
78 | baseUrl: joinUriParts(AGENT_HOST, ["oid4vci"]),
79 | router: openId4VciRouter,
80 | endpoints: {
81 | credential: {
82 | credentialRequestToCredentialMapper,
83 | },
84 | },
85 | }),
86 | openId4VcHolder: new OpenId4VcHolderModule(),
87 | openId4VcVerifier: new OpenId4VcVerifierModule({
88 | baseUrl: joinUriParts(AGENT_HOST, ["siop"]),
89 | router: openId4VpRouter,
90 | }),
91 | },
92 | });
93 |
--------------------------------------------------------------------------------
/agent/src/constants.ts:
--------------------------------------------------------------------------------
1 | if (
2 | !process.env.ED25519_SEED ||
3 | !process.env.P256_SEED ||
4 | !process.env.AGENT_HOST ||
5 | !process.env.AGENT_WALLET_KEY
6 | ) {
7 | throw new Error(
8 | "ED25519_SEED, P256_SEED, AGENT_HOST or AGENT_WALLET_KEY env variable not set"
9 | );
10 | }
11 |
12 | const AGENT_HOST = process.env.AGENT_HOST;
13 | const AGENT_WALLET_KEY = process.env.AGENT_WALLET_KEY;
14 |
15 | const CHEQD_TESTNET_COSMOS_PAYER_SEED =
16 | process.env.CHEQD_TESTNET_COSMOS_PAYER_SEED;
17 |
18 | const ED25519_SEED = process.env.ED25519_SEED;
19 | const P256_SEED = process.env.P256_SEED;
20 |
21 | export {
22 | AGENT_HOST,
23 | AGENT_WALLET_KEY,
24 | CHEQD_TESTNET_COSMOS_PAYER_SEED,
25 | ED25519_SEED,
26 | P256_SEED,
27 | };
28 |
--------------------------------------------------------------------------------
/agent/src/did/cheqd.ts:
--------------------------------------------------------------------------------
1 | import { agent } from "../agent";
2 | import { CheqdDidCreateOptions } from "@credo-ts/cheqd";
3 | import {
4 | DidDocumentBuilder,
5 | utils,
6 | KeyType,
7 | getEd25519VerificationKey2018,
8 | } from "@credo-ts/core";
9 |
10 | export async function createDidCheqd() {
11 | // NOTE: we need to pass custom document for cheqd if we want to add it to `assertionMethod`
12 |
13 | const did = `did:cheqd:testnet:${utils.uuid()}`;
14 | const key = await agent.wallet.createKey({
15 | keyType: KeyType.Ed25519,
16 | });
17 |
18 | const ed25519VerificationMethod = getEd25519VerificationKey2018({
19 | key,
20 | id: `${did}#key-1`,
21 | controller: did,
22 | });
23 |
24 | const didDocument = new DidDocumentBuilder(did)
25 | .addContext("https://w3id.org/security/suites/ed25519-2018/v1")
26 | .addVerificationMethod(ed25519VerificationMethod)
27 | .addAssertionMethod(ed25519VerificationMethod.id)
28 | .addAuthentication(ed25519VerificationMethod.id)
29 | .build();
30 |
31 | const didResult = await agent.dids.create({
32 | method: "cheqd",
33 | didDocument,
34 | secret: {},
35 | });
36 |
37 | if (didResult.didState.state === "failed") {
38 | throw new Error(`cheqd DID creation failed. ${didResult.didState.reason}`);
39 | }
40 |
41 | return [did];
42 | }
43 |
--------------------------------------------------------------------------------
/agent/src/did/createKeys.ts:
--------------------------------------------------------------------------------
1 | import { KeyType, TypedArrayEncoder } from "@credo-ts/core";
2 | import { agent } from "../agent";
3 | import { ED25519_SEED, P256_SEED } from "../constants";
4 |
5 | export async function createKeys() {
6 | const ed25519Key = await agent.wallet.createKey({
7 | keyType: KeyType.Ed25519,
8 | seed: TypedArrayEncoder.fromString(ED25519_SEED),
9 | });
10 |
11 | const p256Key = await agent.wallet.createKey({
12 | keyType: KeyType.P256,
13 | seed: TypedArrayEncoder.fromString(P256_SEED),
14 | });
15 |
16 | return [ed25519Key, p256Key];
17 | }
18 |
--------------------------------------------------------------------------------
/agent/src/did/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./util";
2 | export * from "./web";
3 | export * from "./jwk";
4 | export * from "./key";
5 | export * from "./cheqd";
6 | export * from "./setup";
7 |
--------------------------------------------------------------------------------
/agent/src/did/jwk.ts:
--------------------------------------------------------------------------------
1 | import { DidJwk, Key, getJwkFromKey } from "@credo-ts/core";
2 | import { agent } from "../agent";
3 |
4 | export async function createDidJwk(keys: Key[]) {
5 | const createdDids: string[] = [];
6 | for (const key of keys) {
7 | const didJwk = DidJwk.fromJwk(getJwkFromKey(key));
8 | await agent.dids.import({
9 | overwrite: true,
10 | did: didJwk.did,
11 | });
12 | createdDids.push(didJwk.did);
13 | }
14 |
15 | return createdDids;
16 | }
17 |
--------------------------------------------------------------------------------
/agent/src/did/key.ts:
--------------------------------------------------------------------------------
1 | import { Key, DidKey } from "@credo-ts/core";
2 | import { agent } from "../agent";
3 |
4 | export async function createDidKey(keys: Key[]) {
5 | const createdDids: string[] = [];
6 |
7 | for (const key of keys) {
8 | const didKey = new DidKey(key);
9 | await agent.dids.import({
10 | overwrite: true,
11 | did: didKey.did,
12 | });
13 | createdDids.push(didKey.did);
14 | }
15 |
16 | return createdDids;
17 | }
18 |
--------------------------------------------------------------------------------
/agent/src/did/setup.ts:
--------------------------------------------------------------------------------
1 | import { WalletKeyExistsError } from "@credo-ts/core";
2 | import { createDidCheqd } from "./cheqd";
3 | import { createKeys } from "./createKeys";
4 | import { createDidJwk } from "./jwk";
5 | import { createDidKey } from "./key";
6 | import { createDidWeb } from "./web";
7 | import { agent } from "../agent";
8 |
9 | const availableDids: string[] = [];
10 |
11 | export async function setupAllDids() {
12 | try {
13 | const keys = await createKeys();
14 |
15 | availableDids.push(...(await createDidKey(keys)));
16 | availableDids.push(...(await createDidJwk(keys)));
17 | availableDids.push(...(await createDidWeb(keys)));
18 | availableDids.push(...(await createDidCheqd()));
19 |
20 | await agent.genericRecords.save({
21 | id: "AVAILABLE_DIDS",
22 | content: {
23 | availableDids,
24 | },
25 | });
26 | } catch (error) {
27 | // If the key already exists, we assume the dids are already created
28 | if (error instanceof WalletKeyExistsError) {
29 | const availableDidsRecord = await agent.genericRecords.findById(
30 | "AVAILABLE_DIDS"
31 | );
32 | if (!availableDidsRecord) {
33 | throw new Error("No available dids record found");
34 | }
35 | availableDids.push(
36 | ...(availableDidsRecord.content.availableDids as string[])
37 | );
38 |
39 | return;
40 | }
41 |
42 | throw error;
43 | }
44 | }
45 |
46 | export function getAvailableDids() {
47 | return availableDids;
48 | }
49 |
--------------------------------------------------------------------------------
/agent/src/did/util.ts:
--------------------------------------------------------------------------------
1 | import { agent } from "../agent";
2 |
3 | export async function hasDidForMethod(method: string) {
4 | const [createdDid] = await agent.dids.getCreatedDids({ method });
5 |
6 | return createdDid !== undefined;
7 | }
8 |
9 | export async function getDidForMethod(method: string) {
10 | const [createdDid] = await agent.dids.getCreatedDids({ method });
11 |
12 | if (!createdDid) {
13 | throw new Error(`did for method ${method} does not exist`);
14 | }
15 |
16 | return createdDid.did;
17 | }
18 |
--------------------------------------------------------------------------------
/agent/src/did/web.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DidDocumentBuilder,
3 | Key,
4 | getKeyDidMappingByKeyType,
5 | } from "@credo-ts/core";
6 | import { agent } from "../agent";
7 | import { AGENT_HOST } from "../constants";
8 |
9 | const cleanHost = encodeURIComponent(
10 | AGENT_HOST.replace("https://", "").replace("http://", "")
11 | );
12 |
13 | const didWeb = `did:web:${cleanHost}`;
14 |
15 | export async function createDidWeb(keys: Key[]) {
16 | const verificationMethods = keys.flatMap((key) =>
17 | getKeyDidMappingByKeyType(key.keyType).getVerificationMethods(didWeb, key)
18 | );
19 |
20 | const didDocumentBuilder = new DidDocumentBuilder(didWeb);
21 |
22 | for (const verificationMethod of verificationMethods) {
23 | didDocumentBuilder
24 | .addVerificationMethod(verificationMethod)
25 | .addAssertionMethod(verificationMethod.id)
26 | .addAuthentication(verificationMethod.id);
27 | }
28 |
29 | const didDocument = didDocumentBuilder.build();
30 |
31 | await agent.dids.import({
32 | did: didWeb,
33 | didDocument,
34 | });
35 |
36 | return [didWeb];
37 | }
38 |
39 | export async function getWebDidDocument() {
40 | const [createdDid] = await agent.dids.getCreatedDids({ did: didWeb });
41 |
42 | if (!createdDid || !createdDid.didDocument) {
43 | throw new Error('did does not exist');
44 | }
45 |
46 | return createdDid.didDocument;
47 | }
48 |
--------------------------------------------------------------------------------
/agent/src/endpoints.ts:
--------------------------------------------------------------------------------
1 | import { agent } from "./agent";
2 | import express, { NextFunction, Request, Response } from "express";
3 | import z from "zod";
4 | import { credentialSupportedIds } from "./issuerMetadata";
5 | import { getIssuer } from "./issuer";
6 | import {
7 | DifPresentationExchangeService,
8 | JsonTransformer,
9 | KeyDidCreateOptions,
10 | KeyType,
11 | RecordNotFoundError,
12 | W3cJsonLdVerifiableCredential,
13 | W3cJsonLdVerifiablePresentation,
14 | W3cJwtVerifiableCredential,
15 | W3cJwtVerifiablePresentation,
16 | getJwkFromKey,
17 | getKeyFromVerificationMethod,
18 | parseDid,
19 | } from "@credo-ts/core";
20 | import { getAvailableDids, getWebDidDocument } from "./did";
21 | import { getVerifier } from "./verifier";
22 | import { OpenId4VcIssuanceSessionRepository } from "@credo-ts/openid4vc/build/openid4vc-issuer/repository";
23 | import { OfferSessionMetadata } from "./session";
24 | import { OpenId4VcVerificationSessionState } from "@credo-ts/openid4vc";
25 |
26 | const zCreateOfferRequest = z.object({
27 | // FIXME: rename offeredCredentials to credentialSupportedIds in AFJ
28 | credentialSupportedIds: z.array(z.enum(credentialSupportedIds)),
29 | issuerDidMethod: z.string(),
30 | });
31 |
32 | export const apiRouter = express.Router();
33 | apiRouter.use(express.json());
34 | apiRouter.use(express.text());
35 | apiRouter.post(
36 | "/offers/create",
37 | async (request: Request, response: Response) => {
38 | const issuer = await getIssuer();
39 | // FIXME: somehow JSON doesn't work
40 | const createOfferRequest = zCreateOfferRequest.parse(
41 | typeof request.body === "string" ? JSON.parse(request.body) : request.body
42 | );
43 |
44 | const offer = await agent.modules.openId4VcIssuer.createCredentialOffer({
45 | issuerId: issuer.issuerId,
46 | offeredCredentials: createOfferRequest.credentialSupportedIds,
47 | preAuthorizedCodeFlowConfig: {
48 | userPinRequired: false,
49 | },
50 | });
51 |
52 | // FIXME: in 0.5.1 we can pass the issuanceMetadata to the createCredentialOffer method
53 | // directly
54 | const issuanceSessionRepository = agent.dependencyManager.resolve(
55 | OpenId4VcIssuanceSessionRepository
56 | );
57 | offer.issuanceSession.issuanceMetadata = {
58 | issuerDidMethod: createOfferRequest.issuerDidMethod,
59 | } satisfies OfferSessionMetadata;
60 | await issuanceSessionRepository.update(
61 | agent.context,
62 | offer.issuanceSession
63 | );
64 |
65 | return response.json(offer);
66 | }
67 | );
68 |
69 | apiRouter.get("/issuer", async (_, response: Response) => {
70 | const issuer = await getIssuer();
71 | const availableDids = getAvailableDids();
72 |
73 | return response.json({
74 | credentialsSupported: issuer.credentialsSupported,
75 | display: issuer.display,
76 | availableDidMethods: Array.from(
77 | new Set(availableDids.map((did) => `did:${parseDid(did).method}`))
78 | ),
79 | });
80 | });
81 |
82 | const zReceiveOfferRequest = z.object({
83 | credentialOfferUri: z.string().url(),
84 | });
85 |
86 | apiRouter.post(
87 | "/offers/receive",
88 | async (request: Request, response: Response) => {
89 | const receiveOfferRequest = zReceiveOfferRequest.parse(
90 | typeof request.body === "string" ? JSON.parse(request.body) : request.body
91 | );
92 |
93 | const resolvedOffer =
94 | await agent.modules.openId4VcHolder.resolveCredentialOffer(
95 | receiveOfferRequest.credentialOfferUri
96 | );
97 | const credentials =
98 | await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(
99 | resolvedOffer,
100 | {
101 | verifyCredentialStatus: false,
102 | credentialBindingResolver: async ({
103 | keyType,
104 | supportsJwk,
105 | supportedDidMethods,
106 | }) => {
107 | if (supportedDidMethods?.includes("did:key")) {
108 | const didKeyResult = await agent.dids.create(
109 | {
110 | method: "key",
111 | options: {
112 | keyType,
113 | },
114 | }
115 | );
116 |
117 | if (didKeyResult.didState.state !== "finished") {
118 | throw new Error("did creation failed");
119 | }
120 | const firstVerificationMethod =
121 | didKeyResult.didState.didDocument.verificationMethod?.[0];
122 | if (!firstVerificationMethod) {
123 | throw new Error("did document has no verification method");
124 | }
125 |
126 | return {
127 | method: "did",
128 | didUrl: firstVerificationMethod.id,
129 | };
130 | }
131 |
132 | if (supportsJwk) {
133 | const key = await agent.wallet.createKey({
134 | keyType,
135 | });
136 |
137 | return {
138 | method: "jwk",
139 | jwk: getJwkFromKey(key),
140 | };
141 | }
142 |
143 | throw new Error(
144 | "only did:key and jwk are supported for holder binding"
145 | );
146 | },
147 | }
148 | );
149 |
150 | return response.json({
151 | credentials: credentials.map((credential) => {
152 | if (credential instanceof W3cJsonLdVerifiableCredential) {
153 | return {
154 | pretty: credential.toJson(),
155 | encoded: credential.toJson(),
156 | };
157 | }
158 |
159 | if (credential instanceof W3cJwtVerifiableCredential) {
160 | return {
161 | pretty: JsonTransformer.toJSON(credential.credential),
162 | encoded: credential.serializedJwt,
163 | };
164 | }
165 |
166 | return {
167 | pretty: {
168 | ...credential,
169 | compact: undefined,
170 | },
171 | encoded: credential.compact,
172 | };
173 | }),
174 | });
175 | }
176 | );
177 |
178 | const zCreatePresentationRequestBody = z.object({
179 | presentationDefinition: z.record(z.string(), z.any()),
180 | });
181 |
182 | apiRouter.post(
183 | "/requests/create",
184 | async (request: Request, response: Response) => {
185 | const verifier = await getVerifier();
186 |
187 | // FIXME: somehow JSON doesn't work
188 | const createPresentationRequestBody = zCreatePresentationRequestBody.parse(
189 | typeof request.body === "string" ? JSON.parse(request.body) : request.body
190 | );
191 |
192 | // DIIPv1 uses ES256/P256 keys so we use that to create the request
193 | const webDid = await getWebDidDocument();
194 | const verificationMethods = webDid.verificationMethod ?? [];
195 | const keys = verificationMethods.map((v) =>
196 | getKeyFromVerificationMethod(v)
197 | );
198 | const verificationMethodIndex = keys.findIndex(
199 | (k) => k.keyType === KeyType.P256
200 | );
201 |
202 | if (verificationMethodIndex === -1) {
203 | return response.status(500).json({
204 | error: "No P-256 verification method found",
205 | });
206 | }
207 |
208 | const { authorizationRequest, verificationSession } =
209 | await agent.modules.openId4VcVerifier.createAuthorizationRequest({
210 | verifierId: verifier.verifierId,
211 | requestSigner: {
212 | didUrl: verificationMethods[verificationMethodIndex].id,
213 | method: "did",
214 | },
215 | presentationExchange: {
216 | definition:
217 | createPresentationRequestBody.presentationDefinition as any,
218 | },
219 | });
220 |
221 | return response.json({
222 | authorizationRequestUri: authorizationRequest,
223 | verificationSessionId: verificationSession.id,
224 | });
225 | }
226 | );
227 |
228 | const zReceivePresentationRequestBody = z.object({
229 | authorizationRequestUri: z.string().url(),
230 | });
231 |
232 | apiRouter.get("/requests/:verificationSessionId", async (request, response) => {
233 | const verificationSessionId = request.params.verificationSessionId;
234 |
235 | try {
236 | const verificationSession =
237 | await agent.modules.openId4VcVerifier.getVerificationSessionById(
238 | verificationSessionId
239 | );
240 |
241 | if (
242 | verificationSession.state ===
243 | OpenId4VcVerificationSessionState.ResponseVerified
244 | ) {
245 | const verified =
246 | await agent.modules.openId4VcVerifier.getVerifiedAuthorizationResponse(
247 | verificationSessionId
248 | );
249 |
250 | return response.json({
251 | verificationSessionId: verificationSession.id,
252 | responseStatus: verificationSession.state,
253 | error: verificationSession.errorMessage,
254 |
255 | presentations: verified.presentationExchange?.presentations.map(
256 | (presentation) => {
257 | if (presentation instanceof W3cJsonLdVerifiablePresentation) {
258 | return {
259 | pretty: presentation.toJson(),
260 | encoded: presentation.toJson(),
261 | };
262 | }
263 |
264 | if (presentation instanceof W3cJwtVerifiablePresentation) {
265 | return {
266 | pretty: JsonTransformer.toJSON(presentation.presentation),
267 | encoded: presentation.serializedJwt,
268 | };
269 | }
270 |
271 | return {
272 | pretty: {
273 | ...presentation,
274 | compact: undefined,
275 | },
276 | encoded: presentation.compact,
277 | };
278 | }
279 | ),
280 | submission: verified.presentationExchange?.submission,
281 | definition: verified.presentationExchange?.definition,
282 | });
283 | }
284 |
285 | return response.json({
286 | verificationSessionId: verificationSession.id,
287 | responseStatus: verificationSession.state,
288 | error: verificationSession.errorMessage,
289 | });
290 | } catch (error) {
291 | if (error instanceof RecordNotFoundError) {
292 | return response.status(404).send("Verification session not found");
293 | }
294 | }
295 | });
296 |
297 | apiRouter.post(
298 | "/requests/receive",
299 | async (request: Request, response: Response) => {
300 | const receivePresentationRequestBody =
301 | zReceivePresentationRequestBody.parse(
302 | typeof request.body === "string"
303 | ? JSON.parse(request.body)
304 | : request.body
305 | );
306 |
307 | const resolved =
308 | await agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest(
309 | receivePresentationRequestBody.authorizationRequestUri
310 | );
311 |
312 | if (!resolved.presentationExchange) {
313 | return response.status(500).json({
314 | error:
315 | "Expected presentation_definition to be included in authorization request",
316 | });
317 | }
318 |
319 | // FIXME: expose PresentationExchange API (or allow auto-select in another way)
320 | const presentationExchangeService = agent.dependencyManager.resolve(
321 | DifPresentationExchangeService
322 | );
323 |
324 | const selectedCredentials =
325 | presentationExchangeService.selectCredentialsForRequest(
326 | resolved.presentationExchange?.credentialsForRequest
327 | );
328 |
329 | const { submittedResponse, serverResponse } =
330 | await agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({
331 | authorizationRequest: resolved.authorizationRequest,
332 | presentationExchange: {
333 | credentials: selectedCredentials,
334 | },
335 | });
336 |
337 | return response.status(serverResponse.status).json(submittedResponse);
338 | }
339 | );
340 |
341 | apiRouter.use(
342 | (error: Error, request: Request, response: Response, next: NextFunction) => {
343 | console.error("Unhandled error", error);
344 | return response.status(500).json({
345 | error: error.message,
346 | });
347 | }
348 | );
349 |
--------------------------------------------------------------------------------
/agent/src/issuer.ts:
--------------------------------------------------------------------------------
1 | import { OpenId4VciCredentialRequestToCredentialMapper } from "@credo-ts/openid4vc";
2 | import {
3 | W3cCredential,
4 | parseDid,
5 | KeyType,
6 | getKeyFromVerificationMethod,
7 | } from "@credo-ts/core";
8 | import { agent } from "./agent";
9 | import {
10 | animoOpenId4VcPlaygroundCredentialJwtVc,
11 | animoOpenId4VcPlaygroundCredentialLdpVc,
12 | animoOpenId4VcPlaygroundCredentialSdJwtVcDid,
13 | animoOpenId4VcPlaygroundCredentialSdJwtVcJwk,
14 | credentialsSupported,
15 | issuerDisplay,
16 | } from "./issuerMetadata";
17 | import { OfferSessionMetadata } from "./session";
18 | import { getAvailableDids } from "./did";
19 |
20 | const issuerId = "e451c49f-1186-4fe4-816d-a942151dfd59";
21 |
22 | export async function createIssuer() {
23 | return agent.modules.openId4VcIssuer.createIssuer({
24 | issuerId,
25 | credentialsSupported,
26 | display: issuerDisplay,
27 | });
28 | }
29 |
30 | export async function doesIssuerExist() {
31 | try {
32 | await agent.modules.openId4VcIssuer.getByIssuerId(issuerId);
33 | return true;
34 | } catch (error) {
35 | return false;
36 | }
37 | }
38 |
39 | export async function getIssuer() {
40 | return agent.modules.openId4VcIssuer.getByIssuerId(issuerId);
41 | }
42 |
43 | export async function updateIssuer() {
44 | await agent.modules.openId4VcIssuer.updateIssuerMetadata({
45 | issuerId,
46 | credentialsSupported,
47 | display: issuerDisplay,
48 | });
49 | }
50 |
51 | export const credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper =
52 | async ({
53 | issuanceSession,
54 | credentialsSupported,
55 | // FIXME: it would be useful if holderBinding would include some metadata on the key type / alg used
56 | // for the key binding
57 | holderBinding,
58 | }) => {
59 | const credentialSupported = credentialsSupported[0];
60 |
61 | // not sure if this check is needed anymore
62 | if (!issuanceSession) throw new Error("Issuance session not found");
63 | if (!issuanceSession.issuanceMetadata)
64 | throw new Error("No issuance metadata");
65 |
66 | const { issuerDidMethod } =
67 | issuanceSession.issuanceMetadata as unknown as OfferSessionMetadata;
68 |
69 | const possibleDids = getAvailableDids().filter((d) =>
70 | d.startsWith(issuerDidMethod)
71 | );
72 |
73 | let holderKeyType: KeyType;
74 | if (holderBinding.method === "jwk") {
75 | holderKeyType = holderBinding.jwk.keyType;
76 | } else {
77 | const holderDidDocument = await agent.dids.resolveDidDocument(
78 | holderBinding.didUrl
79 | );
80 | const verificationMethod = holderDidDocument.dereferenceKey(
81 | holderBinding.didUrl
82 | );
83 | holderKeyType = getKeyFromVerificationMethod(verificationMethod).keyType;
84 | }
85 |
86 | if (possibleDids.length === 0) {
87 | throw new Error("No available DIDs for the issuer method");
88 | }
89 |
90 | let issuerDidUrl: string | undefined = undefined;
91 |
92 | for (const possibleDid of possibleDids) {
93 | const didDocument = await agent.dids.resolveDidDocument(possibleDid);
94 | // Set the first verificationMethod as backup, in case we won't find a match
95 | if (!issuerDidUrl && didDocument.verificationMethod?.[0].id) {
96 | issuerDidUrl = didDocument.verificationMethod?.[0].id;
97 | }
98 |
99 | const matchingVerificationMethod = didDocument.assertionMethod?.find(
100 | (assertionMethod) => {
101 | const verificationMethod =
102 | typeof assertionMethod === "string"
103 | ? didDocument.dereferenceVerificationMethod(assertionMethod)
104 | : assertionMethod;
105 | const keyType =
106 | getKeyFromVerificationMethod(verificationMethod).keyType;
107 | return keyType === holderKeyType;
108 | }
109 | );
110 |
111 | if (matchingVerificationMethod) {
112 | issuerDidUrl =
113 | typeof matchingVerificationMethod === "string"
114 | ? matchingVerificationMethod
115 | : matchingVerificationMethod.id;
116 | break;
117 | }
118 | }
119 |
120 | if (!issuerDidUrl) {
121 | throw new Error("No matching verification method found");
122 | }
123 |
124 | if (
125 | credentialSupported.format === "vc+sd-jwt" &&
126 | (credentialSupported.id ===
127 | animoOpenId4VcPlaygroundCredentialSdJwtVcDid.id ||
128 | credentialSupported.id ===
129 | animoOpenId4VcPlaygroundCredentialSdJwtVcJwk.id)
130 | ) {
131 | return {
132 | format: "vc+sd-jwt",
133 | holder: holderBinding,
134 | credentialSupportedId: credentialSupported.id,
135 | payload: {
136 | vct: credentialSupported.vct,
137 |
138 | playground: {
139 | framework: "Aries Framework JavaScript",
140 | language: "TypeScript",
141 | version: "1.0",
142 | createdBy: "Animo Solutions",
143 | },
144 | },
145 | issuer: {
146 | didUrl: issuerDidUrl,
147 | method: "did",
148 | },
149 | disclosureFrame: {
150 | playground: {
151 | language: true,
152 | version: true,
153 | },
154 | } as any,
155 | };
156 | }
157 |
158 | if (
159 | (credentialSupported.format === "jwt_vc_json" &&
160 | credentialSupported.id ===
161 | animoOpenId4VcPlaygroundCredentialJwtVc.id) ||
162 | (credentialSupported.format === "ldp_vc" &&
163 | credentialSupported.id === animoOpenId4VcPlaygroundCredentialLdpVc.id)
164 | ) {
165 | if (holderBinding.method !== "did") {
166 | throw new Error("Only did holder binding supported for JWT VC");
167 | }
168 | return {
169 | format:
170 | credentialSupported.format === "jwt_vc_json" ? "jwt_vc" : "ldp_vc",
171 | verificationMethod: issuerDidUrl,
172 | credentialSupportedId: credentialSupported.id,
173 | credential: W3cCredential.fromJson({
174 | // FIXME: we need to include/cache default contexts in AFJ
175 | // It quite slow the first time now
176 | // And not secure
177 | "@context":
178 | credentialSupported.format === "ldp_vc"
179 | ? [
180 | "https://www.w3.org/2018/credentials/v1",
181 | // Fields must be defined for JSON-LD
182 | {
183 | "@vocab":
184 | "https://www.w3.org/ns/credentials/issuer-dependent#",
185 | },
186 | ]
187 | : ["https://www.w3.org/2018/credentials/v1"],
188 | // TODO: should 'VerifiableCredential' be in the issuer metadata type?
189 | // FIXME: jwt verification did not fail when this was array within array
190 | // W3cCredential is not validated in AFJ???
191 | type: ["VerifiableCredential", ...credentialSupported.types],
192 | issuanceDate: new Date().toISOString(),
193 | issuer: parseDid(issuerDidUrl).did,
194 | credentialSubject: {
195 | id: parseDid(holderBinding.didUrl).did,
196 | playground: {
197 | framework: "Aries Framework JavaScript",
198 | language: "TypeScript",
199 | version: "1.0",
200 | createdBy: "Animo Solutions",
201 | },
202 | },
203 | }),
204 | };
205 | }
206 |
207 | throw new Error(`Unsupported credential ${credentialSupported.id}`);
208 | };
209 |
--------------------------------------------------------------------------------
/agent/src/issuerMetadata.ts:
--------------------------------------------------------------------------------
1 | import { JwaSignatureAlgorithm } from "@credo-ts/core";
2 | import {
3 | OpenId4VciCredentialSupportedWithId,
4 | OpenId4VciCredentialFormatProfile,
5 | OpenId4VciIssuerMetadataDisplay,
6 | } from "@credo-ts/openid4vc";
7 |
8 | const ANIMO_BLUE = "#5e7db6";
9 | const ANIMO_RED = "#E17471";
10 | const ANIMO_DARK_BACKGROUND = "#202223";
11 | const WHITE = "#FFFFFF";
12 |
13 | export const issuerDisplay = [
14 | {
15 | background_color: ANIMO_DARK_BACKGROUND,
16 | name: "Animo OpenID4VC Playground",
17 | locale: "en",
18 | logo: {
19 | alt_text: "Animo logo",
20 | url: "https://i.imgur.com/PUAIUed.jpeg",
21 | },
22 | text_color: WHITE,
23 | },
24 | ] satisfies OpenId4VciIssuerMetadataDisplay[];
25 |
26 | export const animoOpenId4VcPlaygroundCredentialSdJwtVcDid = {
27 | id: "AnimoOpenId4VcPlaygroundSdJwtVcDid",
28 | format: OpenId4VciCredentialFormatProfile.SdJwtVc,
29 | vct: "AnimoOpenId4VcPlayground",
30 | cryptographic_binding_methods_supported: ["did:key", "did:jwk"],
31 | cryptographic_suites_supported: [
32 | JwaSignatureAlgorithm.EdDSA,
33 | JwaSignatureAlgorithm.ES256,
34 | ],
35 | display: [
36 | {
37 | name: "SD-JWT-VC",
38 | description: "DID holder binding",
39 | background_color: ANIMO_DARK_BACKGROUND,
40 | locale: "en",
41 | text_color: WHITE,
42 | },
43 | ],
44 | } as const satisfies OpenId4VciCredentialSupportedWithId;
45 |
46 | export const animoOpenId4VcPlaygroundCredentialSdJwtVcJwk = {
47 | id: "AnimoOpenId4VcPlaygroundSdJwtVcJwk",
48 | format: OpenId4VciCredentialFormatProfile.SdJwtVc,
49 | vct: "AnimoOpenId4VcPlayground",
50 | cryptographic_binding_methods_supported: ["jwk"],
51 | cryptographic_suites_supported: [
52 | JwaSignatureAlgorithm.EdDSA,
53 | JwaSignatureAlgorithm.ES256,
54 | ],
55 | display: [
56 | {
57 | name: "SD-JWT-VC",
58 | description: "JWK holder binding",
59 | background_color: ANIMO_DARK_BACKGROUND,
60 | locale: "en",
61 | text_color: WHITE,
62 | },
63 | ],
64 | } as const satisfies OpenId4VciCredentialSupportedWithId;
65 |
66 | export const animoOpenId4VcPlaygroundCredentialJwtVc = {
67 | id: "AnimoOpenId4VcPlaygroundJwtVc",
68 | format: OpenId4VciCredentialFormatProfile.JwtVcJson,
69 | types: ["AnimoOpenId4VcPlayground"],
70 | cryptographic_binding_methods_supported: ["did:key", "did:jwk"],
71 | cryptographic_suites_supported: [
72 | JwaSignatureAlgorithm.EdDSA,
73 | JwaSignatureAlgorithm.ES256,
74 | ],
75 | display: [
76 | {
77 | name: "JWT VC",
78 | background_color: ANIMO_DARK_BACKGROUND,
79 | locale: "en",
80 | text_color: WHITE,
81 | },
82 | ],
83 | } as const satisfies OpenId4VciCredentialSupportedWithId;
84 |
85 | export const animoOpenId4VcPlaygroundCredentialLdpVc = {
86 | id: "AnimoOpenId4VcPlaygroundLdpVc",
87 | format: OpenId4VciCredentialFormatProfile.LdpVc,
88 | types: ["AnimoOpenId4VcPlayground"],
89 | "@context": ["https://www.w3.org/2018/credentials/v1"],
90 | cryptographic_binding_methods_supported: ["did:key", "did:jwk"],
91 | cryptographic_suites_supported: [
92 | JwaSignatureAlgorithm.EdDSA,
93 | // TODO: is it needed that proof type is added here?
94 | // According to spec yes, but it's only used for the request proof,
95 | // which is a jwt/cwt (so alg)
96 | "Ed25519Signature2018",
97 | ],
98 | display: [
99 | {
100 | name: "LDP VC",
101 | background_color: ANIMO_DARK_BACKGROUND,
102 | locale: "en",
103 | text_color: WHITE,
104 | },
105 | ],
106 | } as const satisfies OpenId4VciCredentialSupportedWithId;
107 |
108 | export const credentialsSupported = [
109 | animoOpenId4VcPlaygroundCredentialSdJwtVcDid,
110 | animoOpenId4VcPlaygroundCredentialSdJwtVcJwk,
111 | animoOpenId4VcPlaygroundCredentialJwtVc,
112 | // Not really working yet
113 | // FIXME: Ed25519Signature2018 required ed25519 context url
114 | // but that is bullshit, as you can just use another verification
115 | // method to issue/verify such as JsonWebKey or MultiKey
116 | // animoOpenId4VcPlaygroundCredentialLdpVc,
117 | ] as const satisfies OpenId4VciCredentialSupportedWithId[];
118 | type CredentialSupportedId = (typeof credentialsSupported)[number]["id"];
119 | export const credentialSupportedIds = credentialsSupported.map((s) => s.id) as [
120 | CredentialSupportedId,
121 | ...CredentialSupportedId[]
122 | ];
123 |
--------------------------------------------------------------------------------
/agent/src/server.ts:
--------------------------------------------------------------------------------
1 | import { agent, openId4VciRouter, openId4VpRouter } from "./agent";
2 | import { apiRouter } from "./endpoints";
3 | import { createIssuer, doesIssuerExist, updateIssuer } from "./issuer";
4 | import { createVerifier, doesVerifierExist } from "./verifier";
5 | import express, { Response } from "express";
6 | import { getWebDidDocument, setupAllDids } from "./did";
7 | import cors from "cors";
8 |
9 | async function run() {
10 | await agent.initialize();
11 |
12 | if (!(await doesIssuerExist())) {
13 | await createIssuer();
14 | } else {
15 | // We update the issuer metadata on every startup to sync the static issuer metadata with the issuer metadata record
16 | await updateIssuer();
17 | }
18 |
19 | if (!(await doesVerifierExist())) {
20 | await createVerifier();
21 | }
22 |
23 | await setupAllDids();
24 |
25 | const app = express();
26 | app.use(cors({ origin: "*" }));
27 |
28 | app.use("/oid4vci", openId4VciRouter);
29 | app.use("/siop", openId4VpRouter);
30 | app.use("/api", apiRouter);
31 | app.use("/.well-known/did.json", async (_, response: Response) => {
32 | const didWeb = await getWebDidDocument();
33 | return response.json(didWeb.toJSON());
34 | });
35 |
36 | app.listen(3001, () =>
37 | agent.config.logger.info("app listening on port 3001")
38 | );
39 |
40 | // @ts-ignore
41 | app.use((err, _, res, __) => {
42 | console.error(err.stack);
43 | res.status(500).send("Something broke!");
44 | });
45 | }
46 |
47 | run();
48 |
--------------------------------------------------------------------------------
/agent/src/session.ts:
--------------------------------------------------------------------------------
1 | export interface OfferSessionMetadata {
2 | issuerDidMethod: string;
3 | }
4 |
--------------------------------------------------------------------------------
/agent/src/verifier.ts:
--------------------------------------------------------------------------------
1 | import { agent } from "./agent";
2 |
3 | const verifierId = "c01ea0f3-34df-41d5-89d1-50ef3d181855";
4 |
5 | export async function createVerifier() {
6 | return agent.modules.openId4VcVerifier.createVerifier({
7 | verifierId,
8 | });
9 | }
10 |
11 | export async function doesVerifierExist() {
12 | try {
13 | await agent.modules.openId4VcVerifier.getVerifierByVerifierId(verifierId);
14 | return true;
15 | } catch (error) {
16 | return false;
17 | }
18 | }
19 |
20 | export async function getVerifier() {
21 | return agent.modules.openId4VcVerifier.getVerifierByVerifierId(verifierId);
22 | }
23 |
--------------------------------------------------------------------------------
/agent/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "outDir": "dist",
5 | "module": "commonjs",
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "skipLibCheck": true
10 | },
11 | "include": ["src"]
12 | }
13 |
--------------------------------------------------------------------------------
/app/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_API_URL=http://localhost:3001
--------------------------------------------------------------------------------
/app/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/app/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx
2 |
3 | COPY ./out /usr/share/nginx/html
4 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf
5 |
6 | EXPOSE 80
--------------------------------------------------------------------------------
/app/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | body {
12 | color: rgb(var(--foreground-rgb));
13 | background: linear-gradient(
14 | to bottom,
15 | transparent,
16 | rgb(var(--background-end-rgb))
17 | )
18 | rgb(var(--background-start-rgb));
19 | }
20 |
--------------------------------------------------------------------------------
/app/app/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "OpenID4VC Playground",
9 | description: "By Animo",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) {
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/app/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Main } from "@/components/main";
2 |
3 | export default function Home() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "gray",
10 | "cssVariables": false
11 | },
12 | "aliases": {
13 | "utils": "@/lib/utils",
14 | "components": "@/components"
15 | }
16 | }
--------------------------------------------------------------------------------
/app/components/IssueTab.tsx:
--------------------------------------------------------------------------------
1 | import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
2 | import { Label } from "@/components/ui/label";
3 | import {
4 | SelectValue,
5 | SelectTrigger,
6 | SelectItem,
7 | SelectGroup,
8 | SelectContent,
9 | Select,
10 | } from "@/components/ui/select";
11 | import {
12 | TooltipProvider,
13 | Tooltip,
14 | TooltipTrigger,
15 | TooltipContent,
16 | } from "@/components/ui/tooltip";
17 | import QRCode from "react-qr-code";
18 | import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
19 | import { Button } from "@/components/ui/button";
20 | import { Card } from "@/components/ui/card";
21 | import { FormEvent, useEffect, useState } from "react";
22 | import { getIssuer, createOffer } from "../lib/api";
23 |
24 | export function IssueTab() {
25 | const [credentialType, setCredentialType] = useState();
26 | const [issuerDid, setIssuerDid] = useState();
27 | const [credentialOfferUri, setCredentialOfferUri] = useState();
28 | const [issuer, setIssuer] = useState<{
29 | credentialsSupported: Array<{
30 | id: string;
31 | display: [{ name: string; description?: string }];
32 | }>;
33 | availableDidMethods: string[];
34 | display: {};
35 | }>();
36 |
37 | useEffect(() => {
38 | getIssuer().then(setIssuer);
39 | }, []);
40 | async function onSubmitIssueCredential(e: FormEvent) {
41 | e.preventDefault();
42 | const _issuerDidMethod = issuerDid ?? issuer?.availableDidMethods[0];
43 | const _credentialType =
44 | credentialType ?? issuer?.credentialsSupported[0].id;
45 | if (!_issuerDidMethod || !_credentialType) {
46 | throw new Error("No issuer or credential type");
47 | }
48 |
49 | const offer = await createOffer({
50 | credentialSupportedId: _credentialType,
51 | issuerDidMethod: _issuerDidMethod,
52 | });
53 | setCredentialOfferUri(offer.credentialOffer);
54 | }
55 |
56 | return (
57 |
58 |
152 |
153 |
154 | Warning
155 |
156 | When using the{" "}
157 |
158 | Paradym Wallet
159 |
160 | , only issuance of JWT credentials (not SD-JWT credentials) using a
161 | did method other than did:cheqd
is supported.
162 |
163 |
164 |
165 | );
166 | }
167 |
--------------------------------------------------------------------------------
/app/components/ReceiveTab.tsx:
--------------------------------------------------------------------------------
1 | import { FormEvent, useState } from "react";
2 | import { receiveOffer } from "../lib/api";
3 | import { Label } from "@/components/ui/label";
4 | import { HighLight } from "@/components/highLight";
5 | import { Button } from "@/components/ui/button";
6 | import { Card } from "@/components/ui/card";
7 |
8 | export function ReceiveTab() {
9 | const [receiveCredentialOfferUri, setReceiveCredentialOfferUri] =
10 | useState();
11 | const [receivedCredentials, setReceivedCredentials] = useState();
12 |
13 | async function onSubmitReceiveOffer(e: FormEvent) {
14 | e.preventDefault();
15 | if (!receiveCredentialOfferUri) return;
16 |
17 | setReceivedCredentials(await receiveOffer(receiveCredentialOfferUri));
18 | }
19 |
20 | return (
21 |
22 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/app/components/VerifyTab.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | TooltipProvider,
3 | Tooltip,
4 | TooltipTrigger,
5 | TooltipContent,
6 | } from "@/components/ui/tooltip";
7 | import QRCode from "react-qr-code";
8 | import { Button } from "@/components/ui/button";
9 | import { Card } from "@/components/ui/card";
10 | import { FormEvent, useState } from "react";
11 | import { createRequest, getRequestStatus } from "../lib/api";
12 | import { useInterval } from "../lib/hooks";
13 | import { HighLight } from "./highLight";
14 | import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
15 | import { ExclamationTriangleIcon, CheckboxIcon } from "@radix-ui/react-icons";
16 | import { TypographyH4 } from "./ui/typography";
17 |
18 | export function VerifyTab() {
19 | const [authorizationRequestUri, setAuthorizationRequestUri] =
20 | useState();
21 | const [verificationSessionId, setVerificationSessionId] = useState();
22 | const [requestStatus, setRequestStatus] = useState<{
23 | verificationSessionId: string;
24 | responseStatus:
25 | | "RequestCreated"
26 | | "RequestUriRetrieved"
27 | | "ResponseVerified"
28 | | "Error";
29 | error?: string;
30 | submission?: Record;
31 | definition?: Record;
32 | presentations?: Array>;
33 | }>();
34 |
35 | const enabled =
36 | verificationSessionId !== undefined &&
37 | requestStatus?.responseStatus !== "ResponseVerified" &&
38 | requestStatus?.responseStatus !== "Error";
39 |
40 | const authorizationRequestUriHasBeenFetched =
41 | requestStatus?.responseStatus === "RequestUriRetrieved";
42 | const hasResponse = requestStatus?.responseStatus === "ResponseVerified";
43 | const isSuccess = requestStatus?.responseStatus === "ResponseVerified";
44 |
45 | useInterval({
46 | callback: async () => {
47 | if (!verificationSessionId) return;
48 |
49 | const requestStatus = await getRequestStatus({ verificationSessionId });
50 | setRequestStatus(requestStatus);
51 | },
52 | interval: 500,
53 | enabled,
54 | });
55 |
56 | async function onSubmitCreateRequest(e: FormEvent) {
57 | e.preventDefault();
58 |
59 | // Clear state
60 | setAuthorizationRequestUri(undefined);
61 | setVerificationSessionId(undefined);
62 | setRequestStatus(undefined);
63 |
64 | const request = await createRequest({
65 | presentationDefinition: {
66 | id: crypto.randomUUID(),
67 | // TODO: show this in the paradym wallet
68 | name: "Animo Playground Credential",
69 | input_descriptors: [
70 | {
71 | id: crypto.randomUUID(),
72 | constraints: {
73 | // FIXME: pex we should always do limit disclosure if it's possible
74 | // FIXME: limit_disclosure is not working
75 | limit_disclosure: "preferred",
76 | fields: [
77 | {
78 | path: [
79 | "$.playground.framework",
80 | "$.credentialSubject.playground.framework",
81 | "$.vc.credentialSubject.playground.framework",
82 | ],
83 | filter: {
84 | type: "string",
85 | const: "Aries Framework JavaScript",
86 | },
87 | },
88 | ],
89 | },
90 | name: "Animo Playground Credential",
91 | purpose: "Just for fun",
92 | },
93 | ],
94 | },
95 | });
96 |
97 | setVerificationSessionId(request.verificationSessionId);
98 | setAuthorizationRequestUri(request.authorizationRequestUri);
99 | }
100 |
101 | return (
102 |
103 |
104 | {!hasResponse && (
105 |
106 | {authorizationRequestUri ? (
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
116 | navigator.clipboard.writeText(
117 | e.currentTarget.innerText
118 | )
119 | }
120 | className="text-gray-500 break-all cursor-pointer"
121 | >
122 | {authorizationRequestUri}
123 |
124 |
125 |
126 |
127 |
128 | Click to copy
129 |
130 |
131 |
132 | ) : authorizationRequestUriHasBeenFetched ? (
133 |
134 | Authorization request has been retrieved. Waiting for
135 | response...
136 |
137 | ) : (
138 |
139 | Authorization request will be displayed here
140 |
141 | )}
142 |
143 | )}
144 | {hasResponse && (
145 |
146 |
147 | {isSuccess ? (
148 |
149 | ) : (
150 |
151 | )}
152 |
153 | {isSuccess
154 | ? "Verification Successful"
155 | : "Verification Unsuccessful"}
156 |
157 | {!isSuccess && (
158 |
159 | {requestStatus?.error ?? "Unknown error occurred"}
160 |
161 | )}
162 |
163 |
164 | Presentation Definition
165 |
169 |
170 |
171 | Presentation Submission
172 |
176 |
177 |
178 | Presentations
179 |
183 |
184 |
185 | )}
186 |
191 | Verify Credential
192 |
193 |
194 |
195 | );
196 | }
197 |
--------------------------------------------------------------------------------
/app/components/highLight.module.css:
--------------------------------------------------------------------------------
1 | .line {
2 | text-align: left;
3 | padding: 0.5em;
4 | line-height: 1.3;
5 | font-size: 0.8rem;
6 | font-family: "JetBrains Mono", monospace;
7 | overflow-x: auto;
8 | }
9 |
10 | .lineNumber {
11 | display: inline-block;
12 | width: 2em;
13 | user-select: none;
14 | opacity: 0.3;
15 | }
16 |
17 | pre.language-json,
18 | code.language-json {
19 | white-space: normal;
20 | overflow: auto;
21 | word-break: break-word;
22 | }
23 |
--------------------------------------------------------------------------------
/app/components/highLight.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Highlight, themes } from "prism-react-renderer";
3 | import { cn } from "../lib/utils";
4 | import styles from "./highLight.module.css";
5 |
6 | export const HighLight = ({
7 | code,
8 | language,
9 | }: {
10 | code: string;
11 | language: string;
12 | }) => (
13 |
14 | {({ className, style, tokens, getLineProps, getTokenProps }) => (
15 |
19 | {tokens.map((line, i) => (
20 |
21 | {i + 1}
22 | {line.map((token, key) => (
23 |
28 | ))}
29 |
30 | ))}
31 |
32 | )}
33 |
34 | );
35 |
--------------------------------------------------------------------------------
/app/components/main.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | /**
3 | * This code was generated by v0 by Vercel.
4 | * @see https://v0.dev/t/yoUa0S8t4oT
5 | */
6 | import { TabsTrigger, TabsList, TabsContent, Tabs } from "@/components/ui/tabs";
7 | import { IssueTab } from "./IssueTab";
8 | import { ReceiveTab } from "./ReceiveTab";
9 | import { VerifyTab } from "./VerifyTab";
10 |
11 | export function Main() {
12 | return (
13 | <>
14 |
15 |
16 |
17 | {/* eslint-disable-next-line @next/next/no-img-element */}
18 |
28 |
29 |
30 | Playground
31 |
32 |
33 |
34 |
35 |
36 | Issue
37 | Receive
38 | Verify
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | >
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/app/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border border-gray-200 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-gray-950",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-white text-gray-950",
12 | destructive: "border-red-500/50 text-red-500 [&>svg]:text-red-500",
13 | success: "border-green-500/50 text-green-500 [&>svg]:text-green-500",
14 | warning: "border-orange-500/50 text-orange-500 [&>svg]:text-orange-500",
15 | },
16 | },
17 | defaultVariants: {
18 | variant: "default",
19 | },
20 | }
21 | );
22 |
23 | const Alert = React.forwardRef<
24 | HTMLDivElement,
25 | React.HTMLAttributes & VariantProps
26 | >(({ className, variant, ...props }, ref) => (
27 |
33 | ));
34 | Alert.displayName = "Alert";
35 |
36 | const AlertTitle = React.forwardRef<
37 | HTMLParagraphElement,
38 | React.HTMLAttributes
39 | >(({ className, ...props }, ref) => (
40 |
45 | ));
46 | AlertTitle.displayName = "AlertTitle";
47 |
48 | const AlertDescription = React.forwardRef<
49 | HTMLParagraphElement,
50 | React.HTMLAttributes
51 | >(({ className, ...props }, ref) => (
52 |
57 | ));
58 | AlertDescription.displayName = "AlertDescription";
59 |
60 | export { Alert, AlertTitle, AlertDescription };
61 |
--------------------------------------------------------------------------------
/app/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/app/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/app/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/app/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/app/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/app/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/app/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/app/components/ui/typography.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from "react";
2 |
3 | export function TypographyH1({ children }: PropsWithChildren) {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | }
10 |
11 | export function TypographyH2({ children }: PropsWithChildren) {
12 | return (
13 |
14 | {children}
15 |
16 | );
17 | }
18 |
19 | export function TypographyH3({ children }: PropsWithChildren) {
20 | return (
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
27 | export function TypographyH4({ children }: PropsWithChildren) {
28 | return (
29 |
30 | {children}
31 |
32 | );
33 | }
34 |
35 | export function TypographyP({ children }: PropsWithChildren) {
36 | return {children}
;
37 | }
38 |
--------------------------------------------------------------------------------
/app/lib/api.ts:
--------------------------------------------------------------------------------
1 | import { NEXT_PUBLIC_API_URL } from "./constants";
2 |
3 | export async function createOffer({
4 | credentialSupportedId,
5 | issuerDidMethod,
6 | }: {
7 | credentialSupportedId: string;
8 | issuerDidMethod: string;
9 | }) {
10 | const response = await fetch(`${NEXT_PUBLIC_API_URL}/api/offers/create`, {
11 | method: "POST",
12 | headers: {
13 | "Content-Type": "application/json",
14 | },
15 | body: JSON.stringify({
16 | credentialSupportedIds: [credentialSupportedId],
17 | issuerDidMethod,
18 | }),
19 | });
20 |
21 | if (!response.ok) {
22 | throw new Error("Failed to create offer");
23 | }
24 |
25 | return response.json();
26 | }
27 |
28 | export async function getIssuer() {
29 | const response = await fetch(`${NEXT_PUBLIC_API_URL}/api/issuer`);
30 |
31 | if (!response.ok) {
32 | throw new Error("Failed to get issuer");
33 | }
34 |
35 | return response.json();
36 | }
37 |
38 | export async function receiveOffer(offerUri: string) {
39 | const response = await fetch(`${NEXT_PUBLIC_API_URL}/api/offers/receive`, {
40 | headers: {
41 | "Content-Type": "application/json",
42 | },
43 | method: "POST",
44 | body: JSON.stringify({
45 | credentialOfferUri: offerUri,
46 | }),
47 | });
48 |
49 | if (!response.ok) {
50 | throw new Error("Failed to receive offer");
51 | }
52 |
53 | return response.json();
54 | }
55 |
56 | export async function createRequest({
57 | presentationDefinition,
58 | }: {
59 | presentationDefinition: any;
60 | }) {
61 | const response = await fetch(`${NEXT_PUBLIC_API_URL}/api/requests/create`, {
62 | method: "POST",
63 | headers: {
64 | "Content-Type": "application/json",
65 | },
66 | body: JSON.stringify({
67 | presentationDefinition,
68 | }),
69 | });
70 |
71 | if (!response.ok) {
72 | throw new Error("Failed to create request");
73 | }
74 |
75 | return response.json();
76 | }
77 |
78 | export async function getRequestStatus({
79 | verificationSessionId,
80 | }: {
81 | verificationSessionId: string;
82 | }) {
83 | const response = await fetch(
84 | `${NEXT_PUBLIC_API_URL}/api/requests/${verificationSessionId}`,
85 | {
86 | method: "GET",
87 | headers: {
88 | "Content-Type": "application/json",
89 | },
90 | }
91 | );
92 |
93 | if (!response.ok) {
94 | throw new Error("Failed to get request status");
95 | }
96 |
97 | return response.json();
98 | }
99 |
100 | export async function receiveRequest(requestUri: string) {
101 | const response = await fetch(`${NEXT_PUBLIC_API_URL}/api/requests/receive`, {
102 | headers: {
103 | "Content-Type": "application/json",
104 | },
105 | method: "POST",
106 | body: JSON.stringify({
107 | authorizationRequestUri: requestUri,
108 | }),
109 | });
110 |
111 | if (!response.ok) {
112 | throw new Error("Failed to receive request");
113 | }
114 |
115 | return response.json();
116 | }
117 |
--------------------------------------------------------------------------------
/app/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const NEXT_PUBLIC_API_URL =
2 | process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
3 |
--------------------------------------------------------------------------------
/app/lib/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | type Callback = () => void | Promise;
4 |
5 | export const useInterval = ({
6 | callback,
7 | interval,
8 | enabled = true,
9 | }: {
10 | callback: Callback;
11 | interval: number;
12 | enabled?: boolean;
13 | }) => {
14 | const savedCallback = useRef();
15 |
16 | useEffect(() => {
17 | savedCallback.current = callback;
18 | }, [callback]);
19 |
20 | useEffect(() => {
21 | if (!enabled) return;
22 | let id = setInterval(() => savedCallback.current?.(), interval);
23 | return () => clearInterval(id);
24 | }, [interval, enabled]);
25 | };
26 |
--------------------------------------------------------------------------------
/app/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/app/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | output: "export",
4 | env: {
5 | NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/app/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name localhost;
4 | root /usr/share/nginx/html;
5 | index index.html index.htm;
6 |
7 | location / {
8 | try_files $uri $uri/ /index.html;
9 | }
10 |
11 | # Any route containing a file extension (e.g. /devicesfile.js)
12 | location ~ ^.+\..+$ {
13 | try_files $uri =404;
14 | }
15 | }
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@radix-ui/react-icons": "^1.3.0",
13 | "@radix-ui/react-label": "^2.0.2",
14 | "@radix-ui/react-select": "^2.0.0",
15 | "@radix-ui/react-slot": "^1.0.2",
16 | "@radix-ui/react-tabs": "^1.0.4",
17 | "@radix-ui/react-tooltip": "^1.0.7",
18 | "class-variance-authority": "^0.7.0",
19 | "clsx": "^2.1.0",
20 | "lucide-react": "^0.309.0",
21 | "next": "14.0.4",
22 | "prism-react-renderer": "^2.3.1",
23 | "react": "^18.2.0",
24 | "react-dom": "^18.2.0",
25 | "react-qr-code": "^2.0.12",
26 | "tailwind-merge": "^2.2.1",
27 | "tailwindcss-animate": "^1.0.7"
28 | },
29 | "devDependencies": {
30 | "@types/node": "^20.11.7",
31 | "@types/react": "^18.2.48",
32 | "@types/react-dom": "^18.2.18",
33 | "autoprefixer": "^10.4.17",
34 | "eslint": "^8.56.0",
35 | "eslint-config-next": "14.0.4",
36 | "postcss": "^8.4.33",
37 | "tailwindcss": "^3.4.1",
38 | "typescript": "^5.3.3"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/app/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import colors from "tailwindcss/colors";
3 |
4 | const config: Config = {
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | darkMode: "class",
11 | theme: {
12 | colors: {
13 | ...colors,
14 | background: "white",
15 | card: "white",
16 | // "card-foreground": "black",
17 | "primary-foreground": "white",
18 | primary: "black",
19 | popover: "white",
20 | // 'green-500': ''
21 | },
22 | extend: {
23 | backgroundImage: {
24 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
25 | "gradient-conic":
26 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
27 | },
28 | },
29 | },
30 | plugins: [require("tailwindcss-animate")],
31 | };
32 | export default config;
33 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/assets/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/animo/openid4vc-playground/9e56b2f432998c3c0540ccf0809a8a0184e72132/assets/preview.png
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 |
3 | services:
4 | server:
5 | image: ghcr.io/animo/openid4vc-playground/agent:main
6 | deploy:
7 | placement:
8 | constraints:
9 | - node.role == worker
10 | - node.labels.type == community
11 | labels:
12 | traefik.enable: "true"
13 |
14 | traefik.http.routers.openid4vc-server.rule: Host(`openid4vc.animo.id`) && (PathPrefix(`/api`) || PathPrefix(`/oid4vci`) || PathPrefix(`/siop`) || PathPrefix(`/.well-known`))
15 | traefik.http.routers.openid4vc-server.entrypoints: web-secure
16 | traefik.http.routers.openid4vc-server.tls.certresolver: zerossl
17 | traefik.http.routers.openid4vc-server.service: openid4vc-server-service
18 | traefik.http.services.openid4vc-server-service.loadbalancer.server.port: 3001
19 |
20 | environment:
21 | AGENT_WALLET_KEY: ${AGENT_WALLET_KEY}
22 | DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED: ${DID_INDY_INDICIO_TESTNET_PUBLIC_DID_SEED}
23 | CHEQD_TESTNET_COSMOS_PAYER_SEED: ${CHEQD_TESTNET_COSMOS_PAYER_SEED}
24 | ED25519_SEED: ${ED25519_SEED}
25 | P256_SEED: ${P256_SEED}
26 | AGENT_HOST: "https://openid4vc.animo.id"
27 |
28 | networks:
29 | - traefik
30 |
31 | ports:
32 | - 5001
33 |
34 | client:
35 | image: ghcr.io/animo/openid4vc-playground/app:main
36 | deploy:
37 | placement:
38 | constraints:
39 | - node.role == worker
40 | - node.labels.type == community
41 | labels:
42 | traefik.enable: "true"
43 | traefik.http.routers.openid4vc-app.rule: Host(`openid4vc.animo.id`) && !PathPrefix(`/oid4vci`) && !PathPrefix(`/siop`) && !PathPrefix(`/api`) && !PathPrefix(`/.well-known`)
44 | traefik.http.routers.openid4vc-app.entrypoints: web-secure
45 | traefik.http.routers.openid4vc-app.tls.certresolver: zerossl
46 | traefik.http.routers.openid4vc-app.service: openid4vc-app-service
47 | traefik.http.services.openid4vc-app-service.loadbalancer.server.port: 80
48 | update_config:
49 | monitor: 30s
50 | delay: 10s
51 | order: start-first
52 | ports:
53 | - 80
54 | networks:
55 | - traefik
56 |
57 | networks:
58 | traefik:
59 | external: true
60 | name: traefik
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openid4vc-playground"
3 | }
4 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - agent
3 | - app
4 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------