From 0b0b38ba288188a4c2f2e94c595c5ac9517d5562 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Thu, 29 Feb 2024 18:20:03 +0100 Subject: [PATCH 01/49] Add base endpoints for Stripe integration --- README.md | 7 + docker/Dockerfile | 8 + package-lock.json | 18 +- package.json | 3 +- src/app.ts | 19 ++ src/controllers/portal-customer.ts.ts | 31 +++ src/controllers/prices.ts | 54 +++++ src/controllers/product.ts | 120 +++++++++++ src/controllers/subscriptions.ts | 298 ++++++++++++++++++++++++++ src/types/environment.d.ts | 4 + src/types/portal.ts | 100 +++++++++ 11 files changed, 659 insertions(+), 3 deletions(-) create mode 100644 src/controllers/portal-customer.ts.ts create mode 100644 src/controllers/prices.ts create mode 100644 src/controllers/product.ts create mode 100644 src/controllers/subscriptions.ts create mode 100644 src/types/portal.ts diff --git a/README.md b/README.md index cb8842b2..adeff22f 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,13 @@ some tokens on the testnet for making the process simpler. 2. `FAUCET_URI`: Faucet service API endpoint (Default: `https://faucet-api.cheqd.network/credit`) 3. `TESTNET_MINIMUM_BALANCE`: Minimum balance on account before it is automatically topped up from the faucet. This value should be expressed as an integer in `CHEQ` tokens, which will then be converted in the background to `ncheq` denomination. Account balance check is carried out on every account creation/login. (Default: 10,000 CHEQ testnet tokens) +#### Stripe integration + +The application supports Stripe integration for payment processing. + +1. `STRIPE_SECRET_KEY`: Secret key for Stripe API. Please, keep it secret on deploying +2. `STRIPE_PUBLISHABLE_KEY` - Publishable key for Stripe API. + ### 3rd Party Connectors The app supports 3rd party connectors for credential storage and delivery. diff --git a/docker/Dockerfile b/docker/Dockerfile index e992bd46..9f7f3c8a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -81,6 +81,10 @@ ARG ENABLE_ACCOUNT_TOPUP=false ARG FAUCET_URI=https://faucet-api.cheqd.network/credit ARG TESTNET_MINIMUM_BALANCE=1000 +# Stripe +ARG STRIPE_SECRET_KEY +ARG STRIPE_PUBLISHABLE_KEY + # Environment variables: base configuration ENV NPM_CONFIG_LOGLEVEL ${NPM_CONFIG_LOGLEVEL} ENV PORT ${PORT} @@ -125,6 +129,10 @@ ENV POLYGON_RPC_URL ${POLYGON_RPC_URL} ENV VERIDA_PRIVATE_KEY ${VERIDA_PRIVATE_KEY} ENV POLYGON_PRIVATE_KEY ${POLYGON_PRIVATE_KEY} +# Environment variables: Stripe +ENV STRIPE_SECRET_KEY ${STRIPE_SECRET_KEY} +ENV STRIPE_PUBLISHABLE_KEY ${STRIPE_PUBLISHABLE_KEY} + # Set ownership permissions RUN chown -R node:node /home/node/app diff --git a/package-lock.json b/package-lock.json index b5a1f66a..ea2cc887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cheqd/credential-service", - "version": "2.18.0-develop.1", + "version": "2.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cheqd/credential-service", - "version": "2.18.0-develop.1", + "version": "2.18.0", "license": "Apache-2.0", "dependencies": { "@cheqd/did-provider-cheqd": "^3.7.0", @@ -53,9 +53,11 @@ "pg-connection-string": "^2.6.2", "secp256k1": "^5.0.0", "sqlite3": "^5.1.7", + "stripe": "^14.18.0", "swagger-ui-dist": "5.10.5", "swagger-ui-express": "^5.0.0", "typeorm": "^0.3.20", + "uint8arrays": "^5.0.2", "uri-js": "^4.4.1" }, "devDependencies": { @@ -30235,6 +30237,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "14.18.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.18.0.tgz", + "integrity": "sha512-yLqKPqYgGJbMxrQiE4+i2i00ZVA2NRIZbZ1rejzj5XR3F3Uc+1iy9QE133knZudhVGMw367b8vTpB8D9pYMETw==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/strnum": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", diff --git a/package.json b/package.json index 5dde656f..44082157 100644 --- a/package.json +++ b/package.json @@ -94,8 +94,9 @@ "pg-connection-string": "^2.6.2", "secp256k1": "^5.0.0", "sqlite3": "^5.1.7", - "swagger-ui-express": "^5.0.0", + "stripe": "^14.18.0", "swagger-ui-dist": "5.10.5", + "swagger-ui-express": "^5.0.0", "typeorm": "^0.3.20", "uint8arrays": "^5.0.2", "uri-js": "^4.4.1" diff --git a/src/app.ts b/src/app.ts index f5580293..675742b9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -26,6 +26,9 @@ import { KeyController } from './controllers/key.js'; import { DIDController } from './controllers/did.js'; import { ResourceController } from './controllers/resource.js'; import { FailedResponseTracker } from './middleware/event-tracker.js'; +import { ProductController } from './controllers/product.js'; +import { SubscriptionController } from './controllers/subscriptions.js'; +import { PriceController } from './controllers/prices.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -206,6 +209,22 @@ class App { express.static(path.join(process.cwd(), '/dist'), { extensions: ['js'], index: false }) ); + // Portal + // Product + app.get('/portal/product/list', ProductController.productListValidator, new ProductController().getListProducts); + app.get('/portal/product/:productId', ProductController.productGetValidator, new ProductController().getProduct); + + // Prices + app.get('/portal/price/list', PriceController.priceListValidator, new PriceController().getListPrices); + + // Subscription + app.post('/portal/subscription/create', SubscriptionController.subscriptionCreateValidator, new SubscriptionController().create); + app.post('/portal/subscription/update', SubscriptionController.subscriptionUpdateValidator, new SubscriptionController().update); + app.get('/portal/subscription/get/:subscriptionId', SubscriptionController.subscriptionGetValidator, new SubscriptionController().get); + app.get('/portal/subscription/list', SubscriptionController.subscriptionListValidator, new SubscriptionController().list); + app.delete('/portal/subscription/cancel', SubscriptionController.subscriptionCancelValidator, new SubscriptionController().cancel); + app.post('/portal/subscription/resume', SubscriptionController.subscriptionResumeValidator, new SubscriptionController().resume); + // 404 for all other requests app.all('*', (_req, res) => res.status(StatusCodes.BAD_REQUEST).send('Bad request')); } diff --git a/src/controllers/portal-customer.ts.ts b/src/controllers/portal-customer.ts.ts new file mode 100644 index 00000000..aa63e846 --- /dev/null +++ b/src/controllers/portal-customer.ts.ts @@ -0,0 +1,31 @@ +import * as dotenv from 'dotenv'; +import type { Request, Response } from 'express'; +import { check } from 'express-validator'; +import { validationResult } from './validator'; +import type { PortalCustomerGetUnsuccessfulResponseBody } from '../types/portal'; + +dotenv.config(); + +export class PortalCustomerController { + static portalCustomerGetValidator = [ + check('logToUserId') + .optional() + .isString() + .withMessage('logToUserId should be a string') + .bail(), + ]; + + async get(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(400).json({ + error: result.array().pop()?.msg + } satisfies PortalCustomerGetUnsuccessfulResponseBody); + } + + return response.status(500).json({ + error: "Not implemented yet" + }) + } +} \ No newline at end of file diff --git a/src/controllers/prices.ts b/src/controllers/prices.ts new file mode 100644 index 00000000..6ab5a199 --- /dev/null +++ b/src/controllers/prices.ts @@ -0,0 +1,54 @@ +import { Stripe } from 'stripe'; +import type { Request, Response } from 'express'; +import * as dotenv from 'dotenv'; +import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../types/portal.js'; +import { StatusCodes } from 'http-status-codes'; +import { validationResult } from './validator/index.js'; +import { check } from 'express-validator'; + +dotenv.config(); + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + +export class PriceController { + + static priceListValidator = [ + check('productId') + .optional() + .isString() + .withMessage('productId should be a string') + .bail(), + ]; + + async getListPrices(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies PriceListUnsuccessfulResponseBody); + } + // Get query parameters + const productId = request.query.productId; + + try { + // Fetch the list of prices + const prices = productId + ? await stripe.prices.list({ + product: productId as string, + active: true, + }) + : await stripe.prices.list({ + active: true, + }) + + return response.status(StatusCodes.OK).json({ + prices: prices + } satisfies PriceListResponseBody); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies PriceListUnsuccessfulResponseBody); + } + } +} \ No newline at end of file diff --git a/src/controllers/product.ts b/src/controllers/product.ts new file mode 100644 index 00000000..5ada6dde --- /dev/null +++ b/src/controllers/product.ts @@ -0,0 +1,120 @@ +import { Stripe } from 'stripe'; +import type { Request, Response } from 'express'; +import * as dotenv from 'dotenv'; +import type { ProductGetResponseBody, ProductGetUnsuccessfulResponseBody, ProductListResponseBody, ProductListUnsuccessfulResponseBody, ProductWithPrices } from '../types/portal.js'; +import { StatusCodes } from 'http-status-codes'; +import { validationResult } from './validator/index.js'; +import { check } from 'express-validator'; + +dotenv.config(); + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + +export class ProductController { + + static productListValidator = [ + check('prices') + .optional() + .isBoolean() + .withMessage('prices should be a boolean') + .bail(), + ]; + + static productGetValidator = [ + check('productId') + .exists() + .withMessage('productId was not provided') + .bail(), + check('prices') + .optional() + .isBoolean() + .withMessage('prices should be a boolean') + .bail(), + ]; + + async getListProducts(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies ProductListUnsuccessfulResponseBody); + } + // Get query parameters + const prices = request.query.prices === 'false' ? false : true; + + try { + const products = await stripe.products.list({ + active: true, + }) as Stripe.ApiList; + + // If no products found return 404 + if (!products.data) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: 'Seems like there no any active products on Stripe side. Please add some.' + } satisfies ProductListUnsuccessfulResponseBody); + } + + if (prices) { + for (const product of products.data) { + const prices = await stripe.prices.list({ + product: product.id, + active: true, + }); + product.prices = prices.data; + } + } + + return response.status(StatusCodes.OK).json({ + products: products + } satisfies ProductListResponseBody); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies ProductListUnsuccessfulResponseBody); + } + } + + async getProduct(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies ProductGetUnsuccessfulResponseBody); + } + // Get query parameters + const prices = request.query.prices === 'false' ? false : true; + const productId = request.params.productId as string; + + try { + // Get the product + const product = await stripe.products.retrieve(productId) as ProductWithPrices; + + // If no product found return 404 + if (!product) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: 'No product found with id: ' + productId + } satisfies ProductGetUnsuccessfulResponseBody); + } + + if (prices) { + const prices = await stripe.prices.list({ + product: product.id, + active: true, + }); + if (prices.data) { + product.prices = prices.data; + } + } + + return response.status(StatusCodes.OK).json({ + product: product + } satisfies ProductGetResponseBody); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies ProductGetUnsuccessfulResponseBody); + } + } +} \ No newline at end of file diff --git a/src/controllers/subscriptions.ts b/src/controllers/subscriptions.ts new file mode 100644 index 00000000..7fe948bf --- /dev/null +++ b/src/controllers/subscriptions.ts @@ -0,0 +1,298 @@ +import { Stripe } from 'stripe'; +import type { Request, Response } from 'express'; +import * as dotenv from 'dotenv'; +import type { SubscriptionCreateRequestBody, SubscriptionCreateResponseBody, SubscriptionCreateUnsuccessfulResponseBody, SubscriptionCancelResponseBody, SubscriptionCancelUnsuccessfulResponseBody, SubscriptionGetResponseBody, SubscriptionGetUnsuccessfulResponseBody, SubscriptionListResponseBody, SubscriptionListUnsuccessfulResponseBody, SubscriptionUpdateRequestBody, SubscriptionUpdateResponseBody, SubscriptionUpdateUnsuccessfulResponseBody, SubscriptionResumeUnsuccessfulResponseBody, SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, SubscriptionCancelRequestBody } from '../types/portal.js'; +import { StatusCodes } from 'http-status-codes'; +import { validationResult } from './validator/index.js'; +import { check } from 'express-validator'; + +dotenv.config(); + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + +export class SubscriptionController { + + static subscriptionCreateValidator = [ + check('customerId') + .exists() + .withMessage('customerId was not provided') + .bail(), + check('items') + .exists() + .withMessage('items was not provided') + .bail() + .isArray() + .withMessage('items should be an array') + .bail(), + check('items.*.price') + .exists() + .withMessage('price was not provided') + .bail() + .isString() + .withMessage('price should be a string') + .bail(), + check('idempotencyKey') + .optional() + .isString() + .withMessage('idempotencyKey should be a string') + .bail(), + ]; + + static subscriptionUpdateValidator = [ + check('subscriptionId') + .exists() + .withMessage('subscriptionId was not provided') + .bail(), + check('updateParams') + .exists() + .withMessage('updateParams was not provided') + .bail(), + check('idempotencyKey') + .optional() + .isString() + .withMessage('idempotencyKey should be a string') + .bail(), + ]; + + static subscriptionGetValidator = [ + check('subscriptionId') + .exists() + .withMessage('subscriptionId was not provided') + .bail() + .isString() + .withMessage('subscriptionId should be a string') + .bail(), + ]; + + static subscriptionListValidator = [ + check('customerId') + .optional() + .isString() + .withMessage('customerId should be a string') + .bail(), + ]; + + static subscriptionCancelValidator = [ + check('subscriptionId') + .exists() + .withMessage('subscriptionId was not provided') + .bail() + .isString() + .withMessage('subscriptionId should be a string') + .bail(), + check('idempotencyKey') + .optional() + .isString() + .withMessage('idempotencyKey should be a string') + .bail(), + ]; + + static subscriptionResumeValidator = [ + check('subscriptionId') + .exists() + .withMessage('subscriptionId was not provided') + .bail() + .isString() + .withMessage('subscriptionId should be a string') + .bail(), + check('idempotencyKey') + .optional() + .isString() + .withMessage('idempotencyKey should be a string') + .bail(), + ]; + + async create(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + + const { customerId, items, idempotencyKey } = request.body satisfies SubscriptionCreateRequestBody; + try { + // Create the subscription + const subscription = await stripe.subscriptions.create({ + customer: customerId, + items: items, + }, + { + idempotencyKey: idempotencyKey, + }); + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscription was not created`, + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscription: subscription + } satisfies SubscriptionCreateResponseBody ); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + } + + async update(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies SubscriptionUpdateUnsuccessfulResponseBody); + } + + const { subscriptionId, updateParams, idempotencyKey} = request.body satisfies SubscriptionUpdateRequestBody; + try { + // Update the subscription + const subscription = await stripe.subscriptions.update( + subscriptionId, + updateParams, + { + idempotencyKey: idempotencyKey, + }); + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscription was not updated`, + } satisfies SubscriptionUpdateUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscription: subscription + } satisfies SubscriptionUpdateResponseBody); + + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies SubscriptionUpdateUnsuccessfulResponseBody); + } + } + + public async list(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies SubscriptionListUnsuccessfulResponseBody); + } + const customerId = request.query.customerId; + try { + // Get the subscriptions + const subscriptions = customerId + ? await stripe.subscriptions.list({ + customer: customerId as string, + }) + : await stripe.subscriptions.list(); + + if (subscriptions.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscriptions were not found`, + } satisfies SubscriptionListUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscriptions: subscriptions + } satisfies SubscriptionListResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies SubscriptionListUnsuccessfulResponseBody); + } + } + + async get(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies SubscriptionGetUnsuccessfulResponseBody); + } + const subscriptionId = request.query.subscriptionId; + try { + // Get the subscription + const subscription = await stripe.subscriptions.retrieve(subscriptionId as string); + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscription was not found`, + } satisfies SubscriptionGetUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscription: subscription + } satisfies SubscriptionGetResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies SubscriptionGetUnsuccessfulResponseBody); + } + } + + async cancel(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies SubscriptionCancelUnsuccessfulResponseBody); + } + const {subscriptionId, idempotencyKey } = request.body satisfies SubscriptionCancelRequestBody; + + try { + // Cancel the subscription + const subscription = await stripe.subscriptions.cancel( + subscriptionId as string, + { + idempotencyKey: idempotencyKey + }); + + // Check if the subscription was cancelled + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscription was not deleted`, + } satisfies SubscriptionCancelUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscription: subscription + } satisfies SubscriptionCancelResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies SubscriptionCancelUnsuccessfulResponseBody); + } + } + + async resume(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies SubscriptionResumeUnsuccessfulResponseBody); + } + const { subscriptionId, idempotencyKey } = request.body satisfies SubscriptionResumeRequestBody; + try { + // Resume the subscription + const subscription = await stripe.subscriptions.resume( + subscriptionId as string, + { + idempotencyKey: idempotencyKey + }); + + // Check if the subscription was resumed + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscription was not resumed`, + } satisfies SubscriptionResumeUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscription: subscription + } satisfies SubscriptionResumeResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies SubscriptionResumeUnsuccessfulResponseBody); + } + } +} \ No newline at end of file diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index 49bd71c6..f29c106e 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -56,6 +56,10 @@ declare global { // Creds CREDS_DECRYPTION_SECRET: string; + + // Stripe + STRIPE_SECRET_KEY: string; + STRIPE_PUBLISHABLE_KEY: string; } } diff --git a/src/types/portal.ts b/src/types/portal.ts new file mode 100644 index 00000000..e4de0863 --- /dev/null +++ b/src/types/portal.ts @@ -0,0 +1,100 @@ +import type Stripe from "stripe"; +import type { UnsuccessfulResponseBody } from "./shared.js"; + +export type ProductWithPrices = Stripe.Product & { + prices?: Stripe.Price[]; +}; + +export type ProductListUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type ProductGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; + + +export type ProductListResponseBody = { + products: Stripe.ApiList; +} + +export type ProductGetResponseBody = { + product: ProductWithPrices; +} + +// Prices +// List +export type PriceListResponseBody = { + prices: Stripe.ApiList; +} +export type PriceListUnsuccessfulResponseBody = UnsuccessfulResponseBody; + +// Subscription +// Create +export type SubscriptionCreateRequestBody = { + customerId: string; + items: [{ price: string }]; + idempotencyKey?: string; +} + +export type SubscriptionCreateResponseBody = { + subscription: Stripe.Response; +} + +// Update +export type SubscriptionUpdateRequestBody = { + subscriptionId: string; + updateParams: Stripe.SubscriptionUpdateParams; + idempotencyKey?: string; + +} + +export type SubscriptionUpdateResponseBody = { + subscription: Stripe.Response; +} + +// Get +export type SubscriptionGetRequestBody = { + subscriptionId: string; +} + +export type SubscriptionGetResponseBody = { + subscription: Stripe.Response; +} + +// List +export type SubscriptionListRequestBody = { + customerId: string; +} + +export type SubscriptionListResponseBody = { + subscriptions: Stripe.Response>; +} + +// Delete +export type SubscriptionCancelRequestBody = { + subscriptionId: string; +} + +export type SubscriptionCancelResponseBody = { + subscription: Stripe.Subscription; + idempotencyKey?: string; +} + +//Resume + +export type SubscriptionResumeRequestBody = { + subscriptionId: string; + idempotencyKey?: string; +} + +export type SubscriptionResumeResponseBody = { + subscription: Stripe.Response; +} + +export type SubscriptionCreateUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type SubscriptionListUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type SubscriptionGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type SubscriptionUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type SubscriptionCancelUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type SubscriptionResumeUnsuccessfulResponseBody = UnsuccessfulResponseBody; + +// Customer +// Get + +export type PortalCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; \ No newline at end of file From e50cece593dfc0c3e54ae928a66563e5b625be54 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Fri, 1 Mar 2024 18:27:54 +0100 Subject: [PATCH 02/49] Add swagger docs and make it as separate swagger for admin and api --- package.json | 4 +- src/app.ts | 44 +- .../portal-customer.ts} | 4 +- src/controllers/{ => admin}/prices.ts | 35 +- src/controllers/{ => admin}/product.ts | 90 ++- src/controllers/{ => admin}/subscriptions.ts | 185 ++++- src/controllers/{ => api}/account.ts | 28 +- .../{ => api}/credential-status.ts | 24 +- src/controllers/{ => api}/credential.ts | 22 +- src/controllers/{ => api}/did.ts | 20 +- src/controllers/{ => api}/key.ts | 14 +- src/controllers/{ => api}/presentation.ts | 18 +- src/controllers/{ => api}/resource.ts | 18 +- src/static/swagger-admin-options.json | 31 + src/static/swagger-admin.json | 662 ++++++++++++++++++ ...-options.json => swagger-api-options.json} | 0 src/static/{swagger.json => swagger-api.json} | 0 src/types/swagger-admin-types.ts | 175 +++++ ...{swagger-types.ts => swagger-api-types.ts} | 0 19 files changed, 1263 insertions(+), 111 deletions(-) rename src/controllers/{portal-customer.ts.ts => admin/portal-customer.ts} (92%) rename src/controllers/{ => admin}/prices.ts (61%) rename src/controllers/{ => admin}/product.ts (62%) rename src/controllers/{ => admin}/subscriptions.ts (67%) rename src/controllers/{ => api}/account.ts (94%) rename src/controllers/{ => api}/credential-status.ts (98%) rename src/controllers/{ => api}/credential.ts (97%) rename src/controllers/{ => api}/did.ts (98%) rename src/controllers/{ => api}/key.ts (96%) rename src/controllers/{ => api}/presentation.ts (95%) rename src/controllers/{ => api}/resource.ts (95%) create mode 100644 src/static/swagger-admin-options.json create mode 100644 src/static/swagger-admin.json rename src/static/{swagger-options.json => swagger-api-options.json} (100%) rename src/static/{swagger.json => swagger-api.json} (100%) create mode 100644 src/types/swagger-admin-types.ts rename src/types/{swagger-types.ts => swagger-api-types.ts} (100%) diff --git a/package.json b/package.json index 44082157..e929ca56 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "scripts": { "build": "npm run build:swagger && npm run build:app", "build:app": "tsc", - "build:swagger": "swagger-jsdoc --definition src/static/swagger-options.json -o src/static/swagger.json ./src/controllers/*.ts ./src/types/swagger-types.ts", + "build:swagger-api": "swagger-jsdoc --definition src/static/swagger-api-options.json -o src/static/swagger-api.json ./src/controllers/api/*.ts ./src/types/swagger-api-types.ts", + "build:swagger-admin": "swagger-jsdoc --definition src/static/swagger-admin-options.json -o src/static/swagger-admin.json ./src/controllers/admin/*.ts ./src/types/swagger-admin-types.ts", + "build:swagger": "npm run build:swagger-api && npm run build:swagger-admin", "start": "node dist/index.js", "start:local": "npm run build:app && npm run start", "format": "prettier --write '**/*.{js,ts,cjs,mjs,json}'", diff --git a/src/app.ts b/src/app.ts index 675742b9..4d4ce21f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,11 +7,11 @@ import path from 'path'; import swaggerUi from 'swagger-ui-express'; import { StatusCodes } from 'http-status-codes'; -import { CredentialController } from './controllers/credential.js'; -import { AccountController } from './controllers/account.js'; +import { CredentialController } from './controllers/api/credential.js'; +import { AccountController } from './controllers/api/account.js'; import { Authentication } from './middleware/authentication.js'; import { Connection } from './database/connection/connection.js'; -import { CredentialStatusController } from './controllers/credential-status.js'; +import { CredentialStatusController } from './controllers/api/credential-status.js'; import { CORS_ALLOWED_ORIGINS, CORS_ERROR_MSG } from './types/constants.js'; import { LogToWebHook } from './middleware/hook.js'; import { Middleware } from './middleware/middleware.js'; @@ -20,15 +20,16 @@ import * as dotenv from 'dotenv'; dotenv.config(); // Define Swagger file -import swaggerDocument from './static/swagger.json' assert { type: 'json' }; -import { PresentationController } from './controllers/presentation.js'; -import { KeyController } from './controllers/key.js'; -import { DIDController } from './controllers/did.js'; -import { ResourceController } from './controllers/resource.js'; +import swaggerAPIDocument from './static/swagger-api.json' assert { type: 'json' }; +import swaggerAdminDocument from './static/swagger-admin.json' assert { type: 'json' }; +import { PresentationController } from './controllers/api/presentation.js'; +import { KeyController } from './controllers/api/key.js'; +import { DIDController } from './controllers/api/did.js'; +import { ResourceController } from './controllers/api/resource.js'; import { FailedResponseTracker } from './middleware/event-tracker.js'; -import { ProductController } from './controllers/product.js'; -import { SubscriptionController } from './controllers/subscriptions.js'; -import { PriceController } from './controllers/prices.js'; +import { ProductController } from './controllers/admin/product.js'; +import { SubscriptionController } from './controllers/admin/subscriptions.js'; +import { PriceController } from './controllers/admin/prices.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -99,7 +100,8 @@ class App { } this.express.use(express.text()); - this.express.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument, swaggerOptions)); + this.express.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerAPIDocument, swaggerOptions)); + this.express.use('/admin/swagger', swaggerUi.serve, swaggerUi.setup(swaggerAdminDocument)) this.express.use(auth.handleError); this.express.use(async (req, res, next) => await auth.accessControl(req, res, next)); } @@ -211,19 +213,19 @@ class App { // Portal // Product - app.get('/portal/product/list', ProductController.productListValidator, new ProductController().getListProducts); - app.get('/portal/product/:productId', ProductController.productGetValidator, new ProductController().getProduct); + app.get('/admin/product/list', ProductController.productListValidator, new ProductController().getListProducts); + app.get('/admin/product/:productId', ProductController.productGetValidator, new ProductController().getProduct); // Prices - app.get('/portal/price/list', PriceController.priceListValidator, new PriceController().getListPrices); + app.get('/admin/price/list', PriceController.priceListValidator, new PriceController().getListPrices); // Subscription - app.post('/portal/subscription/create', SubscriptionController.subscriptionCreateValidator, new SubscriptionController().create); - app.post('/portal/subscription/update', SubscriptionController.subscriptionUpdateValidator, new SubscriptionController().update); - app.get('/portal/subscription/get/:subscriptionId', SubscriptionController.subscriptionGetValidator, new SubscriptionController().get); - app.get('/portal/subscription/list', SubscriptionController.subscriptionListValidator, new SubscriptionController().list); - app.delete('/portal/subscription/cancel', SubscriptionController.subscriptionCancelValidator, new SubscriptionController().cancel); - app.post('/portal/subscription/resume', SubscriptionController.subscriptionResumeValidator, new SubscriptionController().resume); + app.post('/admin/subscription/create', SubscriptionController.subscriptionCreateValidator, new SubscriptionController().create); + app.post('/admin/subscription/update', SubscriptionController.subscriptionUpdateValidator, new SubscriptionController().update); + app.get('/admin/subscription/get/:subscriptionId', SubscriptionController.subscriptionGetValidator, new SubscriptionController().get); + app.get('/admin/subscription/list', SubscriptionController.subscriptionListValidator, new SubscriptionController().list); + app.delete('/admin/subscription/cancel', SubscriptionController.subscriptionCancelValidator, new SubscriptionController().cancel); + app.post('/admin/subscription/resume', SubscriptionController.subscriptionResumeValidator, new SubscriptionController().resume); // 404 for all other requests app.all('*', (_req, res) => res.status(StatusCodes.BAD_REQUEST).send('Bad request')); diff --git a/src/controllers/portal-customer.ts.ts b/src/controllers/admin/portal-customer.ts similarity index 92% rename from src/controllers/portal-customer.ts.ts rename to src/controllers/admin/portal-customer.ts index aa63e846..67a262e9 100644 --- a/src/controllers/portal-customer.ts.ts +++ b/src/controllers/admin/portal-customer.ts @@ -1,8 +1,8 @@ import * as dotenv from 'dotenv'; import type { Request, Response } from 'express'; import { check } from 'express-validator'; -import { validationResult } from './validator'; -import type { PortalCustomerGetUnsuccessfulResponseBody } from '../types/portal'; +import { validationResult } from '../validator'; +import type { PortalCustomerGetUnsuccessfulResponseBody } from '../../types/portal.js'; dotenv.config(); diff --git a/src/controllers/prices.ts b/src/controllers/admin/prices.ts similarity index 61% rename from src/controllers/prices.ts rename to src/controllers/admin/prices.ts index 6ab5a199..cb9e3178 100644 --- a/src/controllers/prices.ts +++ b/src/controllers/admin/prices.ts @@ -1,9 +1,9 @@ import { Stripe } from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../types/portal.js'; +import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; -import { validationResult } from './validator/index.js'; +import { validationResult } from '../validator/index.js'; import { check } from 'express-validator'; dotenv.config(); @@ -20,6 +20,37 @@ export class PriceController { .bail(), ]; + /** + * @openapi + * + * /admin/price/list: + * get: + * summary: Get a list of prices + * description: Get a list of prices + * tags: [Price] + * parameters: + * - in: query + * name: productId + * schema: + * type: string + * description: The product id. If passed - returns filtered by this product list of prices. + * required: false + * responses: + * 200: + * description: A list of prices + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PriceListResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ async getListPrices(request: Request, response: Response) { const result = validationResult(request); // handle error diff --git a/src/controllers/product.ts b/src/controllers/admin/product.ts similarity index 62% rename from src/controllers/product.ts rename to src/controllers/admin/product.ts index 5ada6dde..d1d23786 100644 --- a/src/controllers/product.ts +++ b/src/controllers/admin/product.ts @@ -1,9 +1,9 @@ import { Stripe } from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import type { ProductGetResponseBody, ProductGetUnsuccessfulResponseBody, ProductListResponseBody, ProductListUnsuccessfulResponseBody, ProductWithPrices } from '../types/portal.js'; +import type { ProductGetResponseBody, ProductGetUnsuccessfulResponseBody, ProductListResponseBody, ProductListUnsuccessfulResponseBody, ProductWithPrices } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; -import { validationResult } from './validator/index.js'; +import { validationResult } from '../validator/index.js'; import { check } from 'express-validator'; dotenv.config(); @@ -32,6 +32,37 @@ export class ProductController { .bail(), ]; + /** + * @openapi + * + * /admin/product/list: + * get: + * summary: Get a list of products + * description: Get a list of products which are on a Stripe side + * tags: [Product] + * parameters: + * - in: query + * name: prices + * schema: + * type: boolean + * description: If setup to true - returns the list of products with prices inside. Default - true + * required: false + * responses: + * 200: + * description: A list of products + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProductListResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ async getListProducts(request: Request, response: Response) { const result = validationResult(request); // handle error @@ -75,6 +106,44 @@ export class ProductController { } } + + /** + * @openapi + * + * /admin/product/{productId}: + * get: + * summary: Get a product + * description: Get a product by id + * tags: [Product] + * parameters: + * - in: path + * name: productId + * schema: + * type: string + * description: The product id which identifies the product in Stripe + * required: true + * - in: query + * name: prices + * schema: + * type: boolean + * description: If setup to true - returns the product with prices inside. Default - true + * required: false + * responses: + * 200: + * description: A product + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProductGetResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ async getProduct(request: Request, response: Response) { const result = validationResult(request); // handle error @@ -91,13 +160,6 @@ export class ProductController { // Get the product const product = await stripe.products.retrieve(productId) as ProductWithPrices; - // If no product found return 404 - if (!product) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: 'No product found with id: ' + productId - } satisfies ProductGetUnsuccessfulResponseBody); - } - if (prices) { const prices = await stripe.prices.list({ product: product.id, @@ -112,6 +174,16 @@ export class ProductController { product: product } satisfies ProductGetResponseBody); } catch (error) { + + // define error + const errorRef = error as Record; + + if (errorRef?.statusCode === 404) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Product with id ${productId} not found` + } satisfies ProductGetUnsuccessfulResponseBody); + } + return response.status(500).json({ error: `Internal error: ${(error as Error)?.message || error}` } satisfies ProductGetUnsuccessfulResponseBody); diff --git a/src/controllers/subscriptions.ts b/src/controllers/admin/subscriptions.ts similarity index 67% rename from src/controllers/subscriptions.ts rename to src/controllers/admin/subscriptions.ts index 7fe948bf..3b149a9a 100644 --- a/src/controllers/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -1,9 +1,9 @@ import { Stripe } from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import type { SubscriptionCreateRequestBody, SubscriptionCreateResponseBody, SubscriptionCreateUnsuccessfulResponseBody, SubscriptionCancelResponseBody, SubscriptionCancelUnsuccessfulResponseBody, SubscriptionGetResponseBody, SubscriptionGetUnsuccessfulResponseBody, SubscriptionListResponseBody, SubscriptionListUnsuccessfulResponseBody, SubscriptionUpdateRequestBody, SubscriptionUpdateResponseBody, SubscriptionUpdateUnsuccessfulResponseBody, SubscriptionResumeUnsuccessfulResponseBody, SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, SubscriptionCancelRequestBody } from '../types/portal.js'; +import type { SubscriptionCreateRequestBody, SubscriptionCreateResponseBody, SubscriptionCreateUnsuccessfulResponseBody, SubscriptionCancelResponseBody, SubscriptionCancelUnsuccessfulResponseBody, SubscriptionGetResponseBody, SubscriptionGetUnsuccessfulResponseBody, SubscriptionListResponseBody, SubscriptionListUnsuccessfulResponseBody, SubscriptionUpdateRequestBody, SubscriptionUpdateResponseBody, SubscriptionUpdateUnsuccessfulResponseBody, SubscriptionResumeUnsuccessfulResponseBody, SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, SubscriptionCancelRequestBody } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; -import { validationResult } from './validator/index.js'; +import { validationResult } from '../validator/index.js'; import { check } from 'express-validator'; dotenv.config(); @@ -102,6 +102,34 @@ export class SubscriptionController { .bail(), ]; + + /** + * @openapi + * + * /admin/subscription/create: + * post: + * summary: Create a subscription + * description: Creates a new subscription for an existing customer + * tags: [Subscription] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionCreateRequestBody' + * responses: + * 201: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionCreateResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + */ async create(request: Request, response: Response) { // Validate request const result = validationResult(request); @@ -126,7 +154,7 @@ export class SubscriptionController { error: `Subscription was not created`, } satisfies SubscriptionCreateUnsuccessfulResponseBody); } - return response.status(StatusCodes.OK).json({ + return response.status(StatusCodes.CREATED).json({ subscription: subscription } satisfies SubscriptionCreateResponseBody ); } catch (error) { @@ -136,6 +164,33 @@ export class SubscriptionController { } } + /** + * @openapi + * + * /admin/subscription/update: + * post: + * summary: Update a subscription + * description: Updates an existing subscription + * tags: [Subscription] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionUpdateRequestBody' + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionUpdateResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + */ async update(request: Request, response: Response) { // Validate request const result = validationResult(request); @@ -170,6 +225,38 @@ export class SubscriptionController { } } + /** + * @openapi + * + * /admin/subscription/list: + * get: + * summary: Get a list of subscriptions + * description: Get a list of subscriptions + * tags: [Subscription] + * parameters: + * - in: query + * name: customerId + * schema: + * type: string + * description: The customer id. If passed - returns filtered by this customer list of subscriptions. + * required: false + * responses: + * 200: + * description: A list of subscriptions + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionListResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + public async list(request: Request, response: Response) { // Validate request const result = validationResult(request); @@ -202,6 +289,38 @@ export class SubscriptionController { } } + + /** + * @openapi + * + * /admin/subscription/get/{subscriptionId}: + * get: + * summary: Get a subscription + * description: Get a subscription + * tags: [Subscription] + * parameters: + * - in: path + * name: subscriptionId + * schema: + * type: string + * description: The subscription id + * required: true + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionGetResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ async get(request: Request, response: Response) { // Validate request const result = validationResult(request); @@ -210,7 +329,7 @@ export class SubscriptionController { error: result.array().pop()?.msg } satisfies SubscriptionGetUnsuccessfulResponseBody); } - const subscriptionId = request.query.subscriptionId; + const subscriptionId = request.params.subscriptionId; try { // Get the subscription const subscription = await stripe.subscriptions.retrieve(subscriptionId as string); @@ -229,6 +348,35 @@ export class SubscriptionController { } } + /** + * @openapi + * + * /admin/subscription/cancel: + * post: + * summary: Cancel a subscription + * description: Cancels an existing subscription + * tags: [Subscription] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionCancelRequestBody' + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionCancelResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ async cancel(request: Request, response: Response) { // Validate request const result = validationResult(request); @@ -263,6 +411,35 @@ export class SubscriptionController { } } + /** + * @openapi + * + * /admin/subscription/resume: + * post: + * summary: Resume a subscription + * description: Resumes an existing subscription + * tags: [Subscription] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionResumeRequestBody' + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionResumeResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ async resume(request: Request, response: Response) { // Validate request const result = validationResult(request); diff --git a/src/controllers/account.ts b/src/controllers/api/account.ts similarity index 94% rename from src/controllers/account.ts rename to src/controllers/api/account.ts index 739e4943..0e32cac1 100644 --- a/src/controllers/account.ts +++ b/src/controllers/api/account.ts @@ -1,25 +1,25 @@ import type { Request, Response } from 'express'; import { CheqdNetwork, checkBalance } from '@cheqd/sdk'; -import { TESTNET_MINIMUM_BALANCE, DEFAULT_DENOM_EXPONENT } from '../types/constants.js'; -import { CustomerService } from '../services/customer.js'; -import { LogToHelper } from '../middleware/auth/logto-helper.js'; -import { FaucetHelper } from '../helpers/faucet.js'; +import { TESTNET_MINIMUM_BALANCE, DEFAULT_DENOM_EXPONENT } from '../../types/constants.js'; +import { CustomerService } from '../../services/customer.js'; +import { LogToHelper } from '../../middleware/auth/logto-helper.js'; +import { FaucetHelper } from '../../helpers/faucet.js'; import { StatusCodes } from 'http-status-codes'; -import { LogToWebHook } from '../middleware/hook.js'; -import { UserService } from '../services/user.js'; -import { RoleService } from '../services/role.js'; -import { PaymentAccountService } from '../services/payment-account.js'; -import type { CustomerEntity } from '../database/entities/customer.entity.js'; -import type { UserEntity } from '../database/entities/user.entity.js'; -import type { PaymentAccountEntity } from '../database/entities/payment.account.entity.js'; -import { IdentityServiceStrategySetup } from '../services/identity/index.js'; +import { LogToWebHook } from '../../middleware/hook.js'; +import { UserService } from '../../services/user.js'; +import { RoleService } from '../../services/role.js'; +import { PaymentAccountService } from '../../services/payment-account.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; +import type { UserEntity } from '../../database/entities/user.entity.js'; +import type { PaymentAccountEntity } from '../../database/entities/payment.account.entity.js'; +import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; import type { QueryCustomerResponseBody, QueryIdTokenResponseBody, UnsuccessfulQueryCustomerResponseBody, UnsuccessfulQueryIdTokenResponseBody, -} from '../types/customer.js'; -import type { UnsuccessfulResponseBody } from '../types/shared.js'; +} from '../../types/customer.js'; +import type { UnsuccessfulResponseBody } from '../../types/shared.js'; import { check, validationResult } from 'express-validator'; export class AccountController { diff --git a/src/controllers/credential-status.ts b/src/controllers/api/credential-status.ts similarity index 98% rename from src/controllers/credential-status.ts rename to src/controllers/api/credential-status.ts index b4677767..480c2493 100644 --- a/src/controllers/credential-status.ts +++ b/src/controllers/api/credential-status.ts @@ -1,21 +1,21 @@ import type { Request, Response } from 'express'; -import { check, validationResult, query } from './validator/index.js'; +import { check, validationResult, query } from '../validator/index.js'; import { fromString } from 'uint8arrays'; import { StatusCodes } from 'http-status-codes'; -import { IdentityServiceStrategySetup } from '../services/identity/index.js'; -import type { ValidationErrorResponseBody } from '../types/shared.js'; -import type { CheckStatusListSuccessfulResponseBody, FeePaymentOptions } from '../types/credential-status.js'; +import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; +import type { ValidationErrorResponseBody } from '../../types/shared.js'; +import type { CheckStatusListSuccessfulResponseBody, FeePaymentOptions } from '../../types/credential-status.js'; import { DefaultStatusAction, DefaultStatusActionPurposeMap, DefaultStatusActions, MinimalPaymentCondition, -} from '../types/credential-status.js'; +} from '../../types/credential-status.js'; import type { SearchStatusListQuery, SearchStatusListSuccessfulResponseBody, SearchStatusListUnsuccessfulResponseBody, -} from '../types/credential-status.js'; +} from '../../types/credential-status.js'; import type { CheckStatusListRequestBody, CheckStatusListRequestQuery, @@ -35,7 +35,7 @@ import type { UpdateUnencryptedStatusListRequestQuery, UpdateUnencryptedStatusListSuccessfulResponseBody, UpdateUnencryptedStatusListUnsuccessfulResponseBody, -} from '../types/credential-status.js'; +} from '../../types/credential-status.js'; import { BulkRevocationResult, BulkSuspensionResult, @@ -44,11 +44,11 @@ import { DefaultStatusList2021StatusPurposeTypes, } from '@cheqd/did-provider-cheqd'; import type { AlternativeUri } from '@cheqd/ts-proto/cheqd/resource/v2/resource.js'; -import { toNetwork } from '../helpers/helpers.js'; -import { eventTracker } from '../services/track/tracker.js'; -import type { ICredentialStatusTrack, ITrackOperation, IFeePaymentOptions } from '../types/track.js'; -import { OperationCategoryNameEnum, OperationNameEnum } from '../types/constants.js'; -import { FeeAnalyzer } from '../helpers/fee-analyzer.js'; +import { toNetwork } from '../../helpers/helpers.js'; +import { eventTracker } from '../../services/track/tracker.js'; +import type { ICredentialStatusTrack, ITrackOperation, IFeePaymentOptions } from '../../types/track.js'; +import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; +import { FeeAnalyzer } from '../../helpers/fee-analyzer.js'; export class CredentialStatusController { static createUnencryptedValidator = [ diff --git a/src/controllers/credential.ts b/src/controllers/api/credential.ts similarity index 97% rename from src/controllers/credential.ts rename to src/controllers/api/credential.ts index 99eda5b0..8737de86 100644 --- a/src/controllers/credential.ts +++ b/src/controllers/api/credential.ts @@ -2,13 +2,13 @@ import type { Request, Response } from 'express'; import type { VerifiableCredential } from '@veramo/core'; import { StatusCodes } from 'http-status-codes'; -import { check, validationResult, query } from './validator/index.js'; +import { check, validationResult, query } from '../validator/index.js'; -import { Credentials } from '../services/credentials.js'; -import { IdentityServiceStrategySetup } from '../services/identity/index.js'; -import type { ValidationErrorResponseBody } from '../types/shared.js'; -import { CheqdW3CVerifiableCredential } from '../services/w3c-credential.js'; -import { isCredentialIssuerDidDeactivated } from '../services/helpers.js'; +import { Credentials } from '../../services/credentials.js'; +import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; +import type { ValidationErrorResponseBody } from '../../types/shared.js'; +import { CheqdW3CVerifiableCredential } from '../../services/w3c-credential.js'; +import { isCredentialIssuerDidDeactivated } from '../../services/helpers.js'; import type { IssueCredentialRequestBody, IssueCredentialResponseBody, @@ -29,12 +29,12 @@ import type { VerifyCredentialRequestBody, VerifyCredentialRequestQuery, VerifyCredentialResponseBody, -} from '../types/credential.js'; -import { VeridaDIDValidator } from './validator/did.js'; +} from '../../types/credential.js'; +import { VeridaDIDValidator } from '../validator/did.js'; import { Cheqd } from '@cheqd/did-provider-cheqd'; -import { OperationCategoryNameEnum, OperationNameEnum } from '../types/constants.js'; -import { eventTracker } from '../services/track/tracker.js'; -import type { ICredentialStatusTrack, ICredentialTrack, ITrackOperation } from '../types/track.js'; +import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; +import { eventTracker } from '../../services/track/tracker.js'; +import type { ICredentialStatusTrack, ICredentialTrack, ITrackOperation } from '../../types/track.js'; export class CredentialController { public static issueValidator = [ diff --git a/src/controllers/did.ts b/src/controllers/api/did.ts similarity index 98% rename from src/controllers/did.ts rename to src/controllers/api/did.ts index a4f064b0..1b062492 100644 --- a/src/controllers/did.ts +++ b/src/controllers/api/did.ts @@ -9,8 +9,8 @@ import { createDidVerificationMethod, } from '@cheqd/sdk'; import { StatusCodes } from 'http-status-codes'; -import { IdentityServiceStrategySetup } from '../services/identity/index.js'; -import { decryptPrivateKey, generateDidDoc, getQueryParams } from '../helpers/helpers.js'; +import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; +import { decryptPrivateKey, generateDidDoc, getQueryParams } from '../../helpers/helpers.js'; import { bases } from 'multiformats/basics'; import { base64ToBytes } from 'did-jwt'; import type { @@ -32,16 +32,16 @@ import type { GetDIDRequestParams, ResolveDIDRequestParams, DeactivateDIDRequestBody, -} from '../types/did.js'; -import { check, validationResult, param } from './validator/index.js'; +} from '../../types/did.js'; +import { check, validationResult, param } from '../validator/index.js'; import type { IKey, RequireOnly } from '@veramo/core'; import { extractPublicKeyHex } from '@veramo/utils'; -import type { ValidationErrorResponseBody } from '../types/shared.js'; -import type { KeyImport } from '../types/key.js'; -import { eventTracker } from '../services/track/tracker.js'; -import { OperationCategoryNameEnum, OperationNameEnum } from '../types/constants.js'; -import type { IDIDTrack, ITrackOperation } from '../types/track.js'; -import { arePublicKeyHexsInWallet } from '../services/helpers.js'; +import type { ValidationErrorResponseBody } from '../../types/shared.js'; +import type { KeyImport } from '../../types/key.js'; +import { eventTracker } from '../../services/track/tracker.js'; +import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; +import type { IDIDTrack, ITrackOperation } from '../../types/track.js'; +import { arePublicKeyHexsInWallet } from '../../services/helpers.js'; import { CheqdProviderErrorCodes } from '@cheqd/did-provider-cheqd'; import type { CheqdProviderError } from '@cheqd/did-provider-cheqd'; diff --git a/src/controllers/key.ts b/src/controllers/api/key.ts similarity index 96% rename from src/controllers/key.ts rename to src/controllers/api/key.ts index eec745a6..d480178d 100644 --- a/src/controllers/key.ts +++ b/src/controllers/api/key.ts @@ -1,7 +1,7 @@ import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import { IdentityServiceStrategySetup } from '../services/identity/index.js'; -import { decryptPrivateKey } from '../helpers/helpers.js'; +import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; +import { decryptPrivateKey } from '../../helpers/helpers.js'; import { toString } from 'uint8arrays'; import type { CreateKeyResponseBody, @@ -12,11 +12,11 @@ import type { UnsuccessfulCreateKeyResponseBody, UnsuccessfulImportKeyResponseBody, UnsuccessfulQueryKeyResponseBody, -} from '../types/key.js'; -import { check } from './validator/index.js'; -import { eventTracker } from '../services/track/tracker.js'; -import type { IKeyTrack, ITrackOperation } from '../types/track.js'; -import { OperationCategoryNameEnum, OperationNameEnum } from '../types/constants.js'; +} from '../../types/key.js'; +import { check } from '../validator/index.js'; +import { eventTracker } from '../../services/track/tracker.js'; +import type { IKeyTrack, ITrackOperation } from '../../types/track.js'; +import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; // ToDo: Make the format of /key/create and /key/read the same // ToDo: Add valdiation for /key/import diff --git a/src/controllers/presentation.ts b/src/controllers/api/presentation.ts similarity index 95% rename from src/controllers/presentation.ts rename to src/controllers/api/presentation.ts index 4446b652..d3ec020e 100644 --- a/src/controllers/presentation.ts +++ b/src/controllers/api/presentation.ts @@ -1,9 +1,9 @@ import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import { check, validationResult, query } from './validator/index.js'; -import { IdentityServiceStrategySetup } from '../services/identity/index.js'; -import { CheqdW3CVerifiablePresentation } from '../services/w3c-presentation.js'; +import { check, validationResult, query } from '../validator/index.js'; +import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; +import { CheqdW3CVerifiablePresentation } from '../../services/w3c-presentation.js'; import type { CreatePresentationRequestBody, CreatePresentationResponseBody, @@ -12,12 +12,12 @@ import type { VerifyPresentationRequestBody, VerifyPresentationResponseBody, VerifyPresentationResponseQuery, -} from '../types/presentation.js'; -import { isIssuerDidDeactivated } from '../services/helpers.js'; -import type { ValidationErrorResponseBody } from '../types/shared.js'; -import { OperationCategoryNameEnum, OperationNameEnum } from '../types/constants.js'; -import { eventTracker } from '../services/track/tracker.js'; -import type { IFeePaymentOptions, IPresentationTrack, ITrackOperation } from '../types/track.js'; +} from '../../types/presentation.js'; +import { isIssuerDidDeactivated } from '../../services/helpers.js'; +import type { ValidationErrorResponseBody } from '../../types/shared.js'; +import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; +import { eventTracker } from '../../services/track/tracker.js'; +import type { IFeePaymentOptions, IPresentationTrack, ITrackOperation } from '../../types/track.js'; export class PresentationController { public static presentationCreateValidator = [ diff --git a/src/controllers/resource.ts b/src/controllers/api/resource.ts similarity index 95% rename from src/controllers/resource.ts rename to src/controllers/api/resource.ts index a4aaf599..5251fd5b 100644 --- a/src/controllers/resource.ts +++ b/src/controllers/api/resource.ts @@ -3,13 +3,13 @@ import { fromString } from 'uint8arrays'; import { v4 } from 'uuid'; import type { MsgCreateResourcePayload } from '@cheqd/ts-proto/cheqd/resource/v2/index.js'; import { StatusCodes } from 'http-status-codes'; -import { IdentityServiceStrategySetup } from '../services/identity/index.js'; -import { getQueryParams } from '../helpers/helpers.js'; +import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; +import { getQueryParams } from '../../helpers/helpers.js'; import { DIDMetadataDereferencingResult, DefaultResolverUrl } from '@cheqd/did-provider-cheqd'; -import type { ValidationErrorResponseBody } from '../types/shared.js'; -import type { IResourceTrack, ITrackOperation } from '../types/track.js'; -import { OperationCategoryNameEnum, OperationNameEnum } from '../types/constants.js'; -import { check, validationResult, param, query } from './validator/index.js'; +import type { ValidationErrorResponseBody } from '../../types/shared.js'; +import type { IResourceTrack, ITrackOperation } from '../../types/track.js'; +import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; +import { check, validationResult, param, query } from '../validator/index.js'; import type { CreateResourceRequestBody, CreateResourceResponseBody, @@ -17,9 +17,9 @@ import type { SearchResourceRequestParams, UnsuccessfulCreateResourceResponseBody, UnsuccessfulQueryResourceResponseBody, -} from '../types/resource.js'; -import { eventTracker } from '../services/track/tracker.js'; -import { arePublicKeyHexsInWallet } from '../services/helpers.js'; +} from '../../types/resource.js'; +import { eventTracker } from '../../services/track/tracker.js'; +import { arePublicKeyHexsInWallet } from '../../services/helpers.js'; export class ResourceController { public static createResourceValidator = [ diff --git a/src/static/swagger-admin-options.json b/src/static/swagger-admin-options.json new file mode 100644 index 00000000..4e4498c5 --- /dev/null +++ b/src/static/swagger-admin-options.json @@ -0,0 +1,31 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Credential Service admin API for cheqd network", + "version": "2.0.0", + "description": "Admin API which handles users subscriptions and payments", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Product" + }, + { + "name": "Price" + }, + { + "name": "Customer" + }, + { + "name": "Subscription" + } + ] +} diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json new file mode 100644 index 00000000..022bcf36 --- /dev/null +++ b/src/static/swagger-admin.json @@ -0,0 +1,662 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Credential Service admin API for cheqd network", + "version": "2.0.0", + "description": "Admin API which handles users subscriptions and payments", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Product" + }, + { + "name": "Price" + }, + { + "name": "Customer" + }, + { + "name": "Subscription" + } + ], + "paths": { + "/admin/price/list": { + "get": { + "summary": "Get a list of prices", + "description": "Get a list of prices", + "tags": [ + "Price" + ], + "parameters": [ + { + "in": "query", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id. If passed - returns filtered by this product list of prices.", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of prices", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriceListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/list": { + "get": { + "summary": "Get a list of products", + "description": "Get a list of products which are on a Stripe side", + "tags": [ + "Product" + ], + "parameters": [ + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the list of products with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of products", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/{productId}": { + "get": { + "summary": "Get a product", + "description": "Get a product by id", + "tags": [ + "Product" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id which identifies the product in Stripe", + "required": true + } + }, + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the product with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/create": { + "post": { + "summary": "Create a subscription", + "description": "Creates a new subscription for an existing customer", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/update": { + "post": { + "summary": "Update a subscription", + "description": "Updates an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/list": { + "get": { + "summary": "Get a list of subscriptions", + "description": "Get a list of subscriptions", + "tags": [ + "Subscription" + ], + "parameters": [ + { + "in": "query", + "name": "customerId", + "schema": { + "type": "string", + "description": "The customer id. If passed - returns filtered by this customer list of subscriptions.", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of subscriptions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/get/{subscriptionId}": { + "get": { + "summary": "Get a subscription", + "description": "Get a subscription", + "tags": [ + "Subscription" + ], + "parameters": [ + { + "in": "path", + "name": "subscriptionId", + "schema": { + "type": "string", + "description": "The subscription id", + "required": true + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/cancel": { + "post": { + "summary": "Cancel a subscription", + "description": "Cancels an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/resume": {}, + "post": { + "summary": "Resume a subscription", + "description": "Resumes an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "components": { + "schemas": { + "PriceListResponseBody": { + "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", + "type": "object", + "properties": { + "prices": { + "type": "array", + "items": { + "type": "object", + "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" + } + } + } + }, + "ProductListResponseBody": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "type": "object", + "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" + } + } + } + }, + "ProductGetResponseBody": { + "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", + "type": "object" + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + }, + "SubscriptionCreateRequestBody": { + "description": "The request body for creating a subscription", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + } + } + } + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, + "SubscriptionCreateResponseBody": { + "description": "The response body for creating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionUpdateRequestBody": { + "description": "The request body for updating a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "updateParams": { + "type": "object", + "description": "The subscription update parameters. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/update)" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, + "SubscriptionUpdateResponseBody": { + "description": "The response body for updating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionGetRequestBody": { + "description": "The request body for getting a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionGetResponseBody": { + "description": "The response body for getting a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionListRequestBody": { + "description": "The request body for listing subscriptions", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + } + } + }, + "SubscriptionListResponseBody": { + "description": "The response body for listing subscriptions", + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + } + }, + "SubscriptionCancelRequestBody": { + "description": "The request body for canceling a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionCancelResponseBody": { + "description": "The response body for canceling a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, + "SubscriptionResumeRequestBody": { + "description": "The request body for resuming a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, + "SubscriptionResumeResponseBody": { + "description": "The response body for resuming a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + }, + "NotFoundError": { + "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Not Found Error" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/static/swagger-options.json b/src/static/swagger-api-options.json similarity index 100% rename from src/static/swagger-options.json rename to src/static/swagger-api-options.json diff --git a/src/static/swagger.json b/src/static/swagger-api.json similarity index 100% rename from src/static/swagger.json rename to src/static/swagger-api.json diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts new file mode 100644 index 00000000..e7ac6ea1 --- /dev/null +++ b/src/types/swagger-admin-types.ts @@ -0,0 +1,175 @@ +/** + * @openapi + * + * components: + * schemas: + * PriceListResponseBody: + * description: A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list) + * type: object + * properties: + * prices: + * type: array + * items: + * type: object + * description: A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object) + * ProductListResponseBody: + * type: object + * properties: + * products: + * type: array + * items: + * type: object + * description: A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object) + * ProductGetResponseBody: + * description: A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve) + * type: object + * InvalidRequest: + * description: A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body. + * type: object + * properties: + * error: + * type: string + * example: InvalidRequest + * InternalError: + * description: An internal error has occurred. Additional state information plus metadata may be available in the response body. + * type: object + * properties: + * error: + * type: string + * example: Internal Error + * UnauthorizedError: + * description: Access token is missing or invalid + * type: object + * properties: + * error: + * type: string + * example: Unauthorized Error + * SubscriptionCreateRequestBody: + * description: The request body for creating a subscription + * type: object + * properties: + * customerId: + * type: string + * description: The Stripe customer id + * example: cus_1234567890 + * items: + * type: array + * items: + * type: object + * properties: + * price: + * type: string + * description: The price id + * example: price_1234567890 + * idempotencyKey: + * type: string + * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. + * example: 1234567890 + * + * SubscriptionCreateResponseBody: + * description: The response body for creating a subscription + * type: object + * properties: + * subscription: + * type: object + * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object) + * SubscriptionUpdateRequestBody: + * description: The request body for updating a subscription + * type: object + * properties: + * subscriptionId: + * type: string + * description: The subscription id + * example: sub_1234567890 + * updateParams: + * type: object + * description: The subscription update parameters. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/update) + * idempotencyKey: + * type: string + * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. + * example: 1234567890 + * SubscriptionUpdateResponseBody: + * description: The response body for updating a subscription + * type: object + * properties: + * subscription: + * type: object + * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object) + * SubscriptionGetRequestBody: + * description: The request body for getting a subscription + * type: object + * properties: + * subscriptionId: + * type: string + * description: The subscription id + * example: sub_1234567890 + * SubscriptionGetResponseBody: + * description: The response body for getting a subscription + * type: object + * properties: + * subscription: + * type: object + * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object) + * SubscriptionListRequestBody: + * description: The request body for listing subscriptions + * type: object + * properties: + * customerId: + * type: string + * description: The Stripe customer id + * example: cus_1234567890 + * SubscriptionListResponseBody: + * description: The response body for listing subscriptions + * type: object + * properties: + * subscriptions: + * type: array + * items: + * type: object + * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] + * SubscriptionCancelRequestBody: + * description: The request body for canceling a subscription + * type: object + * properties: + * subscriptionId: + * type: string + * description: The subscription id + * example: sub_1234567890 + * SubscriptionCancelResponseBody: + * description: The response body for canceling a subscription + * type: object + * properties: + * subscription: + * type: object + * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] + * idempotencyKey: + * type: string + * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. + * example: 1234567890 + * SubscriptionResumeRequestBody: + * description: The request body for resuming a subscription + * type: object + * properties: + * subscriptionId: + * type: string + * description: The subscription id + * example: sub_1234567890 + * idempotencyKey: + * type: string + * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. + * example: 1234567890 + * SubscriptionResumeResponseBody: + * description: The response body for resuming a subscription + * type: object + * properties: + * subscription: + * type: object + * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] + * NotFoundError: + * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. + * type: object + * properties: + * error: + * type: string + * example: Not Found Error + */ diff --git a/src/types/swagger-types.ts b/src/types/swagger-api-types.ts similarity index 100% rename from src/types/swagger-types.ts rename to src/types/swagger-api-types.ts From 77bb82227548cb177c6d1f7076956142d30bf0c1 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 10 Mar 2024 00:05:16 +0100 Subject: [PATCH 03/49] - Add Stripe webhook handling - Add subscription entity - Add chekout API guarding --- README.md | 1 + docker/Dockerfile | 2 + src/app.ts | 14 +- src/controllers/admin/checkout-session.ts | 117 +++++ src/controllers/admin/portal-customer.ts | 2 + src/controllers/admin/product.ts | 4 +- src/controllers/admin/subscriptions.ts | 15 +- src/controllers/admin/webhook.ts | 121 +++++ src/controllers/api/account.ts | 23 +- src/controllers/api/credential.ts | 2 +- src/controllers/api/did.ts | 6 +- src/database/entities/customer.entity.ts | 6 + src/database/entities/subscription.entity.ts | 89 ++++ src/database/migrations/AlterCustomerTable.ts | 20 + .../AlterOperationTableNewCategory.ts | 22 + .../migrations/CreateSubscriptionTable.ts | 38 ++ src/database/types/enum.ts | 3 +- src/database/types/types.ts | 11 + src/middleware/auth/base-auth-handler.ts | 22 +- .../auth/routes/admin/admin-auth.ts | 18 + src/middleware/auth/user-info-fetcher/base.ts | 7 +- .../auth/user-info-fetcher/m2m-token.ts | 5 +- .../auth/user-info-fetcher/portal-token.ts | 86 ++++ src/middleware/authentication.ts | 11 +- src/services/admin/subscription.ts | 74 +++ src/services/{ => api}/api-key.ts | 8 +- src/services/{ => api}/coin.ts | 6 +- src/services/{ => api}/credentials.ts | 10 +- src/services/{ => api}/customer.ts | 27 +- src/services/{ => api}/identifier.ts | 6 +- src/services/{ => api}/key.ts | 6 +- src/services/{ => api}/operation.ts | 6 +- src/services/{ => api}/payment-account.ts | 8 +- src/services/{ => api}/payment.ts | 12 +- src/services/{ => api}/resource.ts | 10 +- src/services/{ => api}/role.ts | 4 +- src/services/{ => api}/store.ts | 0 src/services/{ => api}/user.ts | 8 +- src/services/helpers.ts | 2 +- src/services/identity/postgres.ts | 8 +- src/services/track/admin/account-submitter.ts | 60 +++ .../track/admin/subscription-submitter.ts | 161 ++++++ .../track/api/credential-status-subscriber.ts | 44 ++ .../track/api/credential-subscriber.ts | 44 ++ src/services/track/api/did-subscriber.ts | 43 ++ src/services/track/api/key-subscriber.ts | 45 ++ .../track/api/presentation-subscriber.ts | 44 ++ src/services/track/api/resource-subscriber.ts | 134 +++++ src/services/track/base.ts | 2 +- src/services/track/observer.ts | 30 +- src/services/track/operation-subscriber.ts | 157 ++++++ src/services/track/submitter.ts | 24 + src/services/track/subscribers.ts | 463 ------------------ src/services/track/tracker.ts | 52 +- src/services/track/types.ts | 3 +- src/static/swagger-admin-options.json | 3 + src/static/swagger-admin.json | 67 +++ src/types/constants.ts | 9 + src/types/environment.d.ts | 1 + src/types/portal.ts | 20 +- src/types/swagger-admin-types.ts | 20 + src/types/track.ts | 2 +- 62 files changed, 1699 insertions(+), 569 deletions(-) create mode 100644 src/controllers/admin/checkout-session.ts create mode 100644 src/controllers/admin/webhook.ts create mode 100644 src/database/entities/subscription.entity.ts create mode 100644 src/database/migrations/AlterCustomerTable.ts create mode 100644 src/database/migrations/AlterOperationTableNewCategory.ts create mode 100644 src/database/migrations/CreateSubscriptionTable.ts create mode 100644 src/middleware/auth/routes/admin/admin-auth.ts create mode 100644 src/middleware/auth/user-info-fetcher/portal-token.ts create mode 100644 src/services/admin/subscription.ts rename src/services/{ => api}/api-key.ts (88%) rename src/services/{ => api}/coin.ts (87%) rename src/services/{ => api}/credentials.ts (82%) rename src/services/{ => api}/customer.ts (76%) rename src/services/{ => api}/identifier.ts (81%) rename src/services/{ => api}/key.ts (86%) rename src/services/{ => api}/operation.ts (90%) rename src/services/{ => api}/payment-account.ts (88%) rename src/services/{ => api}/payment.ts (84%) rename src/services/{ => api}/resource.ts (89%) rename src/services/{ => api}/role.ts (92%) rename src/services/{ => api}/store.ts (100%) rename src/services/{ => api}/user.ts (85%) create mode 100644 src/services/track/admin/account-submitter.ts create mode 100644 src/services/track/admin/subscription-submitter.ts create mode 100644 src/services/track/api/credential-status-subscriber.ts create mode 100644 src/services/track/api/credential-subscriber.ts create mode 100644 src/services/track/api/did-subscriber.ts create mode 100644 src/services/track/api/key-subscriber.ts create mode 100644 src/services/track/api/presentation-subscriber.ts create mode 100644 src/services/track/api/resource-subscriber.ts create mode 100644 src/services/track/operation-subscriber.ts create mode 100644 src/services/track/submitter.ts delete mode 100644 src/services/track/subscribers.ts diff --git a/README.md b/README.md index adeff22f..916a6c33 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ The application supports Stripe integration for payment processing. 1. `STRIPE_SECRET_KEY`: Secret key for Stripe API. Please, keep it secret on deploying 2. `STRIPE_PUBLISHABLE_KEY` - Publishable key for Stripe API. +3. `STRIPE_WEBHOOK_SECRET` - Secret for Stripe Webhook. ### 3rd Party Connectors diff --git a/docker/Dockerfile b/docker/Dockerfile index 9f7f3c8a..c3da44e0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -84,6 +84,7 @@ ARG TESTNET_MINIMUM_BALANCE=1000 # Stripe ARG STRIPE_SECRET_KEY ARG STRIPE_PUBLISHABLE_KEY +ARG STRIPE_WEBHOOK_SECRET # Environment variables: base configuration ENV NPM_CONFIG_LOGLEVEL ${NPM_CONFIG_LOGLEVEL} @@ -132,6 +133,7 @@ ENV POLYGON_PRIVATE_KEY ${POLYGON_PRIVATE_KEY} # Environment variables: Stripe ENV STRIPE_SECRET_KEY ${STRIPE_SECRET_KEY} ENV STRIPE_PUBLISHABLE_KEY ${STRIPE_PUBLISHABLE_KEY} +ENV STRIPE_WEBHOOK_SECRET ${STRIPE_WEBHOOK_SECRET} # Set ownership permissions RUN chown -R node:node /home/node/app diff --git a/src/app.ts b/src/app.ts index 4d4ce21f..06771e39 100644 --- a/src/app.ts +++ b/src/app.ts @@ -21,7 +21,7 @@ dotenv.config(); // Define Swagger file import swaggerAPIDocument from './static/swagger-api.json' assert { type: 'json' }; -import swaggerAdminDocument from './static/swagger-admin.json' assert { type: 'json' }; +// import swaggerAdminDocument from './static/swagger-admin.json' assert { type: 'json' }; import { PresentationController } from './controllers/api/presentation.js'; import { KeyController } from './controllers/api/key.js'; import { DIDController } from './controllers/api/did.js'; @@ -30,6 +30,8 @@ import { FailedResponseTracker } from './middleware/event-tracker.js'; import { ProductController } from './controllers/admin/product.js'; import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; +import { WebhookController } from './controllers/admin/webhook.js'; +import { CheckoutSessionController } from './controllers/admin/checkout-session.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -100,8 +102,8 @@ class App { } this.express.use(express.text()); - this.express.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerAPIDocument, swaggerOptions)); - this.express.use('/admin/swagger', swaggerUi.serve, swaggerUi.setup(swaggerAdminDocument)) + this.express.use('/swagger', swaggerUi.serveFiles(swaggerAPIDocument, swaggerOptions), swaggerUi.setup(swaggerAPIDocument, swaggerOptions)); + // this.express.use('/admin/swagger', swaggerUi.serveFiles(swaggerAdminDocument), swaggerUi.setup(swaggerAdminDocument)) this.express.use(auth.handleError); this.express.use(async (req, res, next) => await auth.accessControl(req, res, next)); } @@ -227,6 +229,12 @@ class App { app.delete('/admin/subscription/cancel', SubscriptionController.subscriptionCancelValidator, new SubscriptionController().cancel); app.post('/admin/subscription/resume', SubscriptionController.subscriptionResumeValidator, new SubscriptionController().resume); + // Checkout session + app.post('/admin/checkout/session/create', CheckoutSessionController.checkoutSessionCreateValidator, new CheckoutSessionController().create); + + // Webhook + app.post('/admin/webhook', new WebhookController().handleWebhook); + // 404 for all other requests app.all('*', (_req, res) => res.status(StatusCodes.BAD_REQUEST).send('Bad request')); } diff --git a/src/controllers/admin/checkout-session.ts b/src/controllers/admin/checkout-session.ts new file mode 100644 index 00000000..4477e0e6 --- /dev/null +++ b/src/controllers/admin/checkout-session.ts @@ -0,0 +1,117 @@ + +import Stripe from 'stripe'; +import type { Request, Response } from 'express'; +import * as dotenv from 'dotenv'; +import type { CheckoutSessionCreateRequestBody, CheckoutSessionCreateUnsuccessfulResponseBody } from '../../types/portal.js'; +import { check, validationResult } from '../validator/index.js'; +import { StatusCodes } from 'http-status-codes'; + +dotenv.config(); + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + + +export class CheckoutSessionController { + static checkoutSessionCreateValidator = [ + check('price') + .exists() + .withMessage('price is required') + .bail() + .isString() + .withMessage('price should be a string') + .bail(), + check('successURL') + .exists() + .withMessage('successURL is required') + .bail() + .isString() + .withMessage('successURL should be a string') + .bail(), + check('cancelURL') + .exists() + .withMessage('cancelURL is required') + .bail() + .isString() + .withMessage('cancelURL should be a string') + .bail(), + check('idempotencyKey') + .optional() + .isString() + .withMessage('idempotencyKey should be a string') + .bail(), + ]; + + /** + * @openapi + * + * /admin/checkout/session/create: + * post: + * summary: Create a checkout session + * description: Create a checkout session + * tags: [Checkout] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CheckoutSessionCreateRequestBody' + * responses: + * 303: + * description: A redirect to Stripe prebuilt checkout page + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + */ + + public async create(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg + } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); + } + + const { price, successURL, cancelURL, quantity, idempotencyKey } = request.body satisfies CheckoutSessionCreateRequestBody; + try { + const session = await stripe.checkout.sessions.create({ + mode: 'subscription', + customer: response.locals.customer.stripeCustomerId, + line_items: [ + { + price: price, + quantity: quantity || 1, + }, + ], + success_url: successURL, + cancel_url: cancelURL, + }, { + idempotencyKey + }); + + if (session.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Checkout session was not created' + } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); + } + + if (!session.url) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Checkout session URL was not provided' + } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); + } + + return response.json({ + url: session.url as string + }) + + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}` + } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); + } + } +} \ No newline at end of file diff --git a/src/controllers/admin/portal-customer.ts b/src/controllers/admin/portal-customer.ts index 67a262e9..c092d4e9 100644 --- a/src/controllers/admin/portal-customer.ts +++ b/src/controllers/admin/portal-customer.ts @@ -24,6 +24,8 @@ export class PortalCustomerController { } satisfies PortalCustomerGetUnsuccessfulResponseBody); } + + return response.status(500).json({ error: "Not implemented yet" }) diff --git a/src/controllers/admin/product.ts b/src/controllers/admin/product.ts index d1d23786..23f81c35 100644 --- a/src/controllers/admin/product.ts +++ b/src/controllers/admin/product.ts @@ -178,13 +178,13 @@ export class ProductController { // define error const errorRef = error as Record; - if (errorRef?.statusCode === 404) { + if (errorRef?.statusCode === StatusCodes.NOT_FOUND) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Product with id ${productId} not found` } satisfies ProductGetUnsuccessfulResponseBody); } - return response.status(500).json({ + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: `Internal error: ${(error as Error)?.message || error}` } satisfies ProductGetUnsuccessfulResponseBody); } diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 3b149a9a..41180ebc 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -1,13 +1,14 @@ -import { Stripe } from 'stripe'; +import Stripe from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import type { SubscriptionCreateRequestBody, SubscriptionCreateResponseBody, SubscriptionCreateUnsuccessfulResponseBody, SubscriptionCancelResponseBody, SubscriptionCancelUnsuccessfulResponseBody, SubscriptionGetResponseBody, SubscriptionGetUnsuccessfulResponseBody, SubscriptionListResponseBody, SubscriptionListUnsuccessfulResponseBody, SubscriptionUpdateRequestBody, SubscriptionUpdateResponseBody, SubscriptionUpdateUnsuccessfulResponseBody, SubscriptionResumeUnsuccessfulResponseBody, SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, SubscriptionCancelRequestBody } from '../../types/portal.js'; +import type { SubscriptionCreateRequestBody, SubscriptionCreateResponseBody, SubscriptionCreateUnsuccessfulResponseBody, SubscriptionCancelResponseBody, SubscriptionCancelUnsuccessfulResponseBody, SubscriptionGetResponseBody, SubscriptionGetUnsuccessfulResponseBody, SubscriptionListResponseBody, SubscriptionListUnsuccessfulResponseBody, SubscriptionUpdateRequestBody, SubscriptionUpdateResponseBody, SubscriptionUpdateUnsuccessfulResponseBody, SubscriptionResumeUnsuccessfulResponseBody, SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, SubscriptionCancelRequestBody, PaymentBehavior } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; import { validationResult } from '../validator/index.js'; import { check } from 'express-validator'; dotenv.config(); + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class SubscriptionController { @@ -145,15 +146,17 @@ export class SubscriptionController { const subscription = await stripe.subscriptions.create({ customer: customerId, items: items, + payment_behavior: "default_incomplete" as PaymentBehavior, }, { idempotencyKey: idempotencyKey, }); if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.NOT_FOUND).json({ + return response.status(StatusCodes.BAD_GATEWAY).json({ error: `Subscription was not created`, } satisfies SubscriptionCreateUnsuccessfulResponseBody); } + return response.status(StatusCodes.CREATED).json({ subscription: subscription } satisfies SubscriptionCreateResponseBody ); @@ -210,7 +213,7 @@ export class SubscriptionController { idempotencyKey: idempotencyKey, }); if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.NOT_FOUND).json({ + return response.status(StatusCodes.BAD_GATEWAY).json({ error: `Subscription was not updated`, } satisfies SubscriptionUpdateUnsuccessfulResponseBody); } @@ -397,7 +400,7 @@ export class SubscriptionController { // Check if the subscription was cancelled if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.NOT_FOUND).json({ + return response.status(StatusCodes.BAD_GATEWAY).json({ error: `Subscription was not deleted`, } satisfies SubscriptionCancelUnsuccessfulResponseBody); } @@ -459,7 +462,7 @@ export class SubscriptionController { // Check if the subscription was resumed if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.NOT_FOUND).json({ + return response.status(StatusCodes.BAD_GATEWAY).json({ error: `Subscription was not resumed`, } satisfies SubscriptionResumeUnsuccessfulResponseBody); } diff --git a/src/controllers/admin/webhook.ts b/src/controllers/admin/webhook.ts new file mode 100644 index 00000000..14debffb --- /dev/null +++ b/src/controllers/admin/webhook.ts @@ -0,0 +1,121 @@ +import Stripe from 'stripe'; +import type { Request, Response } from 'express'; +import * as dotenv from 'dotenv'; +import { StatusCodes } from 'http-status-codes'; +import { EventTracker, eventTracker } from '../../services/track/tracker.js'; +import type { INotifyMessage } from '../../types/track.js'; +import { OperationNameEnum } from '../../types/constants.js'; +import type { ISubmitOperation, ISubmitData } from '../../services/track/submitter.js'; + +dotenv.config(); + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); +export class WebhookController { + public async handleWebhook(request: Request, response: Response) { + let event = request.body; + let subscription; + let status; + const builSubmitOperation = ( function(subscription: Stripe.Subscription, name: string) { + return { + operation: name, + data: { + subscriptionId: subscription.id, + stripeCustomerId: subscription.customer as string, + status: subscription.status, + currentPeriodStart: new Date(subscription.current_period_start * 1000), + currentPeriodEnd: new Date(subscription.current_period_end * 1000), + trialStart: subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, + trialEnd: subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined, + } satisfies ISubmitData, + } satisfies ISubmitOperation; + }) + // Only verify the event if you have an endpoint secret defined. + // Otherwise use the basic event deserialized with JSON.parse + // Get the signature sent by Stripe + + if (!process.env.STRIPE_WEBHOOK_SECRET) { + await eventTracker.notify({ + message: 'Stripe webhook secret not found. Webhook ID: ${request.body.id}.', + severity: 'error', + } satisfies INotifyMessage) + return response.sendStatus(StatusCodes.BAD_REQUEST); + } + + const signature = request.headers['stripe-signature']; + if (!signature) { + await eventTracker.notify({ + message: 'Webhook signature not found. Webhook ID: ${request.body.id}.', + severity: 'error', + } satisfies INotifyMessage) + return response.sendStatus(StatusCodes.BAD_REQUEST); + } + + try { + event = stripe.webhooks.constructEvent( + request.rawBody, + signature, + process.env.STRIPE_WEBHOOK_SECRET + ); + } catch (err) { + + await eventTracker.notify({ + message: `Webhook signature verification failed. Webhook ID: ${request.body.id}. Error: ${(err as Record)?.message || err}`, + severity: 'error', + } satisfies INotifyMessage) + return response.sendStatus(StatusCodes.BAD_REQUEST); + } + // Handle the event + switch (event.type) { + case 'customer.subscription.trial_will_end': + subscription = event.data.object; + status = subscription.status; + await eventTracker.notify({ + message: EventTracker.compileBasicNotification(`Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.trial_will_end'), + severity: 'info', + } satisfies INotifyMessage) + await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_TRIAL_WILL_END)); + break; + case 'customer.subscription.deleted': + subscription = event.data.object; + status = subscription.status; + await eventTracker.notify({ + message: EventTracker.compileBasicNotification(`Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.deleted'), + severity: 'info', + } satisfies INotifyMessage) + await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CANCEL)); + // Then define and call a method to handle the subscription deleted. + // handleSubscriptionDeleted(subscriptionDeleted); + break; + case 'customer.subscription.created': + subscription = event.data.object; + status = subscription.status; + await eventTracker.notify({ + message: EventTracker.compileBasicNotification(`Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.created'), + severity: 'info', + } satisfies INotifyMessage) + await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CREATE)); + // Then define and call a method to handle the subscription created. + // handleSubscriptionCreated(subscription); + break; + case 'customer.subscription.updated': + subscription = event.data.object; + status = subscription.status; + await eventTracker.notify({ + message: EventTracker.compileBasicNotification(`Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.updated'), + severity: 'info', + } satisfies INotifyMessage) + await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE)); + // Then define and call a method to handle the subscription update. + // handleSubscriptionUpdated(subscription); + break; + default: + // Unexpected event type + eventTracker.notify({ + message: EventTracker.compileBasicNotification(`Unexpected event type: ${event.type}`, 'Stripe Webhook: unexpected'), + severity: 'error', + } satisfies INotifyMessage) + } + // Return a 200 response to acknowledge receipt of the event + return response.status(StatusCodes.OK).send(); + } +} \ No newline at end of file diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 0e32cac1..a9312916 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -1,14 +1,14 @@ import type { Request, Response } from 'express'; import { CheqdNetwork, checkBalance } from '@cheqd/sdk'; -import { TESTNET_MINIMUM_BALANCE, DEFAULT_DENOM_EXPONENT } from '../../types/constants.js'; -import { CustomerService } from '../../services/customer.js'; +import { TESTNET_MINIMUM_BALANCE, DEFAULT_DENOM_EXPONENT, OperationNameEnum } from '../../types/constants.js'; +import { CustomerService } from '../../services/api/customer.js'; import { LogToHelper } from '../../middleware/auth/logto-helper.js'; import { FaucetHelper } from '../../helpers/faucet.js'; import { StatusCodes } from 'http-status-codes'; import { LogToWebHook } from '../../middleware/hook.js'; -import { UserService } from '../../services/user.js'; -import { RoleService } from '../../services/role.js'; -import { PaymentAccountService } from '../../services/payment-account.js'; +import { UserService } from '../../services/api/user.js'; +import { RoleService } from '../../services/api/role.js'; +import { PaymentAccountService } from '../../services/api/payment-account.js'; import type { CustomerEntity } from '../../database/entities/customer.entity.js'; import type { UserEntity } from '../../database/entities/user.entity.js'; import type { PaymentAccountEntity } from '../../database/entities/payment.account.entity.js'; @@ -21,6 +21,8 @@ import type { } from '../../types/customer.js'; import type { UnsuccessfulResponseBody } from '../../types/shared.js'; import { check, validationResult } from 'express-validator'; +import { eventTracker } from '../../services/track/tracker.js'; +import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from '../../services/track/submitter.js'; export class AccountController { public static createValidator = [ @@ -302,6 +304,17 @@ export class AccountController { } } } + // 8. Add the Stripe account to the Customer + if (customer.stripeCustomerId === null) { + eventTracker.submit({ + operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, + data: { + name: customer.name, + customerId: customer.customerId, + } satisfies ISubmitStripeCustomerCreateData, + } satisfies ISubmitOperation) + } + return response.status(StatusCodes.OK).json({}); } diff --git a/src/controllers/api/credential.ts b/src/controllers/api/credential.ts index 8737de86..7cbea5f1 100644 --- a/src/controllers/api/credential.ts +++ b/src/controllers/api/credential.ts @@ -4,7 +4,7 @@ import { StatusCodes } from 'http-status-codes'; import { check, validationResult, query } from '../validator/index.js'; -import { Credentials } from '../../services/credentials.js'; +import { Credentials } from '../../services/api/credentials.js'; import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; import type { ValidationErrorResponseBody } from '../../types/shared.js'; import { CheqdW3CVerifiableCredential } from '../../services/w3c-credential.js'; diff --git a/src/controllers/api/did.ts b/src/controllers/api/did.ts index 1b062492..d41e06f9 100644 --- a/src/controllers/api/did.ts +++ b/src/controllers/api/did.ts @@ -665,11 +665,13 @@ export class DIDController { // Extract did from params const { did } = request.params as GetDIDRequestParams; // Get strategy e.g. postgres or local - const identityServiceStrategySetup = new IdentityServiceStrategySetup(response.locals.customer.customerId); + const identityServiceStrategySetup = response.locals.customer + ? new IdentityServiceStrategySetup(response.locals.customer.customerId) + : new IdentityServiceStrategySetup(); try { const didDocument = did - ? await identityServiceStrategySetup.agent.resolveDid(request.params.did) + ? await identityServiceStrategySetup.agent.resolveDid(did) : await identityServiceStrategySetup.agent.listDids(response.locals.customer); return response diff --git a/src/database/entities/customer.entity.ts b/src/database/entities/customer.entity.ts index 6426c24d..6d5f8a52 100644 --- a/src/database/entities/customer.entity.ts +++ b/src/database/entities/customer.entity.ts @@ -26,6 +26,12 @@ export class CustomerEntity { }) updatedAt!: Date; + @Column({ + type: 'text', + nullable: true, + }) + stripeCustomerId!: string; + @BeforeInsert() setCreatedAt() { this.createdAt = new Date(); diff --git a/src/database/entities/subscription.entity.ts b/src/database/entities/subscription.entity.ts new file mode 100644 index 00000000..125d89de --- /dev/null +++ b/src/database/entities/subscription.entity.ts @@ -0,0 +1,89 @@ +import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; + +import * as dotenv from 'dotenv'; +import { CustomerEntity } from './customer.entity.js'; +dotenv.config(); + +@Entity('subscription') +export class SubscriptionEntity { + @Column({ + type: 'text', + nullable: false, + primary: true, + }) + subscriptionId!: string; + + @Column({ + type: 'text', + nullable: false, + }) + status!: string; + + @Column({ + type: 'timestamptz', + nullable: false, + }) + currentPeriodStart!: Date; + + @Column({ + type: 'timestamptz', + nullable: false, + }) + currentPeriodEnd!: Date; + + @Column({ + type: 'timestamptz', + nullable: true, + }) + trialStart!: Date; + + @Column({ + type: 'timestamptz', + nullable: true, + }) + trialEnd!: Date; + + @Column({ + type: 'timestamptz', + nullable: false, + }) + createdAt!: Date; + + @Column({ + type: 'timestamptz', + nullable: true, + }) + updatedAt!: Date; + + @BeforeInsert() + setCreatedAt() { + this.createdAt = new Date(); + } + + @BeforeUpdate() + setUpdateAt() { + this.updatedAt = new Date(); + } + + @ManyToOne(() => CustomerEntity, (customer) => customer.customerId) + @JoinColumn({ name: 'customerId' }) + customer!: CustomerEntity; + + constructor( + subscriptionId: string, + customer: CustomerEntity, + status: string, + currentPeriodStart: Date, + currentPeriodEnd: Date, + trialStart: Date, + trialEnd: Date, + ){ + this.subscriptionId = subscriptionId; + this.customer = customer; + this.status = status; + this.currentPeriodStart = currentPeriodStart; + this.currentPeriodEnd = currentPeriodEnd; + this.trialStart = trialStart; + this.trialEnd = trialEnd; + } +} diff --git a/src/database/migrations/AlterCustomerTable.ts b/src/database/migrations/AlterCustomerTable.ts new file mode 100644 index 00000000..7792647d --- /dev/null +++ b/src/database/migrations/AlterCustomerTable.ts @@ -0,0 +1,20 @@ +import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AlterCustomerTable1695740346000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const table_name = 'customer'; + + await queryRunner.addColumn( + table_name, + new TableColumn({ + name: 'stripeAccountId', + type: 'text', + isNullable: true, + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + throw new Error('illegal_operation: cannot roll back initial migration'); + } +} diff --git a/src/database/migrations/AlterOperationTableNewCategory.ts b/src/database/migrations/AlterOperationTableNewCategory.ts new file mode 100644 index 00000000..fd33b50e --- /dev/null +++ b/src/database/migrations/AlterOperationTableNewCategory.ts @@ -0,0 +1,22 @@ +import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm'; +import { categoryEnum } from '../types/enum.js'; + +export class AlterOperationTable1695740346001 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const table_name = 'operation'; + await queryRunner.changeColumn( + table_name, + 'category', + new TableColumn({ + name: 'category', + type: 'enum', + isNullable: false, + enum: categoryEnum.toStringList(), + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + throw new Error('illegal_operation: cannot roll back initial migration'); + } +} diff --git a/src/database/migrations/CreateSubscriptionTable.ts b/src/database/migrations/CreateSubscriptionTable.ts new file mode 100644 index 00000000..e117ae37 --- /dev/null +++ b/src/database/migrations/CreateSubscriptionTable.ts @@ -0,0 +1,38 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm"; + + +export class CreateSubscritpionTable1695740346003 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + const table = new Table({ + name: 'subscription', + columns: [ + { name: 'subscriptionId', type: 'text', isNullable: false, isPrimary: true }, + { name: 'customerId', type: 'uuid', isNullable: false }, + { name: 'status', type: 'text', isNullable: false }, + { name: 'trialStart', type: 'timestamptz', isNullable: true }, + { name: 'trialEnd', type: 'timestamptz', isNullable: true }, + { name: 'currentPeriodStart', type: 'timestamptz', isNullable: false }, + { name: 'currentPeriodEnd', type: 'timestamptz', isNullable: false }, + { name: 'createdAt', type: 'timestamptz', isNullable: false }, + { name: 'updatedAt', type: 'timestamptz', isNullable: true }, + ], + }); + await queryRunner.createTable(table, true); + + await queryRunner.createForeignKey( + table, + new TableForeignKey({ + columnNames: ['customerId'], + referencedColumnNames: ['customerId'], + referencedTableName: 'customer', + onDelete: 'CASCADE', + }) + ); + + } + + public async down(queryRunner: QueryRunner): Promise { + throw new Error('illegal_operation: cannot roll back initial migration'); + } +} \ No newline at end of file diff --git a/src/database/types/enum.ts b/src/database/types/enum.ts index 72f3bfae..8316c32a 100644 --- a/src/database/types/enum.ts +++ b/src/database/types/enum.ts @@ -5,9 +5,10 @@ export const categoryEnum = { CREDENTIAL_STATUS: 'credential-status', PRESENTATION: 'presentation', KEY: 'key', + SUBSCRIPTION: 'subscription', toStringList: function (): string[] { - return [this.DID, this.RESOURCE, this.CREDENTIAL, this.CREDENTIAL_STATUS, this.PRESENTATION, this.KEY]; + return [this.DID, this.RESOURCE, this.CREDENTIAL, this.CREDENTIAL_STATUS, this.PRESENTATION, this.KEY, this.SUBSCRIPTION]; }, }; diff --git a/src/database/types/types.ts b/src/database/types/types.ts index 2e47cfa9..61e38972 100644 --- a/src/database/types/types.ts +++ b/src/database/types/types.ts @@ -32,6 +32,10 @@ import { AlterOperationTable1695740345978 } from '../migrations/AlterOperationTa import { AlterPaymentTable1695740345979 } from '../migrations/AlterPaymentTable.js'; import { CreateCoinTable1695740345977 } from '../migrations/CreateCoinTable.js'; import { CoinEntity } from '../entities/coin.entity.js'; +import { AlterCustomerTable1695740346000 } from '../migrations/AlterCustomerTable.js'; +import { AlterOperationTable1695740346001 } from '../migrations/AlterOperationTableNewCategory.js'; +import { SubscriptionEntity } from '../entities/subscription.entity.js'; +import { CreateSubscritpionTable1695740346003 } from '../migrations/CreateSubscriptionTable.js'; dotenv.config(); const { EXTERNAL_DB_CONNECTION_URL, EXTERNAL_DB_CERT } = process.env; @@ -96,6 +100,12 @@ export class Postgres implements AbstractDatabase { AlterOperationTable1695740345978, // Change payment table structure AlterPaymentTable1695740345979, + // Add stripeCustomerId to customer table + AlterCustomerTable1695740346000, + // Add new category + AlterOperationTable1695740346001, + // Add subscription table + CreateSubscritpionTable1695740346003, ], entities: [ ...Entities, @@ -110,6 +120,7 @@ export class Postgres implements AbstractDatabase { IdentifierEntity, APIKeyEntity, CoinEntity, + SubscriptionEntity, ], logging: ['error', 'info', 'warn'], }); diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index c479bb25..255810fe 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -10,6 +10,7 @@ import { IAuthHandler, RuleRoutine, IAPIGuard } from './routine.js'; import type { IAuthResponse, MethodToScopeRule } from '../../types/authentication.js'; import { M2MTokenUserInfoFetcher } from './user-info-fetcher/m2m-token.js'; import { decodeJwt } from 'jose'; +import { PortalUserInfoFetcher } from './user-info-fetcher/portal-token.js'; export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { userInfoFetcher: IUserInfoFetcher = {} as IUserInfoFetcher; @@ -111,7 +112,7 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { private nextHandler: IAuthHandler; oauthProvider: IOAuthProvider; private static bearerTokenIdentifier = 'Bearer'; - private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap']; + private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap', '/admin/webhook', '/admin/swagger']; constructor() { super(); @@ -130,15 +131,20 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { private chooseUserFetcherStrategy(request: Request): void { const token = BaseAuthHandler.extractBearerTokenFromHeaders(request.headers) as string; - if (token) { - const payload = decodeJwt(token); - if (payload.aud === process.env.LOGTO_APP_ID) { - this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); + const headerIdToken = request.headers['id-token'] as string; + if (headerIdToken && token) { + this.setUserInfoStrategy(new PortalUserInfoFetcher(token, headerIdToken)); + } else { + if (token) { + const payload = decodeJwt(token); + if (payload.aud === process.env.LOGTO_APP_ID) { + this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); + } else { + this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); + } } else { - this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); + this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); } - } else { - this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); } } diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts new file mode 100644 index 00000000..013ee342 --- /dev/null +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -0,0 +1,18 @@ +import type { Request, Response } from 'express'; +import { BaseAuthHandler } from '../../base-auth-handler.js'; +import type { IAuthResponse } from '../../../../types/authentication.js'; + +export class AdminHandler extends BaseAuthHandler { + constructor() { + super(); + // ToDo: define how to get namespace information + this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:testnet', { skipNamespace: true}); + this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:mainnet', { skipNamespace: true}); + } + public async handle(request: Request, response: Response): Promise { + if (!request.path.includes('/admin/')) { + return super.handle(request, response); + } + return this.guardAPI(request); + } +} diff --git a/src/middleware/auth/user-info-fetcher/base.ts b/src/middleware/auth/user-info-fetcher/base.ts index 9366d30b..9cc7ca03 100644 --- a/src/middleware/auth/user-info-fetcher/base.ts +++ b/src/middleware/auth/user-info-fetcher/base.ts @@ -2,6 +2,11 @@ import type { Request } from 'express'; import type { IAuthResponse } from '../../../types/authentication.js'; import type { IOAuthProvider } from '../oauth/base.js'; + +export interface IUserInfoOptions { + [key: string]: any; +} + export interface IUserInfoFetcher { - fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise; + fetchUserInfo(request: Request, oauthProvider: IOAuthProvider, options?: IUserInfoOptions): Promise; } diff --git a/src/middleware/auth/user-info-fetcher/m2m-token.ts b/src/middleware/auth/user-info-fetcher/m2m-token.ts index 4c6df508..a865decb 100644 --- a/src/middleware/auth/user-info-fetcher/m2m-token.ts +++ b/src/middleware/auth/user-info-fetcher/m2m-token.ts @@ -39,10 +39,7 @@ export class M2MTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetc if (!payload.sub) { return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No sub found in the token.`); } - const { error, data: scopes } = await oauthProvider.getAppScopes(payload.sub); - if (error) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); - } + const scopes = payload.scope ? (payload.scope as string).split(' ') : []; this.setScopes(scopes); return this.returnOk(); } catch (error) { diff --git a/src/middleware/auth/user-info-fetcher/portal-token.ts b/src/middleware/auth/user-info-fetcher/portal-token.ts new file mode 100644 index 00000000..08efa54c --- /dev/null +++ b/src/middleware/auth/user-info-fetcher/portal-token.ts @@ -0,0 +1,86 @@ +import type { Request } from 'express'; +import { AuthReturn } from '../routine.js'; +import type { IAuthResponse } from '../../../types/authentication.js'; +import { StatusCodes } from 'http-status-codes'; +import type { IUserInfoFetcher } from './base.js'; +import type { IOAuthProvider } from '../oauth/base.js'; +import { createRemoteJWKSet, jwtVerify } from 'jose'; + +import * as dotenv from 'dotenv'; +dotenv.config(); + +export class PortalUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { + private m2mToken: string; + private idToken + + constructor(m2mToken: string, idToken: string) { + super(); + this.m2mToken = m2mToken; + this.idToken = idToken; + } + + async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { + // Get customerId from header + if (!this.idToken) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No idToken found in the header.`); + } + + // Check the idToken, provided in header + const idTokenVerification = await this.verifyIdToken(oauthProvider); + if (idTokenVerification.error) { + return idTokenVerification; + } + // return this.returnOk(); + + return this.verifyM2MToken(oauthProvider); + } + + public async verifyIdToken(oauthProvider: IOAuthProvider): Promise { + try { + const { payload } = await jwtVerify( + this.idToken, // The raw Bearer Token extracted from the request header + createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + { + // expected issuer of the token, should be issued by the Logto server + issuer: oauthProvider.endpoint_issuer, + } + ); + // Setup the scopes from the token + if (!payload.sub) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No sub found in the token. Cannot set customerId.`); + } + this.setUserId(payload.sub); + return this.returnOk(); + } + catch (error) { + console.error(error); + return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + } + } + + public async verifyM2MToken(oauthProvider: IOAuthProvider): Promise { + try { + const { payload } = await jwtVerify( + this.m2mToken, // The raw Bearer Token extracted from the request header + createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + { + // expected issuer of the token, should be issued by the Logto server + issuer: oauthProvider.endpoint_issuer, + } + ); + // Setup the scopes from the token + if (!payload.sub) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No sub found in the token.`); + } + const { error, data: scopes } = await oauthProvider.getAppScopes(payload.sub); + if (error) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); + } + this.setScopes(scopes); + return this.returnOk(); + } catch (error) { + console.error(error); + return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + } + } +} diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 3322c7fd..2f3e6fa7 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -11,12 +11,13 @@ import { ResourceAuthHandler } from './auth/routes/resource-auth.js'; import type { BaseAuthHandler } from './auth/base-auth-handler.js'; import { LogToHelper } from './auth/logto-helper.js'; import { PresentationAuthHandler } from './auth/routes/presentation-auth.js'; -import { UserService } from '../services/user.js'; +import { UserService } from '../services/api/user.js'; import { configLogToExpress } from '../types/constants.js'; import { handleAuthRoutes, withLogto } from '@logto/express'; import { LogToProvider } from './auth/oauth/logto-provider.js'; import { AuthInfoHandler } from './auth/routes/auth-user-info.js'; -import { CustomerService } from '../services/customer.js'; +import { CustomerService } from '../services/api/customer.js'; +import { AdminHandler } from './auth/routes/admin/admin-auth.js'; dotenv.config(); @@ -52,6 +53,8 @@ export class Authentication { const presentationAuthHandler = new PresentationAuthHandler(); const authInfoHandler = new AuthInfoHandler(); + const adminAuthHandler = new AdminHandler(); + // Set logToHelper. We do it for avoiding re-asking LogToHelper.setup() in each auth handler // cause it does a lot of requests to LogTo this.authHandler.setOAuthProvider(oauthProvider); @@ -62,6 +65,7 @@ export class Authentication { resourceAuthHandler.setOAuthProvider(oauthProvider); presentationAuthHandler.setOAuthProvider(oauthProvider); authInfoHandler.setOAuthProvider(oauthProvider); + adminAuthHandler.setOAuthProvider(oauthProvider); // Set chain of responsibility this.authHandler @@ -71,7 +75,8 @@ export class Authentication { .setNext(credentialStatusAuthHandler) .setNext(resourceAuthHandler) .setNext(presentationAuthHandler) - .setNext(authInfoHandler); + .setNext(authInfoHandler) + .setNext(adminAuthHandler); this.isSetup = true; } diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts new file mode 100644 index 00000000..9006adf9 --- /dev/null +++ b/src/services/admin/subscription.ts @@ -0,0 +1,74 @@ +import type { Repository } from 'typeorm'; + +import { Connection } from '../../database/connection/connection.js'; +import { SubscriptionEntity } from '../../database/entities/subscription.entity.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; + +export class SubscriptionService { + public subscriptionRepository: Repository; + + // Get rid of such code and move it to the builder + public static instance = new SubscriptionService(); + + constructor() { + this.subscriptionRepository = Connection.instance.dbConnection.getRepository(SubscriptionEntity); + } + + public async create( + subscriptionId: string, + customer: CustomerEntity, + status: string, + currentPeriodStart: Date, + currentPeriodEnd: Date, + trialStart: Date, + trialEnd: Date, + ): Promise { + const subscriptionEntity = new SubscriptionEntity( + subscriptionId, + customer, + status, + currentPeriodStart, + currentPeriodEnd, + trialStart, + trialEnd, + ); + const res = await this.subscriptionRepository.insert(subscriptionEntity); + if (!res) throw new Error(`Cannot create a new subscription`); + + return subscriptionEntity; + } + + public async update( + subscriptionId: string, + status?: string, + currentPeriodStart?: Date, + currentPeriodEnd?: Date, + trialStart?: Date, + trialEnd?: Date, + ) { + const existing = await this.subscriptionRepository.findOneBy({ subscriptionId }); + if (!existing) { + throw new Error(`Subscription with id ${subscriptionId} not found`); + } + if (status) existing.status = status; + if (currentPeriodStart) existing.currentPeriodStart = currentPeriodStart; + if (currentPeriodEnd) existing.currentPeriodEnd = currentPeriodEnd; + if (trialStart) existing.trialStart = trialStart; + if (trialEnd) existing.trialEnd = trialEnd; + return await this.subscriptionRepository.save(existing); + } + + public async get(subscriptionId?: string): Promise { + return await this.subscriptionRepository.findOne({ + where: { subscriptionId }, + relations: ['customer'], + }); + } + + public async findOne(where: Record) { + return await this.subscriptionRepository.findOne({ + where: where, + relations: ['customer'], + }); + } +} diff --git a/src/services/api-key.ts b/src/services/api/api-key.ts similarity index 88% rename from src/services/api-key.ts rename to src/services/api/api-key.ts index a09ac860..63bdedb8 100644 --- a/src/services/api-key.ts +++ b/src/services/api/api-key.ts @@ -1,11 +1,11 @@ import type { Repository } from 'typeorm'; import { decodeJWT } from 'did-jwt'; -import { Connection } from '../database/connection/connection.js'; +import { Connection } from '../../database/connection/connection.js'; import * as dotenv from 'dotenv'; -import type { CustomerEntity } from '../database/entities/customer.entity.js'; -import { APIKeyEntity } from '../database/entities/api.key.entity.js'; -import type { UserEntity } from '../database/entities/user.entity.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; +import { APIKeyEntity } from '../../database/entities/api.key.entity.js'; +import type { UserEntity } from '../../database/entities/user.entity.js'; import { v4 } from 'uuid'; dotenv.config(); diff --git a/src/services/coin.ts b/src/services/api/coin.ts similarity index 87% rename from src/services/coin.ts rename to src/services/api/coin.ts index 5edc6f81..c0cde9b6 100644 --- a/src/services/coin.ts +++ b/src/services/api/coin.ts @@ -1,8 +1,8 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; -import { CoinEntity } from '../database/entities/coin.entity.js'; -import { MINIMAL_DENOM } from '../types/constants.js'; +import { Connection } from '../../database/connection/connection.js'; +import { CoinEntity } from '../../database/entities/coin.entity.js'; +import { MINIMAL_DENOM } from '../../types/constants.js'; import { v4 } from 'uuid'; export class CoinService { diff --git a/src/services/credentials.ts b/src/services/api/credentials.ts similarity index 82% rename from src/services/credentials.ts rename to src/services/api/credentials.ts index 8e6f74f9..70c79653 100644 --- a/src/services/credentials.ts +++ b/src/services/api/credentials.ts @@ -1,11 +1,11 @@ import type { CredentialPayload, VerifiableCredential } from '@veramo/core'; -import { VC_CONTEXT, VC_TYPE } from '../types/constants.js'; -import type { CredentialRequest } from '../types/credential.js'; -import { IdentityServiceStrategySetup } from './identity/index.js'; +import { VC_CONTEXT, VC_TYPE } from '../../types/constants.js'; +import type { CredentialRequest } from '../../types/credential.js'; +import { IdentityServiceStrategySetup } from '../identity/index.js'; import { v4 } from 'uuid'; import * as dotenv from 'dotenv'; -import type { CustomerEntity } from '../database/entities/customer.entity.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; dotenv.config(); const { ENABLE_VERIDA_CONNECTOR } = process.env; @@ -40,7 +40,7 @@ export class Credentials { if (!request.credentialSchema) throw new Error('Credential schema is required'); // dynamic import to avoid circular dependency - const { VeridaService } = await import('./connectors/verida.js'); + const { VeridaService } = await import('../connectors/verida.js'); await VeridaService.instance.sendCredential( request.subjectDid, diff --git a/src/services/customer.ts b/src/services/api/customer.ts similarity index 76% rename from src/services/customer.ts rename to src/services/api/customer.ts index 4f103dad..9accc137 100644 --- a/src/services/customer.ts +++ b/src/services/api/customer.ts @@ -1,8 +1,8 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; -import { CustomerEntity } from '../database/entities/customer.entity.js'; -import { IdentityServiceStrategySetup } from './identity/index.js'; +import { Connection } from '../../database/connection/connection.js'; +import { CustomerEntity } from '../../database/entities/customer.entity.js'; +import { IdentityServiceStrategySetup } from '../identity/index.js'; import * as dotenv from 'dotenv'; import { PaymentAccountService } from './payment-account.js'; @@ -42,13 +42,18 @@ export class CustomerService { }; } - public async update(customerId: string, name: string) { + public async update(customerId: string, name?: string, stripeCustomerId?: string) { const existingCustomer = await this.customerRepository.findOneBy({ customerId }); if (!existingCustomer) { throw new Error(`CustomerId not found`); } + if (name) { + existingCustomer.name = name; + } - existingCustomer.name = name; + if (stripeCustomerId) { + existingCustomer.stripeCustomerId = stripeCustomerId; + } return await this.customerRepository.save(existingCustomer); } @@ -56,12 +61,22 @@ export class CustomerService { return this.customerRepository.findOneBy({ customerId }); } - public async findOne(name: string) { + public async findOne(name?: string) { return await this.customerRepository.findOne({ where: { name }, }); } + public async find(where: Record) { + try { + return await this.customerRepository.find({ + where: where, + }); + } catch { + return []; + } + } + public async isExist(where: Record) { try { return (await this.customerRepository.findOne({ where })) ? true : false; diff --git a/src/services/identifier.ts b/src/services/api/identifier.ts similarity index 81% rename from src/services/identifier.ts rename to src/services/api/identifier.ts index 457c95d6..8226b75a 100644 --- a/src/services/identifier.ts +++ b/src/services/api/identifier.ts @@ -1,9 +1,9 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; -import { IdentifierEntity } from '../database/entities/identifier.entity.js'; +import { Connection } from '../../database/connection/connection.js'; +import { IdentifierEntity } from '../../database/entities/identifier.entity.js'; import * as dotenv from 'dotenv'; -import type { CustomerEntity } from '../database/entities/customer.entity.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; dotenv.config(); export class IdentifierService { diff --git a/src/services/key.ts b/src/services/api/key.ts similarity index 86% rename from src/services/key.ts rename to src/services/api/key.ts index 797425f6..97ec80fe 100644 --- a/src/services/key.ts +++ b/src/services/api/key.ts @@ -1,11 +1,11 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; +import { Connection } from '../../database/connection/connection.js'; import * as dotenv from 'dotenv'; -import { KeyEntity } from '../database/entities/key.entity.js'; +import { KeyEntity } from '../../database/entities/key.entity.js'; import type { Key } from '@veramo/data-store'; -import type { CustomerEntity } from '../database/entities/customer.entity.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; dotenv.config(); export class KeyService { diff --git a/src/services/operation.ts b/src/services/api/operation.ts similarity index 90% rename from src/services/operation.ts rename to src/services/api/operation.ts index 2c078153..82fb97d7 100644 --- a/src/services/operation.ts +++ b/src/services/api/operation.ts @@ -1,11 +1,11 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; +import { Connection } from '../../database/connection/connection.js'; import * as dotenv from 'dotenv'; -import { OperationEntity } from '../database/entities/operation.entity.js'; +import { OperationEntity } from '../../database/entities/operation.entity.js'; import { v4 } from 'uuid'; -import type { CoinEntity } from '../database/entities/coin.entity.js'; +import type { CoinEntity } from '../../database/entities/coin.entity.js'; dotenv.config(); export class OperationService { diff --git a/src/services/payment-account.ts b/src/services/api/payment-account.ts similarity index 88% rename from src/services/payment-account.ts rename to src/services/api/payment-account.ts index 0ed4e0b6..b0f66530 100644 --- a/src/services/payment-account.ts +++ b/src/services/api/payment-account.ts @@ -1,11 +1,11 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; +import { Connection } from '../../database/connection/connection.js'; import * as dotenv from 'dotenv'; -import { PaymentAccountEntity } from '../database/entities/payment.account.entity.js'; -import type { CustomerEntity } from '../database/entities/customer.entity.js'; -import type { KeyEntity } from '../database/entities/key.entity.js'; +import { PaymentAccountEntity } from '../../database/entities/payment.account.entity.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; +import type { KeyEntity } from '../../database/entities/key.entity.js'; import { getCosmosAccount } from '@cheqd/sdk'; dotenv.config(); diff --git a/src/services/payment.ts b/src/services/api/payment.ts similarity index 84% rename from src/services/payment.ts rename to src/services/api/payment.ts index ac9f45d0..6a4f2c54 100644 --- a/src/services/payment.ts +++ b/src/services/api/payment.ts @@ -1,14 +1,14 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; +import { Connection } from '../../database/connection/connection.js'; import * as dotenv from 'dotenv'; -import type { CustomerEntity } from '../database/entities/customer.entity.js'; -import { PaymentEntity } from '../database/entities/payment.entity.js'; -import type { OperationEntity } from '../database/entities/operation.entity.js'; -import type { ResourceEntity } from '../database/entities/resource.entity.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; +import { PaymentEntity } from '../../database/entities/payment.entity.js'; +import type { OperationEntity } from '../../database/entities/operation.entity.js'; +import type { ResourceEntity } from '../../database/entities/resource.entity.js'; import type { CheqdNetwork } from '@cheqd/sdk'; -import type { CoinEntity } from '../database/entities/coin.entity.js'; +import type { CoinEntity } from '../../database/entities/coin.entity.js'; dotenv.config(); export class PaymentService { diff --git a/src/services/resource.ts b/src/services/api/resource.ts similarity index 89% rename from src/services/resource.ts rename to src/services/api/resource.ts index 0908801a..47579e63 100644 --- a/src/services/resource.ts +++ b/src/services/api/resource.ts @@ -1,12 +1,12 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; +import { Connection } from '../../database/connection/connection.js'; import * as dotenv from 'dotenv'; -import { ResourceEntity } from '../database/entities/resource.entity.js'; -import type { IdentifierEntity } from '../database/entities/identifier.entity.js'; -import type { KeyEntity } from '../database/entities/key.entity.js'; -import type { CustomerEntity } from '../database/entities/customer.entity.js'; +import { ResourceEntity } from '../../database/entities/resource.entity.js'; +import type { IdentifierEntity } from '../../database/entities/identifier.entity.js'; +import type { KeyEntity } from '../../database/entities/key.entity.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; import type { LinkedResourceMetadataResolutionResult } from '@cheqd/did-provider-cheqd'; dotenv.config(); diff --git a/src/services/role.ts b/src/services/api/role.ts similarity index 92% rename from src/services/role.ts rename to src/services/api/role.ts index 1ac40e6e..46ec24fc 100644 --- a/src/services/role.ts +++ b/src/services/api/role.ts @@ -1,9 +1,9 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; +import { Connection } from '../../database/connection/connection.js'; import * as dotenv from 'dotenv'; -import { RoleEntity } from '../database/entities/role.entity.js'; +import { RoleEntity } from '../../database/entities/role.entity.js'; dotenv.config(); export class RoleService { diff --git a/src/services/store.ts b/src/services/api/store.ts similarity index 100% rename from src/services/store.ts rename to src/services/api/store.ts diff --git a/src/services/user.ts b/src/services/api/user.ts similarity index 85% rename from src/services/user.ts rename to src/services/api/user.ts index 57721422..fe068e82 100644 --- a/src/services/user.ts +++ b/src/services/api/user.ts @@ -1,11 +1,11 @@ import type { Repository } from 'typeorm'; -import { Connection } from '../database/connection/connection.js'; -import type { CustomerEntity } from '../database/entities/customer.entity.js'; +import { Connection } from '../../database/connection/connection.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; import * as dotenv from 'dotenv'; -import { UserEntity } from '../database/entities/user.entity.js'; -import type { RoleEntity } from '../database/entities/role.entity.js'; +import { UserEntity } from '../../database/entities/user.entity.js'; +import type { RoleEntity } from '../../database/entities/role.entity.js'; dotenv.config(); export class UserService { diff --git a/src/services/helpers.ts b/src/services/helpers.ts index 3a7a284a..6a719d9d 100644 --- a/src/services/helpers.ts +++ b/src/services/helpers.ts @@ -2,7 +2,7 @@ import type { TPublicKeyEd25519 } from '@cheqd/did-provider-cheqd'; import type { CustomerEntity } from '../database/entities/customer.entity.js'; import type { IBooleanResponse } from '../types/shared.js'; import { IdentityServiceStrategySetup } from './identity/index.js'; -import { KeyService } from './key.js'; +import { KeyService } from './api/key.js'; import type { CheqdW3CVerifiableCredential } from './w3c-credential.js'; import type { CheqdW3CVerifiablePresentation } from './w3c-presentation.js'; diff --git a/src/services/identity/postgres.ts b/src/services/identity/postgres.ts index eaccb909..cd433ba5 100644 --- a/src/services/identity/postgres.ts +++ b/src/services/identity/postgres.ts @@ -42,13 +42,13 @@ import type { CustomerEntity } from '../../database/entities/customer.entity.js' import { Veramo } from './agent.js'; import { DefaultIdentityService } from './default.js'; import * as dotenv from 'dotenv'; -import { KeyService } from '../key.js'; -import { PaymentAccountService } from '../payment-account.js'; +import { KeyService } from '../api/key.js'; +import { PaymentAccountService } from '../api/payment-account.js'; import { CheqdNetwork } from '@cheqd/sdk'; -import { IdentifierService } from '../identifier.js'; +import { IdentifierService } from '../api/identifier.js'; import type { KeyEntity } from '../../database/entities/key.entity.js'; import type { UserEntity } from '../../database/entities/user.entity.js'; -import { APIKeyService } from '../api-key.js'; +import { APIKeyService } from '../api/api-key.js'; import type { APIKeyEntity } from '../../database/entities/api.key.entity.js'; import { KeyDIDProvider } from '@veramo/did-provider-key'; import type { AbstractIdentifierProvider } from '@veramo/did-manager'; diff --git a/src/services/track/admin/account-submitter.ts b/src/services/track/admin/account-submitter.ts new file mode 100644 index 00000000..b6261ec1 --- /dev/null +++ b/src/services/track/admin/account-submitter.ts @@ -0,0 +1,60 @@ +import Stripe from "stripe"; +import type { IObserver } from "../types.js"; +import { OperationNameEnum } from "../../../types/constants.js"; +import type { INotifyMessage } from "../../../types/track.js"; +import { EventTracker } from "../tracker.js"; +import { StatusCodes } from "http-status-codes"; +import { CustomerService } from "../../api/customer.js"; +import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from "../submitter.js"; + + +export class PortalAccountCreateSubmitter implements IObserver { + private emitter: EventEmitter; + private stripe: Stripe; + + constructor(emitter: EventEmitter) { + this.emitter = emitter; + this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + } + + notify(notifyMessage: INotifyMessage): void { + this.emitter.emit('notify', notifyMessage); + } + + async update(operation: ISubmitOperation): Promise { + if (operation.operation === OperationNameEnum.STRIPE_ACCOUNT_CREATE) { + await this.submitStripeAccountCreate(operation); + } + } + + async submitStripeAccountCreate(operation: ISubmitOperation): Promise { + const data = operation.data as ISubmitStripeCustomerCreateData; + + try { + // Create a new Stripe account + const account = await this.stripe.customers.create({ + name: data.name, + email: data.email, + }); + if (account.lastResponse.statusCode !== StatusCodes.OK) { + await this.notify({ + message: EventTracker.compileBasicNotification(`Failed to create Stripe account with name: ${data.name}.`, operation.operation), + severity: 'error', + } as INotifyMessage); + return; + } + + // Update the CaaS customer with the new Stripe account + await CustomerService.instance.update(data.customerId, undefined, account.id); + await this.notify({ + message: EventTracker.compileBasicNotification(`Stripe account created with name: ${data.name}.`, operation.operation), + severity: 'info', + } as INotifyMessage); + } catch (error) { + await this.notify({ + message: EventTracker.compileBasicNotification(`Failed to create Stripe account with name: ${data.name as string}.`, operation.operation), + severity: 'error', + } as INotifyMessage); + } + } +} diff --git a/src/services/track/admin/subscription-submitter.ts b/src/services/track/admin/subscription-submitter.ts new file mode 100644 index 00000000..d6f2144b --- /dev/null +++ b/src/services/track/admin/subscription-submitter.ts @@ -0,0 +1,161 @@ +import { OperationNameEnum } from "../../../types/constants.js"; +import type { INotifyMessage } from "../../../types/track.js"; +import { SubscriptionService } from "../../admin/subscription.js"; +import { CustomerService } from "../../api/customer.js"; +import type { ISubmitOperation, ISubmitSubscriptionData } from "../submitter.js"; +import { EventTracker, eventTracker } from "../tracker.js"; +import type { IObserver } from "../types.js"; + +//eslint-disable-next-line + +function isPromise(object: any): object is Promise { + return object && Promise.resolve(object) === object; + } + +export const eventDecorator = () => { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + const { value } = descriptor; + //eslint-disable-next-line + descriptor.value = async (...args: any) => { + try { + const response = value.apply(target, args); + return isPromise(response) ? await response : Promise.resolve(response); + } catch (error) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification(`Error while calling function: ${propertyKey}: ${(error as Record)?.message || error}`), + severity: 'error', + } satisfies INotifyMessage); + } + }; + return descriptor; + } +} + +export class SubscriptionSubmitter implements IObserver { + + private emitter: EventEmitter; + + constructor(emitter: EventEmitter) { + this.emitter = emitter; + } + + notify(notifyMessage: INotifyMessage): void { + this.emitter.emit('notify', notifyMessage); + } + + async update(operation: ISubmitOperation): Promise { + switch (operation.operation) { + case OperationNameEnum.SUBSCRIPTION_CREATE: + await this.submitSubscriptionCreate(operation); + break; + + case OperationNameEnum.SUBSCRIPTION_UPDATE: + await this.submitSubscriptionUpdate(operation); + break; + + case OperationNameEnum.SUBSCRIPTION_CANCEL: + await this.submitSubscriptionCancel(operation); + break; + } + } + + // @eventDecorator() + async submitSubscriptionCreate(operation: ISubmitOperation): Promise { + const data = operation.data as ISubmitSubscriptionData; + try{ + const customers = await CustomerService.instance.find({ + stripeCustomerId: data.stripeCustomerId + }) + + if (customers.length !== 1) { + this.notify({ + message: EventTracker.compileBasicNotification(`It should be only 1 Stripe account associated with CaaS customer. Stripe accountId: ${data.stripeCustomerId}.`, operation.operation), + severity: 'error', + }) + } + const subscription = await SubscriptionService.instance.create( + data.subscriptionId, + customers[0], + data.status, + data.currentPeriodStart, + data.currentPeriodEnd, + data.trialStart as Date, + data.trialEnd as Date, + ) + if (!subscription) { + this.notify({ + message: EventTracker.compileBasicNotification(`Failed to create a new subscription with id: ${data.subscriptionId}.`, operation.operation), + severity: 'error', + }) + } + + this.notify({ + message: EventTracker.compileBasicNotification(`Subscription created with id: ${data.subscriptionId}.`, operation.operation), + severity: 'info', + }) + } catch (error) { + this.notify({ + message: EventTracker.compileBasicNotification(`Failed to create a new subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation), + severity: 'error', + }) + } + } + + // @eventDecorator() + async submitSubscriptionUpdate(operation: ISubmitOperation): Promise { + const data = operation.data as ISubmitSubscriptionData; + try { + const subscription = await SubscriptionService.instance.update( + data.subscriptionId, + data.status, + data.currentPeriodStart, + data.currentPeriodEnd, + data.trialStart as Date, + data.trialEnd as Date, + ) + if (!subscription) { + this.notify({ + message: EventTracker.compileBasicNotification(`Failed to update subscription with id: ${data.subscriptionId}.`, operation.operation), + severity: 'error', + }) + } + + this.notify({ + message: EventTracker.compileBasicNotification(`Subscription updated with id: ${data.subscriptionId}.`, operation.operation), + severity: 'info', + }) + } catch (error) { + this.notify({ + message: EventTracker.compileBasicNotification(`Failed to update subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation), + severity: 'error', + }) + } + } + + // @eventDecorator() + async submitSubscriptionCancel(operation: ISubmitOperation): Promise { + const data = operation.data as ISubmitSubscriptionData; + try { + const subscription = await SubscriptionService.instance.update( + data.subscriptionId, + data.status, + ) + if (!subscription) { + this.notify({ + message: EventTracker.compileBasicNotification(`Failed to cancel subscription with id: ${data.subscriptionId}.`, operation.operation), + severity: 'error', + }) + } + + this.notify({ + message: EventTracker.compileBasicNotification(`Subscription canceled with id: ${data.subscriptionId}.`, operation.operation), + severity: 'info', + }) + } catch (error) { + this.notify({ + message: EventTracker.compileBasicNotification(`Failed to cancel subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation), + severity: 'error', + }) + } + } +} \ No newline at end of file diff --git a/src/services/track/api/credential-status-subscriber.ts b/src/services/track/api/credential-status-subscriber.ts new file mode 100644 index 00000000..41c5d6c9 --- /dev/null +++ b/src/services/track/api/credential-status-subscriber.ts @@ -0,0 +1,44 @@ +import { OperationCategoryNameEnum } from '../../../types/constants.js'; +import type { + ICredentialStatusTrack, ITrackOperation, + ITrackResult +} from '../../../types/track.js'; +import type { IObserver } from '../types.js'; +import { BaseOperationObserver } from '../base.js'; + + +export class CredentialStatusSubscriber extends BaseOperationObserver implements IObserver { + isReactionNeeded(trackOperation: ITrackOperation): boolean { + // Credential tracker reacts on CredentialStatusList, Credential operations like revocation + // and Resource operations like create, update, delete + return trackOperation.category === OperationCategoryNameEnum.CREDENTIAL_STATUS; + } + + public compileMessage(trackResult: ITrackResult): string { + const base_message = super.compileMessage(trackResult); + const data = trackResult.operation.data as ICredentialStatusTrack; + return `${base_message} | Target DID: ${data.did} | Encrypted: ${data.encrypted} | StatusListName: ${data.resource?.resourceName}`; + } + + async update(trackOperation: ITrackOperation): Promise { + if (!this.isReactionNeeded(trackOperation)) { + // Just skip this operation + return; + } + // tracking resource creation in DB + const result = await this.trackCredentialStatusOperation(trackOperation); + // notify about the result of tracking, e.g. log or datadog + await this.notify({ + message: this.compileMessage(result), + severity: result.error ? 'error' : 'info', + }); + } + + async trackCredentialStatusOperation(trackOperation: ITrackOperation): Promise { + // We don't have specific credential status writes, so we just track credential creation + return { + operation: trackOperation, + error: '', + } satisfies ITrackResult; + } +} diff --git a/src/services/track/api/credential-subscriber.ts b/src/services/track/api/credential-subscriber.ts new file mode 100644 index 00000000..b1a7af29 --- /dev/null +++ b/src/services/track/api/credential-subscriber.ts @@ -0,0 +1,44 @@ +import { OperationCategoryNameEnum } from '../../../types/constants.js'; +import type { + ICredentialTrack, ITrackOperation, + ITrackResult +} from '../../../types/track.js'; +import type { IObserver } from '../types.js'; +import { BaseOperationObserver } from '../base.js'; + + +export class CredentialSubscriber extends BaseOperationObserver implements IObserver { + isReactionNeeded(trackOperation: ITrackOperation): boolean { + // Credential tracker reacts on CredentialStatusList, Credential operations like revocation + // and Resource operations like create, update, delete + return trackOperation.category === OperationCategoryNameEnum.CREDENTIAL; + } + + public compileMessage(trackResult: ITrackResult): string { + const base_message = super.compileMessage(trackResult); + const data = trackResult.operation.data as ICredentialTrack; + return `${base_message} | Credential holder: ${data.did}`; + } + + async update(trackOperation: ITrackOperation): Promise { + if (!this.isReactionNeeded(trackOperation)) { + // Just skip this operation + return; + } + // tracking resource creation in DB + const result = await this.trackCredentialOperation(trackOperation); + // notify about the result of tracking, e.g. log or datadog + await this.notify({ + message: this.compileMessage(result), + severity: result.error ? 'error' : 'info', + }); + } + + async trackCredentialOperation(trackOperation: ITrackOperation): Promise { + // We don't have specific credential writes, so we just track credential creation + return { + operation: trackOperation, + error: '', + } satisfies ITrackResult; + } +} diff --git a/src/services/track/api/did-subscriber.ts b/src/services/track/api/did-subscriber.ts new file mode 100644 index 00000000..b6be6b07 --- /dev/null +++ b/src/services/track/api/did-subscriber.ts @@ -0,0 +1,43 @@ +import { OperationCategoryNameEnum } from '../../../types/constants.js'; +import type { + ITrackOperation, + ITrackResult, + IDIDTrack +} from '../../../types/track.js'; +import type { IObserver } from '../types.js'; +import { BaseOperationObserver } from '../base.js'; + + +export class DIDSubscriber extends BaseOperationObserver implements IObserver { + isReactionNeeded(trackOperation: ITrackOperation): boolean { + return trackOperation.category === OperationCategoryNameEnum.DID; + } + + public compileMessage(trackResult: ITrackResult): string { + const base_message = super.compileMessage(trackResult); + const data = trackResult.operation.data as IDIDTrack; + return `${base_message} | Target DID: ${data.did}`; + } + + async update(trackOperation: ITrackOperation): Promise { + if (!this.isReactionNeeded(trackOperation)) { + // Just skip this operation + return; + } + // tracking resource creation in DB + const result = await this.trackDIDOperation(trackOperation); + // notify about the result of tracking, e.g. log or datadog + await this.notify({ + message: this.compileMessage(result), + severity: result.error ? 'error' : 'info', + }); + } + + async trackDIDOperation(trackOperation: ITrackOperation): Promise { + // We don't have specific DID related operations to track + return { + operation: trackOperation, + error: '', + } satisfies ITrackResult; + } +} diff --git a/src/services/track/api/key-subscriber.ts b/src/services/track/api/key-subscriber.ts new file mode 100644 index 00000000..c5ff1d0b --- /dev/null +++ b/src/services/track/api/key-subscriber.ts @@ -0,0 +1,45 @@ +import { OperationCategoryNameEnum } from '../../../types/constants.js'; +import type { + IKeyTrack, + ITrackOperation, + ITrackResult +} from '../../../types/track.js'; +import type { IObserver } from '../types.js'; +import { BaseOperationObserver } from '../base.js'; + + +export class KeySubscriber extends BaseOperationObserver implements IObserver { + isReactionNeeded(trackOperation: ITrackOperation): boolean { + // Credential tracker reacts on CredentialStatusList, Credential operations like revocation + // and Resource operations like create, update, delete + return trackOperation.category === OperationCategoryNameEnum.KEY; + } + + public compileMessage(trackResult: ITrackResult): string { + const base_message = super.compileMessage(trackResult); + const data = trackResult.operation.data as IKeyTrack; + return `${base_message} | keyRef: ${data.keyRef} | keyType: ${data.keyType}`; + } + + async update(trackOperation: ITrackOperation): Promise { + if (!this.isReactionNeeded(trackOperation)) { + // Just skip this operation + return; + } + // tracking resource creation in DB + const result = await this.trackKeyOperation(trackOperation); + // notify about the result of tracking, e.g. log or datadog + await this.notify({ + message: this.compileMessage(result), + severity: result.error ? 'error' : 'info', + }); + } + + async trackKeyOperation(trackOperation: ITrackOperation): Promise { + // We don't have specific presentation writes, so we just track presentation creation + return { + operation: trackOperation, + error: '', + } satisfies ITrackResult; + } +} diff --git a/src/services/track/api/presentation-subscriber.ts b/src/services/track/api/presentation-subscriber.ts new file mode 100644 index 00000000..4d66101f --- /dev/null +++ b/src/services/track/api/presentation-subscriber.ts @@ -0,0 +1,44 @@ +import { OperationCategoryNameEnum } from '../../../types/constants.js'; +import type { + IPresentationTrack, ITrackOperation, + ITrackResult +} from '../../../types/track.js'; +import type { IObserver } from '../types.js'; +import { BaseOperationObserver } from '../base.js'; + + +export class PresentationSubscriber extends BaseOperationObserver implements IObserver { + isReactionNeeded(trackOperation: ITrackOperation): boolean { + // Credential tracker reacts on CredentialStatusList, Credential operations like revocation + // and Resource operations like create, update, delete + return trackOperation.category === OperationCategoryNameEnum.PRESENTATION; + } + + public compileMessage(trackResult: ITrackResult): string { + const base_message = super.compileMessage(trackResult); + const data = trackResult.operation.data as IPresentationTrack; + return `${base_message} | Presentation holder: ${data.holder}`; + } + + async update(trackOperation: ITrackOperation): Promise { + if (!this.isReactionNeeded(trackOperation)) { + // Just skip this operation + return; + } + // tracking resource creation in DB + const result = await this.trackPresentationOperation(trackOperation); + // notify about the result of tracking, e.g. log or datadog + await this.notify({ + message: this.compileMessage(result), + severity: result.error ? 'error' : 'info', + }); + } + + async trackPresentationOperation(trackOperation: ITrackOperation): Promise { + // We don't have specific presentation writes, so we just track presentation creation + return { + operation: trackOperation, + error: '', + } satisfies ITrackResult; + } +} diff --git a/src/services/track/api/resource-subscriber.ts b/src/services/track/api/resource-subscriber.ts new file mode 100644 index 00000000..67e5cc5f --- /dev/null +++ b/src/services/track/api/resource-subscriber.ts @@ -0,0 +1,134 @@ +import type { LinkedResourceMetadataResolutionResult } from '@cheqd/did-provider-cheqd'; +import { OperationNameEnum, OperationCategoryNameEnum } from '../../../types/constants.js'; +import type { + ICredentialStatusTrack, + ICredentialTrack, + IResourceTrack, + ITrackOperation, + ITrackResult} from '../../../types/track.js'; +import { isCredentialStatusTrack, isCredentialTrack, isResourceTrack } from '../helpers.js'; +import { IdentifierService } from '../../api/identifier.js'; +import { KeyService } from '../../api/key.js'; +import { ResourceService } from '../../api/resource.js'; +import type { IObserver } from '../types.js'; +import { BaseOperationObserver } from '../base.js'; + + +export class ResourceSubscriber extends BaseOperationObserver implements IObserver { + private static acceptedOperations = [ + OperationNameEnum.RESOURCE_CREATE, + OperationNameEnum.CREDENTIAL_REVOKE, + OperationNameEnum.CREDENTIAL_SUSPEND, + OperationNameEnum.CREDENTIAL_UNSUSPEND, + OperationNameEnum.CREDENTIAL_STATUS_CREATE_UNENCRYPTED, + OperationNameEnum.CREDENTIAL_STATUS_CREATE_ENCRYPTED, + OperationNameEnum.CREDENTIAL_STATUS_UPDATE_UNENCRYPTED, + OperationNameEnum.CREDENTIAL_STATUS_UPDATE_ENCRYPTED, + ]; + + isReactionNeeded(trackOperation: ITrackOperation): boolean { + // Resource tracker reacts on CredentialStatusList, Credential operations like revocation + // and Resource operations like create, update, delete + const isCategoryAccepted = trackOperation.category === OperationCategoryNameEnum.RESOURCE || + trackOperation.category === OperationCategoryNameEnum.CREDENTIAL || + trackOperation.category === OperationCategoryNameEnum.CREDENTIAL_STATUS; + const isOperationAccepted = ResourceSubscriber.acceptedOperations.includes( + trackOperation.name as OperationNameEnum + ); + return isCategoryAccepted && isOperationAccepted; + } + + public compileMessage(trackResult: ITrackResult): string { + const base_message = super.compileMessage(trackResult); + const data = trackResult.operation.data as IResourceTrack; + return `${base_message} | Resource DID: ${data.did} | ResourceName: ${data.resource.resourceName} | ResourceType: ${data.resource.resourceType} | ResourceId: ${data.resource.resourceId}`; + } + + async update(trackOperation: ITrackOperation): Promise { + if (!this.isReactionNeeded(trackOperation)) { + // Just skip this operation + return; + } + trackOperation.category = OperationCategoryNameEnum.RESOURCE; + trackOperation.name = OperationNameEnum.RESOURCE_CREATE; + // tracking resource creation in DB + const result = await this.trackResourceOperation(trackOperation); + // notify about the result of tracking, e.g. log or datadog + await this.notify({ + message: this.compileMessage(result), + severity: result.error ? 'error' : 'info', + }); + } + + async trackResourceOperation(trackOperation: ITrackOperation): Promise { + // Resource operation may be with CredentialStatusList or with Credential operations like revocation + // and others and also with Resource operations like create, update, delete + const customer = trackOperation.customer; + const data = trackOperation.data as IResourceTrack | ICredentialStatusTrack | ICredentialTrack; + const did = data.did; + let encrypted = false; + let symmetricKey = ''; + let resource: LinkedResourceMetadataResolutionResult | undefined = undefined; + + if (!customer) { + return { + operation: trackOperation, + error: `Customer for resource operation was not specified`, + }; + } + + if (isResourceTrack(data)) { + encrypted = false; + symmetricKey = ''; + resource = (data as IResourceTrack).resource; + } + if (isCredentialStatusTrack(data)) { + encrypted = (data as ICredentialStatusTrack).encrypted || false; + symmetricKey = (data as ICredentialStatusTrack).symmetricKey || ''; + resource = (data as ICredentialStatusTrack).resource; + } + if (isCredentialTrack(data)) { + encrypted = (data as ICredentialTrack).encrypted || false; + symmetricKey = (data as ICredentialTrack).symmetricKey || ''; + resource = (data as ICredentialTrack).resource; + } + + if (!resource) { + return { + operation: trackOperation, + error: `Resource for ${did} was not specified`, + }; + } + + const identifier = await IdentifierService.instance.get(did); + if (!identifier) { + throw new Error(`Identifier ${did} not found`); + } + if (!identifier.controllerKeyId) { + throw new Error(`Identifier ${did} does not have link to the controller key...`); + } + const key = await KeyService.instance.get(identifier.controllerKeyId); + if (!key) { + throw new Error(`Key for ${did} not found`); + } + + const resourceEntity = await ResourceService.instance.createFromLinkedResource( + resource, + customer, + key, + identifier, + encrypted, + symmetricKey + ); + if (!resourceEntity) { + return { + operation: trackOperation, + error: `Resource for ${did} was not tracked`, + }; + } + return { + operation: trackOperation, + error: '', + }; + } +} diff --git a/src/services/track/base.ts b/src/services/track/base.ts index 8db75156..c6d69a94 100644 --- a/src/services/track/base.ts +++ b/src/services/track/base.ts @@ -12,7 +12,7 @@ export class BaseOperationObserver implements IObserver { throw new Error('Method not implemented.'); } - async notify(notifyMessage: INotifyMessage): Promise { + notify(notifyMessage: INotifyMessage): void { this.emitter.emit('notify', notifyMessage); } diff --git a/src/services/track/observer.ts b/src/services/track/observer.ts index 2672d504..3282df73 100644 --- a/src/services/track/observer.ts +++ b/src/services/track/observer.ts @@ -1,6 +1,6 @@ import type { ITrackSubject, IObserver, ITrackType } from './types.js'; -export class Observer implements ITrackSubject { +export class TrackSubject implements ITrackSubject { private observers: IObserver[] = []; public attach(observer: IObserver): void { @@ -24,3 +24,31 @@ export class Observer implements ITrackSubject { Promise.all(this.observers.map((observer) => observer.update(operation))); } } + +export class SubmitSubject implements ITrackSubject { + private observers: IObserver[] = []; + + public attach(observer: IObserver): void { + const isExist = this.observers.includes(observer); + if (isExist) { + return console.warn('TrackOperation: Observer has been attached already.'); + } + this.observers.push(observer); + } + + public detach(observer: IObserver): void { + const observerIndex = this.observers.indexOf(observer); + if (observerIndex === -1) { + return console.warn('TrackOperation: Nonexistent observer.'); + } + + this.observers.splice(observerIndex, 1); + } + + public async notify(operation: ITrackType): Promise { + for (const observer of this.observers) { + await observer.update(operation); + } + } +} + diff --git a/src/services/track/operation-subscriber.ts b/src/services/track/operation-subscriber.ts new file mode 100644 index 00000000..eb1b27a7 --- /dev/null +++ b/src/services/track/operation-subscriber.ts @@ -0,0 +1,157 @@ +import { OperationNameEnum, OperationDefaultFeeEnum } from '../../types/constants.js'; +import { + type IResourceTrack, + type ITrackOperation, + type ITrackResult, + TrackOperationWithPayment, +} from '../../types/track.js'; +import { toCoin } from './helpers.js'; +import { OperationService } from '../api/operation.js'; +import type { IObserver } from './types.js'; +import { BaseOperationObserver } from './base.js'; +import type { LogLevelDesc } from 'loglevel'; +import type { OperationEntity } from '../../database/entities/operation.entity.js'; +import { PaymentService } from '../api/payment.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; +import type { Coin } from '@cosmjs/amino'; +import { CoinService } from '../api/coin.js'; + +export class DBOperationSubscriber extends BaseOperationObserver implements IObserver { + protected logSeverity: LogLevelDesc = 'debug'; + + async update(trackOperation: ITrackOperation): Promise { + // tracking operation in our DB. It handles all the operations + const result = await this.trackOperation(trackOperation); + const message = result.error + ? `Error while writing information about operation ${trackOperation.name} to DB: ${result.error}` + : `Information about operation ${trackOperation.name} was successfully written to DB`; + // notify about the result of tracking, e.g. log or datadog + await this.notify({ + message: message, + severity: result.error ? 'error' : this.logSeverity, + }); + } + + async trackPayments( + operationWithPayment: TrackOperationWithPayment, + operationEntity: OperationEntity + ): Promise { + const resource = await operationWithPayment.getResourceEntity(); + if (!resource) { + return { + operation: operationWithPayment, + error: `Resource for operation ${operationWithPayment.name} not found. Customer: ${operationWithPayment.customer.customerId}`, + } satisfies ITrackResult; + } + + for (const feePayment of operationWithPayment.feePaymentOptions) { + // Create Fee Coin + const feeCoin = await CoinService.instance.create(BigInt(feePayment.fee.amount), feePayment.fee.denom); + // Create Amount Coin + const amountCoin = await CoinService.instance.create( + BigInt(feePayment.amount.amount), + feePayment.amount.denom + ); + + const payment = await PaymentService.instance.create( + feePayment.txHash as string, + operationWithPayment.customer as CustomerEntity, + operationEntity, + feeCoin, + amountCoin, + feePayment.successful, + feePayment.network, + resource, + feePayment.fromAddress, + feePayment.toAddress, + feePayment.timestamp + ); + if (!payment) { + return { + operation: operationWithPayment, + error: `Payment for operation ${operationWithPayment.name} was not written to DB`, + } satisfies ITrackResult; + } + } + return { + operation: operationWithPayment, + error: '', + } satisfies ITrackResult; + } + + async getDefaultFeeCoin(operation: ITrackOperation): Promise { + const defaultFee = 0; + switch (operation.name) { + case OperationNameEnum.DID_CREATE: + return toCoin(BigInt(OperationDefaultFeeEnum.DID_CREATE)); + case OperationNameEnum.DID_UPDATE: + return toCoin(BigInt(OperationDefaultFeeEnum.DID_UPDATE)); + case OperationNameEnum.DID_DEACTIVATE: + return toCoin(BigInt(OperationDefaultFeeEnum.DID_DEACTIVATE)); + } + if ( + operation.name === OperationNameEnum.RESOURCE_CREATE || + operation.name === OperationNameEnum.CREDENTIAL_STATUS_CREATE_ENCRYPTED || + operation.name === OperationNameEnum.CREDENTIAL_STATUS_CREATE_UNENCRYPTED || + operation.name === OperationNameEnum.CREDENTIAL_STATUS_UPDATE_ENCRYPTED || + operation.name === OperationNameEnum.CREDENTIAL_STATUS_UPDATE_UNENCRYPTED + ) { + const resource = (operation.data as IResourceTrack).resource; + if (!resource) { + return toCoin(BigInt(defaultFee)); + } + if (resource.mediaType === 'application/json') { + return toCoin(BigInt(OperationDefaultFeeEnum.RESOURCE_CREATE_JSON)); + } + if (resource.mediaType.includes('image')) { + return toCoin(BigInt(OperationDefaultFeeEnum.RESOURCE_CREATE_IMAGE)); + } + return toCoin(BigInt(OperationDefaultFeeEnum.RESOURCE_CREATE_OTHER)); + } + return toCoin(BigInt(defaultFee)); + } + + async trackOperation(trackOperation: ITrackOperation): Promise { + try { + // Create Default Fee Coin + const defaultFeeCoin = await this.getDefaultFeeCoin(trackOperation); + const defaultFee = await CoinService.instance.create(BigInt(defaultFeeCoin.amount), defaultFeeCoin.denom); + // Create operation entity + const operationEntity = await OperationService.instance.create( + trackOperation.category, + trackOperation.name, + defaultFee, + false, + trackOperation.successful + ); + + if (!operationEntity) { + throw new Error(`Operation ${trackOperation.name} was not written to DB`); + } + + // Track payments + if (trackOperation.feePaymentOptions) { + const operationWithPayment = new TrackOperationWithPayment(trackOperation); + const paymentValidation = operationWithPayment.validate(); + if (paymentValidation.error) { + return { + operation: trackOperation, + error: `Error while validating payment options: ${paymentValidation.error}`, + } satisfies ITrackResult; + } + return await this.trackPayments(operationWithPayment, operationEntity); + } + + return { + operation: trackOperation, + error: '', + } satisfies ITrackResult; + } catch (error) { + return { + operation: trackOperation, + error: `Error while writing information about operation ${trackOperation.name} to DB: ${(error as Error)?.message || error}`, + } satisfies ITrackResult; + } + } +} + diff --git a/src/services/track/submitter.ts b/src/services/track/submitter.ts new file mode 100644 index 00000000..44adbd1d --- /dev/null +++ b/src/services/track/submitter.ts @@ -0,0 +1,24 @@ +// Type: Interface +export type ISubmitData = ISubmitStripeCustomerCreateData + | ISubmitSubscriptionData; + +export interface ISubmitStripeCustomerCreateData { + customerId: string; + name: string; + email?: string; +} + +export interface ISubmitSubscriptionData { + stripeCustomerId: string; + status: string; + currentPeriodStart: Date; + currentPeriodEnd: Date; + trialStart?: Date; + trialEnd?: Date; + subscriptionId: string; +} + +export interface ISubmitOperation { + operation: string; + data: ISubmitData; +} diff --git a/src/services/track/subscribers.ts b/src/services/track/subscribers.ts deleted file mode 100644 index 1e992e1b..00000000 --- a/src/services/track/subscribers.ts +++ /dev/null @@ -1,463 +0,0 @@ -import type { LinkedResourceMetadataResolutionResult } from '@cheqd/did-provider-cheqd'; -import { OperationNameEnum, OperationCategoryNameEnum, OperationDefaultFeeEnum } from '../../types/constants.js'; -import { - type ICredentialStatusTrack, - type ICredentialTrack, - type IPresentationTrack, - type IResourceTrack, - type IKeyTrack, - type ITrackOperation, - type ITrackResult, - type IDIDTrack, - TrackOperationWithPayment, -} from '../../types/track.js'; -import { isCredentialStatusTrack, isCredentialTrack, isResourceTrack, toCoin } from './helpers.js'; -import { IdentifierService } from '../identifier.js'; -import { KeyService } from '../key.js'; -import { OperationService } from '../operation.js'; -import { ResourceService } from '../resource.js'; -import type { IObserver } from './types.js'; -import { BaseOperationObserver } from './base.js'; -import type { LogLevelDesc } from 'loglevel'; -import type { OperationEntity } from '../../database/entities/operation.entity.js'; -import { PaymentService } from '../payment.js'; -import type { CustomerEntity } from '../../database/entities/customer.entity.js'; -import type { Coin } from '@cosmjs/amino'; -import { CoinService } from '../coin.js'; - -export class DBOperationSubscriber extends BaseOperationObserver implements IObserver { - protected logSeverity: LogLevelDesc = 'debug'; - - async update(trackOperation: ITrackOperation): Promise { - // tracking operation in our DB. It handles all the operations - const result = await this.trackOperation(trackOperation); - const message = result.error - ? `Error while writing information about operation ${trackOperation.name} to DB: ${result.error}` - : `Information about operation ${trackOperation.name} was successfully written to DB`; - // notify about the result of tracking, e.g. log or datadog - await this.notify({ - message: message, - severity: result.error ? 'error' : this.logSeverity, - }); - } - - async trackPayments( - operationWithPayment: TrackOperationWithPayment, - operationEntity: OperationEntity - ): Promise { - const resource = await operationWithPayment.getResourceEntity(); - if (!resource) { - return { - operation: operationWithPayment, - error: `Resource for operation ${operationWithPayment.name} not found. Customer: ${operationWithPayment.customer.customerId}`, - } satisfies ITrackResult; - } - - for (const feePayment of operationWithPayment.feePaymentOptions) { - // Create Fee Coin - const feeCoin = await CoinService.instance.create(BigInt(feePayment.fee.amount), feePayment.fee.denom); - // Create Amount Coin - const amountCoin = await CoinService.instance.create( - BigInt(feePayment.amount.amount), - feePayment.amount.denom - ); - - const payment = await PaymentService.instance.create( - feePayment.txHash as string, - operationWithPayment.customer as CustomerEntity, - operationEntity, - feeCoin, - amountCoin, - feePayment.successful, - feePayment.network, - resource, - feePayment.fromAddress, - feePayment.toAddress, - feePayment.timestamp - ); - if (!payment) { - return { - operation: operationWithPayment, - error: `Payment for operation ${operationWithPayment.name} was not written to DB`, - } satisfies ITrackResult; - } - } - return { - operation: operationWithPayment, - error: '', - } satisfies ITrackResult; - } - - async getDefaultFeeCoin(operation: ITrackOperation): Promise { - const defaultFee = 0; - switch (operation.name) { - case OperationNameEnum.DID_CREATE: - return toCoin(BigInt(OperationDefaultFeeEnum.DID_CREATE)); - case OperationNameEnum.DID_UPDATE: - return toCoin(BigInt(OperationDefaultFeeEnum.DID_UPDATE)); - case OperationNameEnum.DID_DEACTIVATE: - return toCoin(BigInt(OperationDefaultFeeEnum.DID_DEACTIVATE)); - } - if ( - operation.name === OperationNameEnum.RESOURCE_CREATE || - operation.name === OperationNameEnum.CREDENTIAL_STATUS_CREATE_ENCRYPTED || - operation.name === OperationNameEnum.CREDENTIAL_STATUS_CREATE_UNENCRYPTED || - operation.name === OperationNameEnum.CREDENTIAL_STATUS_UPDATE_ENCRYPTED || - operation.name === OperationNameEnum.CREDENTIAL_STATUS_UPDATE_UNENCRYPTED - ) { - const resource = (operation.data as IResourceTrack).resource; - if (!resource) { - return toCoin(BigInt(defaultFee)); - } - if (resource.mediaType === 'application/json') { - return toCoin(BigInt(OperationDefaultFeeEnum.RESOURCE_CREATE_JSON)); - } - if (resource.mediaType.includes('image')) { - return toCoin(BigInt(OperationDefaultFeeEnum.RESOURCE_CREATE_IMAGE)); - } - return toCoin(BigInt(OperationDefaultFeeEnum.RESOURCE_CREATE_OTHER)); - } - return toCoin(BigInt(defaultFee)); - } - - async trackOperation(trackOperation: ITrackOperation): Promise { - try { - // Create Default Fee Coin - const defaultFeeCoin = await this.getDefaultFeeCoin(trackOperation); - const defaultFee = await CoinService.instance.create(BigInt(defaultFeeCoin.amount), defaultFeeCoin.denom); - // Create operation entity - const operationEntity = await OperationService.instance.create( - trackOperation.category, - trackOperation.name, - defaultFee, - false, - trackOperation.successful - ); - - if (!operationEntity) { - throw new Error(`Operation ${trackOperation.name} was not written to DB`); - } - - // Track payments - if (trackOperation.feePaymentOptions) { - const operationWithPayment = new TrackOperationWithPayment(trackOperation); - const paymentValidation = operationWithPayment.validate(); - if (paymentValidation.error) { - return { - operation: trackOperation, - error: `Error while validating payment options: ${paymentValidation.error}`, - } satisfies ITrackResult; - } - return await this.trackPayments(operationWithPayment, operationEntity); - } - - return { - operation: trackOperation, - error: '', - } satisfies ITrackResult; - } catch (error) { - return { - operation: trackOperation, - error: `Error while writing information about operation ${trackOperation.name} to DB: ${(error as Error)?.message || error}`, - } satisfies ITrackResult; - } - } -} - -export class ResourceSubscriber extends BaseOperationObserver implements IObserver { - private static acceptedOperations = [ - OperationNameEnum.RESOURCE_CREATE, - OperationNameEnum.CREDENTIAL_REVOKE, - OperationNameEnum.CREDENTIAL_SUSPEND, - OperationNameEnum.CREDENTIAL_UNSUSPEND, - OperationNameEnum.CREDENTIAL_STATUS_CREATE_UNENCRYPTED, - OperationNameEnum.CREDENTIAL_STATUS_CREATE_ENCRYPTED, - OperationNameEnum.CREDENTIAL_STATUS_UPDATE_UNENCRYPTED, - OperationNameEnum.CREDENTIAL_STATUS_UPDATE_ENCRYPTED, - ]; - - isReactionNeeded(trackOperation: ITrackOperation): boolean { - // Resource tracker reacts on CredentialStatusList, Credential operations like revocation - // and Resource operations like create, update, delete - const isCategoryAccepted = - trackOperation.category === OperationCategoryNameEnum.RESOURCE || - trackOperation.category === OperationCategoryNameEnum.CREDENTIAL || - trackOperation.category === OperationCategoryNameEnum.CREDENTIAL_STATUS; - const isOperationAccepted = ResourceSubscriber.acceptedOperations.includes( - trackOperation.name as OperationNameEnum - ); - return isCategoryAccepted && isOperationAccepted; - } - - public compileMessage(trackResult: ITrackResult): string { - const base_message = super.compileMessage(trackResult); - const data = trackResult.operation.data as IResourceTrack; - return `${base_message} | Resource DID: ${data.did} | ResourceName: ${data.resource.resourceName} | ResourceType: ${data.resource.resourceType} | ResourceId: ${data.resource.resourceId}`; - } - - async update(trackOperation: ITrackOperation): Promise { - if (!this.isReactionNeeded(trackOperation)) { - // Just skip this operation - return; - } - trackOperation.category = OperationCategoryNameEnum.RESOURCE; - trackOperation.name = OperationNameEnum.RESOURCE_CREATE; - // tracking resource creation in DB - const result = await this.trackResourceOperation(trackOperation); - // notify about the result of tracking, e.g. log or datadog - await this.notify({ - message: this.compileMessage(result), - severity: result.error ? 'error' : 'info', - }); - } - - async trackResourceOperation(trackOperation: ITrackOperation): Promise { - // Resource operation may be with CredentialStatusList or with Credential operations like revocation - // and others and also with Resource operations like create, update, delete - const customer = trackOperation.customer; - const data = trackOperation.data as IResourceTrack | ICredentialStatusTrack | ICredentialTrack; - const did = data.did; - let encrypted = false; - let symmetricKey = ''; - let resource: LinkedResourceMetadataResolutionResult | undefined = undefined; - - if (!customer) { - return { - operation: trackOperation, - error: `Customer for resource operation was not specified`, - }; - } - - if (isResourceTrack(data)) { - encrypted = false; - symmetricKey = ''; - resource = (data as IResourceTrack).resource; - } - if (isCredentialStatusTrack(data)) { - encrypted = (data as ICredentialStatusTrack).encrypted || false; - symmetricKey = (data as ICredentialStatusTrack).symmetricKey || ''; - resource = (data as ICredentialStatusTrack).resource; - } - if (isCredentialTrack(data)) { - encrypted = (data as ICredentialTrack).encrypted || false; - symmetricKey = (data as ICredentialTrack).symmetricKey || ''; - resource = (data as ICredentialTrack).resource; - } - - if (!resource) { - return { - operation: trackOperation, - error: `Resource for ${did} was not specified`, - }; - } - - const identifier = await IdentifierService.instance.get(did); - if (!identifier) { - throw new Error(`Identifier ${did} not found`); - } - if (!identifier.controllerKeyId) { - throw new Error(`Identifier ${did} does not have link to the controller key...`); - } - const key = await KeyService.instance.get(identifier.controllerKeyId); - if (!key) { - throw new Error(`Key for ${did} not found`); - } - - const resourceEntity = await ResourceService.instance.createFromLinkedResource( - resource, - customer, - key, - identifier, - encrypted, - symmetricKey - ); - if (!resourceEntity) { - return { - operation: trackOperation, - error: `Resource for ${did} was not tracked`, - }; - } - return { - operation: trackOperation, - error: '', - }; - } -} - -export class CredentialSubscriber extends BaseOperationObserver implements IObserver { - isReactionNeeded(trackOperation: ITrackOperation): boolean { - // Credential tracker reacts on CredentialStatusList, Credential operations like revocation - // and Resource operations like create, update, delete - return trackOperation.category === OperationCategoryNameEnum.CREDENTIAL; - } - - public compileMessage(trackResult: ITrackResult): string { - const base_message = super.compileMessage(trackResult); - const data = trackResult.operation.data as ICredentialTrack; - return `${base_message} | Credential holder: ${data.did}`; - } - - async update(trackOperation: ITrackOperation): Promise { - if (!this.isReactionNeeded(trackOperation)) { - // Just skip this operation - return; - } - // tracking resource creation in DB - const result = await this.trackCredentialOperation(trackOperation); - // notify about the result of tracking, e.g. log or datadog - await this.notify({ - message: this.compileMessage(result), - severity: result.error ? 'error' : 'info', - }); - } - - async trackCredentialOperation(trackOperation: ITrackOperation): Promise { - // We don't have specific credential writes, so we just track credential creation - return { - operation: trackOperation, - error: '', - } satisfies ITrackResult; - } -} - -export class DIDSubscriber extends BaseOperationObserver implements IObserver { - isReactionNeeded(trackOperation: ITrackOperation): boolean { - return trackOperation.category === OperationCategoryNameEnum.DID; - } - - public compileMessage(trackResult: ITrackResult): string { - const base_message = super.compileMessage(trackResult); - const data = trackResult.operation.data as IDIDTrack; - return `${base_message} | Target DID: ${data.did}`; - } - - async update(trackOperation: ITrackOperation): Promise { - if (!this.isReactionNeeded(trackOperation)) { - // Just skip this operation - return; - } - // tracking resource creation in DB - const result = await this.trackDIDOperation(trackOperation); - // notify about the result of tracking, e.g. log or datadog - await this.notify({ - message: this.compileMessage(result), - severity: result.error ? 'error' : 'info', - }); - } - - async trackDIDOperation(trackOperation: ITrackOperation): Promise { - // We don't have specific DID related operations to track - return { - operation: trackOperation, - error: '', - } satisfies ITrackResult; - } -} - -export class CredentialStatusSubscriber extends BaseOperationObserver implements IObserver { - isReactionNeeded(trackOperation: ITrackOperation): boolean { - // Credential tracker reacts on CredentialStatusList, Credential operations like revocation - // and Resource operations like create, update, delete - return trackOperation.category === OperationCategoryNameEnum.CREDENTIAL_STATUS; - } - - public compileMessage(trackResult: ITrackResult): string { - const base_message = super.compileMessage(trackResult); - const data = trackResult.operation.data as ICredentialStatusTrack; - return `${base_message} | Target DID: ${data.did} | Encrypted: ${data.encrypted} | StatusListName: ${data.resource?.resourceName}`; - } - - async update(trackOperation: ITrackOperation): Promise { - if (!this.isReactionNeeded(trackOperation)) { - // Just skip this operation - return; - } - // tracking resource creation in DB - const result = await this.trackCredentialStatusOperation(trackOperation); - // notify about the result of tracking, e.g. log or datadog - await this.notify({ - message: this.compileMessage(result), - severity: result.error ? 'error' : 'info', - }); - } - - async trackCredentialStatusOperation(trackOperation: ITrackOperation): Promise { - // We don't have specific credential status writes, so we just track credential creation - return { - operation: trackOperation, - error: '', - } satisfies ITrackResult; - } -} - -export class PresentationSubscriber extends BaseOperationObserver implements IObserver { - isReactionNeeded(trackOperation: ITrackOperation): boolean { - // Credential tracker reacts on CredentialStatusList, Credential operations like revocation - // and Resource operations like create, update, delete - return trackOperation.category === OperationCategoryNameEnum.PRESENTATION; - } - - public compileMessage(trackResult: ITrackResult): string { - const base_message = super.compileMessage(trackResult); - const data = trackResult.operation.data as IPresentationTrack; - return `${base_message} | Presentation holder: ${data.holder}`; - } - - async update(trackOperation: ITrackOperation): Promise { - if (!this.isReactionNeeded(trackOperation)) { - // Just skip this operation - return; - } - // tracking resource creation in DB - const result = await this.trackPresentationOperation(trackOperation); - // notify about the result of tracking, e.g. log or datadog - await this.notify({ - message: this.compileMessage(result), - severity: result.error ? 'error' : 'info', - }); - } - - async trackPresentationOperation(trackOperation: ITrackOperation): Promise { - // We don't have specific presentation writes, so we just track presentation creation - return { - operation: trackOperation, - error: '', - } satisfies ITrackResult; - } -} - -export class KeySubscriber extends BaseOperationObserver implements IObserver { - isReactionNeeded(trackOperation: ITrackOperation): boolean { - // Credential tracker reacts on CredentialStatusList, Credential operations like revocation - // and Resource operations like create, update, delete - return trackOperation.category === OperationCategoryNameEnum.KEY; - } - - public compileMessage(trackResult: ITrackResult): string { - const base_message = super.compileMessage(trackResult); - const data = trackResult.operation.data as IKeyTrack; - return `${base_message} | keyRef: ${data.keyRef} | keyType: ${data.keyType}`; - } - - async update(trackOperation: ITrackOperation): Promise { - if (!this.isReactionNeeded(trackOperation)) { - // Just skip this operation - return; - } - // tracking resource creation in DB - const result = await this.trackKeyOperation(trackOperation); - // notify about the result of tracking, e.g. log or datadog - await this.notify({ - message: this.compileMessage(result), - severity: result.error ? 'error' : 'info', - }); - } - - async trackKeyOperation(trackOperation: ITrackOperation): Promise { - // We don't have specific presentation writes, so we just track presentation creation - return { - operation: trackOperation, - error: '', - } satisfies ITrackResult; - } -} diff --git a/src/services/track/tracker.ts b/src/services/track/tracker.ts index f73cb1c1..62713d0c 100644 --- a/src/services/track/tracker.ts +++ b/src/services/track/tracker.ts @@ -3,25 +3,31 @@ import type { INotifyMessage, ITrackOperation } from '../../types/track.js'; import { DatadogNotifier, LoggerNotifier } from './notifiers.js'; import { DBOperationSubscriber, - ResourceSubscriber, - CredentialSubscriber, - DIDSubscriber, - CredentialStatusSubscriber, - PresentationSubscriber, -} from './subscribers.js'; +} from './operation-subscriber.js'; +import { + ResourceSubscriber} from './api/resource-subscriber.js'; +import { PresentationSubscriber } from './api/presentation-subscriber.js'; +import { CredentialStatusSubscriber } from './api/credential-status-subscriber.js'; +import { DIDSubscriber } from './api/did-subscriber.js'; +import { CredentialSubscriber } from './api/credential-subscriber.js'; import type { ITrackType } from './types.js'; import { ENABLE_DATADOG } from '../../types/constants.js'; -import { Observer } from './observer.js'; +import { SubmitSubject, TrackSubject } from './observer.js'; +import type { ISubmitOperation } from './submitter.js'; +import { PortalAccountCreateSubmitter } from "./admin/account-submitter.js"; +import { SubscriptionSubmitter } from './admin/subscription-submitter.js'; export class EventTracker { readonly emitter: EventEmitter; - readonly tracker: Observer; - readonly notifier: Observer; + readonly tracker: TrackSubject; + readonly notifier: TrackSubject; + readonly submitter: SubmitSubject; - constructor(emitter?: EventEmitter, tracker?: Observer, notifier?: Observer) { + constructor(emitter?: EventEmitter, tracker?: TrackSubject, notifier?: TrackSubject, submitter?: SubmitSubject) { this.emitter = emitter || new EventEmitter(); - this.tracker = tracker || new Observer(); - this.notifier = notifier || new Observer(); + this.tracker = tracker || new TrackSubject(); + this.notifier = notifier || new TrackSubject(); + this.submitter = submitter || new SubmitSubject(); if (!tracker) { this.setupDefaultTrackers(); @@ -29,6 +35,9 @@ export class EventTracker { if (!notifier) { this.setupDefaultNotifiers(); } + if (!submitter) { + this.setupDefaultSubmitters(); + } this.setupBaseEvents(); } @@ -48,6 +57,11 @@ export class EventTracker { } } + setupDefaultSubmitters() { + this.submitter.attach(new PortalAccountCreateSubmitter(this.getEmitter())); + this.submitter.attach(new SubscriptionSubmitter(this.getEmitter())); + } + getEmitter(): EventEmitter { return this.emitter; } @@ -55,6 +69,7 @@ export class EventTracker { setupBaseEvents() { this.emitter.on('track', this.track.bind(this)); this.emitter.on('notify', this.notify.bind(this)); + this.emitter.on('submit', this.submit.bind(this)); } async track(trackOperation: ITrackOperation): Promise { @@ -65,6 +80,19 @@ export class EventTracker { await this.notifier.notify(notifyMessage); } + async submit(operation: ISubmitOperation): Promise { + await this.submitter.notify(operation); + } + + static compileBasicNotification(message: string, operation?: string): string { + const parts = []; + const date = new Date().toISOString(); + parts.push(date); + if (operation) parts.push('Operation: ' + operation); + parts.push('Message: ' + message); + return parts.join(' | '); + } + emit(eventName: string | symbol, ...args: ITrackType[]): boolean { return this.getEmitter().emit(eventName, ...args); } diff --git a/src/services/track/types.ts b/src/services/track/types.ts index baf52694..9f84a847 100644 --- a/src/services/track/types.ts +++ b/src/services/track/types.ts @@ -1,6 +1,7 @@ import type { ITrackOperation, INotifyMessage } from '../../types/track'; +import type { ISubmitOperation } from './submitter'; -export type ITrackType = ITrackOperation | INotifyMessage; +export type ITrackType = ITrackOperation | INotifyMessage | ISubmitOperation; export interface IObserver { update(operation: ITrackType): Promise; diff --git a/src/static/swagger-admin-options.json b/src/static/swagger-admin-options.json index 4e4498c5..b3382775 100644 --- a/src/static/swagger-admin-options.json +++ b/src/static/swagger-admin-options.json @@ -26,6 +26,9 @@ }, { "name": "Subscription" + }, + { + "name": "Checkout" } ] } diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 022bcf36..76f95afe 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -26,9 +26,45 @@ }, { "name": "Subscription" + }, + { + "name": "Checkout" } ], "paths": { + "/admin/checkout/session/create": { + "post": { + "summary": "Create a checkout session", + "description": "Create a checkout session", + "tags": [ + "Checkout" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckoutSessionCreateRequestBody" + } + } + } + }, + "responses": { + "303": { + "description": "A redirect to Stripe prebuilt checkout page" + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, "/admin/price/list": { "get": { "summary": "Get a list of prices", @@ -647,6 +683,37 @@ } } }, + "CheckoutSessionCreateRequestBody": { + "description": "The request body for creating a checkout session", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + }, + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + }, + "successURL": { + "type": "string", + "description": "The URL to redirect to after the customer sucessfully completes the checkout", + "example": "https://example.com/success" + }, + "cancelURL": { + "type": "string", + "description": "The URL to redirect to after the customer cancels the checkout", + "example": "https://example.com/cancel" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, "NotFoundError": { "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", "type": "object", diff --git a/src/types/constants.ts b/src/types/constants.ts index 6399c57c..f52948e7 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -74,6 +74,7 @@ export enum OperationCategoryNameEnum { CREDENTIAL = 'credential', PRESENTATION = 'presentation', KEY = 'key', + SUBSCRIPTION = 'subscription', } export enum OperationDefaultFeeEnum { @@ -121,6 +122,14 @@ export enum OperationNameEnum { // Presentation operations PRESENTATION_CREATE = 'presentation-create', PRESENTATION_VERIFY = 'presentation-verify', + // Subscription + SUBSCRIPTION_CREATE = 'subscription-create', + SUBSCRIPTION_CANCEL = 'subscription-cancel', + SUBSCRIPTION_UPDATE = 'subscription-update', + SUBSCRIPTION_TRIAL_WILL_END = 'subscription-trial-will-end', + + // Stripe operations + STRIPE_ACCOUNT_CREATE = 'stripe-account-create', } export const JWT_PROOF_TYPE = 'JwtProof2020'; diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index f29c106e..d114b94e 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -60,6 +60,7 @@ declare global { // Stripe STRIPE_SECRET_KEY: string; STRIPE_PUBLISHABLE_KEY: string; + STRIPE_WEBHOOK_SECRET: string; } } diff --git a/src/types/portal.ts b/src/types/portal.ts index e4de0863..4514a4f0 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -97,4 +97,22 @@ export type SubscriptionResumeUnsuccessfulResponseBody = UnsuccessfulResponseBod // Customer // Get -export type PortalCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; \ No newline at end of file +export type PortalCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; + + +// Checkout Session +export type CheckoutSessionCreateRequestBody = { + price: string; + successURL: string; + cancelURL: string; + quantity?: number; + idempotencyKey?: string; +} +export type CheckoutSessionCreateResponseBody = { + clientSecret: Stripe.Checkout.Session['client_secret']; +} + +export type CheckoutSessionCreateUnsuccessfulResponseBody = UnsuccessfulResponseBody; +// Utils + +export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; \ No newline at end of file diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index e7ac6ea1..da03eaae 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -165,6 +165,26 @@ * subscription: * type: object * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] + * CheckoutSessionCreateRequestBody: + * description: The request body for creating a checkout session + * type: object + * properties: + * price: + * type: string + * description: The price id + * example: price_1234567890 + * successURL: + * type: string + * description: The URL to redirect to after the customer sucessfully completes the checkout + * example: https://example.com/success + * cancelURL: + * type: string + * description: The URL to redirect to after the customer cancels the checkout + * example: https://example.com/cancel + * idempotencyKey: + * type: string + * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. + * example: 1234567890 * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. * type: object diff --git a/src/types/track.ts b/src/types/track.ts index c61e3f2d..807a161c 100644 --- a/src/types/track.ts +++ b/src/types/track.ts @@ -4,7 +4,7 @@ import type { CustomerEntity } from '../database/entities/customer.entity.js'; import type { UserEntity } from '../database/entities/user.entity.js'; import type { LogLevelDesc } from 'loglevel'; import type { ResourceEntity } from '../database/entities/resource.entity.js'; -import { ResourceService } from '../services/resource.js'; +import { ResourceService } from '../services/api/resource.js'; import type { Coin } from '@cosmjs/amino'; export type TrackData = From a036a0223badf57799b3e7af2923bde7ef49e2bd Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 10 Mar 2024 20:46:40 +0100 Subject: [PATCH 04/49] Add auth routes to admin --- src/app.ts | 12 +-- src/controllers/admin/product.ts | 4 +- src/controllers/admin/subscriptions.ts | 18 ++--- src/database/entities/subscription.entity.ts | 8 +- .../auth/routes/admin/admin-auth.ts | 14 ++++ src/middleware/event-tracker.ts | 10 ++- src/services/admin/subscription.ts | 78 ++++++++++++++++++- src/services/api/customer.ts | 7 ++ src/static/swagger-admin.json | 18 +---- src/types/portal.ts | 5 -- 10 files changed, 125 insertions(+), 49 deletions(-) diff --git a/src/app.ts b/src/app.ts index 06771e39..39026780 100644 --- a/src/app.ts +++ b/src/app.ts @@ -21,12 +21,12 @@ dotenv.config(); // Define Swagger file import swaggerAPIDocument from './static/swagger-api.json' assert { type: 'json' }; -// import swaggerAdminDocument from './static/swagger-admin.json' assert { type: 'json' }; +import swaggerAdminDocument from './static/swagger-admin.json' assert { type: 'json' }; import { PresentationController } from './controllers/api/presentation.js'; import { KeyController } from './controllers/api/key.js'; import { DIDController } from './controllers/api/did.js'; import { ResourceController } from './controllers/api/resource.js'; -import { FailedResponseTracker } from './middleware/event-tracker.js'; +import { ResponseTracker } from './middleware/event-tracker.js'; import { ProductController } from './controllers/admin/product.js'; import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; @@ -77,7 +77,7 @@ class App { this.express.use(cookieParser()); const auth = new Authentication(); // EventTracking - this.express.use(new FailedResponseTracker().trackJson); + this.express.use(new ResponseTracker().trackJson); // Authentication if (process.env.ENABLE_AUTHENTICATION === 'true') { this.express.use( @@ -103,7 +103,7 @@ class App { this.express.use(express.text()); this.express.use('/swagger', swaggerUi.serveFiles(swaggerAPIDocument, swaggerOptions), swaggerUi.setup(swaggerAPIDocument, swaggerOptions)); - // this.express.use('/admin/swagger', swaggerUi.serveFiles(swaggerAdminDocument), swaggerUi.setup(swaggerAdminDocument)) + this.express.use('/admin/swagger', swaggerUi.serveFiles(swaggerAdminDocument), swaggerUi.setup(swaggerAdminDocument)) this.express.use(auth.handleError); this.express.use(async (req, res, next) => await auth.accessControl(req, res, next)); } @@ -215,8 +215,8 @@ class App { // Portal // Product - app.get('/admin/product/list', ProductController.productListValidator, new ProductController().getListProducts); - app.get('/admin/product/:productId', ProductController.productGetValidator, new ProductController().getProduct); + app.get('/admin/product/list', ProductController.productListValidator, new ProductController().listProducts); + app.get('/admin/product/get/:productId', ProductController.productGetValidator, new ProductController().getProduct); // Prices app.get('/admin/price/list', PriceController.priceListValidator, new PriceController().getListPrices); diff --git a/src/controllers/admin/product.ts b/src/controllers/admin/product.ts index 23f81c35..90ec309e 100644 --- a/src/controllers/admin/product.ts +++ b/src/controllers/admin/product.ts @@ -63,7 +63,7 @@ export class ProductController { * 404: * $ref: '#/components/schemas/NotFoundError' */ - async getListProducts(request: Request, response: Response) { + async listProducts(request: Request, response: Response) { const result = validationResult(request); // handle error if (!result.isEmpty()) { @@ -110,7 +110,7 @@ export class ProductController { /** * @openapi * - * /admin/product/{productId}: + * /admin/product/get/{productId}: * get: * summary: Get a product * description: Get a product by id diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 41180ebc..35d8c204 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -5,6 +5,7 @@ import type { SubscriptionCreateRequestBody, SubscriptionCreateResponseBody, Sub import { StatusCodes } from 'http-status-codes'; import { validationResult } from '../validator/index.js'; import { check } from 'express-validator'; +import { SubscriptionService } from '../../services/admin/subscription.js'; dotenv.config(); @@ -140,11 +141,11 @@ export class SubscriptionController { } satisfies SubscriptionCreateUnsuccessfulResponseBody); } - const { customerId, items, idempotencyKey } = request.body satisfies SubscriptionCreateRequestBody; + const { items, idempotencyKey } = request.body satisfies SubscriptionCreateRequestBody; try { // Create the subscription const subscription = await stripe.subscriptions.create({ - customer: customerId, + customer: response.locals.customer.stripeCustomerId, items: items, payment_behavior: "default_incomplete" as PaymentBehavior, }, @@ -236,13 +237,6 @@ export class SubscriptionController { * summary: Get a list of subscriptions * description: Get a list of subscriptions * tags: [Subscription] - * parameters: - * - in: query - * name: customerId - * schema: - * type: string - * description: The customer id. If passed - returns filtered by this customer list of subscriptions. - * required: false * responses: * 200: * description: A list of subscriptions @@ -268,8 +262,10 @@ export class SubscriptionController { error: result.array().pop()?.msg } satisfies SubscriptionListUnsuccessfulResponseBody); } - const customerId = request.query.customerId; + const customerId = response.locals.customer.stripeCustomerId; try { + // Sync our DB with Stripe + await SubscriptionService.instance.stripeSync(response.locals.customer) // Get the subscriptions const subscriptions = customerId ? await stripe.subscriptions.list({ @@ -334,6 +330,8 @@ export class SubscriptionController { } const subscriptionId = request.params.subscriptionId; try { + // Sync our DB with Stripe + await SubscriptionService.instance.stripeSync(response.locals.customer) // Get the subscription const subscription = await stripe.subscriptions.retrieve(subscriptionId as string); if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { diff --git a/src/database/entities/subscription.entity.ts b/src/database/entities/subscription.entity.ts index 125d89de..8c990994 100644 --- a/src/database/entities/subscription.entity.ts +++ b/src/database/entities/subscription.entity.ts @@ -75,15 +75,15 @@ export class SubscriptionEntity { status: string, currentPeriodStart: Date, currentPeriodEnd: Date, - trialStart: Date, - trialEnd: Date, + trialStart?: Date, + trialEnd?: Date, ){ this.subscriptionId = subscriptionId; this.customer = customer; this.status = status; this.currentPeriodStart = currentPeriodStart; this.currentPeriodEnd = currentPeriodEnd; - this.trialStart = trialStart; - this.trialEnd = trialEnd; + if (trialStart ) this.trialStart = trialStart; + if (trialEnd) this.trialEnd = trialEnd; } } diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index 013ee342..a7a243d8 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -8,6 +8,20 @@ export class AdminHandler extends BaseAuthHandler { // ToDo: define how to get namespace information this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:testnet', { skipNamespace: true}); this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:mainnet', { skipNamespace: true}); + // Subscriptions + this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { skipNamespace: true}); + this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:mainnet', { skipNamespace: true}); + this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:testnet', { skipNamespace: true}); + this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:mainnet', { skipNamespace: true}); + // Prices + this.registerRoute('/admin/price/list', 'GET', 'admin:price:list:testnet', { skipNamespace: true}); + this.registerRoute('/admin/price/list', 'GET', 'admin:price:list:mainnet', { skipNamespace: true}); + + // Products + this.registerRoute('/admin/product/list', 'GET', 'admin:product:list:testnet', { skipNamespace: true}); + this.registerRoute('/admin/product/list', 'GET', 'admin:product:list:mainnet', { skipNamespace: true}); + this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:testnet', { skipNamespace: true}); + this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:mainnet', { skipNamespace: true}); } public async handle(request: Request, response: Response): Promise { if (!request.path.includes('/admin/')) { diff --git a/src/middleware/event-tracker.ts b/src/middleware/event-tracker.ts index 16d64619..d763dc4c 100644 --- a/src/middleware/event-tracker.ts +++ b/src/middleware/event-tracker.ts @@ -2,7 +2,7 @@ import type { Request, Response, NextFunction } from 'express'; import { eventTracker } from '../services/track/tracker.js'; import type { INotifyMessage } from '../types/track.js'; -export class FailedResponseTracker { +export class ResponseTracker { public async trackJson(request: Request, response: Response, next: NextFunction) { const originalJson = response.json; response.json = (body) => { @@ -14,18 +14,22 @@ export class FailedResponseTracker { parts.push('URL: ' + response.req.url); parts.push('Method: ' + response.req.method); parts.push('Status: ' + response.statusCode); + if (response.locals.customer) { + parts.push('Customer: ' + response.locals.customer.customerId); + } if (body && body.error) { parts.push('Message: ' + body.error); } return parts.join(' | '); }; // Notify - if (body && body.error) { + if (body) { eventTracker.emit('notify', { message: compileMessage(), - severity: 'error', + severity: body.error ? 'error' : 'info', } satisfies INotifyMessage); } + return originalJson.apply(response, [body]); }; return next(); diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts index 9006adf9..62daba14 100644 --- a/src/services/admin/subscription.ts +++ b/src/services/admin/subscription.ts @@ -3,6 +3,15 @@ import type { Repository } from 'typeorm'; import { Connection } from '../../database/connection/connection.js'; import { SubscriptionEntity } from '../../database/entities/subscription.entity.js'; import type { CustomerEntity } from '../../database/entities/customer.entity.js'; +import type { UserEntity } from '../../database/entities/user.entity.js'; +import Stripe from 'stripe'; +import * as dotenv from 'dotenv'; +import { CustomerService } from '../api/customer.js'; +import { EventTracker, eventTracker } from '../track/tracker.js'; + +dotenv.config(); + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class SubscriptionService { public subscriptionRepository: Repository; @@ -20,8 +29,8 @@ export class SubscriptionService { status: string, currentPeriodStart: Date, currentPeriodEnd: Date, - trialStart: Date, - trialEnd: Date, + trialStart?: Date, + trialEnd?: Date, ): Promise { const subscriptionEntity = new SubscriptionEntity( subscriptionId, @@ -71,4 +80,69 @@ export class SubscriptionService { relations: ['customer'], }); } + + public async stripeSync(customer?: CustomerEntity, user?: UserEntity): Promise { + let stripeCustomerId: string; + if (!customer && !user) { + throw new Error('StripeSync: customer or user is required'); + } + if (customer) { + stripeCustomerId = customer.stripeCustomerId; + } else { + stripeCustomerId = user?.customer.stripeCustomerId as string; + } + + const subscriptions = await stripe.subscriptions.list({ customer: stripeCustomerId }); + // Get list of all subscription and sort them by created time to make sure that we are processing them in the correct order + for (const subscription of subscriptions.data.sort((a, b) => a.created - b.created)){ + const existing = await this.subscriptionRepository.findOne({ + where: { subscriptionId: subscription.id }, + }); + if (!existing) { + const customer = await CustomerService.instance.findbyStripeCustomerId(stripeCustomerId); + if (!customer) { + throw new Error(`Customer with stripeCustomerId ${stripeCustomerId} not found`); + } + const res = await this.create( + subscription.id, + customer, + subscription.status, + new Date(subscription.current_period_start * 1000), + new Date(subscription.current_period_end * 1000), + subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, + subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined, + ); + if (!res) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification(`Cannot create a new subscription with id ${subscription.id}`, 'Subscription syncronization'), + severity: 'error', + }); + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification(`New subscription with id ${subscription.id} created`, 'Subscription syncronization'), + severity: 'info', + }); + } else { + const res = await this.update( + subscription.id, + subscription.status, + new Date(subscription.current_period_start * 1000), + new Date(subscription.current_period_end * 1000), + subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, + subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined, + ); + if (!res) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification(`Cannot update subscription with id ${subscription.id}`, 'Subscription syncronization'), + severity: 'error', + }); + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification(`Subscription with id ${subscription.id} updated`, 'Subscription syncronization'), + severity: 'info', + }); + } + } + + } } diff --git a/src/services/api/customer.ts b/src/services/api/customer.ts index 9accc137..b33ab7b5 100644 --- a/src/services/api/customer.ts +++ b/src/services/api/customer.ts @@ -77,6 +77,13 @@ export class CustomerService { } } + public async findbyStripeCustomerId(stripeCustomerId: string) { + return await this.customerRepository.findOne({ + where: { stripeCustomerId }, + }); + + } + public async isExist(where: Record) { try { return (await this.customerRepository.findOne({ where })) ? true : false; diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 76f95afe..8e616f6f 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -153,7 +153,7 @@ } } }, - "/admin/product/{productId}": { + "/admin/product/get/{productId}": { "get": { "summary": "Get a product", "description": "Get a product by id", @@ -291,17 +291,6 @@ "tags": [ "Subscription" ], - "parameters": [ - { - "in": "query", - "name": "customerId", - "schema": { - "type": "string", - "description": "The customer id. If passed - returns filtered by this customer list of subscriptions.", - "required": false - } - } - ], "responses": { "200": { "description": "A list of subscriptions", @@ -687,11 +676,6 @@ "description": "The request body for creating a checkout session", "type": "object", "properties": { - "customerId": { - "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" - }, "price": { "type": "string", "description": "The price id", diff --git a/src/types/portal.ts b/src/types/portal.ts index 4514a4f0..1553fb48 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -27,7 +27,6 @@ export type PriceListUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Subscription // Create export type SubscriptionCreateRequestBody = { - customerId: string; items: [{ price: string }]; idempotencyKey?: string; } @@ -58,10 +57,6 @@ export type SubscriptionGetResponseBody = { } // List -export type SubscriptionListRequestBody = { - customerId: string; -} - export type SubscriptionListResponseBody = { subscriptions: Stripe.Response>; } From b44b3221abaf0ecdf63a6b32593033ad1cdb85cb Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 10 Mar 2024 21:40:12 +0100 Subject: [PATCH 05/49] Lint and format --- src/app.ts | 60 +- src/controllers/admin/checkout-session.ts | 200 +- src/controllers/admin/portal-customer.ts | 38 +- src/controllers/admin/prices.ts | 117 +- src/controllers/admin/product.ts | 336 +- src/controllers/admin/subscriptions.ts | 821 +- src/controllers/admin/webhook.ts | 222 +- src/controllers/api/account.ts | 2 +- src/database/entities/subscription.entity.ts | 94 +- .../migrations/CreateSubscriptionTable.ts | 29 +- src/database/types/enum.ts | 10 +- .../auth/routes/admin/admin-auth.ts | 32 +- src/middleware/auth/user-info-fetcher/base.ts | 1 - .../auth/user-info-fetcher/portal-token.ts | 68 +- src/services/admin/subscription.ts | 249 +- src/services/api/customer.ts | 1 - src/services/track/admin/account-submitter.ts | 106 +- .../track/admin/subscription-submitter.ts | 324 +- .../track/api/credential-status-subscriber.ts | 6 +- .../track/api/credential-subscriber.ts | 6 +- src/services/track/api/did-subscriber.ts | 7 +- src/services/track/api/key-subscriber.ts | 7 +- .../track/api/presentation-subscriber.ts | 6 +- src/services/track/api/resource-subscriber.ts | 7 +- src/services/track/observer.ts | 1 - src/services/track/operation-subscriber.ts | 1 - src/services/track/submitter.ts | 27 +- src/services/track/tracker.ts | 9 +- src/static/swagger-admin.json | 1404 ++-- src/static/swagger-api.json | 6880 ++++++++--------- src/types/portal.ts | 93 +- src/types/swagger-admin-types.ts | 16 +- 32 files changed, 5493 insertions(+), 5687 deletions(-) diff --git a/src/app.ts b/src/app.ts index 39026780..c94dc710 100644 --- a/src/app.ts +++ b/src/app.ts @@ -102,8 +102,16 @@ class App { } this.express.use(express.text()); - this.express.use('/swagger', swaggerUi.serveFiles(swaggerAPIDocument, swaggerOptions), swaggerUi.setup(swaggerAPIDocument, swaggerOptions)); - this.express.use('/admin/swagger', swaggerUi.serveFiles(swaggerAdminDocument), swaggerUi.setup(swaggerAdminDocument)) + this.express.use( + '/swagger', + swaggerUi.serveFiles(swaggerAPIDocument, swaggerOptions), + swaggerUi.setup(swaggerAPIDocument, swaggerOptions) + ); + this.express.use( + '/admin/swagger', + swaggerUi.serveFiles(swaggerAdminDocument), + swaggerUi.setup(swaggerAdminDocument) + ); this.express.use(auth.handleError); this.express.use(async (req, res, next) => await auth.accessControl(req, res, next)); } @@ -216,21 +224,53 @@ class App { // Portal // Product app.get('/admin/product/list', ProductController.productListValidator, new ProductController().listProducts); - app.get('/admin/product/get/:productId', ProductController.productGetValidator, new ProductController().getProduct); + app.get( + '/admin/product/get/:productId', + ProductController.productGetValidator, + new ProductController().getProduct + ); // Prices app.get('/admin/price/list', PriceController.priceListValidator, new PriceController().getListPrices); // Subscription - app.post('/admin/subscription/create', SubscriptionController.subscriptionCreateValidator, new SubscriptionController().create); - app.post('/admin/subscription/update', SubscriptionController.subscriptionUpdateValidator, new SubscriptionController().update); - app.get('/admin/subscription/get/:subscriptionId', SubscriptionController.subscriptionGetValidator, new SubscriptionController().get); - app.get('/admin/subscription/list', SubscriptionController.subscriptionListValidator, new SubscriptionController().list); - app.delete('/admin/subscription/cancel', SubscriptionController.subscriptionCancelValidator, new SubscriptionController().cancel); - app.post('/admin/subscription/resume', SubscriptionController.subscriptionResumeValidator, new SubscriptionController().resume); + app.post( + '/admin/subscription/create', + SubscriptionController.subscriptionCreateValidator, + new SubscriptionController().create + ); + app.post( + '/admin/subscription/update', + SubscriptionController.subscriptionUpdateValidator, + new SubscriptionController().update + ); + app.get( + '/admin/subscription/get/:subscriptionId', + SubscriptionController.subscriptionGetValidator, + new SubscriptionController().get + ); + app.get( + '/admin/subscription/list', + SubscriptionController.subscriptionListValidator, + new SubscriptionController().list + ); + app.delete( + '/admin/subscription/cancel', + SubscriptionController.subscriptionCancelValidator, + new SubscriptionController().cancel + ); + app.post( + '/admin/subscription/resume', + SubscriptionController.subscriptionResumeValidator, + new SubscriptionController().resume + ); // Checkout session - app.post('/admin/checkout/session/create', CheckoutSessionController.checkoutSessionCreateValidator, new CheckoutSessionController().create); + app.post( + '/admin/checkout/session/create', + CheckoutSessionController.checkoutSessionCreateValidator, + new CheckoutSessionController().create + ); // Webhook app.post('/admin/webhook', new WebhookController().handleWebhook); diff --git a/src/controllers/admin/checkout-session.ts b/src/controllers/admin/checkout-session.ts index 4477e0e6..151e7bfb 100644 --- a/src/controllers/admin/checkout-session.ts +++ b/src/controllers/admin/checkout-session.ts @@ -1,8 +1,10 @@ - import Stripe from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import type { CheckoutSessionCreateRequestBody, CheckoutSessionCreateUnsuccessfulResponseBody } from '../../types/portal.js'; +import type { + CheckoutSessionCreateRequestBody, + CheckoutSessionCreateUnsuccessfulResponseBody, +} from '../../types/portal.js'; import { check, validationResult } from '../validator/index.js'; import { StatusCodes } from 'http-status-codes'; @@ -10,108 +12,106 @@ dotenv.config(); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - export class CheckoutSessionController { - static checkoutSessionCreateValidator = [ - check('price') - .exists() - .withMessage('price is required') - .bail() - .isString() - .withMessage('price should be a string') - .bail(), - check('successURL') - .exists() - .withMessage('successURL is required') - .bail() - .isString() - .withMessage('successURL should be a string') - .bail(), - check('cancelURL') - .exists() - .withMessage('cancelURL is required') - .bail() - .isString() - .withMessage('cancelURL should be a string') - .bail(), - check('idempotencyKey') - .optional() - .isString() - .withMessage('idempotencyKey should be a string') - .bail(), - ]; + static checkoutSessionCreateValidator = [ + check('price') + .exists() + .withMessage('price is required') + .bail() + .isString() + .withMessage('price should be a string') + .bail(), + check('successURL') + .exists() + .withMessage('successURL is required') + .bail() + .isString() + .withMessage('successURL should be a string') + .bail(), + check('cancelURL') + .exists() + .withMessage('cancelURL is required') + .bail() + .isString() + .withMessage('cancelURL should be a string') + .bail(), + check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), + ]; - /** - * @openapi - * - * /admin/checkout/session/create: - * post: - * summary: Create a checkout session - * description: Create a checkout session - * tags: [Checkout] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CheckoutSessionCreateRequestBody' - * responses: - * 303: - * description: A redirect to Stripe prebuilt checkout page - * 400: - * $ref: '#/components/schemas/InvalidRequest' - * 401: - * $ref: '#/components/schemas/UnauthorizedError' - * 500: - * $ref: '#/components/schemas/InternalError' - */ + /** + * @openapi + * + * /admin/checkout/session/create: + * post: + * summary: Create a checkout session + * description: Create a checkout session + * tags: [Checkout] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CheckoutSessionCreateRequestBody' + * responses: + * 303: + * description: A redirect to Stripe prebuilt checkout page + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + */ - public async create(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); - } + public async create(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); + } - const { price, successURL, cancelURL, quantity, idempotencyKey } = request.body satisfies CheckoutSessionCreateRequestBody; - try { - const session = await stripe.checkout.sessions.create({ - mode: 'subscription', - customer: response.locals.customer.stripeCustomerId, - line_items: [ - { - price: price, - quantity: quantity || 1, - }, - ], - success_url: successURL, - cancel_url: cancelURL, - }, { - idempotencyKey - }); + const { price, successURL, cancelURL, quantity, idempotencyKey } = + request.body satisfies CheckoutSessionCreateRequestBody; + try { + const session = await stripe.checkout.sessions.create( + { + mode: 'subscription', + customer: response.locals.customer.stripeCustomerId, + line_items: [ + { + price: price, + quantity: quantity || 1, + }, + ], + success_url: successURL, + cancel_url: cancelURL, + }, + { + idempotencyKey, + } + ); - if (session.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: 'Checkout session was not created' - } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); - } + if (session.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Checkout session was not created', + } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); + } - if (!session.url) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: 'Checkout session URL was not provided' - } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); - } + if (!session.url) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Checkout session URL was not provided', + } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); + } - return response.json({ - url: session.url as string - }) - - } catch (error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); - } - } -} \ No newline at end of file + return response.json({ + url: session.url as string, + }); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); + } + } +} diff --git a/src/controllers/admin/portal-customer.ts b/src/controllers/admin/portal-customer.ts index c092d4e9..4c1d1c4d 100644 --- a/src/controllers/admin/portal-customer.ts +++ b/src/controllers/admin/portal-customer.ts @@ -7,27 +7,21 @@ import type { PortalCustomerGetUnsuccessfulResponseBody } from '../../types/port dotenv.config(); export class PortalCustomerController { - static portalCustomerGetValidator = [ - check('logToUserId') - .optional() - .isString() - .withMessage('logToUserId should be a string') - .bail(), - ]; + static portalCustomerGetValidator = [ + check('logToUserId').optional().isString().withMessage('logToUserId should be a string').bail(), + ]; - async get(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(400).json({ - error: result.array().pop()?.msg - } satisfies PortalCustomerGetUnsuccessfulResponseBody); - } + async get(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(400).json({ + error: result.array().pop()?.msg, + } satisfies PortalCustomerGetUnsuccessfulResponseBody); + } - - - return response.status(500).json({ - error: "Not implemented yet" - }) - } -} \ No newline at end of file + return response.status(500).json({ + error: 'Not implemented yet', + }); + } +} diff --git a/src/controllers/admin/prices.ts b/src/controllers/admin/prices.ts index cb9e3178..e78e5dac 100644 --- a/src/controllers/admin/prices.ts +++ b/src/controllers/admin/prices.ts @@ -11,75 +11,70 @@ dotenv.config(); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class PriceController { + static priceListValidator = [ + check('productId').optional().isString().withMessage('productId should be a string').bail(), + ]; - static priceListValidator = [ - check('productId') - .optional() - .isString() - .withMessage('productId should be a string') - .bail(), - ]; - - /** + /** * @openapi * - * /admin/price/list: - * get: - * summary: Get a list of prices - * description: Get a list of prices - * tags: [Price] - * parameters: - * - in: query - * name: productId - * schema: - * type: string - * description: The product id. If passed - returns filtered by this product list of prices. - * required: false - * responses: - * 200: - * description: A list of prices - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/PriceListResponseBody' - * 400: + * /admin/price/list: + * get: + * summary: Get a list of prices + * description: Get a list of prices + * tags: [Price] + * parameters: + * - in: query + * name: productId + * schema: + * type: string + * description: The product id. If passed - returns filtered by this product list of prices. + * required: false + * responses: + * 200: + * description: A list of prices + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PriceListResponseBody' + * 400: * $ref: '#/components/schemas/InvalidRequest' * 401: * $ref: '#/components/schemas/UnauthorizedError' * 500: * $ref: '#/components/schemas/InternalError' - * 404: - * $ref: '#/components/schemas/NotFoundError' + * 404: + * $ref: '#/components/schemas/NotFoundError' */ - async getListPrices(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies PriceListUnsuccessfulResponseBody); - } - // Get query parameters - const productId = request.query.productId; + async getListPrices(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies PriceListUnsuccessfulResponseBody); + } + // Get query parameters + const productId = request.query.productId; - try { - // Fetch the list of prices - const prices = productId - ? await stripe.prices.list({ - product: productId as string, - active: true, - }) - : await stripe.prices.list({ - active: true, - }) + try { + // Fetch the list of prices + const prices = productId + ? await stripe.prices.list({ + product: productId as string, + active: true, + }) + : await stripe.prices.list({ + active: true, + }); - return response.status(StatusCodes.OK).json({ - prices: prices - } satisfies PriceListResponseBody); - } catch (error) { - return response.status(500).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies PriceListUnsuccessfulResponseBody); - } - } -} \ No newline at end of file + return response.status(StatusCodes.OK).json({ + prices: prices, + } satisfies PriceListResponseBody); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies PriceListUnsuccessfulResponseBody); + } + } +} diff --git a/src/controllers/admin/product.ts b/src/controllers/admin/product.ts index 90ec309e..e1c7b114 100644 --- a/src/controllers/admin/product.ts +++ b/src/controllers/admin/product.ts @@ -1,7 +1,13 @@ import { Stripe } from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import type { ProductGetResponseBody, ProductGetUnsuccessfulResponseBody, ProductListResponseBody, ProductListUnsuccessfulResponseBody, ProductWithPrices } from '../../types/portal.js'; +import type { + ProductGetResponseBody, + ProductGetUnsuccessfulResponseBody, + ProductListResponseBody, + ProductListUnsuccessfulResponseBody, + ProductWithPrices, +} from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; import { validationResult } from '../validator/index.js'; import { check } from 'express-validator'; @@ -11,182 +17,168 @@ dotenv.config(); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class ProductController { - - static productListValidator = [ - check('prices') - .optional() - .isBoolean() - .withMessage('prices should be a boolean') - .bail(), - ]; - - static productGetValidator = [ - check('productId') - .exists() - .withMessage('productId was not provided') - .bail(), - check('prices') - .optional() - .isBoolean() - .withMessage('prices should be a boolean') - .bail(), - ]; - - /** - * @openapi - * - * /admin/product/list: - * get: - * summary: Get a list of products - * description: Get a list of products which are on a Stripe side - * tags: [Product] - * parameters: - * - in: query - * name: prices - * schema: - * type: boolean - * description: If setup to true - returns the list of products with prices inside. Default - true - * required: false - * responses: - * 200: - * description: A list of products - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ProductListResponseBody' - * 400: + static productListValidator = [ + check('prices').optional().isBoolean().withMessage('prices should be a boolean').bail(), + ]; + + static productGetValidator = [ + check('productId').exists().withMessage('productId was not provided').bail(), + check('prices').optional().isBoolean().withMessage('prices should be a boolean').bail(), + ]; + + /** + * @openapi + * + * /admin/product/list: + * get: + * summary: Get a list of products + * description: Get a list of products which are on a Stripe side + * tags: [Product] + * parameters: + * - in: query + * name: prices + * schema: + * type: boolean + * description: If setup to true - returns the list of products with prices inside. Default - true + * required: false + * responses: + * 200: + * description: A list of products + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProductListResponseBody' + * 400: * $ref: '#/components/schemas/InvalidRequest' * 401: * $ref: '#/components/schemas/UnauthorizedError' * 500: * $ref: '#/components/schemas/InternalError' - * 404: - * $ref: '#/components/schemas/NotFoundError' + * 404: + * $ref: '#/components/schemas/NotFoundError' */ - async listProducts(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies ProductListUnsuccessfulResponseBody); - } - // Get query parameters - const prices = request.query.prices === 'false' ? false : true; - - try { - const products = await stripe.products.list({ - active: true, - }) as Stripe.ApiList; - - // If no products found return 404 - if (!products.data) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: 'Seems like there no any active products on Stripe side. Please add some.' - } satisfies ProductListUnsuccessfulResponseBody); - } - - if (prices) { - for (const product of products.data) { - const prices = await stripe.prices.list({ - product: product.id, - active: true, - }); - product.prices = prices.data; - } - } - - return response.status(StatusCodes.OK).json({ - products: products - } satisfies ProductListResponseBody); - } catch (error) { - return response.status(500).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies ProductListUnsuccessfulResponseBody); - } - } - - - /** - * @openapi - * - * /admin/product/get/{productId}: - * get: - * summary: Get a product - * description: Get a product by id - * tags: [Product] - * parameters: - * - in: path - * name: productId - * schema: - * type: string - * description: The product id which identifies the product in Stripe - * required: true - * - in: query - * name: prices - * schema: - * type: boolean - * description: If setup to true - returns the product with prices inside. Default - true - * required: false - * responses: - * 200: - * description: A product - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ProductGetResponseBody' - * 400: - * $ref: '#/components/schemas/InvalidRequest' - * 401: - * $ref: '#/components/schemas/UnauthorizedError' - * 500: - * $ref: '#/components/schemas/InternalError' - * 404: - * $ref: '#/components/schemas/NotFoundError' - */ - async getProduct(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies ProductGetUnsuccessfulResponseBody); - } - // Get query parameters - const prices = request.query.prices === 'false' ? false : true; - const productId = request.params.productId as string; - - try { - // Get the product - const product = await stripe.products.retrieve(productId) as ProductWithPrices; - - if (prices) { - const prices = await stripe.prices.list({ - product: product.id, - active: true, - }); - if (prices.data) { - product.prices = prices.data; - } - } - - return response.status(StatusCodes.OK).json({ - product: product - } satisfies ProductGetResponseBody); - } catch (error) { - - // define error + async listProducts(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies ProductListUnsuccessfulResponseBody); + } + // Get query parameters + const prices = request.query.prices === 'false' ? false : true; + + try { + const products = (await stripe.products.list({ + active: true, + })) as Stripe.ApiList; + + // If no products found return 404 + if (!products.data) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: 'Seems like there no any active products on Stripe side. Please add some.', + } satisfies ProductListUnsuccessfulResponseBody); + } + + if (prices) { + for (const product of products.data) { + const prices = await stripe.prices.list({ + product: product.id, + active: true, + }); + product.prices = prices.data; + } + } + + return response.status(StatusCodes.OK).json({ + products: products, + } satisfies ProductListResponseBody); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies ProductListUnsuccessfulResponseBody); + } + } + + /** + * @openapi + * + * /admin/product/get/{productId}: + * get: + * summary: Get a product + * description: Get a product by id + * tags: [Product] + * parameters: + * - in: path + * name: productId + * schema: + * type: string + * description: The product id which identifies the product in Stripe + * required: true + * - in: query + * name: prices + * schema: + * type: boolean + * description: If setup to true - returns the product with prices inside. Default - true + * required: false + * responses: + * 200: + * description: A product + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ProductGetResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + async getProduct(request: Request, response: Response) { + const result = validationResult(request); + // handle error + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies ProductGetUnsuccessfulResponseBody); + } + // Get query parameters + const prices = request.query.prices === 'false' ? false : true; + const productId = request.params.productId as string; + + try { + // Get the product + const product = (await stripe.products.retrieve(productId)) as ProductWithPrices; + + if (prices) { + const prices = await stripe.prices.list({ + product: product.id, + active: true, + }); + if (prices.data) { + product.prices = prices.data; + } + } + + return response.status(StatusCodes.OK).json({ + product: product, + } satisfies ProductGetResponseBody); + } catch (error) { + // define error const errorRef = error as Record; - if (errorRef?.statusCode === StatusCodes.NOT_FOUND) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: `Product with id ${productId} not found` - } satisfies ProductGetUnsuccessfulResponseBody); - } - - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies ProductGetUnsuccessfulResponseBody); - } - } -} \ No newline at end of file + if (errorRef?.statusCode === StatusCodes.NOT_FOUND) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Product with id ${productId} not found`, + } satisfies ProductGetUnsuccessfulResponseBody); + } + + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies ProductGetUnsuccessfulResponseBody); + } + } +} diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 35d8c204..fa8e11b7 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -1,7 +1,25 @@ import Stripe from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import type { SubscriptionCreateRequestBody, SubscriptionCreateResponseBody, SubscriptionCreateUnsuccessfulResponseBody, SubscriptionCancelResponseBody, SubscriptionCancelUnsuccessfulResponseBody, SubscriptionGetResponseBody, SubscriptionGetUnsuccessfulResponseBody, SubscriptionListResponseBody, SubscriptionListUnsuccessfulResponseBody, SubscriptionUpdateRequestBody, SubscriptionUpdateResponseBody, SubscriptionUpdateUnsuccessfulResponseBody, SubscriptionResumeUnsuccessfulResponseBody, SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, SubscriptionCancelRequestBody, PaymentBehavior } from '../../types/portal.js'; +import type { + SubscriptionCreateRequestBody, + SubscriptionCreateResponseBody, + SubscriptionCreateUnsuccessfulResponseBody, + SubscriptionCancelResponseBody, + SubscriptionCancelUnsuccessfulResponseBody, + SubscriptionGetResponseBody, + SubscriptionGetUnsuccessfulResponseBody, + SubscriptionListResponseBody, + SubscriptionListUnsuccessfulResponseBody, + SubscriptionUpdateRequestBody, + SubscriptionUpdateResponseBody, + SubscriptionUpdateUnsuccessfulResponseBody, + SubscriptionResumeUnsuccessfulResponseBody, + SubscriptionResumeResponseBody, + SubscriptionResumeRequestBody, + SubscriptionCancelRequestBody, + PaymentBehavior, +} from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; import { validationResult } from '../validator/index.js'; import { check } from 'express-validator'; @@ -9,116 +27,84 @@ import { SubscriptionService } from '../../services/admin/subscription.js'; dotenv.config(); - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class SubscriptionController { + static subscriptionCreateValidator = [ + check('customerId').exists().withMessage('customerId was not provided').bail(), + check('items') + .exists() + .withMessage('items was not provided') + .bail() + .isArray() + .withMessage('items should be an array') + .bail(), + check('items.*.price') + .exists() + .withMessage('price was not provided') + .bail() + .isString() + .withMessage('price should be a string') + .bail(), + check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), + ]; - static subscriptionCreateValidator = [ - check('customerId') - .exists() - .withMessage('customerId was not provided') - .bail(), - check('items') - .exists() - .withMessage('items was not provided') - .bail() - .isArray() - .withMessage('items should be an array') - .bail(), - check('items.*.price') - .exists() - .withMessage('price was not provided') - .bail() - .isString() - .withMessage('price should be a string') - .bail(), - check('idempotencyKey') - .optional() - .isString() - .withMessage('idempotencyKey should be a string') - .bail(), - ]; - - static subscriptionUpdateValidator = [ - check('subscriptionId') - .exists() - .withMessage('subscriptionId was not provided') - .bail(), - check('updateParams') - .exists() - .withMessage('updateParams was not provided') - .bail(), - check('idempotencyKey') - .optional() - .isString() - .withMessage('idempotencyKey should be a string') - .bail(), - ]; + static subscriptionUpdateValidator = [ + check('subscriptionId').exists().withMessage('subscriptionId was not provided').bail(), + check('updateParams').exists().withMessage('updateParams was not provided').bail(), + check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), + ]; - static subscriptionGetValidator = [ - check('subscriptionId') - .exists() - .withMessage('subscriptionId was not provided') - .bail() - .isString() - .withMessage('subscriptionId should be a string') - .bail(), - ]; + static subscriptionGetValidator = [ + check('subscriptionId') + .exists() + .withMessage('subscriptionId was not provided') + .bail() + .isString() + .withMessage('subscriptionId should be a string') + .bail(), + ]; - static subscriptionListValidator = [ - check('customerId') - .optional() - .isString() - .withMessage('customerId should be a string') - .bail(), - ]; + static subscriptionListValidator = [ + check('customerId').optional().isString().withMessage('customerId should be a string').bail(), + ]; - static subscriptionCancelValidator = [ - check('subscriptionId') - .exists() - .withMessage('subscriptionId was not provided') - .bail() - .isString() - .withMessage('subscriptionId should be a string') - .bail(), - check('idempotencyKey') - .optional() - .isString() - .withMessage('idempotencyKey should be a string') - .bail(), - ]; + static subscriptionCancelValidator = [ + check('subscriptionId') + .exists() + .withMessage('subscriptionId was not provided') + .bail() + .isString() + .withMessage('subscriptionId should be a string') + .bail(), + check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), + ]; - static subscriptionResumeValidator = [ - check('subscriptionId') - .exists() - .withMessage('subscriptionId was not provided') - .bail() - .isString() - .withMessage('subscriptionId should be a string') - .bail(), - check('idempotencyKey') - .optional() - .isString() - .withMessage('idempotencyKey should be a string') - .bail(), - ]; + static subscriptionResumeValidator = [ + check('subscriptionId') + .exists() + .withMessage('subscriptionId was not provided') + .bail() + .isString() + .withMessage('subscriptionId should be a string') + .bail(), + check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), + ]; - - /** - * @openapi - * - * /admin/subscription/create: - * post: - * summary: Create a subscription - * description: Creates a new subscription for an existing customer - * tags: [Subscription] + /** + * @openapi + * + * /admin/subscription/create: + * post: + * summary: Create a subscription + * description: Creates a new subscription for an existing customer + * tags: [Subscription] * requestBody: * content: * application/json: * schema: * $ref: '#/components/schemas/SubscriptionCreateRequestBody' - * responses: + * responses: * 201: * description: The request was successful. * content: @@ -131,346 +117,339 @@ export class SubscriptionController { * $ref: '#/components/schemas/UnauthorizedError' * 500: * $ref: '#/components/schemas/InternalError' - */ - async create(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies SubscriptionCreateUnsuccessfulResponseBody); - } + */ + async create(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } - const { items, idempotencyKey } = request.body satisfies SubscriptionCreateRequestBody; - try { - // Create the subscription - const subscription = await stripe.subscriptions.create({ - customer: response.locals.customer.stripeCustomerId, - items: items, - payment_behavior: "default_incomplete" as PaymentBehavior, - }, - { - idempotencyKey: idempotencyKey, - }); - if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: `Subscription was not created`, - } satisfies SubscriptionCreateUnsuccessfulResponseBody); - } + const { items, idempotencyKey } = request.body satisfies SubscriptionCreateRequestBody; + try { + // Create the subscription + const subscription = await stripe.subscriptions.create( + { + customer: response.locals.customer.stripeCustomerId, + items: items, + payment_behavior: 'default_incomplete' as PaymentBehavior, + }, + { + idempotencyKey: idempotencyKey, + } + ); + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: `Subscription was not created`, + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } - return response.status(StatusCodes.CREATED).json({ - subscription: subscription - } satisfies SubscriptionCreateResponseBody ); - } catch (error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies SubscriptionCreateUnsuccessfulResponseBody); - } - } + return response.status(StatusCodes.CREATED).json({ + subscription: subscription, + } satisfies SubscriptionCreateResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + } - /** - * @openapi - * - * /admin/subscription/update: - * post: - * summary: Update a subscription - * description: Updates an existing subscription - * tags: [Subscription] - * requestBody: - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionUpdateRequestBody' - * responses: - * 200: - * description: The request was successful. - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionUpdateResponseBody' + /** + * @openapi + * + * /admin/subscription/update: + * post: + * summary: Update a subscription + * description: Updates an existing subscription + * tags: [Subscription] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionUpdateRequestBody' + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionUpdateResponseBody' * 400: * $ref: '#/components/schemas/InvalidRequest' * 401: * $ref: '#/components/schemas/UnauthorizedError' * 500: * $ref: '#/components/schemas/InternalError' - */ - async update(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies SubscriptionUpdateUnsuccessfulResponseBody); - } - - const { subscriptionId, updateParams, idempotencyKey} = request.body satisfies SubscriptionUpdateRequestBody; - try { - // Update the subscription - const subscription = await stripe.subscriptions.update( - subscriptionId, - updateParams, - { - idempotencyKey: idempotencyKey, - }); - if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: `Subscription was not updated`, - } satisfies SubscriptionUpdateUnsuccessfulResponseBody); - } - return response.status(StatusCodes.OK).json({ - subscription: subscription - } satisfies SubscriptionUpdateResponseBody); - - } catch (error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies SubscriptionUpdateUnsuccessfulResponseBody); - } - } + */ + async update(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies SubscriptionUpdateUnsuccessfulResponseBody); + } - /** - * @openapi - * - * /admin/subscription/list: - * get: - * summary: Get a list of subscriptions - * description: Get a list of subscriptions - * tags: [Subscription] - * responses: - * 200: - * description: A list of subscriptions - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionListResponseBody' - * 400: - * $ref: '#/components/schemas/InvalidRequest' - * 401: - * $ref: '#/components/schemas/UnauthorizedError' - * 500: - * $ref: '#/components/schemas/InternalError' - * 404: - * $ref: '#/components/schemas/NotFoundError' - */ + const { subscriptionId, updateParams, idempotencyKey } = request.body satisfies SubscriptionUpdateRequestBody; + try { + // Update the subscription + const subscription = await stripe.subscriptions.update(subscriptionId, updateParams, { + idempotencyKey: idempotencyKey, + }); + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: `Subscription was not updated`, + } satisfies SubscriptionUpdateUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscription: subscription, + } satisfies SubscriptionUpdateResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies SubscriptionUpdateUnsuccessfulResponseBody); + } + } - public async list(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies SubscriptionListUnsuccessfulResponseBody); - } - const customerId = response.locals.customer.stripeCustomerId; - try { - // Sync our DB with Stripe - await SubscriptionService.instance.stripeSync(response.locals.customer) - // Get the subscriptions - const subscriptions = customerId - ? await stripe.subscriptions.list({ - customer: customerId as string, - }) - : await stripe.subscriptions.list(); + /** + * @openapi + * + * /admin/subscription/list: + * get: + * summary: Get a list of subscriptions + * description: Get a list of subscriptions + * tags: [Subscription] + * responses: + * 200: + * description: A list of subscriptions + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionListResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ - if (subscriptions.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: `Subscriptions were not found`, - } satisfies SubscriptionListUnsuccessfulResponseBody); - } - return response.status(StatusCodes.OK).json({ - subscriptions: subscriptions - } satisfies SubscriptionListResponseBody); - } catch (error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies SubscriptionListUnsuccessfulResponseBody); - } - } + public async list(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies SubscriptionListUnsuccessfulResponseBody); + } + const customerId = response.locals.customer.stripeCustomerId; + try { + // Sync our DB with Stripe + await SubscriptionService.instance.stripeSync(response.locals.customer); + // Get the subscriptions + const subscriptions = customerId + ? await stripe.subscriptions.list({ + customer: customerId as string, + }) + : await stripe.subscriptions.list(); + if (subscriptions.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscriptions were not found`, + } satisfies SubscriptionListUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscriptions: subscriptions, + } satisfies SubscriptionListResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies SubscriptionListUnsuccessfulResponseBody); + } + } - /** - * @openapi - * - * /admin/subscription/get/{subscriptionId}: - * get: - * summary: Get a subscription - * description: Get a subscription - * tags: [Subscription] - * parameters: - * - in: path - * name: subscriptionId - * schema: - * type: string - * description: The subscription id - * required: true - * responses: - * 200: - * description: The request was successful. - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionGetResponseBody' - * 400: - * $ref: '#/components/schemas/InvalidRequest' - * 401: - * $ref: '#/components/schemas/UnauthorizedError' - * 500: - * $ref: '#/components/schemas/InternalError' - * 404: - * $ref: '#/components/schemas/NotFoundError' - */ - async get(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies SubscriptionGetUnsuccessfulResponseBody); - } - const subscriptionId = request.params.subscriptionId; - try { - // Sync our DB with Stripe - await SubscriptionService.instance.stripeSync(response.locals.customer) - // Get the subscription - const subscription = await stripe.subscriptions.retrieve(subscriptionId as string); - if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: `Subscription was not found`, - } satisfies SubscriptionGetUnsuccessfulResponseBody); - } - return response.status(StatusCodes.OK).json({ - subscription: subscription - } satisfies SubscriptionGetResponseBody); - } catch (error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies SubscriptionGetUnsuccessfulResponseBody); - } - } + /** + * @openapi + * + * /admin/subscription/get/{subscriptionId}: + * get: + * summary: Get a subscription + * description: Get a subscription + * tags: [Subscription] + * parameters: + * - in: path + * name: subscriptionId + * schema: + * type: string + * description: The subscription id + * required: true + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionGetResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + async get(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies SubscriptionGetUnsuccessfulResponseBody); + } + const subscriptionId = request.params.subscriptionId; + try { + // Sync our DB with Stripe + await SubscriptionService.instance.stripeSync(response.locals.customer); + // Get the subscription + const subscription = await stripe.subscriptions.retrieve(subscriptionId as string); + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscription was not found`, + } satisfies SubscriptionGetUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscription: subscription, + } satisfies SubscriptionGetResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies SubscriptionGetUnsuccessfulResponseBody); + } + } - /** - * @openapi - * - * /admin/subscription/cancel: - * post: - * summary: Cancel a subscription - * description: Cancels an existing subscription - * tags: [Subscription] - * requestBody: - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionCancelRequestBody' - * responses: - * 200: - * description: The request was successful. - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionCancelResponseBody' - * 400: - * $ref: '#/components/schemas/InvalidRequest' - * 401: - * $ref: '#/components/schemas/UnauthorizedError' - * 500: - * $ref: '#/components/schemas/InternalError' - * 404: - * $ref: '#/components/schemas/NotFoundError' - */ - async cancel(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies SubscriptionCancelUnsuccessfulResponseBody); - } - const {subscriptionId, idempotencyKey } = request.body satisfies SubscriptionCancelRequestBody; + /** + * @openapi + * + * /admin/subscription/cancel: + * post: + * summary: Cancel a subscription + * description: Cancels an existing subscription + * tags: [Subscription] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionCancelRequestBody' + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionCancelResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + async cancel(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies SubscriptionCancelUnsuccessfulResponseBody); + } + const { subscriptionId, idempotencyKey } = request.body satisfies SubscriptionCancelRequestBody; - try { - // Cancel the subscription - const subscription = await stripe.subscriptions.cancel( - subscriptionId as string, - { - idempotencyKey: idempotencyKey - }); + try { + // Cancel the subscription + const subscription = await stripe.subscriptions.cancel(subscriptionId as string, { + idempotencyKey: idempotencyKey, + }); - // Check if the subscription was cancelled - if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: `Subscription was not deleted`, - } satisfies SubscriptionCancelUnsuccessfulResponseBody); - } - return response.status(StatusCodes.OK).json({ - subscription: subscription - } satisfies SubscriptionCancelResponseBody); - } catch (error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies SubscriptionCancelUnsuccessfulResponseBody); - } - } + // Check if the subscription was cancelled + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: `Subscription was not deleted`, + } satisfies SubscriptionCancelUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscription: subscription, + } satisfies SubscriptionCancelResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies SubscriptionCancelUnsuccessfulResponseBody); + } + } - /** - * @openapi - * - * /admin/subscription/resume: - * post: - * summary: Resume a subscription - * description: Resumes an existing subscription - * tags: [Subscription] - * requestBody: - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionResumeRequestBody' - * responses: - * 200: - * description: The request was successful. - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionResumeResponseBody' - * 400: - * $ref: '#/components/schemas/InvalidRequest' - * 401: - * $ref: '#/components/schemas/UnauthorizedError' - * 500: - * $ref: '#/components/schemas/InternalError' - * 404: - * $ref: '#/components/schemas/NotFoundError' - */ - async resume(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg - } satisfies SubscriptionResumeUnsuccessfulResponseBody); - } - const { subscriptionId, idempotencyKey } = request.body satisfies SubscriptionResumeRequestBody; - try { - // Resume the subscription - const subscription = await stripe.subscriptions.resume( - subscriptionId as string, - { - idempotencyKey: idempotencyKey - }); + /** + * @openapi + * + * /admin/subscription/resume: + * post: + * summary: Resume a subscription + * description: Resumes an existing subscription + * tags: [Subscription] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionResumeRequestBody' + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionResumeResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + async resume(request: Request, response: Response) { + // Validate request + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies SubscriptionResumeUnsuccessfulResponseBody); + } + const { subscriptionId, idempotencyKey } = request.body satisfies SubscriptionResumeRequestBody; + try { + // Resume the subscription + const subscription = await stripe.subscriptions.resume(subscriptionId as string, { + idempotencyKey: idempotencyKey, + }); - // Check if the subscription was resumed - if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: `Subscription was not resumed`, - } satisfies SubscriptionResumeUnsuccessfulResponseBody); - } - return response.status(StatusCodes.OK).json({ - subscription: subscription - } satisfies SubscriptionResumeResponseBody); - } catch (error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `Internal error: ${(error as Error)?.message || error}` - } satisfies SubscriptionResumeUnsuccessfulResponseBody); - } - } -} \ No newline at end of file + // Check if the subscription was resumed + if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: `Subscription was not resumed`, + } satisfies SubscriptionResumeUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + subscription: subscription, + } satisfies SubscriptionResumeResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies SubscriptionResumeUnsuccessfulResponseBody); + } + } +} diff --git a/src/controllers/admin/webhook.ts b/src/controllers/admin/webhook.ts index 14debffb..29363b80 100644 --- a/src/controllers/admin/webhook.ts +++ b/src/controllers/admin/webhook.ts @@ -11,111 +11,123 @@ dotenv.config(); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class WebhookController { - public async handleWebhook(request: Request, response: Response) { - let event = request.body; - let subscription; - let status; - const builSubmitOperation = ( function(subscription: Stripe.Subscription, name: string) { - return { - operation: name, - data: { - subscriptionId: subscription.id, - stripeCustomerId: subscription.customer as string, - status: subscription.status, - currentPeriodStart: new Date(subscription.current_period_start * 1000), - currentPeriodEnd: new Date(subscription.current_period_end * 1000), - trialStart: subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, - trialEnd: subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined, - } satisfies ISubmitData, - } satisfies ISubmitOperation; - }) - // Only verify the event if you have an endpoint secret defined. - // Otherwise use the basic event deserialized with JSON.parse - // Get the signature sent by Stripe + public async handleWebhook(request: Request, response: Response) { + let event = request.body; + let subscription; + let status; + const builSubmitOperation = function (subscription: Stripe.Subscription, name: string) { + return { + operation: name, + data: { + subscriptionId: subscription.id, + stripeCustomerId: subscription.customer as string, + status: subscription.status, + currentPeriodStart: new Date(subscription.current_period_start * 1000), + currentPeriodEnd: new Date(subscription.current_period_end * 1000), + trialStart: subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, + trialEnd: subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined, + } satisfies ISubmitData, + } satisfies ISubmitOperation; + }; + // Only verify the event if you have an endpoint secret defined. + // Otherwise use the basic event deserialized with JSON.parse + // Get the signature sent by Stripe - if (!process.env.STRIPE_WEBHOOK_SECRET) { - await eventTracker.notify({ - message: 'Stripe webhook secret not found. Webhook ID: ${request.body.id}.', - severity: 'error', - } satisfies INotifyMessage) - return response.sendStatus(StatusCodes.BAD_REQUEST); - } + if (!process.env.STRIPE_WEBHOOK_SECRET) { + await eventTracker.notify({ + message: 'Stripe webhook secret not found. Webhook ID: ${request.body.id}.', + severity: 'error', + } satisfies INotifyMessage); + return response.sendStatus(StatusCodes.BAD_REQUEST); + } - const signature = request.headers['stripe-signature']; - if (!signature) { - await eventTracker.notify({ - message: 'Webhook signature not found. Webhook ID: ${request.body.id}.', - severity: 'error', - } satisfies INotifyMessage) - return response.sendStatus(StatusCodes.BAD_REQUEST); - } + const signature = request.headers['stripe-signature']; + if (!signature) { + await eventTracker.notify({ + message: 'Webhook signature not found. Webhook ID: ${request.body.id}.', + severity: 'error', + } satisfies INotifyMessage); + return response.sendStatus(StatusCodes.BAD_REQUEST); + } - try { - event = stripe.webhooks.constructEvent( - request.rawBody, - signature, - process.env.STRIPE_WEBHOOK_SECRET - ); - } catch (err) { - - await eventTracker.notify({ - message: `Webhook signature verification failed. Webhook ID: ${request.body.id}. Error: ${(err as Record)?.message || err}`, - severity: 'error', - } satisfies INotifyMessage) - return response.sendStatus(StatusCodes.BAD_REQUEST); - } - // Handle the event - switch (event.type) { - case 'customer.subscription.trial_will_end': - subscription = event.data.object; - status = subscription.status; - await eventTracker.notify({ - message: EventTracker.compileBasicNotification(`Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.trial_will_end'), - severity: 'info', - } satisfies INotifyMessage) - await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_TRIAL_WILL_END)); - break; - case 'customer.subscription.deleted': - subscription = event.data.object; - status = subscription.status; - await eventTracker.notify({ - message: EventTracker.compileBasicNotification(`Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.deleted'), - severity: 'info', - } satisfies INotifyMessage) - await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CANCEL)); - // Then define and call a method to handle the subscription deleted. - // handleSubscriptionDeleted(subscriptionDeleted); - break; - case 'customer.subscription.created': - subscription = event.data.object; - status = subscription.status; - await eventTracker.notify({ - message: EventTracker.compileBasicNotification(`Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.created'), - severity: 'info', - } satisfies INotifyMessage) - await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CREATE)); - // Then define and call a method to handle the subscription created. - // handleSubscriptionCreated(subscription); - break; - case 'customer.subscription.updated': - subscription = event.data.object; - status = subscription.status; - await eventTracker.notify({ - message: EventTracker.compileBasicNotification(`Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.updated'), - severity: 'info', - } satisfies INotifyMessage) - await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE)); - // Then define and call a method to handle the subscription update. - // handleSubscriptionUpdated(subscription); - break; - default: - // Unexpected event type - eventTracker.notify({ - message: EventTracker.compileBasicNotification(`Unexpected event type: ${event.type}`, 'Stripe Webhook: unexpected'), - severity: 'error', - } satisfies INotifyMessage) - } - // Return a 200 response to acknowledge receipt of the event - return response.status(StatusCodes.OK).send(); - } -} \ No newline at end of file + try { + event = stripe.webhooks.constructEvent(request.rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET); + } catch (err) { + await eventTracker.notify({ + message: `Webhook signature verification failed. Webhook ID: ${request.body.id}. Error: ${(err as Record)?.message || err}`, + severity: 'error', + } satisfies INotifyMessage); + return response.sendStatus(StatusCodes.BAD_REQUEST); + } + // Handle the event + switch (event.type) { + case 'customer.subscription.trial_will_end': + subscription = event.data.object; + status = subscription.status; + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription status is ${status} for subscription with id: ${subscription.id}`, + 'Stripe Webhook: customer.subscription.trial_will_end' + ), + severity: 'info', + } satisfies INotifyMessage); + await eventTracker.submit( + builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_TRIAL_WILL_END) + ); + break; + case 'customer.subscription.deleted': + subscription = event.data.object; + status = subscription.status; + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription status is ${status} for subscription with id: ${subscription.id}`, + 'Stripe Webhook: customer.subscription.deleted' + ), + severity: 'info', + } satisfies INotifyMessage); + await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CANCEL)); + // Then define and call a method to handle the subscription deleted. + // handleSubscriptionDeleted(subscriptionDeleted); + break; + case 'customer.subscription.created': + subscription = event.data.object; + status = subscription.status; + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription status is ${status} for subscription with id: ${subscription.id}`, + 'Stripe Webhook: customer.subscription.created' + ), + severity: 'info', + } satisfies INotifyMessage); + await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CREATE)); + // Then define and call a method to handle the subscription created. + // handleSubscriptionCreated(subscription); + break; + case 'customer.subscription.updated': + subscription = event.data.object; + status = subscription.status; + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription status is ${status} for subscription with id: ${subscription.id}`, + 'Stripe Webhook: customer.subscription.updated' + ), + severity: 'info', + } satisfies INotifyMessage); + await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE)); + // Then define and call a method to handle the subscription update. + // handleSubscriptionUpdated(subscription); + break; + default: + // Unexpected event type + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Unexpected event type: ${event.type}`, + 'Stripe Webhook: unexpected' + ), + severity: 'error', + } satisfies INotifyMessage); + } + // Return a 200 response to acknowledge receipt of the event + return response.status(StatusCodes.OK).send(); + } +} diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index a9312916..1dbd19d2 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -312,7 +312,7 @@ export class AccountController { name: customer.name, customerId: customer.customerId, } satisfies ISubmitStripeCustomerCreateData, - } satisfies ISubmitOperation) + } satisfies ISubmitOperation); } return response.status(StatusCodes.OK).json({}); diff --git a/src/database/entities/subscription.entity.ts b/src/database/entities/subscription.entity.ts index 8c990994..5c2dd197 100644 --- a/src/database/entities/subscription.entity.ts +++ b/src/database/entities/subscription.entity.ts @@ -6,42 +6,42 @@ dotenv.config(); @Entity('subscription') export class SubscriptionEntity { - @Column({ - type: 'text', - nullable: false, - primary: true, - }) + @Column({ + type: 'text', + nullable: false, + primary: true, + }) subscriptionId!: string; - @Column({ - type: 'text', - nullable: false, - }) - status!: string; + @Column({ + type: 'text', + nullable: false, + }) + status!: string; - @Column({ - type: 'timestamptz', - nullable: false, - }) - currentPeriodStart!: Date; + @Column({ + type: 'timestamptz', + nullable: false, + }) + currentPeriodStart!: Date; - @Column({ - type: 'timestamptz', - nullable: false, - }) - currentPeriodEnd!: Date; + @Column({ + type: 'timestamptz', + nullable: false, + }) + currentPeriodEnd!: Date; - @Column({ - type: 'timestamptz', - nullable: true, - }) - trialStart!: Date; + @Column({ + type: 'timestamptz', + nullable: true, + }) + trialStart!: Date; - @Column({ - type: 'timestamptz', - nullable: true, - }) - trialEnd!: Date; + @Column({ + type: 'timestamptz', + nullable: true, + }) + trialEnd!: Date; @Column({ type: 'timestamptz', @@ -65,25 +65,25 @@ export class SubscriptionEntity { this.updatedAt = new Date(); } - @ManyToOne(() => CustomerEntity, (customer) => customer.customerId) - @JoinColumn({ name: 'customerId' }) - customer!: CustomerEntity; + @ManyToOne(() => CustomerEntity, (customer) => customer.customerId) + @JoinColumn({ name: 'customerId' }) + customer!: CustomerEntity; constructor( - subscriptionId: string, - customer: CustomerEntity, - status: string, - currentPeriodStart: Date, - currentPeriodEnd: Date, - trialStart?: Date, - trialEnd?: Date, - ){ + subscriptionId: string, + customer: CustomerEntity, + status: string, + currentPeriodStart: Date, + currentPeriodEnd: Date, + trialStart?: Date, + trialEnd?: Date + ) { this.subscriptionId = subscriptionId; - this.customer = customer; - this.status = status; - this.currentPeriodStart = currentPeriodStart; - this.currentPeriodEnd = currentPeriodEnd; - if (trialStart ) this.trialStart = trialStart; - if (trialEnd) this.trialEnd = trialEnd; + this.customer = customer; + this.status = status; + this.currentPeriodStart = currentPeriodStart; + this.currentPeriodEnd = currentPeriodEnd; + if (trialStart) this.trialStart = trialStart; + if (trialEnd) this.trialEnd = trialEnd; } } diff --git a/src/database/migrations/CreateSubscriptionTable.ts b/src/database/migrations/CreateSubscriptionTable.ts index e117ae37..fc55bc0f 100644 --- a/src/database/migrations/CreateSubscriptionTable.ts +++ b/src/database/migrations/CreateSubscriptionTable.ts @@ -1,22 +1,20 @@ -import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm"; - +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; export class CreateSubscritpionTable1695740346003 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { + public async up(queryRunner: QueryRunner): Promise { const table = new Table({ name: 'subscription', columns: [ { name: 'subscriptionId', type: 'text', isNullable: false, isPrimary: true }, - { name: 'customerId', type: 'uuid', isNullable: false }, - { name: 'status', type: 'text', isNullable: false }, - { name: 'trialStart', type: 'timestamptz', isNullable: true }, - { name: 'trialEnd', type: 'timestamptz', isNullable: true }, - { name: 'currentPeriodStart', type: 'timestamptz', isNullable: false }, - { name: 'currentPeriodEnd', type: 'timestamptz', isNullable: false }, - { name: 'createdAt', type: 'timestamptz', isNullable: false }, - { name: 'updatedAt', type: 'timestamptz', isNullable: true }, - ], + { name: 'customerId', type: 'uuid', isNullable: false }, + { name: 'status', type: 'text', isNullable: false }, + { name: 'trialStart', type: 'timestamptz', isNullable: true }, + { name: 'trialEnd', type: 'timestamptz', isNullable: true }, + { name: 'currentPeriodStart', type: 'timestamptz', isNullable: false }, + { name: 'currentPeriodEnd', type: 'timestamptz', isNullable: false }, + { name: 'createdAt', type: 'timestamptz', isNullable: false }, + { name: 'updatedAt', type: 'timestamptz', isNullable: true }, + ], }); await queryRunner.createTable(table, true); @@ -29,10 +27,9 @@ export class CreateSubscritpionTable1695740346003 implements MigrationInterface onDelete: 'CASCADE', }) ); - - } + } public async down(queryRunner: QueryRunner): Promise { throw new Error('illegal_operation: cannot roll back initial migration'); } -} \ No newline at end of file +} diff --git a/src/database/types/enum.ts b/src/database/types/enum.ts index 8316c32a..92ff2180 100644 --- a/src/database/types/enum.ts +++ b/src/database/types/enum.ts @@ -8,7 +8,15 @@ export const categoryEnum = { SUBSCRIPTION: 'subscription', toStringList: function (): string[] { - return [this.DID, this.RESOURCE, this.CREDENTIAL, this.CREDENTIAL_STATUS, this.PRESENTATION, this.KEY, this.SUBSCRIPTION]; + return [ + this.DID, + this.RESOURCE, + this.CREDENTIAL, + this.CREDENTIAL_STATUS, + this.PRESENTATION, + this.KEY, + this.SUBSCRIPTION, + ]; }, }; diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index a7a243d8..6d7afcc9 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -6,22 +6,30 @@ export class AdminHandler extends BaseAuthHandler { constructor() { super(); // ToDo: define how to get namespace information - this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:testnet', { skipNamespace: true}); - this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:mainnet', { skipNamespace: true}); + this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:testnet', { + skipNamespace: true, + }); + this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:mainnet', { + skipNamespace: true, + }); // Subscriptions - this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { skipNamespace: true}); - this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:mainnet', { skipNamespace: true}); - this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:testnet', { skipNamespace: true}); - this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:mainnet', { skipNamespace: true}); + this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { + skipNamespace: true, + }); + this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:mainnet', { + skipNamespace: true, + }); + this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:testnet', { skipNamespace: true }); + this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:mainnet', { skipNamespace: true }); // Prices - this.registerRoute('/admin/price/list', 'GET', 'admin:price:list:testnet', { skipNamespace: true}); - this.registerRoute('/admin/price/list', 'GET', 'admin:price:list:mainnet', { skipNamespace: true}); + this.registerRoute('/admin/price/list', 'GET', 'admin:price:list:testnet', { skipNamespace: true }); + this.registerRoute('/admin/price/list', 'GET', 'admin:price:list:mainnet', { skipNamespace: true }); // Products - this.registerRoute('/admin/product/list', 'GET', 'admin:product:list:testnet', { skipNamespace: true}); - this.registerRoute('/admin/product/list', 'GET', 'admin:product:list:mainnet', { skipNamespace: true}); - this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:testnet', { skipNamespace: true}); - this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:mainnet', { skipNamespace: true}); + this.registerRoute('/admin/product/list', 'GET', 'admin:product:list:testnet', { skipNamespace: true }); + this.registerRoute('/admin/product/list', 'GET', 'admin:product:list:mainnet', { skipNamespace: true }); + this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:testnet', { skipNamespace: true }); + this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:mainnet', { skipNamespace: true }); } public async handle(request: Request, response: Response): Promise { if (!request.path.includes('/admin/')) { diff --git a/src/middleware/auth/user-info-fetcher/base.ts b/src/middleware/auth/user-info-fetcher/base.ts index 9cc7ca03..ad373936 100644 --- a/src/middleware/auth/user-info-fetcher/base.ts +++ b/src/middleware/auth/user-info-fetcher/base.ts @@ -2,7 +2,6 @@ import type { Request } from 'express'; import type { IAuthResponse } from '../../../types/authentication.js'; import type { IOAuthProvider } from '../oauth/base.js'; - export interface IUserInfoOptions { [key: string]: any; } diff --git a/src/middleware/auth/user-info-fetcher/portal-token.ts b/src/middleware/auth/user-info-fetcher/portal-token.ts index 08efa54c..a972cbfe 100644 --- a/src/middleware/auth/user-info-fetcher/portal-token.ts +++ b/src/middleware/auth/user-info-fetcher/portal-token.ts @@ -11,52 +11,54 @@ dotenv.config(); export class PortalUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { private m2mToken: string; - private idToken + private idToken; constructor(m2mToken: string, idToken: string) { super(); this.m2mToken = m2mToken; - this.idToken = idToken; + this.idToken = idToken; } async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { // Get customerId from header - if (!this.idToken) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No idToken found in the header.`); - } + if (!this.idToken) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No idToken found in the header.`); + } - // Check the idToken, provided in header - const idTokenVerification = await this.verifyIdToken(oauthProvider); - if (idTokenVerification.error) { - return idTokenVerification; - } - // return this.returnOk(); + // Check the idToken, provided in header + const idTokenVerification = await this.verifyIdToken(oauthProvider); + if (idTokenVerification.error) { + return idTokenVerification; + } + // return this.returnOk(); return this.verifyM2MToken(oauthProvider); } - public async verifyIdToken(oauthProvider: IOAuthProvider): Promise { - try { - const { payload } = await jwtVerify( - this.idToken, // The raw Bearer Token extracted from the request header - createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server - { - // expected issuer of the token, should be issued by the Logto server - issuer: oauthProvider.endpoint_issuer, - } - ); - // Setup the scopes from the token - if (!payload.sub) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No sub found in the token. Cannot set customerId.`); - } - this.setUserId(payload.sub); - return this.returnOk(); - } - catch (error) { - console.error(error); - return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); - } - } + public async verifyIdToken(oauthProvider: IOAuthProvider): Promise { + try { + const { payload } = await jwtVerify( + this.idToken, // The raw Bearer Token extracted from the request header + createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + { + // expected issuer of the token, should be issued by the Logto server + issuer: oauthProvider.endpoint_issuer, + } + ); + // Setup the scopes from the token + if (!payload.sub) { + return this.returnError( + StatusCodes.UNAUTHORIZED, + `Unauthorized error: No sub found in the token. Cannot set customerId.` + ); + } + this.setUserId(payload.sub); + return this.returnOk(); + } catch (error) { + console.error(error); + return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + } + } public async verifyM2MToken(oauthProvider: IOAuthProvider): Promise { try { diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts index 62daba14..5c585a2b 100644 --- a/src/services/admin/subscription.ts +++ b/src/services/admin/subscription.ts @@ -19,130 +19,141 @@ export class SubscriptionService { // Get rid of such code and move it to the builder public static instance = new SubscriptionService(); - constructor() { - this.subscriptionRepository = Connection.instance.dbConnection.getRepository(SubscriptionEntity); - } + constructor() { + this.subscriptionRepository = Connection.instance.dbConnection.getRepository(SubscriptionEntity); + } - public async create( - subscriptionId: string, - customer: CustomerEntity, - status: string, - currentPeriodStart: Date, - currentPeriodEnd: Date, - trialStart?: Date, - trialEnd?: Date, - ): Promise { - const subscriptionEntity = new SubscriptionEntity( - subscriptionId, - customer, - status, - currentPeriodStart, - currentPeriodEnd, - trialStart, - trialEnd, - ); - const res = await this.subscriptionRepository.insert(subscriptionEntity); - if (!res) throw new Error(`Cannot create a new subscription`); + public async create( + subscriptionId: string, + customer: CustomerEntity, + status: string, + currentPeriodStart: Date, + currentPeriodEnd: Date, + trialStart?: Date, + trialEnd?: Date + ): Promise { + const subscriptionEntity = new SubscriptionEntity( + subscriptionId, + customer, + status, + currentPeriodStart, + currentPeriodEnd, + trialStart, + trialEnd + ); + const res = await this.subscriptionRepository.insert(subscriptionEntity); + if (!res) throw new Error(`Cannot create a new subscription`); - return subscriptionEntity; - } - - public async update( - subscriptionId: string, - status?: string, - currentPeriodStart?: Date, - currentPeriodEnd?: Date, - trialStart?: Date, - trialEnd?: Date, - ) { - const existing = await this.subscriptionRepository.findOneBy({ subscriptionId }); - if (!existing) { - throw new Error(`Subscription with id ${subscriptionId} not found`); - } - if (status) existing.status = status; - if (currentPeriodStart) existing.currentPeriodStart = currentPeriodStart; - if (currentPeriodEnd) existing.currentPeriodEnd = currentPeriodEnd; - if (trialStart) existing.trialStart = trialStart; - if (trialEnd) existing.trialEnd = trialEnd; - return await this.subscriptionRepository.save(existing); - } + return subscriptionEntity; + } - public async get(subscriptionId?: string): Promise { - return await this.subscriptionRepository.findOne({ - where: { subscriptionId }, - relations: ['customer'], - }); - } + public async update( + subscriptionId: string, + status?: string, + currentPeriodStart?: Date, + currentPeriodEnd?: Date, + trialStart?: Date, + trialEnd?: Date + ) { + const existing = await this.subscriptionRepository.findOneBy({ subscriptionId }); + if (!existing) { + throw new Error(`Subscription with id ${subscriptionId} not found`); + } + if (status) existing.status = status; + if (currentPeriodStart) existing.currentPeriodStart = currentPeriodStart; + if (currentPeriodEnd) existing.currentPeriodEnd = currentPeriodEnd; + if (trialStart) existing.trialStart = trialStart; + if (trialEnd) existing.trialEnd = trialEnd; + return await this.subscriptionRepository.save(existing); + } - public async findOne(where: Record) { - return await this.subscriptionRepository.findOne({ - where: where, - relations: ['customer'], - }); - } + public async get(subscriptionId?: string): Promise { + return await this.subscriptionRepository.findOne({ + where: { subscriptionId }, + relations: ['customer'], + }); + } - public async stripeSync(customer?: CustomerEntity, user?: UserEntity): Promise { - let stripeCustomerId: string; - if (!customer && !user) { - throw new Error('StripeSync: customer or user is required'); - } - if (customer) { - stripeCustomerId = customer.stripeCustomerId; - } else { - stripeCustomerId = user?.customer.stripeCustomerId as string; - } + public async findOne(where: Record) { + return await this.subscriptionRepository.findOne({ + where: where, + relations: ['customer'], + }); + } - const subscriptions = await stripe.subscriptions.list({ customer: stripeCustomerId }); - // Get list of all subscription and sort them by created time to make sure that we are processing them in the correct order - for (const subscription of subscriptions.data.sort((a, b) => a.created - b.created)){ - const existing = await this.subscriptionRepository.findOne({ - where: { subscriptionId: subscription.id }, - }); - if (!existing) { - const customer = await CustomerService.instance.findbyStripeCustomerId(stripeCustomerId); - if (!customer) { - throw new Error(`Customer with stripeCustomerId ${stripeCustomerId} not found`); - } - const res = await this.create( - subscription.id, - customer, - subscription.status, - new Date(subscription.current_period_start * 1000), - new Date(subscription.current_period_end * 1000), - subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, - subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined, - ); - if (!res) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification(`Cannot create a new subscription with id ${subscription.id}`, 'Subscription syncronization'), - severity: 'error', - }); - } - eventTracker.notify({ - message: EventTracker.compileBasicNotification(`New subscription with id ${subscription.id} created`, 'Subscription syncronization'), - severity: 'info', - }); - } else { - const res = await this.update( - subscription.id, - subscription.status, - new Date(subscription.current_period_start * 1000), - new Date(subscription.current_period_end * 1000), - subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, - subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined, - ); - if (!res) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification(`Cannot update subscription with id ${subscription.id}`, 'Subscription syncronization'), - severity: 'error', - }); - } - eventTracker.notify({ - message: EventTracker.compileBasicNotification(`Subscription with id ${subscription.id} updated`, 'Subscription syncronization'), - severity: 'info', - }); - } - } + public async stripeSync(customer?: CustomerEntity, user?: UserEntity): Promise { + let stripeCustomerId: string; + if (!customer && !user) { + throw new Error('StripeSync: customer or user is required'); + } + if (customer) { + stripeCustomerId = customer.stripeCustomerId; + } else { + stripeCustomerId = user?.customer.stripeCustomerId as string; + } - } + const subscriptions = await stripe.subscriptions.list({ customer: stripeCustomerId }); + // Get list of all subscription and sort them by created time to make sure that we are processing them in the correct order + for (const subscription of subscriptions.data.sort((a, b) => a.created - b.created)) { + const existing = await this.subscriptionRepository.findOne({ + where: { subscriptionId: subscription.id }, + }); + if (!existing) { + const customer = await CustomerService.instance.findbyStripeCustomerId(stripeCustomerId); + if (!customer) { + throw new Error(`Customer with stripeCustomerId ${stripeCustomerId} not found`); + } + const res = await this.create( + subscription.id, + customer, + subscription.status, + new Date(subscription.current_period_start * 1000), + new Date(subscription.current_period_end * 1000), + subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, + subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined + ); + if (!res) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Cannot create a new subscription with id ${subscription.id}`, + 'Subscription syncronization' + ), + severity: 'error', + }); + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `New subscription with id ${subscription.id} created`, + 'Subscription syncronization' + ), + severity: 'info', + }); + } else { + const res = await this.update( + subscription.id, + subscription.status, + new Date(subscription.current_period_start * 1000), + new Date(subscription.current_period_end * 1000), + subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, + subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined + ); + if (!res) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Cannot update subscription with id ${subscription.id}`, + 'Subscription syncronization' + ), + severity: 'error', + }); + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription with id ${subscription.id} updated`, + 'Subscription syncronization' + ), + severity: 'info', + }); + } + } + } } diff --git a/src/services/api/customer.ts b/src/services/api/customer.ts index b33ab7b5..85376fef 100644 --- a/src/services/api/customer.ts +++ b/src/services/api/customer.ts @@ -81,7 +81,6 @@ export class CustomerService { return await this.customerRepository.findOne({ where: { stripeCustomerId }, }); - } public async isExist(where: Record) { diff --git a/src/services/track/admin/account-submitter.ts b/src/services/track/admin/account-submitter.ts index b6261ec1..29d7f5f3 100644 --- a/src/services/track/admin/account-submitter.ts +++ b/src/services/track/admin/account-submitter.ts @@ -1,60 +1,68 @@ -import Stripe from "stripe"; -import type { IObserver } from "../types.js"; -import { OperationNameEnum } from "../../../types/constants.js"; -import type { INotifyMessage } from "../../../types/track.js"; -import { EventTracker } from "../tracker.js"; -import { StatusCodes } from "http-status-codes"; -import { CustomerService } from "../../api/customer.js"; -import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from "../submitter.js"; - +import Stripe from 'stripe'; +import type { IObserver } from '../types.js'; +import { OperationNameEnum } from '../../../types/constants.js'; +import type { INotifyMessage } from '../../../types/track.js'; +import { EventTracker } from '../tracker.js'; +import { StatusCodes } from 'http-status-codes'; +import { CustomerService } from '../../api/customer.js'; +import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from '../submitter.js'; export class PortalAccountCreateSubmitter implements IObserver { - private emitter: EventEmitter; - private stripe: Stripe; + private emitter: EventEmitter; + private stripe: Stripe; - constructor(emitter: EventEmitter) { - this.emitter = emitter; - this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - } + constructor(emitter: EventEmitter) { + this.emitter = emitter; + this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + } - notify(notifyMessage: INotifyMessage): void { + notify(notifyMessage: INotifyMessage): void { this.emitter.emit('notify', notifyMessage); } - async update(operation: ISubmitOperation): Promise { - if (operation.operation === OperationNameEnum.STRIPE_ACCOUNT_CREATE) { - await this.submitStripeAccountCreate(operation); - } - } + async update(operation: ISubmitOperation): Promise { + if (operation.operation === OperationNameEnum.STRIPE_ACCOUNT_CREATE) { + await this.submitStripeAccountCreate(operation); + } + } - async submitStripeAccountCreate(operation: ISubmitOperation): Promise { - const data = operation.data as ISubmitStripeCustomerCreateData; + async submitStripeAccountCreate(operation: ISubmitOperation): Promise { + const data = operation.data as ISubmitStripeCustomerCreateData; - try { - // Create a new Stripe account - const account = await this.stripe.customers.create({ - name: data.name, - email: data.email, - }); - if (account.lastResponse.statusCode !== StatusCodes.OK) { - await this.notify({ - message: EventTracker.compileBasicNotification(`Failed to create Stripe account with name: ${data.name}.`, operation.operation), - severity: 'error', - } as INotifyMessage); - return; - } + try { + // Create a new Stripe account + const account = await this.stripe.customers.create({ + name: data.name, + email: data.email, + }); + if (account.lastResponse.statusCode !== StatusCodes.OK) { + await this.notify({ + message: EventTracker.compileBasicNotification( + `Failed to create Stripe account with name: ${data.name}.`, + operation.operation + ), + severity: 'error', + } as INotifyMessage); + return; + } - // Update the CaaS customer with the new Stripe account - await CustomerService.instance.update(data.customerId, undefined, account.id); - await this.notify({ - message: EventTracker.compileBasicNotification(`Stripe account created with name: ${data.name}.`, operation.operation), - severity: 'info', - } as INotifyMessage); - } catch (error) { - await this.notify({ - message: EventTracker.compileBasicNotification(`Failed to create Stripe account with name: ${data.name as string}.`, operation.operation), - severity: 'error', - } as INotifyMessage); - } - } + // Update the CaaS customer with the new Stripe account + await CustomerService.instance.update(data.customerId, undefined, account.id); + await this.notify({ + message: EventTracker.compileBasicNotification( + `Stripe account created with name: ${data.name}.`, + operation.operation + ), + severity: 'info', + } as INotifyMessage); + } catch (error) { + await this.notify({ + message: EventTracker.compileBasicNotification( + `Failed to create Stripe account with name: ${data.name as string}.`, + operation.operation + ), + severity: 'error', + } as INotifyMessage); + } + } } diff --git a/src/services/track/admin/subscription-submitter.ts b/src/services/track/admin/subscription-submitter.ts index d6f2144b..ccfbaf9d 100644 --- a/src/services/track/admin/subscription-submitter.ts +++ b/src/services/track/admin/subscription-submitter.ts @@ -1,161 +1,189 @@ -import { OperationNameEnum } from "../../../types/constants.js"; -import type { INotifyMessage } from "../../../types/track.js"; -import { SubscriptionService } from "../../admin/subscription.js"; -import { CustomerService } from "../../api/customer.js"; -import type { ISubmitOperation, ISubmitSubscriptionData } from "../submitter.js"; -import { EventTracker, eventTracker } from "../tracker.js"; -import type { IObserver } from "../types.js"; +import { OperationNameEnum } from '../../../types/constants.js'; +import type { INotifyMessage } from '../../../types/track.js'; +import { SubscriptionService } from '../../admin/subscription.js'; +import { CustomerService } from '../../api/customer.js'; +import type { ISubmitOperation, ISubmitSubscriptionData } from '../submitter.js'; +import { EventTracker, eventTracker } from '../tracker.js'; +import type { IObserver } from '../types.js'; //eslint-disable-next-line function isPromise(object: any): object is Promise { - return object && Promise.resolve(object) === object; - } + return object && Promise.resolve(object) === object; +} export const eventDecorator = () => { - return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { - const { value } = descriptor; - //eslint-disable-next-line - descriptor.value = async (...args: any) => { - try { - const response = value.apply(target, args); - return isPromise(response) ? await response : Promise.resolve(response); - } catch (error) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification(`Error while calling function: ${propertyKey}: ${(error as Record)?.message || error}`), - severity: 'error', - } satisfies INotifyMessage); - } - }; - return descriptor; - } -} + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + const { value } = descriptor; + //eslint-disable-next-line + descriptor.value = async (...args: any) => { + try { + const response = value.apply(target, args); + return isPromise(response) ? await response : Promise.resolve(response); + } catch (error) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Error while calling function: ${propertyKey}: ${(error as Record)?.message || error}` + ), + severity: 'error', + } satisfies INotifyMessage); + } + }; + return descriptor; + }; +}; export class SubscriptionSubmitter implements IObserver { + private emitter: EventEmitter; - private emitter: EventEmitter; - - constructor(emitter: EventEmitter) { - this.emitter = emitter; - } + constructor(emitter: EventEmitter) { + this.emitter = emitter; + } - notify(notifyMessage: INotifyMessage): void { + notify(notifyMessage: INotifyMessage): void { this.emitter.emit('notify', notifyMessage); } - async update(operation: ISubmitOperation): Promise { - switch (operation.operation) { - case OperationNameEnum.SUBSCRIPTION_CREATE: - await this.submitSubscriptionCreate(operation); - break; - - case OperationNameEnum.SUBSCRIPTION_UPDATE: - await this.submitSubscriptionUpdate(operation); - break; - - case OperationNameEnum.SUBSCRIPTION_CANCEL: - await this.submitSubscriptionCancel(operation); - break; - } - } - - // @eventDecorator() - async submitSubscriptionCreate(operation: ISubmitOperation): Promise { - const data = operation.data as ISubmitSubscriptionData; - try{ - const customers = await CustomerService.instance.find({ - stripeCustomerId: data.stripeCustomerId - }) - - if (customers.length !== 1) { - this.notify({ - message: EventTracker.compileBasicNotification(`It should be only 1 Stripe account associated with CaaS customer. Stripe accountId: ${data.stripeCustomerId}.`, operation.operation), - severity: 'error', - }) - } - const subscription = await SubscriptionService.instance.create( - data.subscriptionId, - customers[0], - data.status, - data.currentPeriodStart, - data.currentPeriodEnd, - data.trialStart as Date, - data.trialEnd as Date, - ) - if (!subscription) { - this.notify({ - message: EventTracker.compileBasicNotification(`Failed to create a new subscription with id: ${data.subscriptionId}.`, operation.operation), - severity: 'error', - }) - } - - this.notify({ - message: EventTracker.compileBasicNotification(`Subscription created with id: ${data.subscriptionId}.`, operation.operation), - severity: 'info', - }) - } catch (error) { - this.notify({ - message: EventTracker.compileBasicNotification(`Failed to create a new subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation), - severity: 'error', - }) - } - } - - // @eventDecorator() - async submitSubscriptionUpdate(operation: ISubmitOperation): Promise { - const data = operation.data as ISubmitSubscriptionData; - try { - const subscription = await SubscriptionService.instance.update( - data.subscriptionId, - data.status, - data.currentPeriodStart, - data.currentPeriodEnd, - data.trialStart as Date, - data.trialEnd as Date, - ) - if (!subscription) { - this.notify({ - message: EventTracker.compileBasicNotification(`Failed to update subscription with id: ${data.subscriptionId}.`, operation.operation), - severity: 'error', - }) - } - - this.notify({ - message: EventTracker.compileBasicNotification(`Subscription updated with id: ${data.subscriptionId}.`, operation.operation), - severity: 'info', - }) - } catch (error) { - this.notify({ - message: EventTracker.compileBasicNotification(`Failed to update subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation), - severity: 'error', - }) - } - } - - // @eventDecorator() - async submitSubscriptionCancel(operation: ISubmitOperation): Promise { - const data = operation.data as ISubmitSubscriptionData; - try { - const subscription = await SubscriptionService.instance.update( - data.subscriptionId, - data.status, - ) - if (!subscription) { - this.notify({ - message: EventTracker.compileBasicNotification(`Failed to cancel subscription with id: ${data.subscriptionId}.`, operation.operation), - severity: 'error', - }) - } - - this.notify({ - message: EventTracker.compileBasicNotification(`Subscription canceled with id: ${data.subscriptionId}.`, operation.operation), - severity: 'info', - }) - } catch (error) { - this.notify({ - message: EventTracker.compileBasicNotification(`Failed to cancel subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation), - severity: 'error', - }) - } - } -} \ No newline at end of file + async update(operation: ISubmitOperation): Promise { + switch (operation.operation) { + case OperationNameEnum.SUBSCRIPTION_CREATE: + await this.submitSubscriptionCreate(operation); + break; + + case OperationNameEnum.SUBSCRIPTION_UPDATE: + await this.submitSubscriptionUpdate(operation); + break; + + case OperationNameEnum.SUBSCRIPTION_CANCEL: + await this.submitSubscriptionCancel(operation); + break; + } + } + + // @eventDecorator() + async submitSubscriptionCreate(operation: ISubmitOperation): Promise { + const data = operation.data as ISubmitSubscriptionData; + try { + const customers = await CustomerService.instance.find({ + stripeCustomerId: data.stripeCustomerId, + }); + + if (customers.length !== 1) { + this.notify({ + message: EventTracker.compileBasicNotification( + `It should be only 1 Stripe account associated with CaaS customer. Stripe accountId: ${data.stripeCustomerId}.`, + operation.operation + ), + severity: 'error', + }); + } + const subscription = await SubscriptionService.instance.create( + data.subscriptionId, + customers[0], + data.status, + data.currentPeriodStart, + data.currentPeriodEnd, + data.trialStart as Date, + data.trialEnd as Date + ); + if (!subscription) { + this.notify({ + message: EventTracker.compileBasicNotification( + `Failed to create a new subscription with id: ${data.subscriptionId}.`, + operation.operation + ), + severity: 'error', + }); + } + + this.notify({ + message: EventTracker.compileBasicNotification( + `Subscription created with id: ${data.subscriptionId}.`, + operation.operation + ), + severity: 'info', + }); + } catch (error) { + this.notify({ + message: EventTracker.compileBasicNotification( + `Failed to create a new subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, + operation.operation + ), + severity: 'error', + }); + } + } + + // @eventDecorator() + async submitSubscriptionUpdate(operation: ISubmitOperation): Promise { + const data = operation.data as ISubmitSubscriptionData; + try { + const subscription = await SubscriptionService.instance.update( + data.subscriptionId, + data.status, + data.currentPeriodStart, + data.currentPeriodEnd, + data.trialStart as Date, + data.trialEnd as Date + ); + if (!subscription) { + this.notify({ + message: EventTracker.compileBasicNotification( + `Failed to update subscription with id: ${data.subscriptionId}.`, + operation.operation + ), + severity: 'error', + }); + } + + this.notify({ + message: EventTracker.compileBasicNotification( + `Subscription updated with id: ${data.subscriptionId}.`, + operation.operation + ), + severity: 'info', + }); + } catch (error) { + this.notify({ + message: EventTracker.compileBasicNotification( + `Failed to update subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, + operation.operation + ), + severity: 'error', + }); + } + } + + // @eventDecorator() + async submitSubscriptionCancel(operation: ISubmitOperation): Promise { + const data = operation.data as ISubmitSubscriptionData; + try { + const subscription = await SubscriptionService.instance.update(data.subscriptionId, data.status); + if (!subscription) { + this.notify({ + message: EventTracker.compileBasicNotification( + `Failed to cancel subscription with id: ${data.subscriptionId}.`, + operation.operation + ), + severity: 'error', + }); + } + + this.notify({ + message: EventTracker.compileBasicNotification( + `Subscription canceled with id: ${data.subscriptionId}.`, + operation.operation + ), + severity: 'info', + }); + } catch (error) { + this.notify({ + message: EventTracker.compileBasicNotification( + `Failed to cancel subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, + operation.operation + ), + severity: 'error', + }); + } + } +} diff --git a/src/services/track/api/credential-status-subscriber.ts b/src/services/track/api/credential-status-subscriber.ts index 41c5d6c9..762d543b 100644 --- a/src/services/track/api/credential-status-subscriber.ts +++ b/src/services/track/api/credential-status-subscriber.ts @@ -1,12 +1,8 @@ import { OperationCategoryNameEnum } from '../../../types/constants.js'; -import type { - ICredentialStatusTrack, ITrackOperation, - ITrackResult -} from '../../../types/track.js'; +import type { ICredentialStatusTrack, ITrackOperation, ITrackResult } from '../../../types/track.js'; import type { IObserver } from '../types.js'; import { BaseOperationObserver } from '../base.js'; - export class CredentialStatusSubscriber extends BaseOperationObserver implements IObserver { isReactionNeeded(trackOperation: ITrackOperation): boolean { // Credential tracker reacts on CredentialStatusList, Credential operations like revocation diff --git a/src/services/track/api/credential-subscriber.ts b/src/services/track/api/credential-subscriber.ts index b1a7af29..7ca1515c 100644 --- a/src/services/track/api/credential-subscriber.ts +++ b/src/services/track/api/credential-subscriber.ts @@ -1,12 +1,8 @@ import { OperationCategoryNameEnum } from '../../../types/constants.js'; -import type { - ICredentialTrack, ITrackOperation, - ITrackResult -} from '../../../types/track.js'; +import type { ICredentialTrack, ITrackOperation, ITrackResult } from '../../../types/track.js'; import type { IObserver } from '../types.js'; import { BaseOperationObserver } from '../base.js'; - export class CredentialSubscriber extends BaseOperationObserver implements IObserver { isReactionNeeded(trackOperation: ITrackOperation): boolean { // Credential tracker reacts on CredentialStatusList, Credential operations like revocation diff --git a/src/services/track/api/did-subscriber.ts b/src/services/track/api/did-subscriber.ts index b6be6b07..5bc38685 100644 --- a/src/services/track/api/did-subscriber.ts +++ b/src/services/track/api/did-subscriber.ts @@ -1,13 +1,8 @@ import { OperationCategoryNameEnum } from '../../../types/constants.js'; -import type { - ITrackOperation, - ITrackResult, - IDIDTrack -} from '../../../types/track.js'; +import type { ITrackOperation, ITrackResult, IDIDTrack } from '../../../types/track.js'; import type { IObserver } from '../types.js'; import { BaseOperationObserver } from '../base.js'; - export class DIDSubscriber extends BaseOperationObserver implements IObserver { isReactionNeeded(trackOperation: ITrackOperation): boolean { return trackOperation.category === OperationCategoryNameEnum.DID; diff --git a/src/services/track/api/key-subscriber.ts b/src/services/track/api/key-subscriber.ts index c5ff1d0b..1e46b6f9 100644 --- a/src/services/track/api/key-subscriber.ts +++ b/src/services/track/api/key-subscriber.ts @@ -1,13 +1,8 @@ import { OperationCategoryNameEnum } from '../../../types/constants.js'; -import type { - IKeyTrack, - ITrackOperation, - ITrackResult -} from '../../../types/track.js'; +import type { IKeyTrack, ITrackOperation, ITrackResult } from '../../../types/track.js'; import type { IObserver } from '../types.js'; import { BaseOperationObserver } from '../base.js'; - export class KeySubscriber extends BaseOperationObserver implements IObserver { isReactionNeeded(trackOperation: ITrackOperation): boolean { // Credential tracker reacts on CredentialStatusList, Credential operations like revocation diff --git a/src/services/track/api/presentation-subscriber.ts b/src/services/track/api/presentation-subscriber.ts index 4d66101f..daee2c07 100644 --- a/src/services/track/api/presentation-subscriber.ts +++ b/src/services/track/api/presentation-subscriber.ts @@ -1,12 +1,8 @@ import { OperationCategoryNameEnum } from '../../../types/constants.js'; -import type { - IPresentationTrack, ITrackOperation, - ITrackResult -} from '../../../types/track.js'; +import type { IPresentationTrack, ITrackOperation, ITrackResult } from '../../../types/track.js'; import type { IObserver } from '../types.js'; import { BaseOperationObserver } from '../base.js'; - export class PresentationSubscriber extends BaseOperationObserver implements IObserver { isReactionNeeded(trackOperation: ITrackOperation): boolean { // Credential tracker reacts on CredentialStatusList, Credential operations like revocation diff --git a/src/services/track/api/resource-subscriber.ts b/src/services/track/api/resource-subscriber.ts index 67e5cc5f..f26dbe05 100644 --- a/src/services/track/api/resource-subscriber.ts +++ b/src/services/track/api/resource-subscriber.ts @@ -5,7 +5,8 @@ import type { ICredentialTrack, IResourceTrack, ITrackOperation, - ITrackResult} from '../../../types/track.js'; + ITrackResult, +} from '../../../types/track.js'; import { isCredentialStatusTrack, isCredentialTrack, isResourceTrack } from '../helpers.js'; import { IdentifierService } from '../../api/identifier.js'; import { KeyService } from '../../api/key.js'; @@ -13,7 +14,6 @@ import { ResourceService } from '../../api/resource.js'; import type { IObserver } from '../types.js'; import { BaseOperationObserver } from '../base.js'; - export class ResourceSubscriber extends BaseOperationObserver implements IObserver { private static acceptedOperations = [ OperationNameEnum.RESOURCE_CREATE, @@ -29,7 +29,8 @@ export class ResourceSubscriber extends BaseOperationObserver implements IObserv isReactionNeeded(trackOperation: ITrackOperation): boolean { // Resource tracker reacts on CredentialStatusList, Credential operations like revocation // and Resource operations like create, update, delete - const isCategoryAccepted = trackOperation.category === OperationCategoryNameEnum.RESOURCE || + const isCategoryAccepted = + trackOperation.category === OperationCategoryNameEnum.RESOURCE || trackOperation.category === OperationCategoryNameEnum.CREDENTIAL || trackOperation.category === OperationCategoryNameEnum.CREDENTIAL_STATUS; const isOperationAccepted = ResourceSubscriber.acceptedOperations.includes( diff --git a/src/services/track/observer.ts b/src/services/track/observer.ts index 3282df73..61dc82f0 100644 --- a/src/services/track/observer.ts +++ b/src/services/track/observer.ts @@ -51,4 +51,3 @@ export class SubmitSubject implements ITrackSubject { } } } - diff --git a/src/services/track/operation-subscriber.ts b/src/services/track/operation-subscriber.ts index eb1b27a7..57409171 100644 --- a/src/services/track/operation-subscriber.ts +++ b/src/services/track/operation-subscriber.ts @@ -154,4 +154,3 @@ export class DBOperationSubscriber extends BaseOperationObserver implements IObs } } } - diff --git a/src/services/track/submitter.ts b/src/services/track/submitter.ts index 44adbd1d..89beb23b 100644 --- a/src/services/track/submitter.ts +++ b/src/services/track/submitter.ts @@ -1,24 +1,23 @@ // Type: Interface -export type ISubmitData = ISubmitStripeCustomerCreateData - | ISubmitSubscriptionData; +export type ISubmitData = ISubmitStripeCustomerCreateData | ISubmitSubscriptionData; export interface ISubmitStripeCustomerCreateData { - customerId: string; - name: string; - email?: string; + customerId: string; + name: string; + email?: string; } export interface ISubmitSubscriptionData { - stripeCustomerId: string; - status: string; - currentPeriodStart: Date; - currentPeriodEnd: Date; - trialStart?: Date; - trialEnd?: Date; - subscriptionId: string; + stripeCustomerId: string; + status: string; + currentPeriodStart: Date; + currentPeriodEnd: Date; + trialStart?: Date; + trialEnd?: Date; + subscriptionId: string; } export interface ISubmitOperation { - operation: string; - data: ISubmitData; + operation: string; + data: ISubmitData; } diff --git a/src/services/track/tracker.ts b/src/services/track/tracker.ts index 62713d0c..b854f2f5 100644 --- a/src/services/track/tracker.ts +++ b/src/services/track/tracker.ts @@ -1,11 +1,8 @@ import EventEmitter from 'node:events'; import type { INotifyMessage, ITrackOperation } from '../../types/track.js'; import { DatadogNotifier, LoggerNotifier } from './notifiers.js'; -import { - DBOperationSubscriber, -} from './operation-subscriber.js'; -import { - ResourceSubscriber} from './api/resource-subscriber.js'; +import { DBOperationSubscriber } from './operation-subscriber.js'; +import { ResourceSubscriber } from './api/resource-subscriber.js'; import { PresentationSubscriber } from './api/presentation-subscriber.js'; import { CredentialStatusSubscriber } from './api/credential-status-subscriber.js'; import { DIDSubscriber } from './api/did-subscriber.js'; @@ -14,7 +11,7 @@ import type { ITrackType } from './types.js'; import { ENABLE_DATADOG } from '../../types/constants.js'; import { SubmitSubject, TrackSubject } from './observer.js'; import type { ISubmitOperation } from './submitter.js'; -import { PortalAccountCreateSubmitter } from "./admin/account-submitter.js"; +import { PortalAccountCreateSubmitter } from './admin/account-submitter.js'; import { SubscriptionSubmitter } from './admin/subscription-submitter.js'; export class EventTracker { diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 8e616f6f..d898b33c 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -1,713 +1,693 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service admin API for cheqd network", - "version": "2.0.0", - "description": "Admin API which handles users subscriptions and payments", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Product" - }, - { - "name": "Price" - }, - { - "name": "Customer" - }, - { - "name": "Subscription" - }, - { - "name": "Checkout" - } - ], - "paths": { - "/admin/checkout/session/create": { - "post": { - "summary": "Create a checkout session", - "description": "Create a checkout session", - "tags": [ - "Checkout" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckoutSessionCreateRequestBody" - } - } - } - }, - "responses": { - "303": { - "description": "A redirect to Stripe prebuilt checkout page" - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/price/list": { - "get": { - "summary": "Get a list of prices", - "description": "Get a list of prices", - "tags": [ - "Price" - ], - "parameters": [ - { - "in": "query", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id. If passed - returns filtered by this product list of prices.", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of prices", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PriceListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/list": { - "get": { - "summary": "Get a list of products", - "description": "Get a list of products which are on a Stripe side", - "tags": [ - "Product" - ], - "parameters": [ - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the list of products with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of products", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/get/{productId}": { - "get": { - "summary": "Get a product", - "description": "Get a product by id", - "tags": [ - "Product" - ], - "parameters": [ - { - "in": "path", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id which identifies the product in Stripe", - "required": true - } - }, - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the product with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A product", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/create": { - "post": { - "summary": "Create a subscription", - "description": "Creates a new subscription for an existing customer", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateRequestBody" - } - } - } - }, - "responses": { - "201": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/update": { - "post": { - "summary": "Update a subscription", - "description": "Updates an existing subscription", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/list": { - "get": { - "summary": "Get a list of subscriptions", - "description": "Get a list of subscriptions", - "tags": [ - "Subscription" - ], - "responses": { - "200": { - "description": "A list of subscriptions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/get/{subscriptionId}": { - "get": { - "summary": "Get a subscription", - "description": "Get a subscription", - "tags": [ - "Subscription" - ], - "parameters": [ - { - "in": "path", - "name": "subscriptionId", - "schema": { - "type": "string", - "description": "The subscription id", - "required": true - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/cancel": { - "post": { - "summary": "Cancel a subscription", - "description": "Cancels an existing subscription", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/resume": {}, - "post": { - "summary": "Resume a subscription", - "description": "Resumes an existing subscription", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "components": { - "schemas": { - "PriceListResponseBody": { - "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", - "type": "object", - "properties": { - "prices": { - "type": "array", - "items": { - "type": "object", - "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" - } - } - } - }, - "ProductListResponseBody": { - "type": "object", - "properties": { - "products": { - "type": "array", - "items": { - "type": "object", - "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" - } - } - } - }, - "ProductGetResponseBody": { - "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", - "type": "object" - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - }, - "SubscriptionCreateRequestBody": { - "description": "The request body for creating a subscription", - "type": "object", - "properties": { - "customerId": { - "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - } - } - } - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "SubscriptionCreateResponseBody": { - "description": "The response body for creating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionUpdateRequestBody": { - "description": "The request body for updating a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - }, - "updateParams": { - "type": "object", - "description": "The subscription update parameters. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/update)" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "SubscriptionUpdateResponseBody": { - "description": "The response body for updating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionGetRequestBody": { - "description": "The request body for getting a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionGetResponseBody": { - "description": "The response body for getting a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionListRequestBody": { - "description": "The request body for listing subscriptions", - "type": "object", - "properties": { - "customerId": { - "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" - } - } - }, - "SubscriptionListResponseBody": { - "description": "The response body for listing subscriptions", - "type": "object", - "properties": { - "subscriptions": { - "type": "array", - "items": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - } - }, - "SubscriptionCancelRequestBody": { - "description": "The request body for canceling a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionCancelResponseBody": { - "description": "The response body for canceling a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "SubscriptionResumeRequestBody": { - "description": "The request body for resuming a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "SubscriptionResumeResponseBody": { - "description": "The response body for resuming a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - }, - "CheckoutSessionCreateRequestBody": { - "description": "The request body for creating a checkout session", - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - }, - "successURL": { - "type": "string", - "description": "The URL to redirect to after the customer sucessfully completes the checkout", - "example": "https://example.com/success" - }, - "cancelURL": { - "type": "string", - "description": "The URL to redirect to after the customer cancels the checkout", - "example": "https://example.com/cancel" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "NotFoundError": { - "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Not Found Error" - } - } - } - } - } -} \ No newline at end of file + "openapi": "3.0.0", + "info": { + "title": "Credential Service admin API for cheqd network", + "version": "2.0.0", + "description": "Admin API which handles users subscriptions and payments", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Product" + }, + { + "name": "Price" + }, + { + "name": "Customer" + }, + { + "name": "Subscription" + }, + { + "name": "Checkout" + } + ], + "paths": { + "/admin/checkout/session/create": { + "post": { + "summary": "Create a checkout session", + "description": "Create a checkout session", + "tags": ["Checkout"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckoutSessionCreateRequestBody" + } + } + } + }, + "responses": { + "303": { + "description": "A redirect to Stripe prebuilt checkout page" + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/price/list": { + "get": { + "summary": "Get a list of prices", + "description": "Get a list of prices", + "tags": ["Price"], + "parameters": [ + { + "in": "query", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id. If passed - returns filtered by this product list of prices.", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of prices", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriceListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/list": { + "get": { + "summary": "Get a list of products", + "description": "Get a list of products which are on a Stripe side", + "tags": ["Product"], + "parameters": [ + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the list of products with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of products", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/get/{productId}": { + "get": { + "summary": "Get a product", + "description": "Get a product by id", + "tags": ["Product"], + "parameters": [ + { + "in": "path", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id which identifies the product in Stripe", + "required": true + } + }, + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the product with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/create": { + "post": { + "summary": "Create a subscription", + "description": "Creates a new subscription for an existing customer", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/update": { + "post": { + "summary": "Update a subscription", + "description": "Updates an existing subscription", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/list": { + "get": { + "summary": "Get a list of subscriptions", + "description": "Get a list of subscriptions", + "tags": ["Subscription"], + "responses": { + "200": { + "description": "A list of subscriptions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/get/{subscriptionId}": { + "get": { + "summary": "Get a subscription", + "description": "Get a subscription", + "tags": ["Subscription"], + "parameters": [ + { + "in": "path", + "name": "subscriptionId", + "schema": { + "type": "string", + "description": "The subscription id", + "required": true + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/cancel": { + "post": { + "summary": "Cancel a subscription", + "description": "Cancels an existing subscription", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/resume": {}, + "post": { + "summary": "Resume a subscription", + "description": "Resumes an existing subscription", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "components": { + "schemas": { + "PriceListResponseBody": { + "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", + "type": "object", + "properties": { + "prices": { + "type": "array", + "items": { + "type": "object", + "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" + } + } + } + }, + "ProductListResponseBody": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "type": "object", + "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" + } + } + } + }, + "ProductGetResponseBody": { + "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", + "type": "object" + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + }, + "SubscriptionCreateRequestBody": { + "description": "The request body for creating a subscription", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + } + } + } + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, + "SubscriptionCreateResponseBody": { + "description": "The response body for creating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionUpdateRequestBody": { + "description": "The request body for updating a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "updateParams": { + "type": "object", + "description": "The subscription update parameters. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/update)" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, + "SubscriptionUpdateResponseBody": { + "description": "The response body for updating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionGetRequestBody": { + "description": "The request body for getting a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionGetResponseBody": { + "description": "The response body for getting a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionListRequestBody": { + "description": "The request body for listing subscriptions", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + } + } + }, + "SubscriptionListResponseBody": { + "description": "The response body for listing subscriptions", + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + } + }, + "SubscriptionCancelRequestBody": { + "description": "The request body for canceling a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionCancelResponseBody": { + "description": "The response body for canceling a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, + "SubscriptionResumeRequestBody": { + "description": "The request body for resuming a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, + "SubscriptionResumeResponseBody": { + "description": "The response body for resuming a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + }, + "CheckoutSessionCreateRequestBody": { + "description": "The request body for creating a checkout session", + "type": "object", + "properties": { + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + }, + "successURL": { + "type": "string", + "description": "The URL to redirect to after the customer sucessfully completes the checkout", + "example": "https://example.com/success" + }, + "cancelURL": { + "type": "string", + "description": "The URL to redirect to after the customer cancels the checkout", + "example": "https://example.com/cancel" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": 1234567890 + } + } + }, + "NotFoundError": { + "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Not Found Error" + } + } + } + } + } +} diff --git a/src/static/swagger-api.json b/src/static/swagger-api.json index d8d26cfe..a490ba8a 100644 --- a/src/static/swagger-api.json +++ b/src/static/swagger-api.json @@ -1,3549 +1,3333 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service API for cheqd network", - "version": "2.0.0", - "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Account" - }, - { - "name": "Key" - }, - { - "name": "DID" - }, - { - "name": "Resource" - }, - { - "name": "Credential" - }, - { - "name": "Presentation" - }, - { - "name": "Credential Status" - } - ], - "externalDocs": { - "description": "Credential Service API Documentation", - "url": "https://docs.cheqd.io/identity" - }, - "paths": { - "/account": { - "get": { - "tags": [ - "Account" - ], - "summary": "Fetch custodian-mode client details.", - "description": "This endpoint returns the custodian-mode client details for authenticated users.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Customer" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/idtoken": { - "get": { - "tags": [ - "Account" - ], - "summary": "Fetch IdToken.", - "description": "This endpoint returns IdToken as JWT with list of user roles inside", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/create": { - "post": { - "tags": [ - "Account" - ], - "summary": "Create an client for an authenticated user.", - "description": "This endpoint creates a client in the custodian-mode for an authenticated user", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/unencrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Create an unencrypted StatusList2021 credential status list.", - "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/encrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Create an encrypted StatusList2021 credential status list.", - "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/unencrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Update an existing unencrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revoke", - "suspend", - "reinstate" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/encrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Update an existing encrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revoke", - "suspend", - "reinstate" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/check": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Check a StatusList2021 index for a given Verifiable Credential.", - "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/search": { - "get": { - "tags": [ - "Credential Status" - ], - "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", - "parameters": [ - { - "in": "query", - "name": "did", - "description": "The DID of the issuer of the status list.", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - }, - { - "in": "query", - "name": "statusListName", - "description": "The name of the StatusList2021 DID-Linked Resource.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusListSearchResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/issue": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Issue a Verifiable Credential", - "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Credential" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/verify": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Verify a Verifiable Credential.", - "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "verify", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyCredentialResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/revoke": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Revoke a Verifiable Credential.", - "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", - "operationId": "revoke", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RevocationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/suspend": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Suspend a Verifiable Credential.", - "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "suspend", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/reinstate": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Reinstate a suspended Verifiable Credential.", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "operationId": "reinstate", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnsuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/create": { - "post": { - "tags": [ - "DID" - ], - "summary": "Create a DID Document.", - "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestFormBased" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestJson" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/update": { - "post": { - "tags": [ - "DID" - ], - "summary": "Update a DID Document.", - "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateResponse" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/import": { - "post": { - "tags": [ - "DID" - ], - "summary": "Import a DID Document.", - "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/deactivate/{did}": { - "post": { - "tags": [ - "DID" - ], - "summary": "Deactivate a DID Document.", - "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to deactivate.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeactivatedDidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/list": { - "get": { - "tags": [ - "DID" - ], - "summary": "Fetch DIDs associated with an account.", - "description": "This endpoint returns the list of DIDs controlled by the account.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/search/{did}": { - "get": { - "tags": [ - "DID" - ], - "summary": "Resolve a DID Document.", - "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to resolve.", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "metadata", - "description": "Return only metadata of DID Document instead of actual DID Document.", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "versionId", - "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "versionTime", - "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "transformKeys", - "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", - "schema": { - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "Ed25519VerificationKey2020", - "JsonWebKey2020" - ] - } - }, - { - "in": "query", - "name": "service", - "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", - "schema": { - "type": "string" - }, - "example": "service-1" - }, - { - "in": "query", - "name": "relativeRef", - "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", - "schema": { - "type": "string" - }, - "example": "/path/to/file" - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/key/create": { - "post": { - "tags": [ - "Key" - ], - "summary": "Create an identity key pair.", - "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/import": { - "post": { - "tags": [ - "Key" - ], - "summary": "Import an identity key pair.", - "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/read/{kid}": { - "get": { - "tags": [ - "Key" - ], - "summary": "Fetch an identity key pair.", - "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", - "parameters": [ - { - "name": "kid", - "description": "Key ID of the identity key pair to fetch.", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/presentation/create": { - "post": { - "tags": [ - "Presentation" - ], - "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", - "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/presentation/verify": { - "post": { - "tags": [ - "Presentation" - ], - "summary": "Verify a Verifiable Presentation generated from credential(s).", - "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyPresentationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/create/{did}": { - "post": { - "tags": [ - "Resource" - ], - "summary": "Create a DID-Linked Resource.", - "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to link the resource to.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/search/{did}": { - "get": { - "tags": [ - "Resource" - ], - "summary": "Get a DID-Linked Resource.", - "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "resourceId", - "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "resourceName", - "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "cheqd-issuer-logo" - }, - { - "in": "query", - "name": "resourceType", - "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "CredentialArtwork" - }, - { - "in": "query", - "name": "resourceVersion", - "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "v1" - }, - { - "in": "query", - "name": "resourceVersionTime", - "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "checksum", - "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" - }, - { - "in": "query", - "name": "resourceMetadata", - "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "any": { - "schema": { - "type": "object" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - } - }, - "components": { - "schemas": { - "AlsoKnownAs": { - "type": "object", - "properties": { - "alsoKnownAs": { - "type": "array", - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "format": "uri", - "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" - }, - "description": { - "type": "string", - "description": "Optional description of the URI." - } - } - } - } - } - }, - "CredentialRequest": { - "description": "Input fields for the creating a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "issuerDid": { - "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "subjectDid": { - "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", - "type": "string", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - }, - "attributes": { - "description": "JSON object containing the attributes to be included in the credential.", - "type": "object", - "example": { - "name": "Bob", - "gender": "male" - } - }, - "@context": { - "description": "Optional properties to be included in the `@context` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://schema.org/schema.jsonld", - "https://veramo.io/contexts/profile/v1" - ] - }, - "type": { - "description": "Optional properties to be included in the `type` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "Person" - ] - }, - "expirationDate": { - "description": "Optional expiration date according to the VC Data Model specification.", - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "format": { - "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", - "type": "string", - "enum": [ - "jwt", - "jsonld" - ], - "example": "jwt" - }, - "credentialStatus": { - "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", - "type": "object", - "required": [ - "statusPurpose", - "statusListName" - ], - "properties": { - "statusPurpose": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - }, - "statusListName": { - "type": "string" - }, - "statusListIndex": { - "type": "number" - }, - "statusListVersion": { - "type": "string", - "format": "date-time" - }, - "statusListRangeStart": { - "type": "number" - }, - "statusListRangeEnd": { - "type": "number" - }, - "indexNotIn": { - "type": "number" - } - }, - "example": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials" - } - } - }, - "required": [ - "issuerDid", - "subjectDid", - "attributes" - ], - "example": { - "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "attributes": { - "gender": "male", - "name": "Bob" - }, - "@context": [ - "https://schema.org" - ], - "type": [ - "Person" - ], - "format": "jwt", - "credentialStatus": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials", - "statusListIndex": 10 - } - } - }, - "Credential": { - "description": "Input fields for revoking/suspending a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ] - }, - "type": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "VerifiableCredential", - "Person" - ] - }, - "expirationDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "issuer": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - } - } - }, - "credentialSubject": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - } - } - }, - "credentialStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" - }, - "statusListIndex": { - "type": "number", - "example": 20 - }, - "statusPurpose": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ], - "example": "suspension" - }, - "type": { - "type": "string", - "enum": [ - "StatusList2021Entry" - ] - } - } - }, - "issuanceDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "proof": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "jwt": { - "type": "string" - } - }, - "example": { - "type": "JwtProof2020", - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ], - "credentialSubject": { - "gender": "male", - "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "name": "Bob" - }, - "credentialStatus": { - "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", - "statusIndex": 20, - "statusPurpose": "suspension", - "type": "StatusList2021Entry" - }, - "issuanceDate": "2023-06-08T13:49:28.000Z", - "issuer": { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "proof": { - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", - "type": "JwtProof2020" - }, - "type": [ - "VerifiableCredential", - "Person" - ] - } - }, - "CredentialRevokeRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", - "oneOf": [ - { - "type": "object" - }, - { - "type": "string" - } - ] - }, - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", - "type": "string" - } - } - }, - "RevocationResult": { - "properties": { - "revoked": { - "type": "boolean", - "example": true - } - } - }, - "SuspensionResult": { - "properties": { - "suspended": { - "type": "boolean", - "example": true - } - } - }, - "UnsuspensionResult": { - "properties": { - "unsuspended": { - "type": "boolean", - "example": true - } - } - }, - "CredentialVerifyRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", - "type": "object" - }, - "policies": { - "description": "Custom verification policies to execute when verifying credential.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "VerifyPresentationResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - } - }, - "VerifyCredentialResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - }, - "example": { - "verified": true, - "polices": {}, - "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "signer": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - } - }, - "PresentationCreateRequest": { - "type": "object", - "required": [ - "credentials" - ], - "properties": { - "credentials": { - "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", - "type": "array", - "items": { - "type": "object" - } - }, - "holderDid": { - "description": "DID of holder", - "type": "string" - }, - "verifierDid": { - "description": "DID of verifier", - "type": "string" - } - } - }, - "PresentationVerifyRequest": { - "type": "object", - "required": [ - "presentation" - ], - "properties": { - "presentation": { - "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", - "type": "object" - }, - "verifierDid": { - "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", - "type": "string" - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": false - }, - "policies": { - "description": "Custom verification policies to execute when verifying presentation.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "CredentialStatusCreateBody": { - "allOf": [ - { - "type": "object", - "required": [ - "did", - "statusListName" - ], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be created.", - "type": "string" - }, - "length": { - "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true, - "default": 140000 - }, - "encoding": { - "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", - "type": "string", - "default": "base64url", - "enum": [ - "base64url", - "base64", - "hex" - ] - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/AlsoKnownAs" - } - ] - }, - "CredentialStatusCreateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "length": 140000, - "encoding": "base64url" - } - }, - "CredentialStatusUnencryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": false - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-employee-credentials", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "1.0.0", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - } - } - }, - "CredentialStatusCreateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusEncryptedPaymentConditionsBody": { - "type": "object", - "properties": { - "feePaymentAddress": { - "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 20 - }, - "feePaymentWindow": { - "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 10 - } - } - }, - "CredentialStatusEncryptedPaymentConditionsJson": { - "type": "object", - "properties": { - "paymentConditions": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - } - } - }, - "CredentialStatusCreateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - }, - { - "type": "object", - "required": [ - "feePaymentAddress", - "feePaymentAmount", - "feePaymentWindow" - ] - } - ] - }, - "CredentialStatusCreateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - }, - { - "type": "object", - "required": [ - "paymentConditions" - ] - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "paymentConditions": [ - { - "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", - "feePaymentAmount": 20, - "feePaymentWindow": 10 - } - ] - } - }, - "CredentialStatusEncryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": true - }, - "encryptedSymmetricKey": { - "type": "string", - "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" - }, - "paymentConditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feePaymentAddress": { - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "type": "string", - "example": "20000000000ncheq" - }, - "intervalInSeconds": { - "type": "number", - "example": 600 - }, - "type": { - "type": "string", - "example": "timelockPayment" - } - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-revocation-encrypted-1", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "2023-06-26T11:45:19.349Z", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - }, - "symmetricKey": { - "type": "string", - "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - } - } - } - }, - "CredentialStatusCreateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusUpdateBody": { - "type": "object", - "required": [ - "did", - "statusListName", - "indices" - ], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", - "type": "string" - }, - "indices": { - "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - } - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - "CredentialStatusUpdateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "indices": [ - 10, - 3199, - 12109, - 130999 - ] - } - }, - "CredentialStatusUpdateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusUpdateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": [ - "symmetricKey" - ], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - }, - "CredentialStatusUpdateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": [ - "symmetricKey" - ], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "indices": [ - 10, - 3199, - 12109, - 130999 - ], - "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - }, - "CredentialStatusUpdateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusCheckRequest": { - "type": "object", - "required": [ - "did", - "statusListName", - "index" - ], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", - "type": "string" - }, - "index": { - "description": "Credential status index to be checked for revocation or suspension.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": true - } - } - }, - "CredentialStatusCheckResult": { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" - } - ] - }, - "CredentialStatusCheckRevocationResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "revoked": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusCheckSuspensionResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "suspended": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusListSearchResult": { - "allOf": [ - { - "type": "object", - "properties": { - "found": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - } - ] - }, - "KeyImportRequest": { - "type": "object", - "properties": { - "alias": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "Ed25519", - "Secp256k1" - ] - }, - "privateKeyHex": { - "type": "string" - } - } - }, - "KeyResult": { - "type": "object", - "properties": { - "kid": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "Ed25519", - "Secp256k1" - ] - }, - "publicKeyHex": { - "type": "string" - } - } - }, - "DidDocument": { - "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "verificationMethod": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/ns/did/v1" - ], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - ], - "verificationMethod": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "type": "Ed25519VerificationKey2018", - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" - } - ], - "authentication": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" - ], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": [ - "https://example.com" - ] - } - ] - } - }, - "DidDocumentWithoutVerificationMethod": { - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/ns/did/v1" - ], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - ], - "authentication": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" - ], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": [ - "https://example.com" - ] - } - ] - } - }, - "DidCreateRequestFormBased": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": [ - "testnet", - "mainnet" - ] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": [ - "uuid", - "base58btc" - ] - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "JsonWebKey2020", - "Ed25519VerificationKey2020" - ] - }, - "service": { - "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", - "type": "array", - "items": { - "type": "object", - "properties": { - "idFragment": { - "type": "string" - }, - "type": { - "type": "string" - }, - "serviceEndpoint": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "example": [ - { - "idFragment": "service-1", - "type": "LinkedDomains", - "serviceEndpoint": [ - "https://example.com" - ] - } - ] - }, - "key": { - "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", - "type": "string" - }, - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://www.w3.org/ns/did/v1" - ] - } - } - }, - "DidCreateRequestJson": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": [ - "testnet", - "mainnet" - ] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": [ - "uuid", - "base58btc" - ] - }, - "assertionMethod": { - "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", - "type": "boolean", - "default": true - }, - "options": { - "type": "object", - "properties": { - "key": { - "type": "string", - "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "JsonWebKey2020", - "Ed25519VerificationKey2020" - ] - } - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" - } - } - }, - "DidImportRequest": { - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "DID to be imported", - "format": "uri", - "required": true - }, - "keys": { - "type": "array", - "description": "List of keys required to import the DID", - "required": true, - "items": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "PresentationCreateResult": { - "type": "object", - "properties": { - "vp": { - "type": "object", - "description": "Verifiable Presentation which could be provided to the verifier." - }, - "nbf": { - "type": "integer", - "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." - }, - "iss": { - "type": "string", - "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" - }, - "aud": { - "type": "array", - "items": { - "type": "string" - }, - "description": "DID of the verifier of the Verifiable Presentation." - } - }, - "example": { - "vp": { - "@context": [ - "https://www.w3.org/2018/credentials/v1" - ], - "type": [ - "VerifiablePresentation" - ], - "verifiableCredential": [ - "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" - ] - }, - "nbf": 1700744275, - "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", - "aud": [ - "did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22" - ] - } - }, - "DidResult": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string" - }, - "keys": { - "type": "array", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - } - } - }, - "DidUpdateResponse": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string", - "description": "The default key id of which is the key associated with the first verificationMethod" - }, - "keys": { - "type": "array", - "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "controllerKeyRefs": { - "type": "array", - "description": "The list of keyRefs which were used for signing the transaction", - "items": { - "type": "string" - } - }, - "controllerKeys": { - "type": "array", - "description": "The list of all possible keys, inlcuding all controller's keys", - "items": { - "type": "string" - } - } - } - }, - "VerificationMethod": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - }, - "controller": { - "type": "string" - }, - "publicKeyMultibase": { - "type": "string" - }, - "publicKeyJwk": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - }, - "Service": { - "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", - "type": "object", - "properties": { - "id": { - "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", - "type": "string", - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" - }, - "type": { - "description": "Service type as defined in DID Specification Registries.", - "type": "string", - "example": "LinkedDomains" - }, - "serviceEndpoint": { - "description": "Service endpoint as defined in DID Core Specification.", - "type": "array", - "items": { - "type": "string", - "example": "https://example.com" - } - } - } - }, - "DidUpdateRequest": { - "type": "object", - "properties": { - "did": { - "description": "DID identifier to be updated.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "service": { - "type": "array", - "description": "Service section of the DID Document.", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "verificationMethod": { - "type": "array", - "description": "Verification Method section of the DID Document.", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "authentication": { - "description": "Authentication section of the DID Document.", - "type": "array", - "items": { - "type": "string" - } - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - } - } - }, - "DidDeactivateRequest": { - "type": "object", - "properties": { - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "CreateResourceRequest": { - "description": "Input fields for DID-Linked Resource creation.", - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "type", - "data", - "encoding" - ], - "properties": { - "data": { - "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", - "type": "string" - }, - "encoding": { - "description": "Encoding format used to encode the data.", - "type": "string", - "enum": [ - "base64url", - "base64", - "hex" - ] - }, - "name": { - "description": "Name of DID-Linked Resource.", - "type": "string" - }, - "type": { - "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", - "type": "string" - }, - "alsoKnownAs": { - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "type": "array", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "description": { - "type": "string" - } - } - } - }, - "version": { - "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", - "type": "string" - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "data": "SGVsbG8gV29ybGQ=", - "encoding": "base64url", - "name": "ResourceName", - "type": "TextDocument" - } - }, - "ResourceList": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "contentMetadata": { - "type": "object" - }, - "contentStream": { - "type": "object" - }, - "dereferencingMetadata": { - "$ref": "#/components/schemas/DereferencingMetadata" - } - } - }, - "DereferencingMetadata": { - "type": "object", - "properties": { - "contentType": { - "type": "string", - "example": "application/did+ld+json" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - } - } - }, - "DidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DidDocumentMetadata" - } - } - }, - "DeactivatedDidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" - } - } - }, - "DidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": false - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "DeactivatedDidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": true - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "ResourceMetadata": { - "type": "object", - "properties": { - "resourceURI": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceCollectionId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "resourceId": { - "type": "string", - "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceName": { - "type": "string", - "example": "cheqd-issuer-logo" - }, - "resourceType": { - "type": "string", - "example": "CredentialArtwork" - }, - "mediaType": { - "type": "string", - "example": "image/png" - }, - "resourceVersion": { - "type": "string", - "example": "1.0" - }, - "checksum": { - "type": "string", - "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" - }, - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "nextVersionId": { - "type": "string", - "example": "d4829ac7-4566-478c-a408-b44767eddadc" - }, - "previousVersionId": { - "type": "string", - "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" - } - } - }, - "DidResolutionMetadata": { - "type": "object", - "properties": { - "contentType": { - "allOf": [ - { - "$ref": "#/components/schemas/ContentType" - } - ], - "example": "application/did+ld+json" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - } - } - }, - "ContentType": { - "type": "string", - "enum": [ - "application/did+json", - "application/did+ld+json", - "application/ld+json", - "application/json" - ] - }, - "DidProperties": { - "type": "object", - "properties": { - "didString": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "method": { - "type": "string", - "example": "cheqd" - }, - "methodSpecificId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - } - } - }, - "Customer": { - "type": "object", - "properties": { - "customerId": { - "type": "string", - "example": "6w5drpiiwhhs" - }, - "address": { - "type": "string", - "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" - } - } - }, - "AccountCreateRequest": { - "type": "object", - "properties": { - "user": { - "type": "object", - "properties": { - "primaryEmail": { - "type": "string" - } - } - } - } - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - } - } - } -} \ No newline at end of file + "openapi": "3.0.0", + "info": { + "title": "Credential Service API for cheqd network", + "version": "2.0.0", + "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Account" + }, + { + "name": "Key" + }, + { + "name": "DID" + }, + { + "name": "Resource" + }, + { + "name": "Credential" + }, + { + "name": "Presentation" + }, + { + "name": "Credential Status" + } + ], + "externalDocs": { + "description": "Credential Service API Documentation", + "url": "https://docs.cheqd.io/identity" + }, + "paths": { + "/account": { + "get": { + "tags": ["Account"], + "summary": "Fetch custodian-mode client details.", + "description": "This endpoint returns the custodian-mode client details for authenticated users.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/idtoken": { + "get": { + "tags": ["Account"], + "summary": "Fetch IdToken.", + "description": "This endpoint returns IdToken as JWT with list of user roles inside", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/create": { + "post": { + "tags": ["Account"], + "summary": "Create an client for an authenticated user.", + "description": "This endpoint creates a client in the custodian-mode for an authenticated user", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/unencrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Create an unencrypted StatusList2021 credential status list.", + "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/encrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Create an encrypted StatusList2021 credential status list.", + "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/unencrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Update an existing unencrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": ["revoke", "suspend", "reinstate"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/encrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Update an existing encrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": ["revoke", "suspend", "reinstate"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/check": { + "post": { + "tags": ["Credential Status"], + "summary": "Check a StatusList2021 index for a given Verifiable Credential.", + "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "required": true, + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/search": { + "get": { + "tags": ["Credential Status"], + "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", + "parameters": [ + { + "in": "query", + "name": "did", + "description": "The DID of the issuer of the status list.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + }, + { + "in": "query", + "name": "statusListName", + "description": "The name of the StatusList2021 DID-Linked Resource.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusListSearchResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/issue": { + "post": { + "tags": ["Credential"], + "summary": "Issue a Verifiable Credential", + "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Credential" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/verify": { + "post": { + "tags": ["Credential"], + "summary": "Verify a Verifiable Credential.", + "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "verify", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCredentialResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/revoke": { + "post": { + "tags": ["Credential"], + "summary": "Revoke a Verifiable Credential.", + "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", + "operationId": "revoke", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RevocationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/suspend": { + "post": { + "tags": ["Credential"], + "summary": "Suspend a Verifiable Credential.", + "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "suspend", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/reinstate": { + "post": { + "tags": ["Credential"], + "summary": "Reinstate a suspended Verifiable Credential.", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "operationId": "reinstate", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnsuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/create": { + "post": { + "tags": ["DID"], + "summary": "Create a DID Document.", + "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestFormBased" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestJson" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/update": { + "post": { + "tags": ["DID"], + "summary": "Update a DID Document.", + "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateResponse" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/import": { + "post": { + "tags": ["DID"], + "summary": "Import a DID Document.", + "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/deactivate/{did}": { + "post": { + "tags": ["DID"], + "summary": "Deactivate a DID Document.", + "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to deactivate.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeactivatedDidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/list": { + "get": { + "tags": ["DID"], + "summary": "Fetch DIDs associated with an account.", + "description": "This endpoint returns the list of DIDs controlled by the account.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/search/{did}": { + "get": { + "tags": ["DID"], + "summary": "Resolve a DID Document.", + "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to resolve.", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "metadata", + "description": "Return only metadata of DID Document instead of actual DID Document.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "versionId", + "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "versionTime", + "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "transformKeys", + "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", + "schema": { + "type": "string", + "enum": ["Ed25519VerificationKey2018", "Ed25519VerificationKey2020", "JsonWebKey2020"] + } + }, + { + "in": "query", + "name": "service", + "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", + "schema": { + "type": "string" + }, + "example": "service-1" + }, + { + "in": "query", + "name": "relativeRef", + "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", + "schema": { + "type": "string" + }, + "example": "/path/to/file" + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/key/create": { + "post": { + "tags": ["Key"], + "summary": "Create an identity key pair.", + "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/import": { + "post": { + "tags": ["Key"], + "summary": "Import an identity key pair.", + "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/read/{kid}": { + "get": { + "tags": ["Key"], + "summary": "Fetch an identity key pair.", + "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", + "parameters": [ + { + "name": "kid", + "description": "Key ID of the identity key pair to fetch.", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/presentation/create": { + "post": { + "tags": ["Presentation"], + "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", + "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/presentation/verify": { + "post": { + "tags": ["Presentation"], + "summary": "Verify a Verifiable Presentation generated from credential(s).", + "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPresentationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/create/{did}": { + "post": { + "tags": ["Resource"], + "summary": "Create a DID-Linked Resource.", + "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to link the resource to.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/search/{did}": { + "get": { + "tags": ["Resource"], + "summary": "Get a DID-Linked Resource.", + "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "resourceId", + "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "resourceName", + "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "cheqd-issuer-logo" + }, + { + "in": "query", + "name": "resourceType", + "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "CredentialArtwork" + }, + { + "in": "query", + "name": "resourceVersion", + "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "v1" + }, + { + "in": "query", + "name": "resourceVersionTime", + "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "checksum", + "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" + }, + { + "in": "query", + "name": "resourceMetadata", + "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "any": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + } + }, + "components": { + "schemas": { + "AlsoKnownAs": { + "type": "object", + "properties": { + "alsoKnownAs": { + "type": "array", + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "uri", + "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" + }, + "description": { + "type": "string", + "description": "Optional description of the URI." + } + } + } + } + } + }, + "CredentialRequest": { + "description": "Input fields for the creating a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "issuerDid": { + "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "subjectDid": { + "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", + "type": "string", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + }, + "attributes": { + "description": "JSON object containing the attributes to be included in the credential.", + "type": "object", + "example": { + "name": "Bob", + "gender": "male" + } + }, + "@context": { + "description": "Optional properties to be included in the `@context` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": ["https://schema.org/schema.jsonld", "https://veramo.io/contexts/profile/v1"] + }, + "type": { + "description": "Optional properties to be included in the `type` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": ["Person"] + }, + "expirationDate": { + "description": "Optional expiration date according to the VC Data Model specification.", + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "format": { + "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", + "type": "string", + "enum": ["jwt", "jsonld"], + "example": "jwt" + }, + "credentialStatus": { + "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", + "type": "object", + "required": ["statusPurpose", "statusListName"], + "properties": { + "statusPurpose": { + "type": "string", + "enum": ["revocation", "suspension"] + }, + "statusListName": { + "type": "string" + }, + "statusListIndex": { + "type": "number" + }, + "statusListVersion": { + "type": "string", + "format": "date-time" + }, + "statusListRangeStart": { + "type": "number" + }, + "statusListRangeEnd": { + "type": "number" + }, + "indexNotIn": { + "type": "number" + } + }, + "example": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials" + } + } + }, + "required": ["issuerDid", "subjectDid", "attributes"], + "example": { + "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "attributes": { + "gender": "male", + "name": "Bob" + }, + "@context": ["https://schema.org"], + "type": ["Person"], + "format": "jwt", + "credentialStatus": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials", + "statusListIndex": 10 + } + } + }, + "Credential": { + "description": "Input fields for revoking/suspending a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ] + }, + "type": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["VerifiableCredential", "Person"] + }, + "expirationDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "issuer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + } + } + }, + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + } + } + }, + "credentialStatus": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" + }, + "statusListIndex": { + "type": "number", + "example": 20 + }, + "statusPurpose": { + "type": "string", + "enum": ["revocation", "suspension"], + "example": "suspension" + }, + "type": { + "type": "string", + "enum": ["StatusList2021Entry"] + } + } + }, + "issuanceDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "proof": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "jwt": { + "type": "string" + } + }, + "example": { + "type": "JwtProof2020", + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ], + "credentialSubject": { + "gender": "male", + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "name": "Bob" + }, + "credentialStatus": { + "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", + "statusIndex": 20, + "statusPurpose": "suspension", + "type": "StatusList2021Entry" + }, + "issuanceDate": "2023-06-08T13:49:28.000Z", + "issuer": { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "proof": { + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", + "type": "JwtProof2020" + }, + "type": ["VerifiableCredential", "Person"] + } + }, + "CredentialRevokeRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", + "oneOf": [ + { + "type": "object" + }, + { + "type": "string" + } + ] + }, + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", + "type": "string" + } + } + }, + "RevocationResult": { + "properties": { + "revoked": { + "type": "boolean", + "example": true + } + } + }, + "SuspensionResult": { + "properties": { + "suspended": { + "type": "boolean", + "example": true + } + } + }, + "UnsuspensionResult": { + "properties": { + "unsuspended": { + "type": "boolean", + "example": true + } + } + }, + "CredentialVerifyRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", + "type": "object" + }, + "policies": { + "description": "Custom verification policies to execute when verifying credential.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "VerifyPresentationResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + } + }, + "VerifyCredentialResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + }, + "example": { + "verified": true, + "polices": {}, + "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "signer": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + } + }, + "PresentationCreateRequest": { + "type": "object", + "required": ["credentials"], + "properties": { + "credentials": { + "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", + "type": "array", + "items": { + "type": "object" + } + }, + "holderDid": { + "description": "DID of holder", + "type": "string" + }, + "verifierDid": { + "description": "DID of verifier", + "type": "string" + } + } + }, + "PresentationVerifyRequest": { + "type": "object", + "required": ["presentation"], + "properties": { + "presentation": { + "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", + "type": "object" + }, + "verifierDid": { + "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", + "type": "string" + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": false + }, + "policies": { + "description": "Custom verification policies to execute when verifying presentation.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "CredentialStatusCreateBody": { + "allOf": [ + { + "type": "object", + "required": ["did", "statusListName"], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be created.", + "type": "string" + }, + "length": { + "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "default": 140000 + }, + "encoding": { + "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", + "type": "string", + "default": "base64url", + "enum": ["base64url", "base64", "hex"] + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/AlsoKnownAs" + } + ] + }, + "CredentialStatusCreateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "length": 140000, + "encoding": "base64url" + } + }, + "CredentialStatusUnencryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": false + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-employee-credentials", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "1.0.0", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + } + } + }, + "CredentialStatusCreateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusEncryptedPaymentConditionsBody": { + "type": "object", + "properties": { + "feePaymentAddress": { + "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 20 + }, + "feePaymentWindow": { + "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 10 + } + } + }, + "CredentialStatusEncryptedPaymentConditionsJson": { + "type": "object", + "properties": { + "paymentConditions": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + } + } + }, + "CredentialStatusCreateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + }, + { + "type": "object", + "required": ["feePaymentAddress", "feePaymentAmount", "feePaymentWindow"] + } + ] + }, + "CredentialStatusCreateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + }, + { + "type": "object", + "required": ["paymentConditions"] + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "paymentConditions": [ + { + "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", + "feePaymentAmount": 20, + "feePaymentWindow": 10 + } + ] + } + }, + "CredentialStatusEncryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": true + }, + "encryptedSymmetricKey": { + "type": "string", + "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" + }, + "paymentConditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feePaymentAddress": { + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "type": "string", + "example": "20000000000ncheq" + }, + "intervalInSeconds": { + "type": "number", + "example": 600 + }, + "type": { + "type": "string", + "example": "timelockPayment" + } + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-revocation-encrypted-1", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "2023-06-26T11:45:19.349Z", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + }, + "symmetricKey": { + "type": "string", + "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + } + } + } + }, + "CredentialStatusCreateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusUpdateBody": { + "type": "object", + "required": ["did", "statusListName", "indices"], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", + "type": "string" + }, + "indices": { + "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + } + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + "CredentialStatusUpdateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "indices": [10, 3199, 12109, 130999] + } + }, + "CredentialStatusUpdateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusUpdateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": ["symmetricKey"], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + }, + "CredentialStatusUpdateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": ["symmetricKey"], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "indices": [10, 3199, 12109, 130999], + "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + }, + "CredentialStatusUpdateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusCheckRequest": { + "type": "object", + "required": ["did", "statusListName", "index"], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", + "type": "string" + }, + "index": { + "description": "Credential status index to be checked for revocation or suspension.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": true + } + } + }, + "CredentialStatusCheckResult": { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" + } + ] + }, + "CredentialStatusCheckRevocationResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "revoked": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusCheckSuspensionResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusListSearchResult": { + "allOf": [ + { + "type": "object", + "properties": { + "found": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + } + ] + }, + "KeyImportRequest": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Ed25519", "Secp256k1"] + }, + "privateKeyHex": { + "type": "string" + } + } + }, + "KeyResult": { + "type": "object", + "properties": { + "kid": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Ed25519", "Secp256k1"] + }, + "publicKeyHex": { + "type": "string" + } + } + }, + "DidDocument": { + "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationMethod": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": ["https://www.w3.org/ns/did/v1"], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], + "verificationMethod": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" + } + ], + "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": ["https://example.com"] + } + ] + } + }, + "DidDocumentWithoutVerificationMethod": { + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": ["https://www.w3.org/ns/did/v1"], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], + "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": ["https://example.com"] + } + ] + } + }, + "DidCreateRequestFormBased": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": ["testnet", "mainnet"] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": ["uuid", "base58btc"] + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] + }, + "service": { + "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", + "type": "array", + "items": { + "type": "object", + "properties": { + "idFragment": { + "type": "string" + }, + "type": { + "type": "string" + }, + "serviceEndpoint": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "example": [ + { + "idFragment": "service-1", + "type": "LinkedDomains", + "serviceEndpoint": ["https://example.com"] + } + ] + }, + "key": { + "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["https://www.w3.org/ns/did/v1"] + } + } + }, + "DidCreateRequestJson": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": ["testnet", "mainnet"] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": ["uuid", "base58btc"] + }, + "assertionMethod": { + "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] + } + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" + } + } + }, + "DidImportRequest": { + "type": "object", + "properties": { + "did": { + "type": "string", + "description": "DID to be imported", + "format": "uri", + "required": true + }, + "keys": { + "type": "array", + "description": "List of keys required to import the DID", + "required": true, + "items": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "PresentationCreateResult": { + "type": "object", + "properties": { + "vp": { + "type": "object", + "description": "Verifiable Presentation which could be provided to the verifier." + }, + "nbf": { + "type": "integer", + "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." + }, + "iss": { + "type": "string", + "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" + }, + "aud": { + "type": "array", + "items": { + "type": "string" + }, + "description": "DID of the verifier of the Verifiable Presentation." + } + }, + "example": { + "vp": { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [ + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" + ] + }, + "nbf": 1700744275, + "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", + "aud": ["did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22"] + } + }, + "DidResult": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string" + }, + "keys": { + "type": "array", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + } + } + }, + "DidUpdateResponse": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string", + "description": "The default key id of which is the key associated with the first verificationMethod" + }, + "keys": { + "type": "array", + "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "controllerKeyRefs": { + "type": "array", + "description": "The list of keyRefs which were used for signing the transaction", + "items": { + "type": "string" + } + }, + "controllerKeys": { + "type": "array", + "description": "The list of all possible keys, inlcuding all controller's keys", + "items": { + "type": "string" + } + } + } + }, + "VerificationMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "controller": { + "type": "string" + }, + "publicKeyMultibase": { + "type": "string" + }, + "publicKeyJwk": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + }, + "Service": { + "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", + "type": "object", + "properties": { + "id": { + "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", + "type": "string", + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" + }, + "type": { + "description": "Service type as defined in DID Specification Registries.", + "type": "string", + "example": "LinkedDomains" + }, + "serviceEndpoint": { + "description": "Service endpoint as defined in DID Core Specification.", + "type": "array", + "items": { + "type": "string", + "example": "https://example.com" + } + } + } + }, + "DidUpdateRequest": { + "type": "object", + "properties": { + "did": { + "description": "DID identifier to be updated.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "service": { + "type": "array", + "description": "Service section of the DID Document.", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "verificationMethod": { + "type": "array", + "description": "Verification Method section of the DID Document.", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "authentication": { + "description": "Authentication section of the DID Document.", + "type": "array", + "items": { + "type": "string" + } + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + } + } + }, + "DidDeactivateRequest": { + "type": "object", + "properties": { + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CreateResourceRequest": { + "description": "Input fields for DID-Linked Resource creation.", + "type": "object", + "additionalProperties": false, + "required": ["name", "type", "data", "encoding"], + "properties": { + "data": { + "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", + "type": "string" + }, + "encoding": { + "description": "Encoding format used to encode the data.", + "type": "string", + "enum": ["base64url", "base64", "hex"] + }, + "name": { + "description": "Name of DID-Linked Resource.", + "type": "string" + }, + "type": { + "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", + "type": "string" + }, + "alsoKnownAs": { + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "type": "array", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "version": { + "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", + "type": "string" + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "data": "SGVsbG8gV29ybGQ=", + "encoding": "base64url", + "name": "ResourceName", + "type": "TextDocument" + } + }, + "ResourceList": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "contentMetadata": { + "type": "object" + }, + "contentStream": { + "type": "object" + }, + "dereferencingMetadata": { + "$ref": "#/components/schemas/DereferencingMetadata" + } + } + }, + "DereferencingMetadata": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "example": "application/did+ld+json" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + } + } + }, + "DidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DidDocumentMetadata" + } + } + }, + "DeactivatedDidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" + } + } + }, + "DidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": false + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "DeactivatedDidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": true + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "ResourceMetadata": { + "type": "object", + "properties": { + "resourceURI": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceCollectionId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "resourceId": { + "type": "string", + "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceName": { + "type": "string", + "example": "cheqd-issuer-logo" + }, + "resourceType": { + "type": "string", + "example": "CredentialArtwork" + }, + "mediaType": { + "type": "string", + "example": "image/png" + }, + "resourceVersion": { + "type": "string", + "example": "1.0" + }, + "checksum": { + "type": "string", + "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" + }, + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "nextVersionId": { + "type": "string", + "example": "d4829ac7-4566-478c-a408-b44767eddadc" + }, + "previousVersionId": { + "type": "string", + "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" + } + } + }, + "DidResolutionMetadata": { + "type": "object", + "properties": { + "contentType": { + "allOf": [ + { + "$ref": "#/components/schemas/ContentType" + } + ], + "example": "application/did+ld+json" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + } + } + }, + "ContentType": { + "type": "string", + "enum": ["application/did+json", "application/did+ld+json", "application/ld+json", "application/json"] + }, + "DidProperties": { + "type": "object", + "properties": { + "didString": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "method": { + "type": "string", + "example": "cheqd" + }, + "methodSpecificId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + } + } + }, + "Customer": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "example": "6w5drpiiwhhs" + }, + "address": { + "type": "string", + "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" + } + } + }, + "AccountCreateRequest": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "primaryEmail": { + "type": "string" + } + } + } + } + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + } + } + } +} diff --git a/src/types/portal.ts b/src/types/portal.ts index 1553fb48..866e73dd 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -1,86 +1,84 @@ -import type Stripe from "stripe"; -import type { UnsuccessfulResponseBody } from "./shared.js"; +import type Stripe from 'stripe'; +import type { UnsuccessfulResponseBody } from './shared.js'; export type ProductWithPrices = Stripe.Product & { - prices?: Stripe.Price[]; + prices?: Stripe.Price[]; }; export type ProductListUnsuccessfulResponseBody = UnsuccessfulResponseBody; export type ProductGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; - export type ProductListResponseBody = { - products: Stripe.ApiList; -} + products: Stripe.ApiList; +}; export type ProductGetResponseBody = { - product: ProductWithPrices; -} + product: ProductWithPrices; +}; // Prices // List export type PriceListResponseBody = { - prices: Stripe.ApiList; -} + prices: Stripe.ApiList; +}; export type PriceListUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Subscription // Create export type SubscriptionCreateRequestBody = { - items: [{ price: string }]; - idempotencyKey?: string; -} + items: [{ price: string }]; + idempotencyKey?: string; +}; export type SubscriptionCreateResponseBody = { - subscription: Stripe.Response; -} + subscription: Stripe.Response; +}; // Update export type SubscriptionUpdateRequestBody = { - subscriptionId: string; - updateParams: Stripe.SubscriptionUpdateParams; - idempotencyKey?: string; - -} + subscriptionId: string; + updateParams: Stripe.SubscriptionUpdateParams; + idempotencyKey?: string; +}; export type SubscriptionUpdateResponseBody = { - subscription: Stripe.Response; -} + subscription: Stripe.Response; +}; // Get export type SubscriptionGetRequestBody = { - subscriptionId: string; -} + subscriptionId: string; +}; export type SubscriptionGetResponseBody = { - subscription: Stripe.Response; -} + subscription: Stripe.Response; +}; // List export type SubscriptionListResponseBody = { - subscriptions: Stripe.Response>; -} + subscriptions: Stripe.Response>; +}; // Delete export type SubscriptionCancelRequestBody = { - subscriptionId: string; -} + subscriptionId: string; +}; export type SubscriptionCancelResponseBody = { - subscription: Stripe.Subscription; - idempotencyKey?: string; -} + subscription: Stripe.Subscription; + idempotencyKey?: string; +}; //Resume export type SubscriptionResumeRequestBody = { - subscriptionId: string; - idempotencyKey?: string; -} + subscriptionId: string; + idempotencyKey?: string; +}; export type SubscriptionResumeResponseBody = { - subscription: Stripe.Response; -} + subscription: Stripe.Response; +}; export type SubscriptionCreateUnsuccessfulResponseBody = UnsuccessfulResponseBody; export type SubscriptionListUnsuccessfulResponseBody = UnsuccessfulResponseBody; @@ -94,20 +92,19 @@ export type SubscriptionResumeUnsuccessfulResponseBody = UnsuccessfulResponseBod export type PortalCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; - // Checkout Session export type CheckoutSessionCreateRequestBody = { - price: string; - successURL: string; - cancelURL: string; - quantity?: number; - idempotencyKey?: string; -} + price: string; + successURL: string; + cancelURL: string; + quantity?: number; + idempotencyKey?: string; +}; export type CheckoutSessionCreateResponseBody = { - clientSecret: Stripe.Checkout.Session['client_secret']; -} + clientSecret: Stripe.Checkout.Session['client_secret']; +}; export type CheckoutSessionCreateUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Utils -export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; \ No newline at end of file +export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index da03eaae..987064c2 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -64,8 +64,8 @@ * idempotencyKey: * type: string * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. - * example: 1234567890 - * + * example: abcdefghijklmnopqrstuvwxyz + * * SubscriptionCreateResponseBody: * description: The response body for creating a subscription * type: object @@ -80,14 +80,14 @@ * subscriptionId: * type: string * description: The subscription id - * example: sub_1234567890 + * example: sub_1234567890 * updateParams: * type: object * description: The subscription update parameters. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/update) * idempotencyKey: * type: string * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. - * example: 1234567890 + * example: abcdefghijklmnopqrstuvwxyz * SubscriptionUpdateResponseBody: * description: The response body for updating a subscription * type: object @@ -125,7 +125,7 @@ * subscriptions: * type: array * items: - * type: object + * type: object * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] * SubscriptionCancelRequestBody: * description: The request body for canceling a subscription @@ -145,7 +145,7 @@ * idempotencyKey: * type: string * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. - * example: 1234567890 + * example: abcdefghijklmnopqrstuvwxyz * SubscriptionResumeRequestBody: * description: The request body for resuming a subscription * type: object @@ -157,7 +157,7 @@ * idempotencyKey: * type: string * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. - * example: 1234567890 + * example: abcdefghijklmnopqrstuvwxyz * SubscriptionResumeResponseBody: * description: The response body for resuming a subscription * type: object @@ -184,7 +184,7 @@ * idempotencyKey: * type: string * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. - * example: 1234567890 + * example: abcdefghijklmnopqrstuvwxyz * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. * type: object From d987e5466c58a8dab58aef8c5bcf04e574296cb9 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 10 Mar 2024 23:08:59 +0100 Subject: [PATCH 06/49] swagger changes --- src/static/swagger-admin.json | 1404 +++---- src/static/swagger-api.json | 6880 +++++++++++++++++---------------- 2 files changed, 4260 insertions(+), 4024 deletions(-) diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index d898b33c..25d1c408 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -1,693 +1,713 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service admin API for cheqd network", - "version": "2.0.0", - "description": "Admin API which handles users subscriptions and payments", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Product" - }, - { - "name": "Price" - }, - { - "name": "Customer" - }, - { - "name": "Subscription" - }, - { - "name": "Checkout" - } - ], - "paths": { - "/admin/checkout/session/create": { - "post": { - "summary": "Create a checkout session", - "description": "Create a checkout session", - "tags": ["Checkout"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckoutSessionCreateRequestBody" - } - } - } - }, - "responses": { - "303": { - "description": "A redirect to Stripe prebuilt checkout page" - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/price/list": { - "get": { - "summary": "Get a list of prices", - "description": "Get a list of prices", - "tags": ["Price"], - "parameters": [ - { - "in": "query", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id. If passed - returns filtered by this product list of prices.", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of prices", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PriceListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/list": { - "get": { - "summary": "Get a list of products", - "description": "Get a list of products which are on a Stripe side", - "tags": ["Product"], - "parameters": [ - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the list of products with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of products", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/get/{productId}": { - "get": { - "summary": "Get a product", - "description": "Get a product by id", - "tags": ["Product"], - "parameters": [ - { - "in": "path", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id which identifies the product in Stripe", - "required": true - } - }, - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the product with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A product", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/create": { - "post": { - "summary": "Create a subscription", - "description": "Creates a new subscription for an existing customer", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateRequestBody" - } - } - } - }, - "responses": { - "201": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/update": { - "post": { - "summary": "Update a subscription", - "description": "Updates an existing subscription", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/list": { - "get": { - "summary": "Get a list of subscriptions", - "description": "Get a list of subscriptions", - "tags": ["Subscription"], - "responses": { - "200": { - "description": "A list of subscriptions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/get/{subscriptionId}": { - "get": { - "summary": "Get a subscription", - "description": "Get a subscription", - "tags": ["Subscription"], - "parameters": [ - { - "in": "path", - "name": "subscriptionId", - "schema": { - "type": "string", - "description": "The subscription id", - "required": true - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/cancel": { - "post": { - "summary": "Cancel a subscription", - "description": "Cancels an existing subscription", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/resume": {}, - "post": { - "summary": "Resume a subscription", - "description": "Resumes an existing subscription", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "components": { - "schemas": { - "PriceListResponseBody": { - "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", - "type": "object", - "properties": { - "prices": { - "type": "array", - "items": { - "type": "object", - "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" - } - } - } - }, - "ProductListResponseBody": { - "type": "object", - "properties": { - "products": { - "type": "array", - "items": { - "type": "object", - "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" - } - } - } - }, - "ProductGetResponseBody": { - "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", - "type": "object" - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - }, - "SubscriptionCreateRequestBody": { - "description": "The request body for creating a subscription", - "type": "object", - "properties": { - "customerId": { - "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - } - } - } - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "SubscriptionCreateResponseBody": { - "description": "The response body for creating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionUpdateRequestBody": { - "description": "The request body for updating a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - }, - "updateParams": { - "type": "object", - "description": "The subscription update parameters. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/update)" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "SubscriptionUpdateResponseBody": { - "description": "The response body for updating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionGetRequestBody": { - "description": "The request body for getting a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionGetResponseBody": { - "description": "The response body for getting a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionListRequestBody": { - "description": "The request body for listing subscriptions", - "type": "object", - "properties": { - "customerId": { - "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" - } - } - }, - "SubscriptionListResponseBody": { - "description": "The response body for listing subscriptions", - "type": "object", - "properties": { - "subscriptions": { - "type": "array", - "items": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - } - }, - "SubscriptionCancelRequestBody": { - "description": "The request body for canceling a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionCancelResponseBody": { - "description": "The response body for canceling a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "SubscriptionResumeRequestBody": { - "description": "The request body for resuming a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "SubscriptionResumeResponseBody": { - "description": "The response body for resuming a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - }, - "CheckoutSessionCreateRequestBody": { - "description": "The request body for creating a checkout session", - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - }, - "successURL": { - "type": "string", - "description": "The URL to redirect to after the customer sucessfully completes the checkout", - "example": "https://example.com/success" - }, - "cancelURL": { - "type": "string", - "description": "The URL to redirect to after the customer cancels the checkout", - "example": "https://example.com/cancel" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": 1234567890 - } - } - }, - "NotFoundError": { - "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Not Found Error" - } - } - } - } - } -} + "openapi": "3.0.0", + "info": { + "title": "Credential Service admin API for cheqd network", + "version": "2.0.0", + "description": "Admin API which handles users subscriptions and payments", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Product" + }, + { + "name": "Price" + }, + { + "name": "Customer" + }, + { + "name": "Subscription" + }, + { + "name": "Checkout" + } + ], + "paths": { + "/admin/checkout/session/create": { + "post": { + "summary": "Create a checkout session", + "description": "Create a checkout session", + "tags": [ + "Checkout" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckoutSessionCreateRequestBody" + } + } + } + }, + "responses": { + "303": { + "description": "A redirect to Stripe prebuilt checkout page" + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/price/list": { + "get": { + "summary": "Get a list of prices", + "description": "Get a list of prices", + "tags": [ + "Price" + ], + "parameters": [ + { + "in": "query", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id. If passed - returns filtered by this product list of prices.", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of prices", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriceListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/list": { + "get": { + "summary": "Get a list of products", + "description": "Get a list of products which are on a Stripe side", + "tags": [ + "Product" + ], + "parameters": [ + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the list of products with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of products", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/get/{productId}": { + "get": { + "summary": "Get a product", + "description": "Get a product by id", + "tags": [ + "Product" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id which identifies the product in Stripe" + }, + "required": true + }, + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the product with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/create": { + "post": { + "summary": "Create a subscription", + "description": "Creates a new subscription for an existing customer", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/update": { + "post": { + "summary": "Update a subscription", + "description": "Updates an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/list": { + "get": { + "summary": "Get a list of subscriptions", + "description": "Get a list of subscriptions", + "tags": [ + "Subscription" + ], + "responses": { + "200": { + "description": "A list of subscriptions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/get/{subscriptionId}": { + "get": { + "summary": "Get a subscription", + "description": "Get a subscription", + "tags": [ + "Subscription" + ], + "parameters": [ + { + "in": "path", + "name": "subscriptionId", + "schema": { + "type": "string", + "description": "The subscription id" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/cancel": { + "post": { + "summary": "Cancel a subscription", + "description": "Cancels an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/resume": {}, + "post": { + "summary": "Resume a subscription", + "description": "Resumes an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "components": { + "schemas": { + "PriceListResponseBody": { + "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", + "type": "object", + "properties": { + "prices": { + "type": "array", + "items": { + "type": "object", + "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" + } + } + } + }, + "ProductListResponseBody": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "type": "object", + "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" + } + } + } + }, + "ProductGetResponseBody": { + "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", + "type": "object" + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + }, + "SubscriptionCreateRequestBody": { + "description": "The request body for creating a subscription", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + } + } + } + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionCreateResponseBody": { + "description": "The response body for creating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionUpdateRequestBody": { + "description": "The request body for updating a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "updateParams": { + "type": "object", + "description": "The subscription update parameters. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/update)" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionUpdateResponseBody": { + "description": "The response body for updating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionGetRequestBody": { + "description": "The request body for getting a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionGetResponseBody": { + "description": "The response body for getting a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionListRequestBody": { + "description": "The request body for listing subscriptions", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + } + } + }, + "SubscriptionListResponseBody": { + "description": "The response body for listing subscriptions", + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + } + }, + "SubscriptionCancelRequestBody": { + "description": "The request body for canceling a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionCancelResponseBody": { + "description": "The response body for canceling a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeRequestBody": { + "description": "The request body for resuming a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeResponseBody": { + "description": "The response body for resuming a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + }, + "CheckoutSessionCreateRequestBody": { + "description": "The request body for creating a checkout session", + "type": "object", + "properties": { + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + }, + "successURL": { + "type": "string", + "description": "The URL to redirect to after the customer sucessfully completes the checkout", + "example": "https://example.com/success" + }, + "cancelURL": { + "type": "string", + "description": "The URL to redirect to after the customer cancels the checkout", + "example": "https://example.com/cancel" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "NotFoundError": { + "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Not Found Error" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/static/swagger-api.json b/src/static/swagger-api.json index a490ba8a..d8d26cfe 100644 --- a/src/static/swagger-api.json +++ b/src/static/swagger-api.json @@ -1,3333 +1,3549 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service API for cheqd network", - "version": "2.0.0", - "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Account" - }, - { - "name": "Key" - }, - { - "name": "DID" - }, - { - "name": "Resource" - }, - { - "name": "Credential" - }, - { - "name": "Presentation" - }, - { - "name": "Credential Status" - } - ], - "externalDocs": { - "description": "Credential Service API Documentation", - "url": "https://docs.cheqd.io/identity" - }, - "paths": { - "/account": { - "get": { - "tags": ["Account"], - "summary": "Fetch custodian-mode client details.", - "description": "This endpoint returns the custodian-mode client details for authenticated users.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Customer" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/idtoken": { - "get": { - "tags": ["Account"], - "summary": "Fetch IdToken.", - "description": "This endpoint returns IdToken as JWT with list of user roles inside", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/create": { - "post": { - "tags": ["Account"], - "summary": "Create an client for an authenticated user.", - "description": "This endpoint creates a client in the custodian-mode for an authenticated user", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/unencrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Create an unencrypted StatusList2021 credential status list.", - "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/encrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Create an encrypted StatusList2021 credential status list.", - "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/unencrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Update an existing unencrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": ["revoke", "suspend", "reinstate"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/encrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Update an existing encrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": ["revoke", "suspend", "reinstate"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/check": { - "post": { - "tags": ["Credential Status"], - "summary": "Check a StatusList2021 index for a given Verifiable Credential.", - "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "required": true, - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/search": { - "get": { - "tags": ["Credential Status"], - "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", - "parameters": [ - { - "in": "query", - "name": "did", - "description": "The DID of the issuer of the status list.", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - }, - { - "in": "query", - "name": "statusListName", - "description": "The name of the StatusList2021 DID-Linked Resource.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusListSearchResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/issue": { - "post": { - "tags": ["Credential"], - "summary": "Issue a Verifiable Credential", - "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Credential" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/verify": { - "post": { - "tags": ["Credential"], - "summary": "Verify a Verifiable Credential.", - "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "verify", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyCredentialResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/revoke": { - "post": { - "tags": ["Credential"], - "summary": "Revoke a Verifiable Credential.", - "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", - "operationId": "revoke", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RevocationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/suspend": { - "post": { - "tags": ["Credential"], - "summary": "Suspend a Verifiable Credential.", - "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "suspend", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/reinstate": { - "post": { - "tags": ["Credential"], - "summary": "Reinstate a suspended Verifiable Credential.", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "operationId": "reinstate", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnsuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/create": { - "post": { - "tags": ["DID"], - "summary": "Create a DID Document.", - "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestFormBased" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestJson" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/update": { - "post": { - "tags": ["DID"], - "summary": "Update a DID Document.", - "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateResponse" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/import": { - "post": { - "tags": ["DID"], - "summary": "Import a DID Document.", - "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/deactivate/{did}": { - "post": { - "tags": ["DID"], - "summary": "Deactivate a DID Document.", - "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to deactivate.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeactivatedDidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/list": { - "get": { - "tags": ["DID"], - "summary": "Fetch DIDs associated with an account.", - "description": "This endpoint returns the list of DIDs controlled by the account.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/search/{did}": { - "get": { - "tags": ["DID"], - "summary": "Resolve a DID Document.", - "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to resolve.", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "metadata", - "description": "Return only metadata of DID Document instead of actual DID Document.", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "versionId", - "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "versionTime", - "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "transformKeys", - "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", - "schema": { - "type": "string", - "enum": ["Ed25519VerificationKey2018", "Ed25519VerificationKey2020", "JsonWebKey2020"] - } - }, - { - "in": "query", - "name": "service", - "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", - "schema": { - "type": "string" - }, - "example": "service-1" - }, - { - "in": "query", - "name": "relativeRef", - "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", - "schema": { - "type": "string" - }, - "example": "/path/to/file" - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/key/create": { - "post": { - "tags": ["Key"], - "summary": "Create an identity key pair.", - "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/import": { - "post": { - "tags": ["Key"], - "summary": "Import an identity key pair.", - "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/read/{kid}": { - "get": { - "tags": ["Key"], - "summary": "Fetch an identity key pair.", - "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", - "parameters": [ - { - "name": "kid", - "description": "Key ID of the identity key pair to fetch.", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/presentation/create": { - "post": { - "tags": ["Presentation"], - "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", - "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/presentation/verify": { - "post": { - "tags": ["Presentation"], - "summary": "Verify a Verifiable Presentation generated from credential(s).", - "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyPresentationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/create/{did}": { - "post": { - "tags": ["Resource"], - "summary": "Create a DID-Linked Resource.", - "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to link the resource to.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/search/{did}": { - "get": { - "tags": ["Resource"], - "summary": "Get a DID-Linked Resource.", - "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "resourceId", - "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "resourceName", - "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "cheqd-issuer-logo" - }, - { - "in": "query", - "name": "resourceType", - "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "CredentialArtwork" - }, - { - "in": "query", - "name": "resourceVersion", - "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "v1" - }, - { - "in": "query", - "name": "resourceVersionTime", - "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "checksum", - "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" - }, - { - "in": "query", - "name": "resourceMetadata", - "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "any": { - "schema": { - "type": "object" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - } - }, - "components": { - "schemas": { - "AlsoKnownAs": { - "type": "object", - "properties": { - "alsoKnownAs": { - "type": "array", - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "format": "uri", - "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" - }, - "description": { - "type": "string", - "description": "Optional description of the URI." - } - } - } - } - } - }, - "CredentialRequest": { - "description": "Input fields for the creating a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "issuerDid": { - "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "subjectDid": { - "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", - "type": "string", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - }, - "attributes": { - "description": "JSON object containing the attributes to be included in the credential.", - "type": "object", - "example": { - "name": "Bob", - "gender": "male" - } - }, - "@context": { - "description": "Optional properties to be included in the `@context` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": ["https://schema.org/schema.jsonld", "https://veramo.io/contexts/profile/v1"] - }, - "type": { - "description": "Optional properties to be included in the `type` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": ["Person"] - }, - "expirationDate": { - "description": "Optional expiration date according to the VC Data Model specification.", - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "format": { - "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", - "type": "string", - "enum": ["jwt", "jsonld"], - "example": "jwt" - }, - "credentialStatus": { - "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", - "type": "object", - "required": ["statusPurpose", "statusListName"], - "properties": { - "statusPurpose": { - "type": "string", - "enum": ["revocation", "suspension"] - }, - "statusListName": { - "type": "string" - }, - "statusListIndex": { - "type": "number" - }, - "statusListVersion": { - "type": "string", - "format": "date-time" - }, - "statusListRangeStart": { - "type": "number" - }, - "statusListRangeEnd": { - "type": "number" - }, - "indexNotIn": { - "type": "number" - } - }, - "example": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials" - } - } - }, - "required": ["issuerDid", "subjectDid", "attributes"], - "example": { - "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "attributes": { - "gender": "male", - "name": "Bob" - }, - "@context": ["https://schema.org"], - "type": ["Person"], - "format": "jwt", - "credentialStatus": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials", - "statusListIndex": 10 - } - } - }, - "Credential": { - "description": "Input fields for revoking/suspending a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ] - }, - "type": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["VerifiableCredential", "Person"] - }, - "expirationDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "issuer": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - } - } - }, - "credentialSubject": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - } - } - }, - "credentialStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" - }, - "statusListIndex": { - "type": "number", - "example": 20 - }, - "statusPurpose": { - "type": "string", - "enum": ["revocation", "suspension"], - "example": "suspension" - }, - "type": { - "type": "string", - "enum": ["StatusList2021Entry"] - } - } - }, - "issuanceDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "proof": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "jwt": { - "type": "string" - } - }, - "example": { - "type": "JwtProof2020", - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ], - "credentialSubject": { - "gender": "male", - "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "name": "Bob" - }, - "credentialStatus": { - "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", - "statusIndex": 20, - "statusPurpose": "suspension", - "type": "StatusList2021Entry" - }, - "issuanceDate": "2023-06-08T13:49:28.000Z", - "issuer": { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "proof": { - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", - "type": "JwtProof2020" - }, - "type": ["VerifiableCredential", "Person"] - } - }, - "CredentialRevokeRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", - "oneOf": [ - { - "type": "object" - }, - { - "type": "string" - } - ] - }, - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", - "type": "string" - } - } - }, - "RevocationResult": { - "properties": { - "revoked": { - "type": "boolean", - "example": true - } - } - }, - "SuspensionResult": { - "properties": { - "suspended": { - "type": "boolean", - "example": true - } - } - }, - "UnsuspensionResult": { - "properties": { - "unsuspended": { - "type": "boolean", - "example": true - } - } - }, - "CredentialVerifyRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", - "type": "object" - }, - "policies": { - "description": "Custom verification policies to execute when verifying credential.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "VerifyPresentationResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - } - }, - "VerifyCredentialResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - }, - "example": { - "verified": true, - "polices": {}, - "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "signer": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - } - }, - "PresentationCreateRequest": { - "type": "object", - "required": ["credentials"], - "properties": { - "credentials": { - "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", - "type": "array", - "items": { - "type": "object" - } - }, - "holderDid": { - "description": "DID of holder", - "type": "string" - }, - "verifierDid": { - "description": "DID of verifier", - "type": "string" - } - } - }, - "PresentationVerifyRequest": { - "type": "object", - "required": ["presentation"], - "properties": { - "presentation": { - "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", - "type": "object" - }, - "verifierDid": { - "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", - "type": "string" - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": false - }, - "policies": { - "description": "Custom verification policies to execute when verifying presentation.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "CredentialStatusCreateBody": { - "allOf": [ - { - "type": "object", - "required": ["did", "statusListName"], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be created.", - "type": "string" - }, - "length": { - "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true, - "default": 140000 - }, - "encoding": { - "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", - "type": "string", - "default": "base64url", - "enum": ["base64url", "base64", "hex"] - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/AlsoKnownAs" - } - ] - }, - "CredentialStatusCreateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "length": 140000, - "encoding": "base64url" - } - }, - "CredentialStatusUnencryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": false - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-employee-credentials", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "1.0.0", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - } - } - }, - "CredentialStatusCreateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusEncryptedPaymentConditionsBody": { - "type": "object", - "properties": { - "feePaymentAddress": { - "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 20 - }, - "feePaymentWindow": { - "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 10 - } - } - }, - "CredentialStatusEncryptedPaymentConditionsJson": { - "type": "object", - "properties": { - "paymentConditions": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - } - } - }, - "CredentialStatusCreateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - }, - { - "type": "object", - "required": ["feePaymentAddress", "feePaymentAmount", "feePaymentWindow"] - } - ] - }, - "CredentialStatusCreateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - }, - { - "type": "object", - "required": ["paymentConditions"] - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "paymentConditions": [ - { - "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", - "feePaymentAmount": 20, - "feePaymentWindow": 10 - } - ] - } - }, - "CredentialStatusEncryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": true - }, - "encryptedSymmetricKey": { - "type": "string", - "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" - }, - "paymentConditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feePaymentAddress": { - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "type": "string", - "example": "20000000000ncheq" - }, - "intervalInSeconds": { - "type": "number", - "example": 600 - }, - "type": { - "type": "string", - "example": "timelockPayment" - } - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-revocation-encrypted-1", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "2023-06-26T11:45:19.349Z", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - }, - "symmetricKey": { - "type": "string", - "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - } - } - } - }, - "CredentialStatusCreateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusUpdateBody": { - "type": "object", - "required": ["did", "statusListName", "indices"], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", - "type": "string" - }, - "indices": { - "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - } - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - "CredentialStatusUpdateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "indices": [10, 3199, 12109, 130999] - } - }, - "CredentialStatusUpdateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusUpdateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": ["symmetricKey"], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - }, - "CredentialStatusUpdateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": ["symmetricKey"], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "indices": [10, 3199, 12109, 130999], - "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - }, - "CredentialStatusUpdateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusCheckRequest": { - "type": "object", - "required": ["did", "statusListName", "index"], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", - "type": "string" - }, - "index": { - "description": "Credential status index to be checked for revocation or suspension.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": true - } - } - }, - "CredentialStatusCheckResult": { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" - } - ] - }, - "CredentialStatusCheckRevocationResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "revoked": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusCheckSuspensionResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "suspended": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusListSearchResult": { - "allOf": [ - { - "type": "object", - "properties": { - "found": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - } - ] - }, - "KeyImportRequest": { - "type": "object", - "properties": { - "alias": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["Ed25519", "Secp256k1"] - }, - "privateKeyHex": { - "type": "string" - } - } - }, - "KeyResult": { - "type": "object", - "properties": { - "kid": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["Ed25519", "Secp256k1"] - }, - "publicKeyHex": { - "type": "string" - } - } - }, - "DidDocument": { - "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "verificationMethod": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": ["https://www.w3.org/ns/did/v1"], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], - "verificationMethod": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "type": "Ed25519VerificationKey2018", - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" - } - ], - "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": ["https://example.com"] - } - ] - } - }, - "DidDocumentWithoutVerificationMethod": { - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": ["https://www.w3.org/ns/did/v1"], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], - "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": ["https://example.com"] - } - ] - } - }, - "DidCreateRequestFormBased": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": ["testnet", "mainnet"] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": ["uuid", "base58btc"] - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] - }, - "service": { - "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", - "type": "array", - "items": { - "type": "object", - "properties": { - "idFragment": { - "type": "string" - }, - "type": { - "type": "string" - }, - "serviceEndpoint": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "example": [ - { - "idFragment": "service-1", - "type": "LinkedDomains", - "serviceEndpoint": ["https://example.com"] - } - ] - }, - "key": { - "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", - "type": "string" - }, - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["https://www.w3.org/ns/did/v1"] - } - } - }, - "DidCreateRequestJson": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": ["testnet", "mainnet"] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": ["uuid", "base58btc"] - }, - "assertionMethod": { - "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", - "type": "boolean", - "default": true - }, - "options": { - "type": "object", - "properties": { - "key": { - "type": "string", - "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] - } - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" - } - } - }, - "DidImportRequest": { - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "DID to be imported", - "format": "uri", - "required": true - }, - "keys": { - "type": "array", - "description": "List of keys required to import the DID", - "required": true, - "items": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "PresentationCreateResult": { - "type": "object", - "properties": { - "vp": { - "type": "object", - "description": "Verifiable Presentation which could be provided to the verifier." - }, - "nbf": { - "type": "integer", - "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." - }, - "iss": { - "type": "string", - "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" - }, - "aud": { - "type": "array", - "items": { - "type": "string" - }, - "description": "DID of the verifier of the Verifiable Presentation." - } - }, - "example": { - "vp": { - "@context": ["https://www.w3.org/2018/credentials/v1"], - "type": ["VerifiablePresentation"], - "verifiableCredential": [ - "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" - ] - }, - "nbf": 1700744275, - "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", - "aud": ["did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22"] - } - }, - "DidResult": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string" - }, - "keys": { - "type": "array", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - } - } - }, - "DidUpdateResponse": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string", - "description": "The default key id of which is the key associated with the first verificationMethod" - }, - "keys": { - "type": "array", - "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "controllerKeyRefs": { - "type": "array", - "description": "The list of keyRefs which were used for signing the transaction", - "items": { - "type": "string" - } - }, - "controllerKeys": { - "type": "array", - "description": "The list of all possible keys, inlcuding all controller's keys", - "items": { - "type": "string" - } - } - } - }, - "VerificationMethod": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - }, - "controller": { - "type": "string" - }, - "publicKeyMultibase": { - "type": "string" - }, - "publicKeyJwk": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - }, - "Service": { - "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", - "type": "object", - "properties": { - "id": { - "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", - "type": "string", - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" - }, - "type": { - "description": "Service type as defined in DID Specification Registries.", - "type": "string", - "example": "LinkedDomains" - }, - "serviceEndpoint": { - "description": "Service endpoint as defined in DID Core Specification.", - "type": "array", - "items": { - "type": "string", - "example": "https://example.com" - } - } - } - }, - "DidUpdateRequest": { - "type": "object", - "properties": { - "did": { - "description": "DID identifier to be updated.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "service": { - "type": "array", - "description": "Service section of the DID Document.", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "verificationMethod": { - "type": "array", - "description": "Verification Method section of the DID Document.", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "authentication": { - "description": "Authentication section of the DID Document.", - "type": "array", - "items": { - "type": "string" - } - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - } - } - }, - "DidDeactivateRequest": { - "type": "object", - "properties": { - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "CreateResourceRequest": { - "description": "Input fields for DID-Linked Resource creation.", - "type": "object", - "additionalProperties": false, - "required": ["name", "type", "data", "encoding"], - "properties": { - "data": { - "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", - "type": "string" - }, - "encoding": { - "description": "Encoding format used to encode the data.", - "type": "string", - "enum": ["base64url", "base64", "hex"] - }, - "name": { - "description": "Name of DID-Linked Resource.", - "type": "string" - }, - "type": { - "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", - "type": "string" - }, - "alsoKnownAs": { - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "type": "array", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "description": { - "type": "string" - } - } - } - }, - "version": { - "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", - "type": "string" - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "data": "SGVsbG8gV29ybGQ=", - "encoding": "base64url", - "name": "ResourceName", - "type": "TextDocument" - } - }, - "ResourceList": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "contentMetadata": { - "type": "object" - }, - "contentStream": { - "type": "object" - }, - "dereferencingMetadata": { - "$ref": "#/components/schemas/DereferencingMetadata" - } - } - }, - "DereferencingMetadata": { - "type": "object", - "properties": { - "contentType": { - "type": "string", - "example": "application/did+ld+json" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - } - } - }, - "DidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DidDocumentMetadata" - } - } - }, - "DeactivatedDidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" - } - } - }, - "DidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": false - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "DeactivatedDidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": true - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "ResourceMetadata": { - "type": "object", - "properties": { - "resourceURI": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceCollectionId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "resourceId": { - "type": "string", - "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceName": { - "type": "string", - "example": "cheqd-issuer-logo" - }, - "resourceType": { - "type": "string", - "example": "CredentialArtwork" - }, - "mediaType": { - "type": "string", - "example": "image/png" - }, - "resourceVersion": { - "type": "string", - "example": "1.0" - }, - "checksum": { - "type": "string", - "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" - }, - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "nextVersionId": { - "type": "string", - "example": "d4829ac7-4566-478c-a408-b44767eddadc" - }, - "previousVersionId": { - "type": "string", - "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" - } - } - }, - "DidResolutionMetadata": { - "type": "object", - "properties": { - "contentType": { - "allOf": [ - { - "$ref": "#/components/schemas/ContentType" - } - ], - "example": "application/did+ld+json" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - } - } - }, - "ContentType": { - "type": "string", - "enum": ["application/did+json", "application/did+ld+json", "application/ld+json", "application/json"] - }, - "DidProperties": { - "type": "object", - "properties": { - "didString": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "method": { - "type": "string", - "example": "cheqd" - }, - "methodSpecificId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - } - } - }, - "Customer": { - "type": "object", - "properties": { - "customerId": { - "type": "string", - "example": "6w5drpiiwhhs" - }, - "address": { - "type": "string", - "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" - } - } - }, - "AccountCreateRequest": { - "type": "object", - "properties": { - "user": { - "type": "object", - "properties": { - "primaryEmail": { - "type": "string" - } - } - } - } - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - } - } - } -} + "openapi": "3.0.0", + "info": { + "title": "Credential Service API for cheqd network", + "version": "2.0.0", + "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Account" + }, + { + "name": "Key" + }, + { + "name": "DID" + }, + { + "name": "Resource" + }, + { + "name": "Credential" + }, + { + "name": "Presentation" + }, + { + "name": "Credential Status" + } + ], + "externalDocs": { + "description": "Credential Service API Documentation", + "url": "https://docs.cheqd.io/identity" + }, + "paths": { + "/account": { + "get": { + "tags": [ + "Account" + ], + "summary": "Fetch custodian-mode client details.", + "description": "This endpoint returns the custodian-mode client details for authenticated users.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/idtoken": { + "get": { + "tags": [ + "Account" + ], + "summary": "Fetch IdToken.", + "description": "This endpoint returns IdToken as JWT with list of user roles inside", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/create": { + "post": { + "tags": [ + "Account" + ], + "summary": "Create an client for an authenticated user.", + "description": "This endpoint creates a client in the custodian-mode for an authenticated user", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/unencrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Create an unencrypted StatusList2021 credential status list.", + "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/encrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Create an encrypted StatusList2021 credential status list.", + "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/unencrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Update an existing unencrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revoke", + "suspend", + "reinstate" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/encrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Update an existing encrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revoke", + "suspend", + "reinstate" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/check": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Check a StatusList2021 index for a given Verifiable Credential.", + "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/search": { + "get": { + "tags": [ + "Credential Status" + ], + "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", + "parameters": [ + { + "in": "query", + "name": "did", + "description": "The DID of the issuer of the status list.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + }, + { + "in": "query", + "name": "statusListName", + "description": "The name of the StatusList2021 DID-Linked Resource.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusListSearchResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/issue": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Issue a Verifiable Credential", + "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Credential" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/verify": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Verify a Verifiable Credential.", + "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "verify", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCredentialResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/revoke": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Revoke a Verifiable Credential.", + "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", + "operationId": "revoke", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RevocationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/suspend": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Suspend a Verifiable Credential.", + "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "suspend", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/reinstate": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Reinstate a suspended Verifiable Credential.", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "operationId": "reinstate", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnsuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/create": { + "post": { + "tags": [ + "DID" + ], + "summary": "Create a DID Document.", + "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestFormBased" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestJson" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/update": { + "post": { + "tags": [ + "DID" + ], + "summary": "Update a DID Document.", + "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateResponse" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/import": { + "post": { + "tags": [ + "DID" + ], + "summary": "Import a DID Document.", + "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/deactivate/{did}": { + "post": { + "tags": [ + "DID" + ], + "summary": "Deactivate a DID Document.", + "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to deactivate.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeactivatedDidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/list": { + "get": { + "tags": [ + "DID" + ], + "summary": "Fetch DIDs associated with an account.", + "description": "This endpoint returns the list of DIDs controlled by the account.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/search/{did}": { + "get": { + "tags": [ + "DID" + ], + "summary": "Resolve a DID Document.", + "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to resolve.", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "metadata", + "description": "Return only metadata of DID Document instead of actual DID Document.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "versionId", + "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "versionTime", + "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "transformKeys", + "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", + "schema": { + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "Ed25519VerificationKey2020", + "JsonWebKey2020" + ] + } + }, + { + "in": "query", + "name": "service", + "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", + "schema": { + "type": "string" + }, + "example": "service-1" + }, + { + "in": "query", + "name": "relativeRef", + "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", + "schema": { + "type": "string" + }, + "example": "/path/to/file" + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/key/create": { + "post": { + "tags": [ + "Key" + ], + "summary": "Create an identity key pair.", + "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/import": { + "post": { + "tags": [ + "Key" + ], + "summary": "Import an identity key pair.", + "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/read/{kid}": { + "get": { + "tags": [ + "Key" + ], + "summary": "Fetch an identity key pair.", + "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", + "parameters": [ + { + "name": "kid", + "description": "Key ID of the identity key pair to fetch.", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/presentation/create": { + "post": { + "tags": [ + "Presentation" + ], + "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", + "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/presentation/verify": { + "post": { + "tags": [ + "Presentation" + ], + "summary": "Verify a Verifiable Presentation generated from credential(s).", + "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPresentationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/create/{did}": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Create a DID-Linked Resource.", + "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to link the resource to.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/search/{did}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get a DID-Linked Resource.", + "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "resourceId", + "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "resourceName", + "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "cheqd-issuer-logo" + }, + { + "in": "query", + "name": "resourceType", + "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "CredentialArtwork" + }, + { + "in": "query", + "name": "resourceVersion", + "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "v1" + }, + { + "in": "query", + "name": "resourceVersionTime", + "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "checksum", + "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" + }, + { + "in": "query", + "name": "resourceMetadata", + "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "any": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + } + }, + "components": { + "schemas": { + "AlsoKnownAs": { + "type": "object", + "properties": { + "alsoKnownAs": { + "type": "array", + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "uri", + "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" + }, + "description": { + "type": "string", + "description": "Optional description of the URI." + } + } + } + } + } + }, + "CredentialRequest": { + "description": "Input fields for the creating a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "issuerDid": { + "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "subjectDid": { + "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", + "type": "string", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + }, + "attributes": { + "description": "JSON object containing the attributes to be included in the credential.", + "type": "object", + "example": { + "name": "Bob", + "gender": "male" + } + }, + "@context": { + "description": "Optional properties to be included in the `@context` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://schema.org/schema.jsonld", + "https://veramo.io/contexts/profile/v1" + ] + }, + "type": { + "description": "Optional properties to be included in the `type` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "Person" + ] + }, + "expirationDate": { + "description": "Optional expiration date according to the VC Data Model specification.", + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "format": { + "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", + "type": "string", + "enum": [ + "jwt", + "jsonld" + ], + "example": "jwt" + }, + "credentialStatus": { + "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", + "type": "object", + "required": [ + "statusPurpose", + "statusListName" + ], + "properties": { + "statusPurpose": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + }, + "statusListName": { + "type": "string" + }, + "statusListIndex": { + "type": "number" + }, + "statusListVersion": { + "type": "string", + "format": "date-time" + }, + "statusListRangeStart": { + "type": "number" + }, + "statusListRangeEnd": { + "type": "number" + }, + "indexNotIn": { + "type": "number" + } + }, + "example": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials" + } + } + }, + "required": [ + "issuerDid", + "subjectDid", + "attributes" + ], + "example": { + "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "attributes": { + "gender": "male", + "name": "Bob" + }, + "@context": [ + "https://schema.org" + ], + "type": [ + "Person" + ], + "format": "jwt", + "credentialStatus": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials", + "statusListIndex": 10 + } + } + }, + "Credential": { + "description": "Input fields for revoking/suspending a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ] + }, + "type": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "VerifiableCredential", + "Person" + ] + }, + "expirationDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "issuer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + } + } + }, + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + } + } + }, + "credentialStatus": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" + }, + "statusListIndex": { + "type": "number", + "example": 20 + }, + "statusPurpose": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ], + "example": "suspension" + }, + "type": { + "type": "string", + "enum": [ + "StatusList2021Entry" + ] + } + } + }, + "issuanceDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "proof": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "jwt": { + "type": "string" + } + }, + "example": { + "type": "JwtProof2020", + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ], + "credentialSubject": { + "gender": "male", + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "name": "Bob" + }, + "credentialStatus": { + "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", + "statusIndex": 20, + "statusPurpose": "suspension", + "type": "StatusList2021Entry" + }, + "issuanceDate": "2023-06-08T13:49:28.000Z", + "issuer": { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "proof": { + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", + "type": "JwtProof2020" + }, + "type": [ + "VerifiableCredential", + "Person" + ] + } + }, + "CredentialRevokeRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", + "oneOf": [ + { + "type": "object" + }, + { + "type": "string" + } + ] + }, + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", + "type": "string" + } + } + }, + "RevocationResult": { + "properties": { + "revoked": { + "type": "boolean", + "example": true + } + } + }, + "SuspensionResult": { + "properties": { + "suspended": { + "type": "boolean", + "example": true + } + } + }, + "UnsuspensionResult": { + "properties": { + "unsuspended": { + "type": "boolean", + "example": true + } + } + }, + "CredentialVerifyRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", + "type": "object" + }, + "policies": { + "description": "Custom verification policies to execute when verifying credential.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "VerifyPresentationResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + } + }, + "VerifyCredentialResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + }, + "example": { + "verified": true, + "polices": {}, + "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "signer": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + } + }, + "PresentationCreateRequest": { + "type": "object", + "required": [ + "credentials" + ], + "properties": { + "credentials": { + "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", + "type": "array", + "items": { + "type": "object" + } + }, + "holderDid": { + "description": "DID of holder", + "type": "string" + }, + "verifierDid": { + "description": "DID of verifier", + "type": "string" + } + } + }, + "PresentationVerifyRequest": { + "type": "object", + "required": [ + "presentation" + ], + "properties": { + "presentation": { + "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", + "type": "object" + }, + "verifierDid": { + "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", + "type": "string" + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": false + }, + "policies": { + "description": "Custom verification policies to execute when verifying presentation.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "CredentialStatusCreateBody": { + "allOf": [ + { + "type": "object", + "required": [ + "did", + "statusListName" + ], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be created.", + "type": "string" + }, + "length": { + "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "default": 140000 + }, + "encoding": { + "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", + "type": "string", + "default": "base64url", + "enum": [ + "base64url", + "base64", + "hex" + ] + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/AlsoKnownAs" + } + ] + }, + "CredentialStatusCreateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "length": 140000, + "encoding": "base64url" + } + }, + "CredentialStatusUnencryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": false + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-employee-credentials", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "1.0.0", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + } + } + }, + "CredentialStatusCreateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusEncryptedPaymentConditionsBody": { + "type": "object", + "properties": { + "feePaymentAddress": { + "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 20 + }, + "feePaymentWindow": { + "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 10 + } + } + }, + "CredentialStatusEncryptedPaymentConditionsJson": { + "type": "object", + "properties": { + "paymentConditions": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + } + } + }, + "CredentialStatusCreateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + }, + { + "type": "object", + "required": [ + "feePaymentAddress", + "feePaymentAmount", + "feePaymentWindow" + ] + } + ] + }, + "CredentialStatusCreateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + }, + { + "type": "object", + "required": [ + "paymentConditions" + ] + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "paymentConditions": [ + { + "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", + "feePaymentAmount": 20, + "feePaymentWindow": 10 + } + ] + } + }, + "CredentialStatusEncryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": true + }, + "encryptedSymmetricKey": { + "type": "string", + "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" + }, + "paymentConditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feePaymentAddress": { + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "type": "string", + "example": "20000000000ncheq" + }, + "intervalInSeconds": { + "type": "number", + "example": 600 + }, + "type": { + "type": "string", + "example": "timelockPayment" + } + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-revocation-encrypted-1", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "2023-06-26T11:45:19.349Z", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + }, + "symmetricKey": { + "type": "string", + "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + } + } + } + }, + "CredentialStatusCreateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusUpdateBody": { + "type": "object", + "required": [ + "did", + "statusListName", + "indices" + ], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", + "type": "string" + }, + "indices": { + "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + } + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + "CredentialStatusUpdateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "indices": [ + 10, + 3199, + 12109, + 130999 + ] + } + }, + "CredentialStatusUpdateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusUpdateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": [ + "symmetricKey" + ], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + }, + "CredentialStatusUpdateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": [ + "symmetricKey" + ], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "indices": [ + 10, + 3199, + 12109, + 130999 + ], + "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + }, + "CredentialStatusUpdateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusCheckRequest": { + "type": "object", + "required": [ + "did", + "statusListName", + "index" + ], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", + "type": "string" + }, + "index": { + "description": "Credential status index to be checked for revocation or suspension.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": true + } + } + }, + "CredentialStatusCheckResult": { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" + } + ] + }, + "CredentialStatusCheckRevocationResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "revoked": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusCheckSuspensionResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusListSearchResult": { + "allOf": [ + { + "type": "object", + "properties": { + "found": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + } + ] + }, + "KeyImportRequest": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Ed25519", + "Secp256k1" + ] + }, + "privateKeyHex": { + "type": "string" + } + } + }, + "KeyResult": { + "type": "object", + "properties": { + "kid": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Ed25519", + "Secp256k1" + ] + }, + "publicKeyHex": { + "type": "string" + } + } + }, + "DidDocument": { + "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationMethod": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + ], + "verificationMethod": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" + } + ], + "authentication": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" + ], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + } + }, + "DidDocumentWithoutVerificationMethod": { + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + ], + "authentication": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" + ], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + } + }, + "DidCreateRequestFormBased": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": [ + "testnet", + "mainnet" + ] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": [ + "uuid", + "base58btc" + ] + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "JsonWebKey2020", + "Ed25519VerificationKey2020" + ] + }, + "service": { + "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", + "type": "array", + "items": { + "type": "object", + "properties": { + "idFragment": { + "type": "string" + }, + "type": { + "type": "string" + }, + "serviceEndpoint": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "example": [ + { + "idFragment": "service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + }, + "key": { + "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/ns/did/v1" + ] + } + } + }, + "DidCreateRequestJson": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": [ + "testnet", + "mainnet" + ] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": [ + "uuid", + "base58btc" + ] + }, + "assertionMethod": { + "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "JsonWebKey2020", + "Ed25519VerificationKey2020" + ] + } + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" + } + } + }, + "DidImportRequest": { + "type": "object", + "properties": { + "did": { + "type": "string", + "description": "DID to be imported", + "format": "uri", + "required": true + }, + "keys": { + "type": "array", + "description": "List of keys required to import the DID", + "required": true, + "items": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "PresentationCreateResult": { + "type": "object", + "properties": { + "vp": { + "type": "object", + "description": "Verifiable Presentation which could be provided to the verifier." + }, + "nbf": { + "type": "integer", + "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." + }, + "iss": { + "type": "string", + "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" + }, + "aud": { + "type": "array", + "items": { + "type": "string" + }, + "description": "DID of the verifier of the Verifiable Presentation." + } + }, + "example": { + "vp": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" + ] + }, + "nbf": 1700744275, + "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", + "aud": [ + "did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22" + ] + } + }, + "DidResult": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string" + }, + "keys": { + "type": "array", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + } + } + }, + "DidUpdateResponse": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string", + "description": "The default key id of which is the key associated with the first verificationMethod" + }, + "keys": { + "type": "array", + "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "controllerKeyRefs": { + "type": "array", + "description": "The list of keyRefs which were used for signing the transaction", + "items": { + "type": "string" + } + }, + "controllerKeys": { + "type": "array", + "description": "The list of all possible keys, inlcuding all controller's keys", + "items": { + "type": "string" + } + } + } + }, + "VerificationMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "controller": { + "type": "string" + }, + "publicKeyMultibase": { + "type": "string" + }, + "publicKeyJwk": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + }, + "Service": { + "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", + "type": "object", + "properties": { + "id": { + "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", + "type": "string", + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" + }, + "type": { + "description": "Service type as defined in DID Specification Registries.", + "type": "string", + "example": "LinkedDomains" + }, + "serviceEndpoint": { + "description": "Service endpoint as defined in DID Core Specification.", + "type": "array", + "items": { + "type": "string", + "example": "https://example.com" + } + } + } + }, + "DidUpdateRequest": { + "type": "object", + "properties": { + "did": { + "description": "DID identifier to be updated.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "service": { + "type": "array", + "description": "Service section of the DID Document.", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "verificationMethod": { + "type": "array", + "description": "Verification Method section of the DID Document.", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "authentication": { + "description": "Authentication section of the DID Document.", + "type": "array", + "items": { + "type": "string" + } + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + } + } + }, + "DidDeactivateRequest": { + "type": "object", + "properties": { + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CreateResourceRequest": { + "description": "Input fields for DID-Linked Resource creation.", + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "type", + "data", + "encoding" + ], + "properties": { + "data": { + "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", + "type": "string" + }, + "encoding": { + "description": "Encoding format used to encode the data.", + "type": "string", + "enum": [ + "base64url", + "base64", + "hex" + ] + }, + "name": { + "description": "Name of DID-Linked Resource.", + "type": "string" + }, + "type": { + "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", + "type": "string" + }, + "alsoKnownAs": { + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "type": "array", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "version": { + "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", + "type": "string" + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "data": "SGVsbG8gV29ybGQ=", + "encoding": "base64url", + "name": "ResourceName", + "type": "TextDocument" + } + }, + "ResourceList": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "contentMetadata": { + "type": "object" + }, + "contentStream": { + "type": "object" + }, + "dereferencingMetadata": { + "$ref": "#/components/schemas/DereferencingMetadata" + } + } + }, + "DereferencingMetadata": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "example": "application/did+ld+json" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + } + } + }, + "DidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DidDocumentMetadata" + } + } + }, + "DeactivatedDidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" + } + } + }, + "DidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": false + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "DeactivatedDidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": true + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "ResourceMetadata": { + "type": "object", + "properties": { + "resourceURI": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceCollectionId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "resourceId": { + "type": "string", + "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceName": { + "type": "string", + "example": "cheqd-issuer-logo" + }, + "resourceType": { + "type": "string", + "example": "CredentialArtwork" + }, + "mediaType": { + "type": "string", + "example": "image/png" + }, + "resourceVersion": { + "type": "string", + "example": "1.0" + }, + "checksum": { + "type": "string", + "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" + }, + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "nextVersionId": { + "type": "string", + "example": "d4829ac7-4566-478c-a408-b44767eddadc" + }, + "previousVersionId": { + "type": "string", + "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" + } + } + }, + "DidResolutionMetadata": { + "type": "object", + "properties": { + "contentType": { + "allOf": [ + { + "$ref": "#/components/schemas/ContentType" + } + ], + "example": "application/did+ld+json" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + } + } + }, + "ContentType": { + "type": "string", + "enum": [ + "application/did+json", + "application/did+ld+json", + "application/ld+json", + "application/json" + ] + }, + "DidProperties": { + "type": "object", + "properties": { + "didString": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "method": { + "type": "string", + "example": "cheqd" + }, + "methodSpecificId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + } + } + }, + "Customer": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "example": "6w5drpiiwhhs" + }, + "address": { + "type": "string", + "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" + } + } + }, + "AccountCreateRequest": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "primaryEmail": { + "type": "string" + } + } + } + } + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + } + } + } +} \ No newline at end of file From a4c28dc96106c329dc20608d2aa68d0a662f35b1 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Mon, 18 Mar 2024 18:10:47 +0100 Subject: [PATCH 07/49] Get rid of /admin/checkout and move it to /admin/subscriptions --- src/app.ts | 11 +- src/controllers/admin/checkout-session.ts | 117 ---------------- src/controllers/admin/subscriptions.ts | 127 +++++++++--------- .../auth/routes/admin/admin-auth.ts | 11 +- src/types/portal.ts | 34 ++--- src/types/swagger-admin-types.ts | 56 +++----- 6 files changed, 103 insertions(+), 253 deletions(-) delete mode 100644 src/controllers/admin/checkout-session.ts diff --git a/src/app.ts b/src/app.ts index c94dc710..7e9d41bf 100644 --- a/src/app.ts +++ b/src/app.ts @@ -31,7 +31,6 @@ import { ProductController } from './controllers/admin/product.js'; import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; import { WebhookController } from './controllers/admin/webhook.js'; -import { CheckoutSessionController } from './controllers/admin/checkout-session.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -245,8 +244,7 @@ class App { new SubscriptionController().update ); app.get( - '/admin/subscription/get/:subscriptionId', - SubscriptionController.subscriptionGetValidator, + '/admin/subscription/get', new SubscriptionController().get ); app.get( @@ -265,13 +263,6 @@ class App { new SubscriptionController().resume ); - // Checkout session - app.post( - '/admin/checkout/session/create', - CheckoutSessionController.checkoutSessionCreateValidator, - new CheckoutSessionController().create - ); - // Webhook app.post('/admin/webhook', new WebhookController().handleWebhook); diff --git a/src/controllers/admin/checkout-session.ts b/src/controllers/admin/checkout-session.ts deleted file mode 100644 index 151e7bfb..00000000 --- a/src/controllers/admin/checkout-session.ts +++ /dev/null @@ -1,117 +0,0 @@ -import Stripe from 'stripe'; -import type { Request, Response } from 'express'; -import * as dotenv from 'dotenv'; -import type { - CheckoutSessionCreateRequestBody, - CheckoutSessionCreateUnsuccessfulResponseBody, -} from '../../types/portal.js'; -import { check, validationResult } from '../validator/index.js'; -import { StatusCodes } from 'http-status-codes'; - -dotenv.config(); - -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - -export class CheckoutSessionController { - static checkoutSessionCreateValidator = [ - check('price') - .exists() - .withMessage('price is required') - .bail() - .isString() - .withMessage('price should be a string') - .bail(), - check('successURL') - .exists() - .withMessage('successURL is required') - .bail() - .isString() - .withMessage('successURL should be a string') - .bail(), - check('cancelURL') - .exists() - .withMessage('cancelURL is required') - .bail() - .isString() - .withMessage('cancelURL should be a string') - .bail(), - check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), - ]; - - /** - * @openapi - * - * /admin/checkout/session/create: - * post: - * summary: Create a checkout session - * description: Create a checkout session - * tags: [Checkout] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CheckoutSessionCreateRequestBody' - * responses: - * 303: - * description: A redirect to Stripe prebuilt checkout page - * 400: - * $ref: '#/components/schemas/InvalidRequest' - * 401: - * $ref: '#/components/schemas/UnauthorizedError' - * 500: - * $ref: '#/components/schemas/InternalError' - */ - - public async create(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); - } - - const { price, successURL, cancelURL, quantity, idempotencyKey } = - request.body satisfies CheckoutSessionCreateRequestBody; - try { - const session = await stripe.checkout.sessions.create( - { - mode: 'subscription', - customer: response.locals.customer.stripeCustomerId, - line_items: [ - { - price: price, - quantity: quantity || 1, - }, - ], - success_url: successURL, - cancel_url: cancelURL, - }, - { - idempotencyKey, - } - ); - - if (session.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: 'Checkout session was not created', - } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); - } - - if (!session.url) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: 'Checkout session URL was not provided', - } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); - } - - return response.json({ - url: session.url as string, - }); - } catch (error) { - return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - error: `Internal error: ${(error as Error)?.message || error}`, - } satisfies CheckoutSessionCreateUnsuccessfulResponseBody); - } - } -} diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index fa8e11b7..9b8293dd 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -17,8 +17,7 @@ import type { SubscriptionResumeUnsuccessfulResponseBody, SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, - SubscriptionCancelRequestBody, - PaymentBehavior, + SubscriptionCancelRequestBody } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; import { validationResult } from '../validator/index.js'; @@ -31,38 +30,14 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class SubscriptionController { static subscriptionCreateValidator = [ - check('customerId').exists().withMessage('customerId was not provided').bail(), - check('items') - .exists() - .withMessage('items was not provided') - .bail() - .isArray() - .withMessage('items should be an array') - .bail(), - check('items.*.price') - .exists() - .withMessage('price was not provided') - .bail() - .isString() - .withMessage('price should be a string') - .bail(), - check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), + check('price').exists().withMessage('price was not provided').bail().isString().withMessage('price should be a string').bail(), + check('successURL').exists().withMessage('successURL was not provided').bail().isString().withMessage('successURL should be a string').bail(), + check('cancelURL').exists().withMessage('cancelURL was not provided').bail().isString().withMessage('cancelURL should be a string').bail(), + check('quantity').optional().isInt().withMessage('quantity should be an integer').bail(), ]; static subscriptionUpdateValidator = [ - check('subscriptionId').exists().withMessage('subscriptionId was not provided').bail(), - check('updateParams').exists().withMessage('updateParams was not provided').bail(), - check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), - ]; - - static subscriptionGetValidator = [ - check('subscriptionId') - .exists() - .withMessage('subscriptionId was not provided') - .bail() - .isString() - .withMessage('subscriptionId should be a string') - .bail(), + check('returnUrl').exists().withMessage('returnUrl was not provided').bail().isString().withMessage('returnUrl should be a string').bail(), ]; static subscriptionListValidator = [ @@ -121,33 +96,48 @@ export class SubscriptionController { async create(request: Request, response: Response) { // Validate request const result = validationResult(request); + // handle error if (!result.isEmpty()) { return response.status(StatusCodes.BAD_REQUEST).json({ error: result.array().pop()?.msg, } satisfies SubscriptionCreateUnsuccessfulResponseBody); } - const { items, idempotencyKey } = request.body satisfies SubscriptionCreateRequestBody; + const { price, successURL, cancelURL, quantity, idempotencyKey } = + request.body satisfies SubscriptionCreateRequestBody; try { - // Create the subscription - const subscription = await stripe.subscriptions.create( + const session = await stripe.checkout.sessions.create( { + mode: 'subscription', customer: response.locals.customer.stripeCustomerId, - items: items, - payment_behavior: 'default_incomplete' as PaymentBehavior, + line_items: [ + { + price: price, + quantity: quantity || 1, + }, + ], + success_url: successURL, + cancel_url: cancelURL, }, { - idempotencyKey: idempotencyKey, + idempotencyKey, } ); - if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + + if (session.lastResponse?.statusCode !== StatusCodes.OK) { return response.status(StatusCodes.BAD_GATEWAY).json({ - error: `Subscription was not created`, + error: 'Checkout session was not created', } satisfies SubscriptionCreateUnsuccessfulResponseBody); } - return response.status(StatusCodes.CREATED).json({ - subscription: subscription, + if (!session.url) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Checkout session URL was not provided', + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + + return response.json({ + clientSecret: session.url as string, } satisfies SubscriptionCreateResponseBody); } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ @@ -165,10 +155,10 @@ export class SubscriptionController { * description: Updates an existing subscription * tags: [Subscription] * requestBody: - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionUpdateRequestBody' + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionUpdateRequestBody' * responses: * 200: * description: The request was successful. @@ -192,20 +182,35 @@ export class SubscriptionController { } satisfies SubscriptionUpdateUnsuccessfulResponseBody); } - const { subscriptionId, updateParams, idempotencyKey } = request.body satisfies SubscriptionUpdateRequestBody; + const { returnUrl } = request.body satisfies SubscriptionUpdateRequestBody; + try { - // Update the subscription - const subscription = await stripe.subscriptions.update(subscriptionId, updateParams, { - idempotencyKey: idempotencyKey, + // Sync with Stripe + await SubscriptionService.instance.stripeSync(response.locals.customer); + + // Get the subscription object from the DB + const subscription = await SubscriptionService.instance.findOne({customer: response.locals.customer}); + if (!subscription) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscription was not found`, + } satisfies SubscriptionUpdateUnsuccessfulResponseBody); + } + + // Create portal link + const session = await stripe.billingPortal.sessions.create({ + customer: response.locals.customer.stripeCustomerId, + return_url: returnUrl, }); - if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { + + if (session.lastResponse?.statusCode !== StatusCodes.OK) { return response.status(StatusCodes.BAD_GATEWAY).json({ - error: `Subscription was not updated`, + error: 'Billing portal session for upgrading the subscription was not created', } satisfies SubscriptionUpdateUnsuccessfulResponseBody); } return response.status(StatusCodes.OK).json({ - subscription: subscription, + clientSecret: session.url, } satisfies SubscriptionUpdateResponseBody); + } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: `Internal error: ${(error as Error)?.message || error}`, @@ -280,13 +285,6 @@ export class SubscriptionController { * summary: Get a subscription * description: Get a subscription * tags: [Subscription] - * parameters: - * - in: path - * name: subscriptionId - * schema: - * type: string - * description: The subscription id - * required: true * responses: * 200: * description: The request was successful. @@ -311,12 +309,17 @@ export class SubscriptionController { error: result.array().pop()?.msg, } satisfies SubscriptionGetUnsuccessfulResponseBody); } - const subscriptionId = request.params.subscriptionId; try { // Sync our DB with Stripe await SubscriptionService.instance.stripeSync(response.locals.customer); - // Get the subscription - const subscription = await stripe.subscriptions.retrieve(subscriptionId as string); + // Get the subscriptionId from the request + const _sub = await SubscriptionService.instance.findOne({customer: response.locals.customer}); + if (!_sub) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Subscription was not found`, + } satisfies SubscriptionGetUnsuccessfulResponseBody); + } + const subscription = await stripe.subscriptions.retrieve(_sub.subscriptionId as string); if (subscription.lastResponse?.statusCode !== StatusCodes.OK) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Subscription was not found`, diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index 6d7afcc9..1b719c6d 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -5,18 +5,17 @@ import type { IAuthResponse } from '../../../../types/authentication.js'; export class AdminHandler extends BaseAuthHandler { constructor() { super(); - // ToDo: define how to get namespace information - this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:testnet', { + // Subscriptions + this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { skipNamespace: true, }); - this.registerRoute('/admin/checkout/session/create', 'POST', 'admin:checkout:session:create:mainnet', { + this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:mainnet', { skipNamespace: true, }); - // Subscriptions - this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { + this.registerRoute('/admin/subscription/update', 'POST', 'admin:subscription:update:testnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:mainnet', { + this.registerRoute('/admin/subscription/update', 'POST', 'admin:subscription:update:mainnet', { skipNamespace: true, }); this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:testnet', { skipNamespace: true }); diff --git a/src/types/portal.ts b/src/types/portal.ts index 866e73dd..01fd8ea1 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -26,25 +26,26 @@ export type PriceListUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Subscription // Create export type SubscriptionCreateRequestBody = { - items: [{ price: string }]; + price: string; + successURL: string; + cancelURL: string; + quantity?: number; idempotencyKey?: string; }; export type SubscriptionCreateResponseBody = { - subscription: Stripe.Response; -}; - -// Update -export type SubscriptionUpdateRequestBody = { - subscriptionId: string; - updateParams: Stripe.SubscriptionUpdateParams; - idempotencyKey?: string; + clientSecret: Stripe.Checkout.Session['client_secret']; }; export type SubscriptionUpdateResponseBody = { - subscription: Stripe.Response; + clientSecret: string; }; +// Update +export type SubscriptionUpdateRequestBody = { + returnUrl: string +} + // Get export type SubscriptionGetRequestBody = { subscriptionId: string; @@ -92,19 +93,6 @@ export type SubscriptionResumeUnsuccessfulResponseBody = UnsuccessfulResponseBod export type PortalCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; -// Checkout Session -export type CheckoutSessionCreateRequestBody = { - price: string; - successURL: string; - cancelURL: string; - quantity?: number; - idempotencyKey?: string; -}; -export type CheckoutSessionCreateResponseBody = { - clientSecret: Stripe.Checkout.Session['client_secret']; -}; - -export type CheckoutSessionCreateUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Utils export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 987064c2..08dad75a 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -48,31 +48,33 @@ * description: The request body for creating a subscription * type: object * properties: - * customerId: + * price: * type: string - * description: The Stripe customer id - * example: cus_1234567890 - * items: - * type: array - * items: - * type: object - * properties: - * price: - * type: string - * description: The price id - * example: price_1234567890 + * description: The price id + * example: price_1234567890 + * successURL: + * type: string + * description: The URL to redirect to after the customer sucessfully completes the checkout + * example: https://example.com/success + * cancelURL: + * type: string + * description: The URL to redirect to after the customer cancels the checkout + * example: https://example.com/cancel * idempotencyKey: * type: string * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. * example: abcdefghijklmnopqrstuvwxyz - * * SubscriptionCreateResponseBody: * description: The response body for creating a subscription * type: object * properties: * subscription: * type: object - * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object) + * description: An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object) + * properties: + * url: + * type: string + * description: URL which user should follow to manage subscription * SubscriptionUpdateRequestBody: * description: The request body for updating a subscription * type: object @@ -94,7 +96,11 @@ * properties: * subscription: * type: object - * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object) + * description: Object with redirect url inside + * properties: + * clientSecret: + * type: string + * description: URL with session URL rediect to * SubscriptionGetRequestBody: * description: The request body for getting a subscription * type: object @@ -165,26 +171,6 @@ * subscription: * type: object * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] - * CheckoutSessionCreateRequestBody: - * description: The request body for creating a checkout session - * type: object - * properties: - * price: - * type: string - * description: The price id - * example: price_1234567890 - * successURL: - * type: string - * description: The URL to redirect to after the customer sucessfully completes the checkout - * example: https://example.com/success - * cancelURL: - * type: string - * description: The URL to redirect to after the customer cancels the checkout - * example: https://example.com/cancel - * idempotencyKey: - * type: string - * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. - * example: abcdefghijklmnopqrstuvwxyz * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. * type: object From 39d8a4222de0539002eeb7a1f86d2eea47ec8dbe Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 19 Mar 2024 14:53:59 +0100 Subject: [PATCH 08/49] Change endpoint in swagger --- src/controllers/admin/subscriptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 9b8293dd..d5aa6fd5 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -280,7 +280,7 @@ export class SubscriptionController { /** * @openapi * - * /admin/subscription/get/{subscriptionId}: + * /admin/subscription/get: * get: * summary: Get a subscription * description: Get a subscription From 46baaa6688f9bb68320e4454659ea75a9e31fde0 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Wed, 20 Mar 2024 15:55:54 +0100 Subject: [PATCH 09/49] Fix swagger generation and add API routes --- src/controllers/admin/subscriptions.ts | 6 +- .../auth/routes/admin/admin-auth.ts | 6 + src/static/swagger-admin.json | 141 ++++-------------- src/types/swagger-admin-types.ts | 16 +- 4 files changed, 43 insertions(+), 126 deletions(-) diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index d5aa6fd5..3ff2cc35 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -156,9 +156,9 @@ export class SubscriptionController { * tags: [Subscription] * requestBody: * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionUpdateRequestBody' + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionUpdateRequestBody' * responses: * 200: * description: The request was successful. diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index 1b719c6d..84cc5a99 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -6,6 +6,12 @@ export class AdminHandler extends BaseAuthHandler { constructor() { super(); // Subscriptions + this.registerRoute('/admin/subscription/create', 'POST', 'admin:subscription:create:testnet', { + skipNamespace: true, + }); + this.registerRoute('/admin/subscription/create', 'POST', 'admin:subscription:create:mainnet', { + skipNamespace: true, + }); this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { skipNamespace: true, }); diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 25d1c408..3a591b7d 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -32,39 +32,6 @@ } ], "paths": { - "/admin/checkout/session/create": { - "post": { - "summary": "Create a checkout session", - "description": "Create a checkout session", - "tags": [ - "Checkout" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CheckoutSessionCreateRequestBody" - } - } - } - }, - "responses": { - "303": { - "description": "A redirect to Stripe prebuilt checkout page" - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, "/admin/price/list": { "get": { "summary": "Get a list of prices", @@ -252,12 +219,11 @@ "tags": [ "Subscription" ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" - } + "requestBody": null, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" } } }, @@ -317,24 +283,13 @@ } } }, - "/admin/subscription/get/{subscriptionId}": { + "/admin/subscription/get": { "get": { "summary": "Get a subscription", "description": "Get a subscription", "tags": [ "Subscription" ], - "parameters": [ - { - "in": "path", - "name": "subscriptionId", - "schema": { - "type": "string", - "description": "The subscription id" - }, - "required": true - } - ], "responses": { "200": { "description": "The request was successful.", @@ -510,28 +465,20 @@ "description": "The request body for creating a subscription", "type": "object", "properties": { - "customerId": { + "price": { "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" + "description": "The price id", + "example": "price_1234567890" }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - } - } - } + "successURL": { + "type": "string", + "description": "The URL to redirect to after the customer sucessfully completes the checkout", + "example": "https://example.com/success" }, - "idempotencyKey": { + "cancelURL": { "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" + "description": "The URL to redirect to after the customer cancels the checkout", + "example": "https://example.com/cancel" } } }, @@ -541,7 +488,13 @@ "properties": { "subscription": { "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", + "properties": { + "url": { + "type": "string", + "description": "URL which user should follow to manage subscription" + } + } } } }, @@ -549,19 +502,9 @@ "description": "The request body for updating a subscription", "type": "object", "properties": { - "subscriptionId": { + "returnUrl": { "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - }, - "updateParams": { - "type": "object", - "description": "The subscription update parameters. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/update)" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" + "description": "URL which is used to redirect to the page with ability to update the subscription" } } }, @@ -571,7 +514,13 @@ "properties": { "subscription": { "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + "description": "Object with redirect url inside", + "properties": { + "clientSecret": { + "type": "string", + "description": "URL with session URL rediect to" + } + } } } }, @@ -672,32 +621,6 @@ } } }, - "CheckoutSessionCreateRequestBody": { - "description": "The request body for creating a checkout session", - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - }, - "successURL": { - "type": "string", - "description": "The URL to redirect to after the customer sucessfully completes the checkout", - "example": "https://example.com/success" - }, - "cancelURL": { - "type": "string", - "description": "The URL to redirect to after the customer cancels the checkout", - "example": "https://example.com/cancel" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, "NotFoundError": { "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", "type": "object", diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 08dad75a..2ed76988 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -60,10 +60,6 @@ * type: string * description: The URL to redirect to after the customer cancels the checkout * example: https://example.com/cancel - * idempotencyKey: - * type: string - * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. - * example: abcdefghijklmnopqrstuvwxyz * SubscriptionCreateResponseBody: * description: The response body for creating a subscription * type: object @@ -79,17 +75,9 @@ * description: The request body for updating a subscription * type: object * properties: - * subscriptionId: - * type: string - * description: The subscription id - * example: sub_1234567890 - * updateParams: - * type: object - * description: The subscription update parameters. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/update) - * idempotencyKey: + * returnUrl: * type: string - * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. - * example: abcdefghijklmnopqrstuvwxyz + * description: URL which is used to redirect to the page with ability to update the subscription * SubscriptionUpdateResponseBody: * description: The response body for updating a subscription * type: object From 769b520fcfd2912e79db837bb546046aaea6c6b7 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Wed, 20 Mar 2024 16:54:58 +0100 Subject: [PATCH 10/49] Fix swagger request body for subscription update --- src/controllers/admin/subscriptions.ts | 8 ++++---- src/static/swagger-admin.json | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 3ff2cc35..8638e39e 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -155,10 +155,10 @@ export class SubscriptionController { * description: Updates an existing subscription * tags: [Subscription] * requestBody: - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SubscriptionUpdateRequestBody' + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SubscriptionUpdateRequestBody' * responses: * 200: * description: The request was successful. diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 3a591b7d..f106acf7 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -219,11 +219,12 @@ "tags": [ "Subscription" ], - "requestBody": null, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" + } } } }, From d39e55f9d2779d42879d9c4b9fb4f811aa0b4966 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Wed, 20 Mar 2024 21:46:26 +0100 Subject: [PATCH 11/49] Fix stripeSync function --- src/app.ts | 5 +-- src/controllers/admin/subscriptions.ts | 44 +++++++++++++++++++++----- src/services/admin/subscription.ts | 9 +++++- src/static/swagger-admin.json | 7 +++- src/types/portal.ts | 4 +-- src/types/swagger-admin-types.ts | 6 +++- 6 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/app.ts b/src/app.ts index 7e9d41bf..a35f2b53 100644 --- a/src/app.ts +++ b/src/app.ts @@ -243,10 +243,7 @@ class App { SubscriptionController.subscriptionUpdateValidator, new SubscriptionController().update ); - app.get( - '/admin/subscription/get', - new SubscriptionController().get - ); + app.get('/admin/subscription/get', new SubscriptionController().get); app.get( '/admin/subscription/list', SubscriptionController.subscriptionListValidator, diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 8638e39e..f54c79b0 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -17,7 +17,7 @@ import type { SubscriptionResumeUnsuccessfulResponseBody, SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, - SubscriptionCancelRequestBody + SubscriptionCancelRequestBody, } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; import { validationResult } from '../validator/index.js'; @@ -30,14 +30,39 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class SubscriptionController { static subscriptionCreateValidator = [ - check('price').exists().withMessage('price was not provided').bail().isString().withMessage('price should be a string').bail(), - check('successURL').exists().withMessage('successURL was not provided').bail().isString().withMessage('successURL should be a string').bail(), - check('cancelURL').exists().withMessage('cancelURL was not provided').bail().isString().withMessage('cancelURL should be a string').bail(), + check('price') + .exists() + .withMessage('price was not provided') + .bail() + .isString() + .withMessage('price should be a string') + .bail(), + check('successURL') + .exists() + .withMessage('successURL was not provided') + .bail() + .isString() + .withMessage('successURL should be a string') + .bail(), + check('cancelURL') + .exists() + .withMessage('cancelURL was not provided') + .bail() + .isString() + .withMessage('cancelURL should be a string') + .bail(), check('quantity').optional().isInt().withMessage('quantity should be an integer').bail(), + check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), ]; static subscriptionUpdateValidator = [ - check('returnUrl').exists().withMessage('returnUrl was not provided').bail().isString().withMessage('returnUrl should be a string').bail(), + check('returnURL') + .exists() + .withMessage('returnUrl was not provided') + .bail() + .isString() + .withMessage('returnUrl should be a string') + .bail(), ]; static subscriptionListValidator = [ @@ -93,6 +118,7 @@ export class SubscriptionController { * 500: * $ref: '#/components/schemas/InternalError' */ + async create(request: Request, response: Response) { // Validate request const result = validationResult(request); @@ -189,7 +215,7 @@ export class SubscriptionController { await SubscriptionService.instance.stripeSync(response.locals.customer); // Get the subscription object from the DB - const subscription = await SubscriptionService.instance.findOne({customer: response.locals.customer}); + const subscription = await SubscriptionService.instance.findOne({ customer: response.locals.customer }); if (!subscription) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Subscription was not found`, @@ -210,7 +236,6 @@ export class SubscriptionController { return response.status(StatusCodes.OK).json({ clientSecret: session.url, } satisfies SubscriptionUpdateResponseBody); - } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: `Internal error: ${(error as Error)?.message || error}`, @@ -313,7 +338,10 @@ export class SubscriptionController { // Sync our DB with Stripe await SubscriptionService.instance.stripeSync(response.locals.customer); // Get the subscriptionId from the request - const _sub = await SubscriptionService.instance.findOne({customer: response.locals.customer}); + const _sub = await SubscriptionService.instance.findOne({ + customer: response.locals.customer, + status: 'active' + }); if (!_sub) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Subscription was not found`, diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts index 5c585a2b..a4343462 100644 --- a/src/services/admin/subscription.ts +++ b/src/services/admin/subscription.ts @@ -92,7 +92,13 @@ export class SubscriptionService { stripeCustomerId = user?.customer.stripeCustomerId as string; } - const subscriptions = await stripe.subscriptions.list({ customer: stripeCustomerId }); + // ToDo: add pagination + + const subscriptions = await stripe.subscriptions.list({ + customer: stripeCustomerId, + status: 'all', + limit: 100, + }); // Get list of all subscription and sort them by created time to make sure that we are processing them in the correct order for (const subscription of subscriptions.data.sort((a, b) => a.created - b.created)) { const existing = await this.subscriptionRepository.findOne({ @@ -129,6 +135,7 @@ export class SubscriptionService { severity: 'info', }); } else { + // ToDo: Update only if there are changes const res = await this.update( subscription.id, subscription.status, diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index f106acf7..cb9b28ea 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -480,6 +480,11 @@ "type": "string", "description": "The URL to redirect to after the customer cancels the checkout", "example": "https://example.com/cancel" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" } } }, @@ -503,7 +508,7 @@ "description": "The request body for updating a subscription", "type": "object", "properties": { - "returnUrl": { + "returnURL": { "type": "string", "description": "URL which is used to redirect to the page with ability to update the subscription" } diff --git a/src/types/portal.ts b/src/types/portal.ts index 01fd8ea1..32d2efd3 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -43,8 +43,8 @@ export type SubscriptionUpdateResponseBody = { // Update export type SubscriptionUpdateRequestBody = { - returnUrl: string -} + returnUrl: string; +}; // Get export type SubscriptionGetRequestBody = { diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 2ed76988..03ab7225 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -60,6 +60,10 @@ * type: string * description: The URL to redirect to after the customer cancels the checkout * example: https://example.com/cancel + * idempotencyKey: + * type: string + * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. + * example: abcdefghijklmnopqrstuvwxyz * SubscriptionCreateResponseBody: * description: The response body for creating a subscription * type: object @@ -75,7 +79,7 @@ * description: The request body for updating a subscription * type: object * properties: - * returnUrl: + * returnURL: * type: string * description: URL which is used to redirect to the page with ability to update the subscription * SubscriptionUpdateResponseBody: From 4fcd102b859e61f1b68073d201b8652092202e64 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 24 Mar 2024 00:09:48 +0100 Subject: [PATCH 12/49] Get permissions from the M2M token --- .../user-info-fetcher/{m2m-token.ts => m2m-creds-token.ts} | 2 +- src/middleware/auth/user-info-fetcher/portal-token.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) rename src/middleware/auth/user-info-fetcher/{m2m-token.ts => m2m-creds-token.ts} (95%) diff --git a/src/middleware/auth/user-info-fetcher/m2m-token.ts b/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts similarity index 95% rename from src/middleware/auth/user-info-fetcher/m2m-token.ts rename to src/middleware/auth/user-info-fetcher/m2m-creds-token.ts index a865decb..f07a9cac 100644 --- a/src/middleware/auth/user-info-fetcher/m2m-token.ts +++ b/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts @@ -9,7 +9,7 @@ import { createRemoteJWKSet, jwtVerify } from 'jose'; import * as dotenv from 'dotenv'; dotenv.config(); -export class M2MTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { +export class M2MCredsTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { token: string; constructor(token: string) { diff --git a/src/middleware/auth/user-info-fetcher/portal-token.ts b/src/middleware/auth/user-info-fetcher/portal-token.ts index a972cbfe..f11844b5 100644 --- a/src/middleware/auth/user-info-fetcher/portal-token.ts +++ b/src/middleware/auth/user-info-fetcher/portal-token.ts @@ -74,10 +74,7 @@ export class PortalUserInfoFetcher extends AuthReturn implements IUserInfoFetche if (!payload.sub) { return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No sub found in the token.`); } - const { error, data: scopes } = await oauthProvider.getAppScopes(payload.sub); - if (error) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); - } + const scopes = payload.scope ? (payload.scope as string).split(' ') : []; this.setScopes(scopes); return this.returnOk(); } catch (error) { From b985ec5c680ffa7b73eba26f2445cba185cfed0d Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 24 Mar 2024 13:06:39 +0100 Subject: [PATCH 13/49] Sync updated and small clean-ups --- src/controllers/admin/prices.ts | 12 +- src/controllers/admin/product.ts | 20 +-- src/controllers/admin/subscriptions.ts | 100 ++++++----- src/controllers/admin/webhook.ts | 7 +- src/controllers/validator/decorator.ts | 32 ++++ src/middleware/auth/base-auth-handler.ts | 4 +- .../auth/routes/admin/admin-auth.ts | 13 ++ src/services/admin/stripe.ts | 164 ++++++++++++++++++ src/services/admin/subscription.ts | 104 ++--------- 9 files changed, 292 insertions(+), 164 deletions(-) create mode 100644 src/controllers/validator/decorator.ts create mode 100644 src/services/admin/stripe.ts diff --git a/src/controllers/admin/prices.ts b/src/controllers/admin/prices.ts index e78e5dac..e0569ad9 100644 --- a/src/controllers/admin/prices.ts +++ b/src/controllers/admin/prices.ts @@ -3,8 +3,8 @@ import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; -import { validationResult } from '../validator/index.js'; -import { check } from 'express-validator'; +import { check } from '../validator/index.js'; +import { validate } from '../validator/decorator.js'; dotenv.config(); @@ -46,14 +46,8 @@ export class PriceController { * 404: * $ref: '#/components/schemas/NotFoundError' */ + @validate async getListPrices(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies PriceListUnsuccessfulResponseBody); - } // Get query parameters const productId = request.query.productId; diff --git a/src/controllers/admin/product.ts b/src/controllers/admin/product.ts index e1c7b114..d28041f7 100644 --- a/src/controllers/admin/product.ts +++ b/src/controllers/admin/product.ts @@ -9,8 +9,8 @@ import type { ProductWithPrices, } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; -import { validationResult } from '../validator/index.js'; -import { check } from 'express-validator'; +import { check } from '../validator/index.js'; +import { validate } from '../validator/decorator.js'; dotenv.config(); @@ -57,14 +57,8 @@ export class ProductController { * 404: * $ref: '#/components/schemas/NotFoundError' */ + @validate async listProducts(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ProductListUnsuccessfulResponseBody); - } // Get query parameters const prices = request.query.prices === 'false' ? false : true; @@ -137,14 +131,8 @@ export class ProductController { * 404: * $ref: '#/components/schemas/NotFoundError' */ + @validate async getProduct(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ProductGetUnsuccessfulResponseBody); - } // Get query parameters const prices = request.query.prices === 'false' ? false : true; const productId = request.params.productId as string; diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index f54c79b0..ea0e6b54 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -20,14 +20,45 @@ import type { SubscriptionCancelRequestBody, } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; -import { validationResult } from '../validator/index.js'; -import { check } from 'express-validator'; +import { check, validationResult } from '../validator/index.js'; import { SubscriptionService } from '../../services/admin/subscription.js'; +import { stripeService } from '../../services/admin/stripe.js'; +import { UnsuccessfulResponseBody } from '../../types/shared.js'; +import { validate } from '../validator/decorator.js'; dotenv.config(); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); +export function stripeSync(target: any, key: string, descriptor: PropertyDescriptor | undefined) { + + // save a reference to the original method this way we keep the values currently in the + // descriptor and don't overwrite what another decorator might have done to the descriptor. + if(descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, key) as PropertyDescriptor; + } + + const originalMethod = descriptor.value; + + //editing the descriptor/value parameter + descriptor.value = async function (...args: any[]) { + const response: Response = args[1]; + if (response.locals.customer) { + try { + await stripeService.syncCustomer(response.locals.customer); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies UnsuccessfulResponseBody); + } + } + return originalMethod.apply(this, args); + }; + + // return edited descriptor as opposed to overwriting the descriptor + return descriptor; +} + export class SubscriptionController { static subscriptionCreateValidator = [ check('price') @@ -58,15 +89,15 @@ export class SubscriptionController { static subscriptionUpdateValidator = [ check('returnURL') .exists() - .withMessage('returnUrl was not provided') + .withMessage('returnURL was not provided') .bail() .isString() - .withMessage('returnUrl should be a string') + .withMessage('returnURL should be a string') .bail(), ]; static subscriptionListValidator = [ - check('customerId').optional().isString().withMessage('customerId should be a string').bail(), + check('stripeCustomerId').optional().isString().withMessage('customerId should be a string').bail(), ]; static subscriptionCancelValidator = [ @@ -119,6 +150,7 @@ export class SubscriptionController { * $ref: '#/components/schemas/InternalError' */ + @validate async create(request: Request, response: Response) { // Validate request const result = validationResult(request); @@ -199,20 +231,11 @@ export class SubscriptionController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate + @stripeSync async update(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies SubscriptionUpdateUnsuccessfulResponseBody); - } - const { returnUrl } = request.body satisfies SubscriptionUpdateRequestBody; - try { - // Sync with Stripe - await SubscriptionService.instance.stripeSync(response.locals.customer); // Get the subscription object from the DB const subscription = await SubscriptionService.instance.findOne({ customer: response.locals.customer }); @@ -251,6 +274,12 @@ export class SubscriptionController { * summary: Get a list of subscriptions * description: Get a list of subscriptions * tags: [Subscription] + * parameters: + * - in: query + * name: stripeCustomerId + * schema: + * type: string + * description: The customer id. If passed - returns filtered by this customer list of subscriptions. * responses: * 200: * description: A list of subscriptions @@ -268,22 +297,15 @@ export class SubscriptionController { * $ref: '#/components/schemas/NotFoundError' */ + @validate + @stripeSync public async list(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies SubscriptionListUnsuccessfulResponseBody); - } - const customerId = response.locals.customer.stripeCustomerId; + const stripeCustomerId = response.locals.customer.stripeCustomerId; try { - // Sync our DB with Stripe - await SubscriptionService.instance.stripeSync(response.locals.customer); // Get the subscriptions - const subscriptions = customerId + const subscriptions = stripeCustomerId ? await stripe.subscriptions.list({ - customer: customerId as string, + customer: stripeCustomerId as string, }) : await stripe.subscriptions.list(); @@ -326,17 +348,10 @@ export class SubscriptionController { * 404: * $ref: '#/components/schemas/NotFoundError' */ + @validate + @stripeSync async get(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies SubscriptionGetUnsuccessfulResponseBody); - } try { - // Sync our DB with Stripe - await SubscriptionService.instance.stripeSync(response.locals.customer); // Get the subscriptionId from the request const _sub = await SubscriptionService.instance.findOne({ customer: response.locals.customer, @@ -392,14 +407,9 @@ export class SubscriptionController { * 404: * $ref: '#/components/schemas/NotFoundError' */ + @validate + @stripeSync async cancel(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies SubscriptionCancelUnsuccessfulResponseBody); - } const { subscriptionId, idempotencyKey } = request.body satisfies SubscriptionCancelRequestBody; try { @@ -453,6 +463,8 @@ export class SubscriptionController { * 404: * $ref: '#/components/schemas/NotFoundError' */ + @validate + @stripeSync async resume(request: Request, response: Response) { // Validate request const result = validationResult(request); diff --git a/src/controllers/admin/webhook.ts b/src/controllers/admin/webhook.ts index 29363b80..42421bd9 100644 --- a/src/controllers/admin/webhook.ts +++ b/src/controllers/admin/webhook.ts @@ -12,6 +12,8 @@ dotenv.config(); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class WebhookController { public async handleWebhook(request: Request, response: Response) { + // Signature verification and webhook handling is placed in the same method + // cause stripe uses the mthod which validate the signature and provides the event. let event = request.body; let subscription; let status; @@ -29,9 +31,6 @@ export class WebhookController { } satisfies ISubmitData, } satisfies ISubmitOperation; }; - // Only verify the event if you have an endpoint secret defined. - // Otherwise use the basic event deserialized with JSON.parse - // Get the signature sent by Stripe if (!process.env.STRIPE_WEBHOOK_SECRET) { await eventTracker.notify({ @@ -114,8 +113,6 @@ export class WebhookController { severity: 'info', } satisfies INotifyMessage); await eventTracker.submit(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE)); - // Then define and call a method to handle the subscription update. - // handleSubscriptionUpdated(subscription); break; default: // Unexpected event type diff --git a/src/controllers/validator/decorator.ts b/src/controllers/validator/decorator.ts new file mode 100644 index 00000000..1bb3673b --- /dev/null +++ b/src/controllers/validator/decorator.ts @@ -0,0 +1,32 @@ + +import type { ValidationErrorResponseBody } from '../../types/shared.js'; +import { Request, Response } from 'express'; +import { validationResult } from './index.js'; +import { StatusCodes } from 'http-status-codes'; + +export function validate(target: any, key: string, descriptor: PropertyDescriptor | undefined) { + + // save a reference to the original method this way we keep the values currently in the + // descriptor and don't overwrite what another decorator might have done to the descriptor. + if(descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, key) as PropertyDescriptor; + } + + const originalMethod = descriptor.value; + + //editing the descriptor/value parameter + descriptor.value = async function (...args: any[]) { + const request: Request = args[0]; + const response: Response = args[1]; + const result = validationResult(request); + if (!result.isEmpty()) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: result.array().pop()?.msg, + } satisfies ValidationErrorResponseBody); + } + return originalMethod.apply(this, args); + }; + + // return edited descriptor as opposed to overwriting the descriptor + return descriptor; +} \ No newline at end of file diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index 255810fe..18ae8e17 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -8,7 +8,7 @@ import { APITokenUserInfoFetcher } from './user-info-fetcher/api-token.js'; import type { IUserInfoFetcher } from './user-info-fetcher/base.js'; import { IAuthHandler, RuleRoutine, IAPIGuard } from './routine.js'; import type { IAuthResponse, MethodToScopeRule } from '../../types/authentication.js'; -import { M2MTokenUserInfoFetcher } from './user-info-fetcher/m2m-token.js'; +import { M2MCredsTokenUserInfoFetcher } from './user-info-fetcher/m2m-creds-token.js'; import { decodeJwt } from 'jose'; import { PortalUserInfoFetcher } from './user-info-fetcher/portal-token.js'; @@ -140,7 +140,7 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { if (payload.aud === process.env.LOGTO_APP_ID) { this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); } else { - this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); + this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token)); } } else { this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index 84cc5a99..4c10144f 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -6,6 +6,7 @@ export class AdminHandler extends BaseAuthHandler { constructor() { super(); // Subscriptions + // skipNamespace is set to true cause we don't have the information about the namespace in the request this.registerRoute('/admin/subscription/create', 'POST', 'admin:subscription:create:testnet', { skipNamespace: true, }); @@ -24,6 +25,18 @@ export class AdminHandler extends BaseAuthHandler { this.registerRoute('/admin/subscription/update', 'POST', 'admin:subscription:update:mainnet', { skipNamespace: true, }); + this.registerRoute('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:testnet', { + skipNamespace: true, + }); + this.registerRoute('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:mainnet', { + skipNamespace: true, + }); + this.registerRoute('/admin/subscription/resume', 'POST', 'admin:subscription:resume:testnet', { + skipNamespace: true, + }); + this.registerRoute('/admin/subscription/resume', 'POST', 'admin:subscription:resume:mainnet', { + skipNamespace: true, + }); this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:testnet', { skipNamespace: true }); this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:mainnet', { skipNamespace: true }); // Prices diff --git a/src/services/admin/stripe.ts b/src/services/admin/stripe.ts new file mode 100644 index 00000000..8db2e3c6 --- /dev/null +++ b/src/services/admin/stripe.ts @@ -0,0 +1,164 @@ +import Stripe from 'stripe'; +import * as dotenv from 'dotenv'; +import { SubscriptionService } from './subscription.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; +import { EventTracker, eventTracker } from '../track/tracker.js'; +import { CustomerService } from '../api/customer.js'; +import type { SubscriptionEntity } from '../../database/entities/subscription.entity.js'; + +dotenv.config(); + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + +export class StripeService { + + private stripeToCustomer: Map = new Map(); + + async syncFull(): Promise { + // Set all customers + await this.setStripeCustomers(); + // Sync all subscriptions + for await (const subscription of stripe.subscriptions.list()) { + const current = await SubscriptionService.instance.subscriptionRepository.findOne({ + where: { subscriptionId: subscription.id } + }) + if (current) { + await this.updateSubscription(subscription, current); + } + else { + await this.createSubscription(subscription); + } + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription syncronization completed`, + 'Subscription syncronization' + ), + severity: 'info', + }) + } + + // Sync all the subscriptions for current customer + async syncCustomer(customer: CustomerEntity): Promise { + for await (const subscription of stripe.subscriptions.list({ + customer: customer.stripeCustomerId, + status: 'all' + })) { + const current = await SubscriptionService.instance.subscriptionRepository.findOne({ + where: { subscriptionId: subscription.id } + }) + if (current) { + await this.updateSubscription(subscription, current); + } + else { + await this.createSubscription(subscription, customer); + } + } + } + + async setStripeCustomers(): Promise { + const customers = await CustomerService.instance.customerRepository.createQueryBuilder('customer') + .select('customer.customerId', 'customer.stripeCustomerId') + .where('customer.stripeCustomerId IS NOT NULL') + .getMany(); + customers.forEach((customer) => { + this.stripeToCustomer.set(customer.stripeCustomerId, customer); + }); + } + + async isExists(subscriptionId: string): Promise { + const subscriptionEntity = await SubscriptionService.instance.subscriptionRepository.findOne({ + where: { subscriptionId } + }); + return !!subscriptionEntity; + } + + async createSubscription(subscription: Stripe.Subscription, customerEntity?: CustomerEntity): Promise { + const customer = customerEntity + ? customerEntity + : this.stripeToCustomer.get(subscription.customer as string); + + if (!customer) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Cannot find a customer for subscription with id ${subscription.id}`, + 'Subscription syncronization' + ), + severity: 'error', + }); + return; + } + // Create a new subscription in the database + const subscriptionEntity = SubscriptionService.instance.create( + subscription.id, + customer, + subscription.status, + new Date(subscription.current_period_start * 1000), + new Date(subscription.current_period_end * 1000), + subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, + subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined + ); + + // Track the event + if (!subscriptionEntity) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Cannot create a new subscription with id ${subscription.id}`, + 'Subscription syncronization' + ), + severity: 'error', + }); + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `New subscription with id ${subscription.id} created`, + 'Subscription syncronization' + ), + severity: 'info', + }); + } + + async updateSubscription(subscription: Stripe.Subscription, current: SubscriptionEntity): Promise { + // Update only if there are changes + if (SubscriptionService.instance.equals(current, subscription)) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription with id ${subscription.id} has no changes`, + 'Subscription syncronization' + ), + severity: 'debug', + }) + return + } + + // Update subscription in the database + const subscriptionEntity = SubscriptionService.instance.update( + subscription.id, + subscription.status, + new Date(subscription.current_period_start * 1000), + new Date(subscription.current_period_end * 1000), + subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, + subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined + ); + + // Track the event + if (!subscriptionEntity) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Cannot update subscription with id ${subscription.id}`, + 'Subscription syncronization' + ), + severity: 'error', + }); + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription with id ${subscription.id} updated`, + 'Subscription syncronization' + ), + severity: 'info', + }); + } +} + +export const stripeService = new StripeService(); \ No newline at end of file diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts index a4343462..07cb2142 100644 --- a/src/services/admin/subscription.ts +++ b/src/services/admin/subscription.ts @@ -3,15 +3,7 @@ import type { Repository } from 'typeorm'; import { Connection } from '../../database/connection/connection.js'; import { SubscriptionEntity } from '../../database/entities/subscription.entity.js'; import type { CustomerEntity } from '../../database/entities/customer.entity.js'; -import type { UserEntity } from '../../database/entities/user.entity.js'; -import Stripe from 'stripe'; -import * as dotenv from 'dotenv'; -import { CustomerService } from '../api/customer.js'; -import { EventTracker, eventTracker } from '../track/tracker.js'; - -dotenv.config(); - -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); +import type Stripe from 'stripe'; export class SubscriptionService { public subscriptionRepository: Repository; @@ -81,86 +73,22 @@ export class SubscriptionService { }); } - public async stripeSync(customer?: CustomerEntity, user?: UserEntity): Promise { - let stripeCustomerId: string; - if (!customer && !user) { - throw new Error('StripeSync: customer or user is required'); - } - if (customer) { - stripeCustomerId = customer.stripeCustomerId; - } else { - stripeCustomerId = user?.customer.stripeCustomerId as string; + public equals(subscriptionEntity: SubscriptionEntity, subscription: Stripe.Subscription): boolean { + const required = + subscriptionEntity.subscriptionId === subscription.id && + subscriptionEntity.status === subscription.status && + subscriptionEntity.currentPeriodStart.getTime() === subscription.current_period_start * 1000 && + subscriptionEntity.currentPeriodEnd.getTime() === subscription.current_period_end * 1000; + if (!required) return false; + // Check trial dates only if they are present in the subscription + if (subscription.trial_start) { + if (!subscriptionEntity.trialStart || subscriptionEntity.trialStart.getTime() !== subscription.trial_start * 1000) + return false; } - - // ToDo: add pagination - - const subscriptions = await stripe.subscriptions.list({ - customer: stripeCustomerId, - status: 'all', - limit: 100, - }); - // Get list of all subscription and sort them by created time to make sure that we are processing them in the correct order - for (const subscription of subscriptions.data.sort((a, b) => a.created - b.created)) { - const existing = await this.subscriptionRepository.findOne({ - where: { subscriptionId: subscription.id }, - }); - if (!existing) { - const customer = await CustomerService.instance.findbyStripeCustomerId(stripeCustomerId); - if (!customer) { - throw new Error(`Customer with stripeCustomerId ${stripeCustomerId} not found`); - } - const res = await this.create( - subscription.id, - customer, - subscription.status, - new Date(subscription.current_period_start * 1000), - new Date(subscription.current_period_end * 1000), - subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, - subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined - ); - if (!res) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Cannot create a new subscription with id ${subscription.id}`, - 'Subscription syncronization' - ), - severity: 'error', - }); - } - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `New subscription with id ${subscription.id} created`, - 'Subscription syncronization' - ), - severity: 'info', - }); - } else { - // ToDo: Update only if there are changes - const res = await this.update( - subscription.id, - subscription.status, - new Date(subscription.current_period_start * 1000), - new Date(subscription.current_period_end * 1000), - subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, - subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined - ); - if (!res) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Cannot update subscription with id ${subscription.id}`, - 'Subscription syncronization' - ), - severity: 'error', - }); - } - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Subscription with id ${subscription.id} updated`, - 'Subscription syncronization' - ), - severity: 'info', - }); - } + if (subscription.trial_end) { + if (!subscriptionEntity.trialEnd || subscriptionEntity.trialEnd.getTime() !== subscription.trial_end * 1000) + return false; } + return true; } } From 64ea85b6f2a6fe1bcd237eb01132c0f99510fad8 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 24 Mar 2024 15:53:14 +0100 Subject: [PATCH 14/49] Clean-ups --- README.md | 7 +- docker/Dockerfile | 2 + src/app.ts | 89 ++++---- src/controllers/admin/prices.ts | 5 +- src/controllers/admin/product.ts | 6 +- src/controllers/admin/subscriptions.ts | 65 +++--- src/controllers/admin/webhook.ts | 19 +- src/controllers/validator/decorator.ts | 34 ++- src/middleware/middleware.ts | 10 + src/services/admin/stripe.ts | 209 ++++++------------ src/services/admin/subscription.ts | 5 +- src/services/track/admin/account-submitter.ts | 5 +- .../track/admin/subscription-submitter.ts | 62 ++---- src/services/track/helpers.ts | 19 ++ src/services/track/submitter.ts | 3 + src/services/track/types.ts | 5 + src/static/swagger-admin.json | 14 +- src/types/environment.d.ts | 1 + src/types/portal.ts | 4 +- src/types/swagger-admin-types.ts | 4 +- 20 files changed, 245 insertions(+), 323 deletions(-) diff --git a/README.md b/README.md index 916a6c33..e2158ef1 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,10 @@ some tokens on the testnet for making the process simpler. The application supports Stripe integration for payment processing. -1. `STRIPE_SECRET_KEY`: Secret key for Stripe API. Please, keep it secret on deploying -2. `STRIPE_PUBLISHABLE_KEY` - Publishable key for Stripe API. -3. `STRIPE_WEBHOOK_SECRET` - Secret for Stripe Webhook. +1. `STRIPE_ENABLED` - Enable/disable Stripe integration (`false` by default) +2. `STRIPE_SECRET_KEY` - Secret key for Stripe API. Please, keep it secret on deploying +3. `STRIPE_PUBLISHABLE_KEY` - Publishable key for Stripe API. +4. `STRIPE_WEBHOOK_SECRET` - Secret for Stripe Webhook. ### 3rd Party Connectors diff --git a/docker/Dockerfile b/docker/Dockerfile index c3da44e0..2eadb5da 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -82,6 +82,7 @@ ARG FAUCET_URI=https://faucet-api.cheqd.network/credit ARG TESTNET_MINIMUM_BALANCE=1000 # Stripe +ARG STRIPE_ENABLED=false ARG STRIPE_SECRET_KEY ARG STRIPE_PUBLISHABLE_KEY ARG STRIPE_WEBHOOK_SECRET @@ -134,6 +135,7 @@ ENV POLYGON_PRIVATE_KEY ${POLYGON_PRIVATE_KEY} ENV STRIPE_SECRET_KEY ${STRIPE_SECRET_KEY} ENV STRIPE_PUBLISHABLE_KEY ${STRIPE_PUBLISHABLE_KEY} ENV STRIPE_WEBHOOK_SECRET ${STRIPE_WEBHOOK_SECRET} +ENV STRIPE_ENABLED ${STRIPE_ENABLED} # Set ownership permissions RUN chown -R node:node /home/node/app diff --git a/src/app.ts b/src/app.ts index a35f2b53..c6043c99 100644 --- a/src/app.ts +++ b/src/app.ts @@ -106,11 +106,14 @@ class App { swaggerUi.serveFiles(swaggerAPIDocument, swaggerOptions), swaggerUi.setup(swaggerAPIDocument, swaggerOptions) ); - this.express.use( - '/admin/swagger', - swaggerUi.serveFiles(swaggerAdminDocument), - swaggerUi.setup(swaggerAdminDocument) - ); + if (process.env.STRIPE_ENABLED === 'true') { + this.express.use( + '/admin/swagger', + swaggerUi.serveFiles(swaggerAdminDocument), + swaggerUi.setup(swaggerAdminDocument) + ); + this.express.use(Middleware.setStripeClient) + } this.express.use(auth.handleError); this.express.use(async (req, res, next) => await auth.accessControl(req, res, next)); } @@ -222,46 +225,48 @@ class App { // Portal // Product - app.get('/admin/product/list', ProductController.productListValidator, new ProductController().listProducts); - app.get( - '/admin/product/get/:productId', - ProductController.productGetValidator, - new ProductController().getProduct - ); + if (process.env.STRIPE_ENABLED === 'true') { + app.get('/admin/product/list', ProductController.productListValidator, new ProductController().listProducts); + app.get( + '/admin/product/get/:productId', + ProductController.productGetValidator, + new ProductController().getProduct + ); - // Prices - app.get('/admin/price/list', PriceController.priceListValidator, new PriceController().getListPrices); + // Prices + app.get('/admin/price/list', PriceController.priceListValidator, new PriceController().getListPrices); - // Subscription - app.post( - '/admin/subscription/create', - SubscriptionController.subscriptionCreateValidator, - new SubscriptionController().create - ); - app.post( - '/admin/subscription/update', - SubscriptionController.subscriptionUpdateValidator, - new SubscriptionController().update - ); - app.get('/admin/subscription/get', new SubscriptionController().get); - app.get( - '/admin/subscription/list', - SubscriptionController.subscriptionListValidator, - new SubscriptionController().list - ); - app.delete( - '/admin/subscription/cancel', - SubscriptionController.subscriptionCancelValidator, - new SubscriptionController().cancel - ); - app.post( - '/admin/subscription/resume', - SubscriptionController.subscriptionResumeValidator, - new SubscriptionController().resume - ); + // Subscription + app.post( + '/admin/subscription/create', + SubscriptionController.subscriptionCreateValidator, + new SubscriptionController().create + ); + app.post( + '/admin/subscription/update', + SubscriptionController.subscriptionUpdateValidator, + new SubscriptionController().update + ); + app.get('/admin/subscription/get', new SubscriptionController().get); + app.get( + '/admin/subscription/list', + SubscriptionController.subscriptionListValidator, + new SubscriptionController().list + ); + app.delete( + '/admin/subscription/cancel', + SubscriptionController.subscriptionCancelValidator, + new SubscriptionController().cancel + ); + app.post( + '/admin/subscription/resume', + SubscriptionController.subscriptionResumeValidator, + new SubscriptionController().resume + ); - // Webhook - app.post('/admin/webhook', new WebhookController().handleWebhook); + // Webhook + app.post('/admin/webhook', new WebhookController().handleWebhook); + } // 404 for all other requests app.all('*', (_req, res) => res.status(StatusCodes.BAD_REQUEST).send('Bad request')); diff --git a/src/controllers/admin/prices.ts b/src/controllers/admin/prices.ts index e0569ad9..935b554d 100644 --- a/src/controllers/admin/prices.ts +++ b/src/controllers/admin/prices.ts @@ -1,4 +1,4 @@ -import { Stripe } from 'stripe'; +import type { Stripe } from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../../types/portal.js'; @@ -8,8 +8,6 @@ import { validate } from '../validator/decorator.js'; dotenv.config(); -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - export class PriceController { static priceListValidator = [ check('productId').optional().isString().withMessage('productId should be a string').bail(), @@ -48,6 +46,7 @@ export class PriceController { */ @validate async getListPrices(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; // Get query parameters const productId = request.query.productId; diff --git a/src/controllers/admin/product.ts b/src/controllers/admin/product.ts index d28041f7..920695a0 100644 --- a/src/controllers/admin/product.ts +++ b/src/controllers/admin/product.ts @@ -1,4 +1,4 @@ -import { Stripe } from 'stripe'; +import type { Stripe } from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; import type { @@ -14,8 +14,6 @@ import { validate } from '../validator/decorator.js'; dotenv.config(); -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - export class ProductController { static productListValidator = [ check('prices').optional().isBoolean().withMessage('prices should be a boolean').bail(), @@ -59,6 +57,7 @@ export class ProductController { */ @validate async listProducts(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; // Get query parameters const prices = request.query.prices === 'false' ? false : true; @@ -133,6 +132,7 @@ export class ProductController { */ @validate async getProduct(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; // Get query parameters const prices = request.query.prices === 'false' ? false : true; const productId = request.params.productId as string; diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index ea0e6b54..f181b38e 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -1,4 +1,4 @@ -import Stripe from 'stripe'; +import type Stripe from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; import type { @@ -20,28 +20,25 @@ import type { SubscriptionCancelRequestBody, } from '../../types/portal.js'; import { StatusCodes } from 'http-status-codes'; -import { check, validationResult } from '../validator/index.js'; +import { check } from '../validator/index.js'; import { SubscriptionService } from '../../services/admin/subscription.js'; import { stripeService } from '../../services/admin/stripe.js'; -import { UnsuccessfulResponseBody } from '../../types/shared.js'; +import type { UnsuccessfulResponseBody } from '../../types/shared.js'; import { validate } from '../validator/decorator.js'; dotenv.config(); -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - export function stripeSync(target: any, key: string, descriptor: PropertyDescriptor | undefined) { - - // save a reference to the original method this way we keep the values currently in the - // descriptor and don't overwrite what another decorator might have done to the descriptor. - if(descriptor === undefined) { - descriptor = Object.getOwnPropertyDescriptor(target, key) as PropertyDescriptor; - } + // save a reference to the original method this way we keep the values currently in the + // descriptor and don't overwrite what another decorator might have done to the descriptor. + if (descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, key) as PropertyDescriptor; + } + + const originalMethod = descriptor.value; - const originalMethod = descriptor.value; - - //editing the descriptor/value parameter - descriptor.value = async function (...args: any[]) { + //editing the descriptor/value parameter + descriptor.value = async function (...args: any[]) { const response: Response = args[1]; if (response.locals.customer) { try { @@ -53,10 +50,10 @@ export function stripeSync(target: any, key: string, descriptor: PropertyDescrip } } return originalMethod.apply(this, args); - }; - - // return edited descriptor as opposed to overwriting the descriptor - return descriptor; + }; + + // return edited descriptor as opposed to overwriting the descriptor + return descriptor; } export class SubscriptionController { @@ -152,14 +149,7 @@ export class SubscriptionController { @validate async create(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies SubscriptionCreateUnsuccessfulResponseBody); - } + const stripe = response.locals.stripe as Stripe; const { price, successURL, cancelURL, quantity, idempotencyKey } = request.body satisfies SubscriptionCreateRequestBody; @@ -195,7 +185,7 @@ export class SubscriptionController { } return response.json({ - clientSecret: session.url as string, + sessionURL: session.url as string, } satisfies SubscriptionCreateResponseBody); } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ @@ -234,9 +224,9 @@ export class SubscriptionController { @validate @stripeSync async update(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; const { returnUrl } = request.body satisfies SubscriptionUpdateRequestBody; try { - // Get the subscription object from the DB const subscription = await SubscriptionService.instance.findOne({ customer: response.locals.customer }); if (!subscription) { @@ -257,7 +247,7 @@ export class SubscriptionController { } satisfies SubscriptionUpdateUnsuccessfulResponseBody); } return response.status(StatusCodes.OK).json({ - clientSecret: session.url, + sessionURL: session.url, } satisfies SubscriptionUpdateResponseBody); } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ @@ -300,6 +290,7 @@ export class SubscriptionController { @validate @stripeSync public async list(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; const stripeCustomerId = response.locals.customer.stripeCustomerId; try { // Get the subscriptions @@ -351,11 +342,12 @@ export class SubscriptionController { @validate @stripeSync async get(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; try { // Get the subscriptionId from the request - const _sub = await SubscriptionService.instance.findOne({ + const _sub = await SubscriptionService.instance.findOne({ customer: response.locals.customer, - status: 'active' + status: 'active', }); if (!_sub) { return response.status(StatusCodes.NOT_FOUND).json({ @@ -410,6 +402,7 @@ export class SubscriptionController { @validate @stripeSync async cancel(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; const { subscriptionId, idempotencyKey } = request.body satisfies SubscriptionCancelRequestBody; try { @@ -466,13 +459,7 @@ export class SubscriptionController { @validate @stripeSync async resume(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies SubscriptionResumeUnsuccessfulResponseBody); - } + const stripe = response.locals.stripe as Stripe; const { subscriptionId, idempotencyKey } = request.body satisfies SubscriptionResumeRequestBody; try { // Resume the subscription diff --git a/src/controllers/admin/webhook.ts b/src/controllers/admin/webhook.ts index 42421bd9..c105c924 100644 --- a/src/controllers/admin/webhook.ts +++ b/src/controllers/admin/webhook.ts @@ -5,11 +5,9 @@ import { StatusCodes } from 'http-status-codes'; import { EventTracker, eventTracker } from '../../services/track/tracker.js'; import type { INotifyMessage } from '../../types/track.js'; import { OperationNameEnum } from '../../types/constants.js'; -import type { ISubmitOperation, ISubmitData } from '../../services/track/submitter.js'; +import { builSubmitOperation } from '../../services/track/helpers.js'; dotenv.config(); - -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export class WebhookController { public async handleWebhook(request: Request, response: Response) { // Signature verification and webhook handling is placed in the same method @@ -17,20 +15,7 @@ export class WebhookController { let event = request.body; let subscription; let status; - const builSubmitOperation = function (subscription: Stripe.Subscription, name: string) { - return { - operation: name, - data: { - subscriptionId: subscription.id, - stripeCustomerId: subscription.customer as string, - status: subscription.status, - currentPeriodStart: new Date(subscription.current_period_start * 1000), - currentPeriodEnd: new Date(subscription.current_period_end * 1000), - trialStart: subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, - trialEnd: subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined, - } satisfies ISubmitData, - } satisfies ISubmitOperation; - }; + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY) if (!process.env.STRIPE_WEBHOOK_SECRET) { await eventTracker.notify({ diff --git a/src/controllers/validator/decorator.ts b/src/controllers/validator/decorator.ts index 1bb3673b..2657d46c 100644 --- a/src/controllers/validator/decorator.ts +++ b/src/controllers/validator/decorator.ts @@ -1,22 +1,20 @@ - import type { ValidationErrorResponseBody } from '../../types/shared.js'; -import { Request, Response } from 'express'; +import type { Request, Response } from 'express'; import { validationResult } from './index.js'; import { StatusCodes } from 'http-status-codes'; export function validate(target: any, key: string, descriptor: PropertyDescriptor | undefined) { - - // save a reference to the original method this way we keep the values currently in the - // descriptor and don't overwrite what another decorator might have done to the descriptor. - if(descriptor === undefined) { - descriptor = Object.getOwnPropertyDescriptor(target, key) as PropertyDescriptor; - } + // save a reference to the original method this way we keep the values currently in the + // descriptor and don't overwrite what another decorator might have done to the descriptor. + if (descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, key) as PropertyDescriptor; + } + + const originalMethod = descriptor.value; - const originalMethod = descriptor.value; - - //editing the descriptor/value parameter - descriptor.value = async function (...args: any[]) { - const request: Request = args[0]; + //editing the descriptor/value parameter + descriptor.value = async function (...args: any[]) { + const request: Request = args[0]; const response: Response = args[1]; const result = validationResult(request); if (!result.isEmpty()) { @@ -25,8 +23,8 @@ export function validate(target: any, key: string, descriptor: PropertyDescripto } satisfies ValidationErrorResponseBody); } return originalMethod.apply(this, args); - }; - - // return edited descriptor as opposed to overwriting the descriptor - return descriptor; -} \ No newline at end of file + }; + + // return edited descriptor as opposed to overwriting the descriptor + return descriptor; +} diff --git a/src/middleware/middleware.ts b/src/middleware/middleware.ts index 7ebade0d..a797e8ec 100644 --- a/src/middleware/middleware.ts +++ b/src/middleware/middleware.ts @@ -1,4 +1,7 @@ import type { Request, Response, NextFunction } from 'express'; +import Stripe from 'stripe'; +import * as dotenv from 'dotenv'; +dotenv.config(); export class Middleware { static async parseUrlEncodedJson(request: Request, response: Response, next: NextFunction) { @@ -23,4 +26,11 @@ export class Middleware { } next(); } + + static async setStripeClient(request: Request, response: Response, next: NextFunction) { + // Set the Stripe client + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + response.locals.stripe = stripe; + next(); + } } diff --git a/src/services/admin/stripe.ts b/src/services/admin/stripe.ts index 8db2e3c6..257dba67 100644 --- a/src/services/admin/stripe.ts +++ b/src/services/admin/stripe.ts @@ -3,162 +3,81 @@ import * as dotenv from 'dotenv'; import { SubscriptionService } from './subscription.js'; import type { CustomerEntity } from '../../database/entities/customer.entity.js'; import { EventTracker, eventTracker } from '../track/tracker.js'; -import { CustomerService } from '../api/customer.js'; import type { SubscriptionEntity } from '../../database/entities/subscription.entity.js'; +import { builSubmitOperation } from '../track/helpers.js'; +import { OperationNameEnum } from '../../types/constants.js'; +import { SubscriptionSubmitter } from '../track/admin/subscription-submitter.js'; dotenv.config(); -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - export class StripeService { - private stripeToCustomer: Map = new Map(); - - async syncFull(): Promise { - // Set all customers - await this.setStripeCustomers(); - // Sync all subscriptions - for await (const subscription of stripe.subscriptions.list()) { - const current = await SubscriptionService.instance.subscriptionRepository.findOne({ - where: { subscriptionId: subscription.id } - }) - if (current) { - await this.updateSubscription(subscription, current); - } - else { - await this.createSubscription(subscription); - } - } - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Subscription syncronization completed`, - 'Subscription syncronization' - ), - severity: 'info', - }) - } - - // Sync all the subscriptions for current customer - async syncCustomer(customer: CustomerEntity): Promise { - for await (const subscription of stripe.subscriptions.list({ - customer: customer.stripeCustomerId, - status: 'all' - })) { - const current = await SubscriptionService.instance.subscriptionRepository.findOne({ - where: { subscriptionId: subscription.id } - }) - if (current) { - await this.updateSubscription(subscription, current); - } - else { - await this.createSubscription(subscription, customer); - } - } - } - - async setStripeCustomers(): Promise { - const customers = await CustomerService.instance.customerRepository.createQueryBuilder('customer') - .select('customer.customerId', 'customer.stripeCustomerId') - .where('customer.stripeCustomerId IS NOT NULL') - .getMany(); - customers.forEach((customer) => { - this.stripeToCustomer.set(customer.stripeCustomerId, customer); - }); - } + submitter: SubscriptionSubmitter; - async isExists(subscriptionId: string): Promise { - const subscriptionEntity = await SubscriptionService.instance.subscriptionRepository.findOne({ - where: { subscriptionId } - }); - return !!subscriptionEntity; + constructor() { + this.submitter = new SubscriptionSubmitter(eventTracker.getEmitter()); } - async createSubscription(subscription: Stripe.Subscription, customerEntity?: CustomerEntity): Promise { - const customer = customerEntity - ? customerEntity - : this.stripeToCustomer.get(subscription.customer as string); + async syncFull(): Promise { + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + // Sync all subscriptions + for await (const subscription of stripe.subscriptions.list({ + status: 'all', + })) { + const current = await SubscriptionService.instance.subscriptionRepository.findOne({ + where: { subscriptionId: subscription.id }, + }); + if (current) { + await this.updateSubscription(subscription, current); + } else { + await this.createSubscription(subscription); + } + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription syncronization completed`, + 'Subscription syncronization' + ), + severity: 'info', + }); + } - if (!customer) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Cannot find a customer for subscription with id ${subscription.id}`, - 'Subscription syncronization' - ), - severity: 'error', - }); - return; - } - // Create a new subscription in the database - const subscriptionEntity = SubscriptionService.instance.create( - subscription.id, - customer, - subscription.status, - new Date(subscription.current_period_start * 1000), - new Date(subscription.current_period_end * 1000), - subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, - subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined - ); + // Sync all the subscriptions for current customer + async syncCustomer(customer: CustomerEntity): Promise { + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + for await (const subscription of stripe.subscriptions.list({ + customer: customer.stripeCustomerId, + status: 'all', + })) { + const current = await SubscriptionService.instance.subscriptionRepository.findOne({ + where: { subscriptionId: subscription.id }, + }); + if (current) { + await this.updateSubscription(subscription, current); + } else { + await this.createSubscription(subscription, customer); + } + } + } - // Track the event - if (!subscriptionEntity) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Cannot create a new subscription with id ${subscription.id}`, - 'Subscription syncronization' - ), - severity: 'error', - }); - } - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `New subscription with id ${subscription.id} created`, - 'Subscription syncronization' - ), - severity: 'info', - }); - } + async createSubscription(subscription: Stripe.Subscription, customer?: CustomerEntity): Promise { + await this.submitter.submitSubscriptionCreate(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CREATE, {customer: customer})); + } - async updateSubscription(subscription: Stripe.Subscription, current: SubscriptionEntity): Promise { - // Update only if there are changes - if (SubscriptionService.instance.equals(current, subscription)) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Subscription with id ${subscription.id} has no changes`, - 'Subscription syncronization' - ), - severity: 'debug', - }) - return - } - - // Update subscription in the database - const subscriptionEntity = SubscriptionService.instance.update( - subscription.id, - subscription.status, - new Date(subscription.current_period_start * 1000), - new Date(subscription.current_period_end * 1000), - subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, - subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined - ); - - // Track the event - if (!subscriptionEntity) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Cannot update subscription with id ${subscription.id}`, - 'Subscription syncronization' - ), - severity: 'error', - }); - } - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Subscription with id ${subscription.id} updated`, - 'Subscription syncronization' - ), - severity: 'info', - }); - } + async updateSubscription(subscription: Stripe.Subscription, current: SubscriptionEntity): Promise { + // Update only if there are changes + if (SubscriptionService.instance.equals(current, subscription)) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription with id ${subscription.id} has no changes`, + 'Subscription syncronization' + ), + severity: 'debug', + }); + return; + } + await this.submitter.submitSubscriptionUpdate(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE)); + } } -export const stripeService = new StripeService(); \ No newline at end of file +export const stripeService = new StripeService(); diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts index 07cb2142..7219c7a6 100644 --- a/src/services/admin/subscription.ts +++ b/src/services/admin/subscription.ts @@ -82,7 +82,10 @@ export class SubscriptionService { if (!required) return false; // Check trial dates only if they are present in the subscription if (subscription.trial_start) { - if (!subscriptionEntity.trialStart || subscriptionEntity.trialStart.getTime() !== subscription.trial_start * 1000) + if ( + !subscriptionEntity.trialStart || + subscriptionEntity.trialStart.getTime() !== subscription.trial_start * 1000 + ) return false; } if (subscription.trial_end) { diff --git a/src/services/track/admin/account-submitter.ts b/src/services/track/admin/account-submitter.ts index 29d7f5f3..de5346fa 100644 --- a/src/services/track/admin/account-submitter.ts +++ b/src/services/track/admin/account-submitter.ts @@ -9,11 +9,9 @@ import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from '../submi export class PortalAccountCreateSubmitter implements IObserver { private emitter: EventEmitter; - private stripe: Stripe; constructor(emitter: EventEmitter) { this.emitter = emitter; - this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY); } notify(notifyMessage: INotifyMessage): void { @@ -28,10 +26,11 @@ export class PortalAccountCreateSubmitter implements IObserver { async submitStripeAccountCreate(operation: ISubmitOperation): Promise { const data = operation.data as ISubmitStripeCustomerCreateData; + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); try { // Create a new Stripe account - const account = await this.stripe.customers.create({ + const account = await stripe.customers.create({ name: data.name, email: data.email, }); diff --git a/src/services/track/admin/subscription-submitter.ts b/src/services/track/admin/subscription-submitter.ts index ccfbaf9d..8dfcc30f 100644 --- a/src/services/track/admin/subscription-submitter.ts +++ b/src/services/track/admin/subscription-submitter.ts @@ -1,38 +1,12 @@ +import type { CustomerEntity } from '../../../database/entities/customer.entity.js'; import { OperationNameEnum } from '../../../types/constants.js'; import type { INotifyMessage } from '../../../types/track.js'; import { SubscriptionService } from '../../admin/subscription.js'; import { CustomerService } from '../../api/customer.js'; import type { ISubmitOperation, ISubmitSubscriptionData } from '../submitter.js'; -import { EventTracker, eventTracker } from '../tracker.js'; +import { EventTracker } from '../tracker.js'; import type { IObserver } from '../types.js'; -//eslint-disable-next-line - -function isPromise(object: any): object is Promise { - return object && Promise.resolve(object) === object; -} - -export const eventDecorator = () => { - return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { - const { value } = descriptor; - //eslint-disable-next-line - descriptor.value = async (...args: any) => { - try { - const response = value.apply(target, args); - return isPromise(response) ? await response : Promise.resolve(response); - } catch (error) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Error while calling function: ${propertyKey}: ${(error as Record)?.message || error}` - ), - severity: 'error', - } satisfies INotifyMessage); - } - }; - return descriptor; - }; -}; - export class SubscriptionSubmitter implements IObserver { private emitter: EventEmitter; @@ -60,26 +34,30 @@ export class SubscriptionSubmitter implements IObserver { } } - // @eventDecorator() async submitSubscriptionCreate(operation: ISubmitOperation): Promise { const data = operation.data as ISubmitSubscriptionData; + let customer: CustomerEntity | undefined = operation.options?.customer; try { - const customers = await CustomerService.instance.find({ - stripeCustomerId: data.stripeCustomerId, - }); - - if (customers.length !== 1) { - this.notify({ - message: EventTracker.compileBasicNotification( - `It should be only 1 Stripe account associated with CaaS customer. Stripe accountId: ${data.stripeCustomerId}.`, - operation.operation - ), - severity: 'error', + if (!customer) { + const customers = await CustomerService.instance.find({ + stripeCustomerId: data.stripeCustomerId, }); + + if (customers.length !== 1) { + this.notify({ + message: EventTracker.compileBasicNotification( + `It should be only 1 Stripe account associated with CaaS customer. Stripe accountId: ${data.stripeCustomerId}.`, + operation.operation + ), + severity: 'error', + }); + } + customer = customers[0]; } + const subscription = await SubscriptionService.instance.create( data.subscriptionId, - customers[0], + customer, data.status, data.currentPeriodStart, data.currentPeriodEnd, @@ -114,7 +92,6 @@ export class SubscriptionSubmitter implements IObserver { } } - // @eventDecorator() async submitSubscriptionUpdate(operation: ISubmitOperation): Promise { const data = operation.data as ISubmitSubscriptionData; try { @@ -154,7 +131,6 @@ export class SubscriptionSubmitter implements IObserver { } } - // @eventDecorator() async submitSubscriptionCancel(operation: ISubmitOperation): Promise { const data = operation.data as ISubmitSubscriptionData; try { diff --git a/src/services/track/helpers.ts b/src/services/track/helpers.ts index 1b720b9b..192c4259 100644 --- a/src/services/track/helpers.ts +++ b/src/services/track/helpers.ts @@ -7,6 +7,9 @@ import type { ICredentialTrack, IDIDTrack, } from '../../types/track.js'; +import type Stripe from 'stripe'; +import type { ISubmitData, ISubmitOperation } from './submitter.js'; +import type { ISubmitOptions } from './types.js'; export function isResourceTrack(data: TrackData): data is IResourceTrack { return Object.keys(data).length === 2 && (data as IResourceTrack).resource !== undefined; @@ -31,3 +34,19 @@ export function isDIDTrack(data: TrackData): data is IDIDTrack { export function toCoin(amount: bigint, denom = MINIMAL_DENOM): Coin { return coin(amount.toString(), denom); } + +export function builSubmitOperation (subscription: Stripe.Subscription, name: string, options?: ISubmitOptions) { + return { + operation: name, + data: { + subscriptionId: subscription.id, + stripeCustomerId: subscription.customer as string, + status: subscription.status, + currentPeriodStart: new Date(subscription.current_period_start * 1000), + currentPeriodEnd: new Date(subscription.current_period_end * 1000), + trialStart: subscription.trial_start ? new Date(subscription.trial_start * 1000) : undefined, + trialEnd: subscription.trial_end ? new Date(subscription.trial_end * 1000) : undefined, + } satisfies ISubmitData, + options, + } satisfies ISubmitOperation; +}; diff --git a/src/services/track/submitter.ts b/src/services/track/submitter.ts index 89beb23b..acaf1c81 100644 --- a/src/services/track/submitter.ts +++ b/src/services/track/submitter.ts @@ -1,3 +1,5 @@ +import type { ISubmitOptions } from "./types"; + // Type: Interface export type ISubmitData = ISubmitStripeCustomerCreateData | ISubmitSubscriptionData; @@ -20,4 +22,5 @@ export interface ISubmitSubscriptionData { export interface ISubmitOperation { operation: string; data: ISubmitData; + options?: ISubmitOptions; } diff --git a/src/services/track/types.ts b/src/services/track/types.ts index 9f84a847..2b61b251 100644 --- a/src/services/track/types.ts +++ b/src/services/track/types.ts @@ -1,3 +1,4 @@ +import type { CustomerEntity } from '../../database/entities/customer.entity'; import type { ITrackOperation, INotifyMessage } from '../../types/track'; import type { ISubmitOperation } from './submitter'; @@ -17,3 +18,7 @@ export interface ITrackSubject { // Notify all observers about an event. notify(operation: ITrackType): void; } + +export interface ISubmitOptions { + customer?: CustomerEntity; +} diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index cb9b28ea..7f52ed7c 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -258,6 +258,16 @@ "tags": [ "Subscription" ], + "parameters": [ + { + "in": "query", + "name": "stripeCustomerId", + "schema": { + "type": "string", + "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." + } + } + ], "responses": { "200": { "description": "A list of subscriptions", @@ -496,7 +506,7 @@ "type": "object", "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", "properties": { - "url": { + "sessionURL": { "type": "string", "description": "URL which user should follow to manage subscription" } @@ -522,7 +532,7 @@ "type": "object", "description": "Object with redirect url inside", "properties": { - "clientSecret": { + "sessionURL": { "type": "string", "description": "URL with session URL rediect to" } diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index d114b94e..abd5ffae 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -58,6 +58,7 @@ declare global { CREDS_DECRYPTION_SECRET: string; // Stripe + STRIPE_ENABLED: string | 'false'; STRIPE_SECRET_KEY: string; STRIPE_PUBLISHABLE_KEY: string; STRIPE_WEBHOOK_SECRET: string; diff --git a/src/types/portal.ts b/src/types/portal.ts index 32d2efd3..b528c710 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -34,11 +34,11 @@ export type SubscriptionCreateRequestBody = { }; export type SubscriptionCreateResponseBody = { - clientSecret: Stripe.Checkout.Session['client_secret']; + sessionURL: Stripe.Checkout.Session['client_secret']; }; export type SubscriptionUpdateResponseBody = { - clientSecret: string; + sessionURL: string; }; // Update diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 03ab7225..4acce51c 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -72,7 +72,7 @@ * type: object * description: An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object) * properties: - * url: + * sessionURL: * type: string * description: URL which user should follow to manage subscription * SubscriptionUpdateRequestBody: @@ -90,7 +90,7 @@ * type: object * description: Object with redirect url inside * properties: - * clientSecret: + * sessionURL: * type: string * description: URL with session URL rediect to * SubscriptionGetRequestBody: From ab0da5bbca6b3bca4c8eb10ee13a7700ad42e65f Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 24 Mar 2024 16:50:07 +0100 Subject: [PATCH 15/49] Add trial period days to subscription creation --- src/app.ts | 3 ++ src/controllers/admin/subscriptions.ts | 47 ++++++++++++++++++++---- src/services/admin/stripe.ts | 49 ++++++++++++++++++++++++++ src/types/portal.ts | 1 + src/types/swagger-admin-types.ts | 8 +++++ 5 files changed, 101 insertions(+), 7 deletions(-) diff --git a/src/app.ts b/src/app.ts index c6043c99..69b070b8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -31,6 +31,7 @@ import { ProductController } from './controllers/admin/product.js'; import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; import { WebhookController } from './controllers/admin/webhook.js'; +import { stripeService } from './services/admin/stripe.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -113,6 +114,8 @@ class App { swaggerUi.setup(swaggerAdminDocument) ); this.express.use(Middleware.setStripeClient) + // ToDo: move it to Setup phase. + this.express.use(async (_req, _res, next) => await stripeService.syncAll(next)) } this.express.use(auth.handleError); this.express.use(async (req, res, next) => await auth.accessControl(req, res, next)); diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index f181b38e..fde4d082 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -28,7 +28,7 @@ import { validate } from '../validator/decorator.js'; dotenv.config(); -export function stripeSync(target: any, key: string, descriptor: PropertyDescriptor | undefined) { +export function syncCustomer(target: any, key: string, descriptor: PropertyDescriptor | undefined) { // save a reference to the original method this way we keep the values currently in the // descriptor and don't overwrite what another decorator might have done to the descriptor. if (descriptor === undefined) { @@ -56,6 +56,35 @@ export function stripeSync(target: any, key: string, descriptor: PropertyDescrip return descriptor; } +export function syncOne(target: any, key: string, descriptor: PropertyDescriptor | undefined) { + // save a reference to the original method this way we keep the values currently in the + // descriptor and don't overwrite what another decorator might have done to the descriptor. + if (descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, key) as PropertyDescriptor; + } + + const originalMethod = descriptor.value; + + //editing the descriptor/value parameter + descriptor.value = async function (...args: any[]) { + const response: Response = args[1]; + if (response.locals.customer) { + try { + await stripeService.syncOne(response.locals.customer); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies UnsuccessfulResponseBody); + } + } + return originalMethod.apply(this, args); + }; + + // return edited descriptor as opposed to overwriting the descriptor + return descriptor; +} + + export class SubscriptionController { static subscriptionCreateValidator = [ check('price') @@ -80,6 +109,7 @@ export class SubscriptionController { .withMessage('cancelURL should be a string') .bail(), check('quantity').optional().isInt().withMessage('quantity should be an integer').bail(), + check('trialPeriodDays').optional().isInt().withMessage('trialPeriodDays should be an integer').bail(), check('idempotencyKey').optional().isString().withMessage('idempotencyKey should be a string').bail(), ]; @@ -151,7 +181,7 @@ export class SubscriptionController { async create(request: Request, response: Response) { const stripe = response.locals.stripe as Stripe; - const { price, successURL, cancelURL, quantity, idempotencyKey } = + const { price, successURL, cancelURL, quantity, idempotencyKey, trialPeriodDays } = request.body satisfies SubscriptionCreateRequestBody; try { const session = await stripe.checkout.sessions.create( @@ -166,6 +196,9 @@ export class SubscriptionController { ], success_url: successURL, cancel_url: cancelURL, + subscription_data: { + trial_period_days: trialPeriodDays, + } }, { idempotencyKey, @@ -222,7 +255,7 @@ export class SubscriptionController { * $ref: '#/components/schemas/InternalError' */ @validate - @stripeSync + @syncOne async update(request: Request, response: Response) { const stripe = response.locals.stripe as Stripe; const { returnUrl } = request.body satisfies SubscriptionUpdateRequestBody; @@ -288,7 +321,7 @@ export class SubscriptionController { */ @validate - @stripeSync + @syncCustomer public async list(request: Request, response: Response) { const stripe = response.locals.stripe as Stripe; const stripeCustomerId = response.locals.customer.stripeCustomerId; @@ -340,7 +373,7 @@ export class SubscriptionController { * $ref: '#/components/schemas/NotFoundError' */ @validate - @stripeSync + @syncOne async get(request: Request, response: Response) { const stripe = response.locals.stripe as Stripe; try { @@ -400,7 +433,7 @@ export class SubscriptionController { * $ref: '#/components/schemas/NotFoundError' */ @validate - @stripeSync + @syncOne async cancel(request: Request, response: Response) { const stripe = response.locals.stripe as Stripe; const { subscriptionId, idempotencyKey } = request.body satisfies SubscriptionCancelRequestBody; @@ -457,7 +490,7 @@ export class SubscriptionController { * $ref: '#/components/schemas/NotFoundError' */ @validate - @stripeSync + @syncOne async resume(request: Request, response: Response) { const stripe = response.locals.stripe as Stripe; const { subscriptionId, idempotencyKey } = request.body satisfies SubscriptionResumeRequestBody; diff --git a/src/services/admin/stripe.ts b/src/services/admin/stripe.ts index 257dba67..1c3613b1 100644 --- a/src/services/admin/stripe.ts +++ b/src/services/admin/stripe.ts @@ -7,17 +7,27 @@ import type { SubscriptionEntity } from '../../database/entities/subscription.en import { builSubmitOperation } from '../track/helpers.js'; import { OperationNameEnum } from '../../types/constants.js'; import { SubscriptionSubmitter } from '../track/admin/subscription-submitter.js'; +import type { NextFunction } from 'express'; dotenv.config(); export class StripeService { submitter: SubscriptionSubmitter; + private isFullySynced = false; constructor() { this.submitter = new SubscriptionSubmitter(eventTracker.getEmitter()); } + async syncAll(next: NextFunction): Promise { + if (!this.isFullySynced) { + await this.syncFull(); + this.isFullySynced = true; + } + next() + } + async syncFull(): Promise { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // Sync all subscriptions @@ -60,6 +70,45 @@ export class StripeService { } } + async syncOne(customer: CustomerEntity): Promise { + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + + const local = await SubscriptionService.instance.findOne({ + customer: customer, + status: 'active', + }); + if (!local) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Active subscription not found for customer with id ${customer.customerId}`, + 'Subscription syncronization' + ), + severity: 'debug', + }); + return; + } + const subscriptionId = local.subscriptionId; + const remote = await stripe.subscriptions.retrieve(subscriptionId); + if (!remote) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription with id ${subscriptionId} could not be retrieved from Stripe`, + 'Subscription syncronization' + ), + severity: 'error', + }); + return; + } + const current = await SubscriptionService.instance.subscriptionRepository.findOne({ + where: { subscriptionId: remote.id }, + }); + if (current) { + await this.updateSubscription(remote, current); + } else { + await this.createSubscription(remote); + } + } + async createSubscription(subscription: Stripe.Subscription, customer?: CustomerEntity): Promise { await this.submitter.submitSubscriptionCreate(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CREATE, {customer: customer})); } diff --git a/src/types/portal.ts b/src/types/portal.ts index b528c710..2e4fcaa7 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -30,6 +30,7 @@ export type SubscriptionCreateRequestBody = { successURL: string; cancelURL: string; quantity?: number; + trialPeriodDays?: number; idempotencyKey?: string; }; diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 4acce51c..33249863 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -60,6 +60,14 @@ * type: string * description: The URL to redirect to after the customer cancels the checkout * example: https://example.com/cancel + * quantity: + * type: number + * description: The quantity of the product + * example: 1 + * trialPeriodDays: + * type: number + * description: The number of days the customer has to pay for the product + * example: 7 * idempotencyKey: * type: string * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. From 71f60d32f175379773f45f57891a483df7812e81 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Mon, 25 Mar 2024 14:22:43 +0100 Subject: [PATCH 16/49] Get also trilaing subsriptions --- src/controllers/admin/subscriptions.ts | 17 +++++++++++------ src/services/admin/stripe.ts | 13 +++++++++---- src/services/admin/subscription.ts | 2 +- src/static/swagger-admin.json | 10 ++++++++++ 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index fde4d082..777edc1e 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -261,7 +261,7 @@ export class SubscriptionController { const { returnUrl } = request.body satisfies SubscriptionUpdateRequestBody; try { // Get the subscription object from the DB - const subscription = await SubscriptionService.instance.findOne({ customer: response.locals.customer }); + const subscription = await SubscriptionService.instance.findOne([{ customer: response.locals.customer }]); if (!subscription) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Subscription was not found`, @@ -373,15 +373,20 @@ export class SubscriptionController { * $ref: '#/components/schemas/NotFoundError' */ @validate - @syncOne + @syncCustomer async get(request: Request, response: Response) { const stripe = response.locals.stripe as Stripe; try { // Get the subscriptionId from the request - const _sub = await SubscriptionService.instance.findOne({ - customer: response.locals.customer, - status: 'active', - }); + const _sub = await SubscriptionService.instance.findOne([ + { + customer: response.locals.customer, + status: 'active'}, + { + customer: response.locals.customer, + status: 'trialing' + } + ]); if (!_sub) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Subscription was not found`, diff --git a/src/services/admin/stripe.ts b/src/services/admin/stripe.ts index 1c3613b1..5e94cab4 100644 --- a/src/services/admin/stripe.ts +++ b/src/services/admin/stripe.ts @@ -73,10 +73,15 @@ export class StripeService { async syncOne(customer: CustomerEntity): Promise { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - const local = await SubscriptionService.instance.findOne({ - customer: customer, - status: 'active', - }); + const local = await SubscriptionService.instance.findOne([ + { + customer: customer, + status: 'active'}, + { + customer: customer, + status: 'trialing' + } + ]); if (!local) { eventTracker.notify({ message: EventTracker.compileBasicNotification( diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts index 7219c7a6..9ef45843 100644 --- a/src/services/admin/subscription.ts +++ b/src/services/admin/subscription.ts @@ -66,7 +66,7 @@ export class SubscriptionService { }); } - public async findOne(where: Record) { + public async findOne(where: Array>) { return await this.subscriptionRepository.findOne({ where: where, relations: ['customer'], diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 7f52ed7c..b5717e54 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -491,6 +491,16 @@ "description": "The URL to redirect to after the customer cancels the checkout", "example": "https://example.com/cancel" }, + "quantity": { + "type": "number", + "description": "The quantity of the product", + "example": 1 + }, + "trialPeriodDays": { + "type": "number", + "description": "The number of days the customer has to pay for the product", + "example": 7 + }, "idempotencyKey": { "type": "string", "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", From 042174bb320dea3fe776f17f8bb2f4b49ea26b82 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Mon, 25 Mar 2024 17:02:54 +0100 Subject: [PATCH 17/49] Get rd of Stripe naming --- src/app.ts | 3 -- src/controllers/admin/subscriptions.ts | 28 ++++++--------- src/controllers/api/account.ts | 2 +- src/database/entities/customer.entity.ts | 2 +- src/database/migrations/AlterCustomerTable.ts | 2 +- src/database/types/types.ts | 2 +- src/services/admin/stripe.ts | 34 +++++++++++++------ src/services/admin/subscription.ts | 9 ++++- src/services/api/customer.ts | 10 +++--- .../track/admin/subscription-submitter.ts | 4 +-- src/services/track/helpers.ts | 2 +- src/services/track/submitter.ts | 2 +- 12 files changed, 55 insertions(+), 45 deletions(-) diff --git a/src/app.ts b/src/app.ts index 69b070b8..c6043c99 100644 --- a/src/app.ts +++ b/src/app.ts @@ -31,7 +31,6 @@ import { ProductController } from './controllers/admin/product.js'; import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; import { WebhookController } from './controllers/admin/webhook.js'; -import { stripeService } from './services/admin/stripe.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -114,8 +113,6 @@ class App { swaggerUi.setup(swaggerAdminDocument) ); this.express.use(Middleware.setStripeClient) - // ToDo: move it to Setup phase. - this.express.use(async (_req, _res, next) => await stripeService.syncAll(next)) } this.express.use(auth.handleError); this.express.use(async (req, res, next) => await auth.accessControl(req, res, next)); diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 777edc1e..e64346ac 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -124,7 +124,7 @@ export class SubscriptionController { ]; static subscriptionListValidator = [ - check('stripeCustomerId').optional().isString().withMessage('customerId should be a string').bail(), + check('paymentProviderId').optional().isString().withMessage('customerId should be a string').bail(), ]; static subscriptionCancelValidator = [ @@ -187,7 +187,7 @@ export class SubscriptionController { const session = await stripe.checkout.sessions.create( { mode: 'subscription', - customer: response.locals.customer.stripeCustomerId, + customer: response.locals.customer.paymentProviderId, line_items: [ { price: price, @@ -261,7 +261,7 @@ export class SubscriptionController { const { returnUrl } = request.body satisfies SubscriptionUpdateRequestBody; try { // Get the subscription object from the DB - const subscription = await SubscriptionService.instance.findOne([{ customer: response.locals.customer }]); + const subscription = await SubscriptionService.instance.findOne({ customer: response.locals.customer }); if (!subscription) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Subscription was not found`, @@ -270,7 +270,7 @@ export class SubscriptionController { // Create portal link const session = await stripe.billingPortal.sessions.create({ - customer: response.locals.customer.stripeCustomerId, + customer: response.locals.customer.paymentProviderId, return_url: returnUrl, }); @@ -299,7 +299,7 @@ export class SubscriptionController { * tags: [Subscription] * parameters: * - in: query - * name: stripeCustomerId + * name: paymentProviderId * schema: * type: string * description: The customer id. If passed - returns filtered by this customer list of subscriptions. @@ -324,12 +324,12 @@ export class SubscriptionController { @syncCustomer public async list(request: Request, response: Response) { const stripe = response.locals.stripe as Stripe; - const stripeCustomerId = response.locals.customer.stripeCustomerId; + const paymentProviderId = response.locals.customer.paymentProviderId; try { // Get the subscriptions - const subscriptions = stripeCustomerId + const subscriptions = paymentProviderId ? await stripe.subscriptions.list({ - customer: stripeCustomerId as string, + customer: paymentProviderId as string, }) : await stripe.subscriptions.list(); @@ -373,20 +373,12 @@ export class SubscriptionController { * $ref: '#/components/schemas/NotFoundError' */ @validate - @syncCustomer + @syncOne async get(request: Request, response: Response) { const stripe = response.locals.stripe as Stripe; try { // Get the subscriptionId from the request - const _sub = await SubscriptionService.instance.findOne([ - { - customer: response.locals.customer, - status: 'active'}, - { - customer: response.locals.customer, - status: 'trialing' - } - ]); + const _sub = await SubscriptionService.instance.findCurrent(response.locals.customer); if (!_sub) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Subscription was not found`, diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 1dbd19d2..65fdc083 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -305,7 +305,7 @@ export class AccountController { } } // 8. Add the Stripe account to the Customer - if (customer.stripeCustomerId === null) { + if (customer.paymentProviderId === null) { eventTracker.submit({ operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { diff --git a/src/database/entities/customer.entity.ts b/src/database/entities/customer.entity.ts index 6d5f8a52..de774c1f 100644 --- a/src/database/entities/customer.entity.ts +++ b/src/database/entities/customer.entity.ts @@ -30,7 +30,7 @@ export class CustomerEntity { type: 'text', nullable: true, }) - stripeCustomerId!: string; + paymentProviderId!: string; @BeforeInsert() setCreatedAt() { diff --git a/src/database/migrations/AlterCustomerTable.ts b/src/database/migrations/AlterCustomerTable.ts index 7792647d..c19df230 100644 --- a/src/database/migrations/AlterCustomerTable.ts +++ b/src/database/migrations/AlterCustomerTable.ts @@ -7,7 +7,7 @@ export class AlterCustomerTable1695740346000 implements MigrationInterface { await queryRunner.addColumn( table_name, new TableColumn({ - name: 'stripeAccountId', + name: 'paymentProviderId', type: 'text', isNullable: true, }) diff --git a/src/database/types/types.ts b/src/database/types/types.ts index 61e38972..eb896d4c 100644 --- a/src/database/types/types.ts +++ b/src/database/types/types.ts @@ -100,7 +100,7 @@ export class Postgres implements AbstractDatabase { AlterOperationTable1695740345978, // Change payment table structure AlterPaymentTable1695740345979, - // Add stripeCustomerId to customer table + // Add paymentProviderId to customer table AlterCustomerTable1695740346000, // Add new category AlterOperationTable1695740346001, diff --git a/src/services/admin/stripe.ts b/src/services/admin/stripe.ts index 5e94cab4..cb27205c 100644 --- a/src/services/admin/stripe.ts +++ b/src/services/admin/stripe.ts @@ -56,7 +56,7 @@ export class StripeService { async syncCustomer(customer: CustomerEntity): Promise { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); for await (const subscription of stripe.subscriptions.list({ - customer: customer.stripeCustomerId, + customer: customer.paymentProviderId, status: 'all', })) { const current = await SubscriptionService.instance.subscriptionRepository.findOne({ @@ -73,15 +73,7 @@ export class StripeService { async syncOne(customer: CustomerEntity): Promise { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - const local = await SubscriptionService.instance.findOne([ - { - customer: customer, - status: 'active'}, - { - customer: customer, - status: 'trialing' - } - ]); + const local = await SubscriptionService.instance.findCurrent(customer) if (!local) { eventTracker.notify({ message: EventTracker.compileBasicNotification( @@ -90,6 +82,28 @@ export class StripeService { ), severity: 'debug', }); + const activeSubs = await stripe.subscriptions.list({ + customer: customer.paymentProviderId, + status: 'active', + }); + const trialSubs = await stripe.subscriptions.list({ + customer: customer.paymentProviderId, + status: 'trialing', + }); + const subs = [...activeSubs.data, ...trialSubs.data]; + if (subs.length > 1) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Multiple active subscriptions found for customer with id ${customer.customerId}`, + 'Subscription syncronization' + ), + severity: 'error', + }); + return; + } + if (subs.length > 0) { + await this.createSubscription(subs[0], customer); + } return; } const subscriptionId = local.subscriptionId; diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts index 9ef45843..0d8ca45f 100644 --- a/src/services/admin/subscription.ts +++ b/src/services/admin/subscription.ts @@ -66,13 +66,20 @@ export class SubscriptionService { }); } - public async findOne(where: Array>) { + public async findOne(where: Record) { return await this.subscriptionRepository.findOne({ where: where, relations: ['customer'], }); } + public async findCurrent(customer: CustomerEntity) { + return await this.subscriptionRepository.findOne({ + where: [{ customer: customer, status: 'active' }, { customer: customer, status: 'trialing' }], + relations: ['customer'], + }); + } + public equals(subscriptionEntity: SubscriptionEntity, subscription: Stripe.Subscription): boolean { const required = subscriptionEntity.subscriptionId === subscription.id && diff --git a/src/services/api/customer.ts b/src/services/api/customer.ts index 85376fef..254290c4 100644 --- a/src/services/api/customer.ts +++ b/src/services/api/customer.ts @@ -42,7 +42,7 @@ export class CustomerService { }; } - public async update(customerId: string, name?: string, stripeCustomerId?: string) { + public async update(customerId: string, name?: string, paymentProviderId?: string) { const existingCustomer = await this.customerRepository.findOneBy({ customerId }); if (!existingCustomer) { throw new Error(`CustomerId not found`); @@ -51,8 +51,8 @@ export class CustomerService { existingCustomer.name = name; } - if (stripeCustomerId) { - existingCustomer.stripeCustomerId = stripeCustomerId; + if (paymentProviderId) { + existingCustomer.paymentProviderId = paymentProviderId; } return await this.customerRepository.save(existingCustomer); } @@ -77,9 +77,9 @@ export class CustomerService { } } - public async findbyStripeCustomerId(stripeCustomerId: string) { + public async findbyPaymentProviderId(paymentProviderId: string) { return await this.customerRepository.findOne({ - where: { stripeCustomerId }, + where: { paymentProviderId: paymentProviderId }, }); } diff --git a/src/services/track/admin/subscription-submitter.ts b/src/services/track/admin/subscription-submitter.ts index 8dfcc30f..bf808a77 100644 --- a/src/services/track/admin/subscription-submitter.ts +++ b/src/services/track/admin/subscription-submitter.ts @@ -40,13 +40,13 @@ export class SubscriptionSubmitter implements IObserver { try { if (!customer) { const customers = await CustomerService.instance.find({ - stripeCustomerId: data.stripeCustomerId, + paymentProviderId: data.paymentProviderId, }); if (customers.length !== 1) { this.notify({ message: EventTracker.compileBasicNotification( - `It should be only 1 Stripe account associated with CaaS customer. Stripe accountId: ${data.stripeCustomerId}.`, + `It should be only 1 Stripe account associated with CaaS customer. Stripe accountId: ${data.paymentProviderId}.`, operation.operation ), severity: 'error', diff --git a/src/services/track/helpers.ts b/src/services/track/helpers.ts index 192c4259..1a25fb3b 100644 --- a/src/services/track/helpers.ts +++ b/src/services/track/helpers.ts @@ -40,7 +40,7 @@ export function builSubmitOperation (subscription: Stripe.Subscription, name: st operation: name, data: { subscriptionId: subscription.id, - stripeCustomerId: subscription.customer as string, + paymentProviderId: subscription.customer as string, status: subscription.status, currentPeriodStart: new Date(subscription.current_period_start * 1000), currentPeriodEnd: new Date(subscription.current_period_end * 1000), diff --git a/src/services/track/submitter.ts b/src/services/track/submitter.ts index acaf1c81..01796f73 100644 --- a/src/services/track/submitter.ts +++ b/src/services/track/submitter.ts @@ -10,7 +10,7 @@ export interface ISubmitStripeCustomerCreateData { } export interface ISubmitSubscriptionData { - stripeCustomerId: string; + paymentProviderId: string; status: string; currentPeriodStart: Date; currentPeriodEnd: Date; From b36693575401ec9969f0e43e7ce7cdc8ef3b7156 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 26 Mar 2024 19:46:44 +0100 Subject: [PATCH 18/49] Small cleeanups and refactoring --- src/helpers/helpers.ts | 19 ------- src/middleware/auth/base-auth-handler.ts | 7 +-- src/middleware/authentication.ts | 68 ++++++++++-------------- 3 files changed, 31 insertions(+), 63 deletions(-) diff --git a/src/helpers/helpers.ts b/src/helpers/helpers.ts index df51363a..3148ca78 100644 --- a/src/helpers/helpers.ts +++ b/src/helpers/helpers.ts @@ -237,22 +237,3 @@ export async function decryptPrivateKey(encryptedPrivateKeyHex: string, ivHex: s return secretKey; } - -// export function eventDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) { -// const originalMethod = descriptor.value; -// descriptor.value = async function (...args: any[]) { -// const response = await originalMethod.apply(this, args) as Response; - -// response.on('finish', () => { -// console.log("Response finished"); -// if (response.statusCode !== StatusCodes.OK) { -// eventTracker.emit('notify', { -// message: `Response status code: ${response.statusCode}. Message to client: ${response.statusMessage}`, -// severity: 'error' -// } satisfies INotifyMessage) -// } -// }) -// return response; -// }; -// return descriptor; -// } diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index 18ae8e17..89c59454 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -25,8 +25,8 @@ export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { if (!rule) { return this.returnError( - StatusCodes.INTERNAL_SERVER_ERROR, - `Internal error. There is no auth rule for such request. Please contact administrator` + StatusCodes.BAD_REQUEST, + `Bad Request. No atuh rules for handling such request: ${request.method} ${request.path}` ); } // If the rule is not found - skip the auth check @@ -148,8 +148,9 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { } } - public setOAuthProvider(oauthProvider: IOAuthProvider): void { + public setOAuthProvider(oauthProvider: IOAuthProvider): IAuthHandler { this.oauthProvider = oauthProvider; + return this; } public setUserInfoStrategy(strategy: IUserInfoFetcher): void { diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 2f3e6fa7..227510c1 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -24,13 +24,13 @@ dotenv.config(); const { ENABLE_EXTERNAL_DB } = process.env; export class Authentication { - private authHandler: BaseAuthHandler; + private initHandler: BaseAuthHandler; private isSetup = false; private logToHelper: LogToHelper; constructor() { // Initial auth handler - this.authHandler = new AccountAuthHandler(); + this.initHandler = new AccountAuthHandler(); this.logToHelper = new LogToHelper(); } @@ -42,41 +42,26 @@ export class Authentication { error: _r.error, }); } - const oauthProvider = new LogToProvider(); - oauthProvider.setHelper(this.logToHelper); - - const didAuthHandler = new DidAuthHandler(); - const keyAuthHandler = new KeyAuthHandler(); - const credentialAuthHandler = new CredentialAuthHandler(); - const credentialStatusAuthHandler = new CredentialStatusAuthHandler(); - const resourceAuthHandler = new ResourceAuthHandler(); - const presentationAuthHandler = new PresentationAuthHandler(); - const authInfoHandler = new AuthInfoHandler(); - - const adminAuthHandler = new AdminHandler(); - - // Set logToHelper. We do it for avoiding re-asking LogToHelper.setup() in each auth handler - // cause it does a lot of requests to LogTo - this.authHandler.setOAuthProvider(oauthProvider); - didAuthHandler.setOAuthProvider(oauthProvider); - keyAuthHandler.setOAuthProvider(oauthProvider); - credentialAuthHandler.setOAuthProvider(oauthProvider); - credentialStatusAuthHandler.setOAuthProvider(oauthProvider); - resourceAuthHandler.setOAuthProvider(oauthProvider); - presentationAuthHandler.setOAuthProvider(oauthProvider); - authInfoHandler.setOAuthProvider(oauthProvider); - adminAuthHandler.setOAuthProvider(oauthProvider); - - // Set chain of responsibility - this.authHandler - .setNext(didAuthHandler) - .setNext(keyAuthHandler) - .setNext(credentialAuthHandler) - .setNext(credentialStatusAuthHandler) - .setNext(resourceAuthHandler) - .setNext(presentationAuthHandler) - .setNext(authInfoHandler) - .setNext(adminAuthHandler); + const fillChain = async (handler: BaseAuthHandler[]) => { + const oauthProvider = new LogToProvider(); + oauthProvider.setHelper(this.logToHelper); + for (let i = 0; i < handler.length - 1; i++) { + handler[i].setOAuthProvider(oauthProvider); + handler[i].setNext(handler[i + 1]); + } + }; + + fillChain([ + this.initHandler, + new DidAuthHandler(), + new KeyAuthHandler(), + new CredentialAuthHandler(), + new CredentialStatusAuthHandler(), + new ResourceAuthHandler(), + new PresentationAuthHandler(), + new AuthInfoHandler(), + new AdminHandler(), + ]) this.isSetup = true; } @@ -93,8 +78,9 @@ export class Authentication { } public async accessControl(request: Request, response: Response, next: NextFunction) { - if (this.authHandler.skipPath(request.path)) return next(); + if (this.initHandler.skipPath(request.path)) return next(); + // ToDo: Make it more readable if (ENABLE_EXTERNAL_DB === 'false') { if (['/account', '/did/create', '/key/create'].includes(request.path)) { return response.status(StatusCodes.METHOD_NOT_ALLOWED).json({ @@ -115,7 +101,7 @@ export class Authentication { } public async withLogtoWrapper(request: Request, response: Response, next: NextFunction) { - if (this.authHandler.skipPath(request.path)) return next(); + if (this.initHandler.skipPath(request.path)) return next(); try { return withLogto({ ...configLogToExpress, scopes: ['roles'] })(request, response, next); } catch (err) { @@ -134,11 +120,11 @@ export class Authentication { public async guard(request: Request, response: Response, next: NextFunction) { const { provider } = request.body as { claim: string; provider: string }; - if (this.authHandler.skipPath(request.path)) return next(); + if (this.initHandler.skipPath(request.path)) return next(); try { // If response got back that means error was raised - const _resp = await this.authHandler.handle(request, response); + const _resp = await this.initHandler.handle(request, response); if (_resp.status !== StatusCodes.OK) { return response.status(_resp.status).json({ error: _resp.error, From 9ef7459c8733b6a43658e6512b11441dfebe27b2 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Wed, 27 Mar 2024 16:26:05 +0100 Subject: [PATCH 19/49] Make swagger changes --- src/static/swagger-admin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index b5717e54..8b44d945 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -261,7 +261,7 @@ "parameters": [ { "in": "query", - "name": "stripeCustomerId", + "name": "paymentProviderId", "schema": { "type": "string", "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." From dea3a2334a5551b111053fda18538190baaf368a Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Mon, 1 Apr 2024 11:52:32 +0200 Subject: [PATCH 20/49] Add encryption/decryption to API key --- src/controllers/api/account.ts | 1 + src/database/entities/api.key.entity.ts | 14 +++--- src/database/migrations/AlterAPIKeyTable.ts | 23 ++++++++++ src/database/types/types.ts | 3 ++ src/services/api/api-key.ts | 48 +++++++++++++++------ 5 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 src/database/migrations/AlterAPIKeyTable.ts diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 65fdc083..9fc99d35 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -90,6 +90,7 @@ export class AccountController { * tags: [Account] * summary: Fetch IdToken. * description: This endpoint returns IdToken as JWT with list of user roles inside + * deprecated: true * responses: * 200: * description: The request was successful. diff --git a/src/database/entities/api.key.entity.ts b/src/database/entities/api.key.entity.ts index 95c3e4e1..1725e2b0 100644 --- a/src/database/entities/api.key.entity.ts +++ b/src/database/entities/api.key.entity.ts @@ -7,15 +7,19 @@ dotenv.config(); @Entity('apiKey') export class APIKeyEntity { - @PrimaryGeneratedColumn('uuid') - apiKeyId!: string; - @Column({ type: 'text', nullable: false, + primary: true, }) apiKey!: string; + @Column({ + type: 'boolean', + nullable: false, + }) + revoked!: boolean; + @Column({ type: 'timestamptz', nullable: false, @@ -56,11 +60,11 @@ export class APIKeyEntity { return this.expiresAt < new Date(); } - constructor(apiKeyId: string, apiKey: string, expiresAt: Date, customer: CustomerEntity, user: UserEntity) { - this.apiKeyId = apiKeyId; + constructor(apiKey: string, expiresAt: Date, customer: CustomerEntity, user: UserEntity, revoked = false) { this.apiKey = apiKey; this.expiresAt = expiresAt; this.customer = customer; this.user = user; + this.revoked = revoked; } } diff --git a/src/database/migrations/AlterAPIKeyTable.ts b/src/database/migrations/AlterAPIKeyTable.ts new file mode 100644 index 00000000..3e0b8746 --- /dev/null +++ b/src/database/migrations/AlterAPIKeyTable.ts @@ -0,0 +1,23 @@ +import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AlterAPIKeyTableAddRevoked1695740346004 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const table_name = 'apiKey'; + + await queryRunner.addColumn( + table_name, + new TableColumn({ + name: 'revoked', + type: 'boolean', + isNullable: true, + }) + ); + + // Remove unused apiKeyId column + await queryRunner.dropColumn(table_name, 'apiKeyId'); + } + + public async down(queryRunner: QueryRunner): Promise { + throw new Error('illegal_operation: cannot roll back initial migration'); + } +} diff --git a/src/database/types/types.ts b/src/database/types/types.ts index eb896d4c..7008623b 100644 --- a/src/database/types/types.ts +++ b/src/database/types/types.ts @@ -36,6 +36,7 @@ import { AlterCustomerTable1695740346000 } from '../migrations/AlterCustomerTabl import { AlterOperationTable1695740346001 } from '../migrations/AlterOperationTableNewCategory.js'; import { SubscriptionEntity } from '../entities/subscription.entity.js'; import { CreateSubscritpionTable1695740346003 } from '../migrations/CreateSubscriptionTable.js'; +import { AlterAPIKeyTableAddRevoked1695740346004 } from '../migrations/AlterAPIKeyTable.js'; dotenv.config(); const { EXTERNAL_DB_CONNECTION_URL, EXTERNAL_DB_CERT } = process.env; @@ -106,6 +107,8 @@ export class Postgres implements AbstractDatabase { AlterOperationTable1695740346001, // Add subscription table CreateSubscritpionTable1695740346003, + // Add revoked field to APIKey table + AlterAPIKeyTableAddRevoked1695740346004, ], entities: [ ...Entities, diff --git a/src/services/api/api-key.ts b/src/services/api/api-key.ts index 63bdedb8..d8e24773 100644 --- a/src/services/api/api-key.ts +++ b/src/services/api/api-key.ts @@ -7,18 +7,21 @@ import type { CustomerEntity } from '../../database/entities/customer.entity.js' import { APIKeyEntity } from '../../database/entities/api.key.entity.js'; import type { UserEntity } from '../../database/entities/user.entity.js'; import { v4 } from 'uuid'; +import { SecretBox } from '@veramo/kms-local'; dotenv.config(); export class APIKeyService { public apiKeyRepository: Repository; + private secretBox: SecretBox; public static instance = new APIKeyService(); constructor() { this.apiKeyRepository = Connection.instance.dbConnection.getRepository(APIKeyEntity); + this.secretBox = new SecretBox(process.env.EXTERNAL_DB_ENCRYPTION_KEY) } - public async create(apiKey: string, customer: CustomerEntity, user: UserEntity): Promise { + public async create(apiKey: string, customer: CustomerEntity, user: UserEntity, revoked = false): Promise { const apiKeyId = v4(); if (!apiKey) { throw new Error('API key is not specified'); @@ -32,26 +35,24 @@ export class APIKeyService { if (!customer) { throw new Error('Customer id is not specified'); } + const encryptedAPIKey = await this.secretBox.encrypt(apiKey); const expiresAt = await this.getExpiryDate(apiKey); - const apiKeyEntity = new APIKeyEntity(apiKeyId, apiKey, expiresAt, customer, user); + const apiKeyEntity = new APIKeyEntity(encryptedAPIKey, expiresAt, customer, user, revoked); const apiKeyRecord = (await this.apiKeyRepository.insert(apiKeyEntity)).identifiers[0]; if (!apiKeyRecord) throw new Error(`Cannot create a new API key`); return apiKeyEntity; } public async update( - apiKeyId: string, - apiKey?: string, + apiKey: string, expiresAt?: Date, customer?: CustomerEntity, - user?: UserEntity + user?: UserEntity, + revoked?: boolean ) { - const existingAPIKey = await this.apiKeyRepository.findOneBy({ apiKeyId }); + const existingAPIKey = await this.apiKeyRepository.findOneBy({ apiKey }); if (!existingAPIKey) { - throw new Error(`API with key id ${apiKeyId} not found`); - } - if (apiKey) { - existingAPIKey.apiKey = apiKey; + throw new Error(`API with key id ${apiKey} not found`); } if (expiresAt) { existingAPIKey.expiresAt = expiresAt; @@ -62,23 +63,42 @@ export class APIKeyService { if (user) { existingAPIKey.user = user; } + if (revoked) { + existingAPIKey.revoked = revoked; + } return await this.apiKeyRepository.save(existingAPIKey); } - public async get(apiKeyId: string) { - return await this.apiKeyRepository.findOne({ - where: { apiKeyId }, + public async get(apiKey: string) { + const apiKeyEntity = await this.apiKeyRepository.findOne({ + where: { apiKey }, relations: ['customer', 'user'], }); + if (!apiKeyEntity) { + throw new Error(`API key ${apiKey} not found`); + } + + if (this.secretBox && apiKeyEntity.apiKey) { + apiKeyEntity.apiKey = await this.secretBox.decrypt(apiKeyEntity.apiKey); + } + return apiKeyEntity; } public async find(where: Record) { try { - return await this.apiKeyRepository.find({ + const apiKeyList = await this.apiKeyRepository.find({ where: where, relations: ['customer', 'user'], }); + // decrypt the API keys + if (this.secretBox) { + for (const apiKey of apiKeyList) { + apiKey.apiKey = await this.secretBox.decrypt(apiKey.apiKey); + } + } + return apiKeyList; + } catch { return []; } From 0413eb299931d9c07ceae363a9f3307709a36542 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Mon, 1 Apr 2024 18:37:05 +0200 Subject: [PATCH 21/49] Change API logic a bit and introduce API key endpoints --- README.md | 3 + package-lock.json | 36 +- package.json | 1 + src/app.ts | 16 +- src/controllers/admin/api-key.ts | 374 + src/controllers/admin/subscriptions.ts | 3 +- src/controllers/admin/webhook.ts | 2 +- src/database/entities/api.key.entity.ts | 18 +- src/database/migrations/AlterAPIKeyTable.ts | 31 +- src/database/types/types.ts | 4 +- .../auth/routes/admin/admin-auth.ts | 11 + src/middleware/authentication.ts | 2 +- src/services/admin/api-key.ts | 160 + src/services/admin/stripe.ts | 151 +- src/services/admin/subscription.ts | 5 +- src/services/api/api-key.ts | 111 - src/services/identity/abstract.ts | 4 + src/services/identity/index.ts | 1 + src/services/identity/postgres.ts | 21 +- src/services/track/helpers.ts | 4 +- src/services/track/submitter.ts | 2 +- src/static/swagger-admin-options.json | 5 +- src/static/swagger-admin.json | 1680 ++-- src/static/swagger-api.json | 6881 ++++++++--------- src/types/constants.ts | 11 + src/types/environment.d.ts | 3 + src/types/portal.ts | 48 + src/types/swagger-admin-types.ts | 149 + 28 files changed, 5311 insertions(+), 4426 deletions(-) create mode 100644 src/controllers/admin/api-key.ts create mode 100644 src/services/admin/api-key.ts delete mode 100644 src/services/api/api-key.ts diff --git a/README.md b/README.md index e2158ef1..cf79aca4 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,9 @@ By default, `ENABLE_AUTHENTICATION` is set to off/`false`. To enable external Ve 2. `LOGTO_WEBHOOK_SECRET`: Webhook secret to authenticate incoming webhook requests from LogTo. 5. **Miscellaneous** 1. `COOKIE_SECRET`: Secret for cookie encryption. + 2. `API_KEY_PREFIX` (optional): Prefix for API keys. (Default "caas") + 3. `API_KEY_LENGTH` (optional): Length of API keys. (Default 32) + 4. `API_KEY_EXPIRATION` (optional): Expiration time for API keys in month. (Default 1 month) #### Faucet settings diff --git a/package-lock.json b/package-lock.json index 7da177b5..c12225e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cheqd/credential-service", - "version": "2.18.1-develop.1", + "version": "2.18.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cheqd/credential-service", - "version": "2.18.1-develop.1", + "version": "2.18.3", "license": "Apache-2.0", "dependencies": { "@cheqd/did-provider-cheqd": "^3.7.0", @@ -43,6 +43,7 @@ "express-validator": "^7.0.1", "helmet": "^7.1.0", "http-status-codes": "^2.3.0", + "js-sha3": "^0.9.3", "json-stringify-safe": "^5.0.1", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", @@ -3711,6 +3712,11 @@ "js-sha3": "0.8.0" } }, + "node_modules/@ethersproject/keccak256/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/@ethersproject/logger": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", @@ -10906,6 +10912,11 @@ "uint8arrays": "^3.0.0" } }, + "node_modules/@verida/account/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/@verida/account/node_modules/multiformats": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", @@ -10988,6 +10999,11 @@ "resolved": "https://registry.npmjs.org/did-resolver/-/did-resolver-3.2.2.tgz", "integrity": "sha512-Eeo2F524VM5N3W4GwglZrnul2y6TLTwMQP3In62JdG34NZoqihYyOZLk+5wUW8sSgvIYIcJM8Dlt3xsdKZZ3tg==" }, + "node_modules/@verida/client-ts/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/@verida/client-ts/node_modules/multiformats": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", @@ -11878,6 +11894,11 @@ "@ethersproject/wordlists": "5.5.0" } }, + "node_modules/@verida/vda-common/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/@verida/vda-common/node_modules/ws": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", @@ -15051,6 +15072,11 @@ "uint8arrays": "^3.0.0" } }, + "node_modules/credential-status/node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/credential-status/node_modules/multiformats": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", @@ -20344,9 +20370,9 @@ "integrity": "sha512-NPrWuHFxFUknr1KqJRDgUQPexQF0uIJWjeT+2KjEePhitQxQEx5EJBG1lVn5/hc8aLycTpXrDOgPQ6Zq+EDiTA==" }, "node_modules/js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz", + "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==" }, "node_modules/js-tokens": { "version": "4.0.0", diff --git a/package.json b/package.json index 48bf9b93..2d3e7a0f 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "express-validator": "^7.0.1", "helmet": "^7.1.0", "http-status-codes": "^2.3.0", + "js-sha3": "^0.9.3", "json-stringify-safe": "^5.0.1", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", diff --git a/src/app.ts b/src/app.ts index c6043c99..0cb65d33 100644 --- a/src/app.ts +++ b/src/app.ts @@ -31,6 +31,7 @@ import { ProductController } from './controllers/admin/product.js'; import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; import { WebhookController } from './controllers/admin/webhook.js'; +import { APIKeyController } from './controllers/admin/api-key.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -112,7 +113,7 @@ class App { swaggerUi.serveFiles(swaggerAdminDocument), swaggerUi.setup(swaggerAdminDocument) ); - this.express.use(Middleware.setStripeClient) + this.express.use(Middleware.setStripeClient); } this.express.use(auth.handleError); this.express.use(async (req, res, next) => await auth.accessControl(req, res, next)); @@ -226,7 +227,11 @@ class App { // Portal // Product if (process.env.STRIPE_ENABLED === 'true') { - app.get('/admin/product/list', ProductController.productListValidator, new ProductController().listProducts); + app.get( + '/admin/product/list', + ProductController.productListValidator, + new ProductController().listProducts + ); app.get( '/admin/product/get/:productId', ProductController.productGetValidator, @@ -264,6 +269,13 @@ class App { new SubscriptionController().resume ); + // API key + app.post('/admin/api-key/create', APIKeyController.apiKeyCreateValidator, new APIKeyController().create); + app.post('/admin/api-key/update', APIKeyController.apiKeyUpdateValidator, new APIKeyController().update); + app.get('/admin/api-key/get', APIKeyController.apiKeyGetValidator, new APIKeyController().get); + app.get('/admin/api-key/list', APIKeyController.apiKeyListValidator, new APIKeyController().list); + app.delete('/admin/api-key/revoke', APIKeyController.apiKeyRevokeValidator, new APIKeyController().revoke); + // Webhook app.post('/admin/webhook', new WebhookController().handleWebhook); } diff --git a/src/controllers/admin/api-key.ts b/src/controllers/admin/api-key.ts new file mode 100644 index 00000000..3341e079 --- /dev/null +++ b/src/controllers/admin/api-key.ts @@ -0,0 +1,374 @@ +import type { Request, Response } from 'express'; +import * as dotenv from 'dotenv'; +import { StatusCodes } from 'http-status-codes'; +import { check } from '../validator/index.js'; +import { validate } from '../validator/decorator.js'; +import { APIKeyService } from '../../services/admin/api-key.js'; +import type { + APIKeyCreateRequestBody, + APIKeyCreateResponseBody, + APIKeyCreateUnsuccessfulResponseBody, + APIKeyGetResponseBody, + APIKeyGetUnsuccessfulResponseBody, + APIKeyListResponseBody, + APIKeyListUnsuccessfulResponseBody, + APIKeyRevokeRequestBody, + APIKeyRevokeResponseBody, + APIKeyRevokeUnsuccessfulResponseBody, + APIKeyUpdateRequestBody, + APIKeyUpdateResponseBody, + APIKeyUpdateUnsuccessfulResponseBody, +} from '../../types/portal.js'; +import { EventTracker, eventTracker } from '../../services/track/tracker.js'; +import { OperationNameEnum } from '../../types/constants.js'; + +dotenv.config(); + +export class APIKeyController { + static apiKeyCreateValidator = [ + check('expiresAt') + .optional() + .isISO8601() + .toDate() + .withMessage('Invalid date format') + .custom((value) => { + if (value < new Date()) { + throw new Error('expiresAt must be in the future'); + } + return true; + }) + .toDate() + .bail(), + ]; + static apiKeyUpdateValidator = [ + check('apiKey') + .exists() + .withMessage('API key is not specified') + .bail() + .isString() + .withMessage('Invalid API key') + .bail(), + check('expiresAt') + .optional() + .isISO8601() + .toDate() + .withMessage('Invalid date format') + .custom((value) => { + if (value < new Date()) { + throw new Error('expiresAt must be in the future'); + } + return true; + }) + .toDate() + .bail(), + check('revoked').optional().isBoolean().withMessage('Invalid boolean value').bail(), + ]; + static apiKeyRevokeValidator = [ + check('apiKey') + .exists() + .withMessage('API key is not specified') + .bail() + .isString() + .withMessage('Invalid API key') + .bail(), + ]; + static apiKeyListValidator = [ + // No validation + ]; + static apiKeyGetValidator = [check('apiKey').optional().isString().withMessage('Invalid API key').bail()]; + + /** + * @openapi + * + * /admin/api-key/create: + * post: + * summary: Create a new API key + * description: Create a new API key + * tags: [API Key] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/APIKeyCreateRequestBody' + * responses: + * 201: + * description: A new API key has been created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/APIKeyCreateResponseBody' + * 400: + * $ref: '#/components/schemas/APIKeyCreateUnsuccessfulResponseBody' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + */ + @validate + public async create(request: Request, response: Response) { + try { + const { expiresAt } = request.body satisfies APIKeyCreateRequestBody; + + const apiKey = APIKeyService.instance.generateAPIKey(); + const apiKeyEntity = await APIKeyService.instance.create(apiKey, response.locals.user, expiresAt); + if (!apiKeyEntity) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: 'Cannot create a new API key', + }); + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `API key for customer: ${response.locals.customer.customerId} has been created`, + OperationNameEnum.API_KEY_CREATE + ), + severity: 'info', + }); + return response.status(StatusCodes.CREATED).json({ + apiKey: apiKeyEntity.apiKey, + expiresAt: apiKeyEntity.expiresAt.toISOString(), + revoked: apiKeyEntity.revoked, + } satisfies APIKeyCreateResponseBody); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies APIKeyCreateUnsuccessfulResponseBody); + } + } + + /** + * @openapi + * + * /admin/api-key/update: + * post: + * summary: Update an existing API key + * description: Update an existing API key + * tags: [API Key] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/APIKeyUpdateRequestBody' + * responses: + * 200: + * description: The API key has been updated + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/APIKeyUpdateResponseBody' + * 400: + * $ref: '#/components/schemas/APIKeyUpdateUnsuccessfulResponseBody' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * + */ + @validate + public async update(request: Request, response: Response) { + try { + const { apiKey, expiresAt, revoked } = request.body satisfies APIKeyUpdateRequestBody; + const apiKeyEntity = await APIKeyService.instance.update(apiKey, expiresAt, revoked); + if (!apiKeyEntity) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: 'Cannot update API key cause it\'s not found', + } satisfies APIKeyUpdateUnsuccessfulResponseBody); + } + + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `API key for customer: ${response.locals.customer.customerId} has been updated`, + OperationNameEnum.API_KEY_UPDATE + ), + severity: 'info', + }); + return response.status(StatusCodes.OK).json({ + apiKey: apiKeyEntity.apiKey, + expiresAt: apiKeyEntity.expiresAt.toISOString(), + revoked: apiKeyEntity.revoked, + } satisfies APIKeyUpdateResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies APIKeyUpdateUnsuccessfulResponseBody); + } + } + + /** + * @openapi + * + * /admin/api-key/revoke: + * delete: + * summary: Revoke an existing API key + * description: Revoke an existing API key + * tags: [API Key] + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/APIKeyRevokeRequestBody' + * responses: + * 200: + * description: The API key has been revoked + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/APIKeyRevokeResponseBody' + * 400: + * $ref: '#/components/schemas/APIKeyRevokeUnsuccessfulResponseBody' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * + */ + @validate + public async revoke(request: Request, response: Response) { + try { + const { apiKey } = request.body satisfies APIKeyRevokeRequestBody; + const apiKeyEntity = await APIKeyService.instance.revoke(apiKey); + if (!apiKeyEntity) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: 'Cannot revoke API key', + } satisfies APIKeyRevokeUnsuccessfulResponseBody); + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `API key for customer: ${response.locals.customer.customerId} has been revoked`, + OperationNameEnum.API_KEY_REVOKE + ), + severity: 'info', + }); + return response.status(StatusCodes.OK).json({ + apiKey: apiKeyEntity.apiKey, + revoked: apiKeyEntity.revoked, + } satisfies APIKeyRevokeResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies APIKeyRevokeUnsuccessfulResponseBody); + } + } + + /** + * @openapi + * + * /admin/api-key/list: + * get: + * summary: List all API keys + * description: List all API keys + * tags: [API Key] + * responses: + * 200: + * description: A list of API keys + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/APIKeyListResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + * + */ + @validate + public async list(request: Request, response: Response) { + try { + const apiKeyList = await APIKeyService.instance.find({ + customer: response.locals.customer, + }); + const keys = apiKeyList.map((apiKey) => { + return { + apiKey: apiKey.apiKey, + expiresAt: apiKey.expiresAt.toISOString(), + revoked: apiKey.revoked, + }; + }); + return response.status(StatusCodes.OK).json({ + apiKeys: keys, + } satisfies APIKeyListResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies APIKeyListUnsuccessfulResponseBody); + } + } + + /** + * @openapi + * + * /admin/api-key/get: + * get: + * summary: Get an API key + * description: Get an API key. If the API key is not provided, the latest not revoked API key it returns. + * tags: [API Key] + * parameters: + * - name: apiKey + * in: query + * required: false + * schema: + * type: string + * responses: + * 200: + * description: The API key + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/APIKeyGetResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + * + */ + @validate + public async get(request: Request, response: Response) { + try { + const apiKey = request.query.apiKey as string; + if (apiKey) { + const apiKeyEntity = await APIKeyService.instance.get(apiKey); + if (!apiKeyEntity) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: 'API key not found', + }); + } + return response.status(StatusCodes.OK).json({ + apiKey: apiKeyEntity.apiKey, + expiresAt: apiKeyEntity.expiresAt.toISOString(), + revoked: apiKeyEntity.revoked, + }); + } + // Otherwise try to get the latest not revoked API key + const keys = await APIKeyService.instance.find( + { + customer: response.locals.customer, + revoked: false, + }, + { + createdAt: 'DESC', + } + ); + if (keys.length == 0) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: 'API key not found', + }); + } + return response.status(StatusCodes.OK).json({ + apiKey: keys[0].apiKey, + expiresAt: keys[0].expiresAt.toISOString(), + revoked: keys[0].revoked, + } satisfies APIKeyGetResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies APIKeyGetUnsuccessfulResponseBody); + } + } +} diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index e64346ac..9c21b2bf 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -84,7 +84,6 @@ export function syncOne(target: any, key: string, descriptor: PropertyDescriptor return descriptor; } - export class SubscriptionController { static subscriptionCreateValidator = [ check('price') @@ -198,7 +197,7 @@ export class SubscriptionController { cancel_url: cancelURL, subscription_data: { trial_period_days: trialPeriodDays, - } + }, }, { idempotencyKey, diff --git a/src/controllers/admin/webhook.ts b/src/controllers/admin/webhook.ts index c105c924..e232b712 100644 --- a/src/controllers/admin/webhook.ts +++ b/src/controllers/admin/webhook.ts @@ -15,7 +15,7 @@ export class WebhookController { let event = request.body; let subscription; let status; - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY) + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); if (!process.env.STRIPE_WEBHOOK_SECRET) { await eventTracker.notify({ diff --git a/src/database/entities/api.key.entity.ts b/src/database/entities/api.key.entity.ts index 1725e2b0..161f0ed7 100644 --- a/src/database/entities/api.key.entity.ts +++ b/src/database/entities/api.key.entity.ts @@ -1,4 +1,4 @@ -import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import * as dotenv from 'dotenv'; import { CustomerEntity } from './customer.entity.js'; @@ -12,6 +12,12 @@ export class APIKeyEntity { nullable: false, primary: true, }) + apiKeyHash!: string; + + @Column({ + type: 'text', + nullable: false, + }) apiKey!: string; @Column({ @@ -60,7 +66,15 @@ export class APIKeyEntity { return this.expiresAt < new Date(); } - constructor(apiKey: string, expiresAt: Date, customer: CustomerEntity, user: UserEntity, revoked = false) { + constructor( + apiKeyHash: string, + apiKey: string, + expiresAt: Date, + customer: CustomerEntity, + user: UserEntity, + revoked = false + ) { + this.apiKeyHash = apiKeyHash; this.apiKey = apiKey; this.expiresAt = expiresAt; this.customer = customer; diff --git a/src/database/migrations/AlterAPIKeyTable.ts b/src/database/migrations/AlterAPIKeyTable.ts index 3e0b8746..23e4f4b7 100644 --- a/src/database/migrations/AlterAPIKeyTable.ts +++ b/src/database/migrations/AlterAPIKeyTable.ts @@ -1,6 +1,8 @@ import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm'; +import pkg from 'js-sha3'; +const { sha3_512 } = pkg; -export class AlterAPIKeyTableAddRevoked1695740346004 implements MigrationInterface { +export class AlterAPIKeyTable1695740346004 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const table_name = 'apiKey'; @@ -15,6 +17,33 @@ export class AlterAPIKeyTableAddRevoked1695740346004 implements MigrationInterfa // Remove unused apiKeyId column await queryRunner.dropColumn(table_name, 'apiKeyId'); + // Add column apiKeyHash + await queryRunner.addColumn( + table_name, + new TableColumn({ + name: 'apiKeyHash', + type: 'text', + isNullable: true, + }) + ); + // Data migration + // Make all current API keys as revoked cause we need to force API key recreation + for (const apiKey of await queryRunner.query(`SELECT * FROM "${table_name}"`)) { + await queryRunner.query( + `UPDATE "${table_name}" SET "apiKeyHash" = '${sha3_512(apiKey.apiKey)}', "revoked" = 'true' WHERE "apiKey" = '${apiKey.apiKey}'` + ); + } + // Make apiKeyHash not nullable + await queryRunner.changeColumn( + table_name, + 'apiKeyHash', + new TableColumn({ + name: 'apiKeyHash', + type: 'text', + isNullable: false, + isPrimary: true, + }) + ); } public async down(queryRunner: QueryRunner): Promise { diff --git a/src/database/types/types.ts b/src/database/types/types.ts index 7008623b..28c6fa1b 100644 --- a/src/database/types/types.ts +++ b/src/database/types/types.ts @@ -36,7 +36,7 @@ import { AlterCustomerTable1695740346000 } from '../migrations/AlterCustomerTabl import { AlterOperationTable1695740346001 } from '../migrations/AlterOperationTableNewCategory.js'; import { SubscriptionEntity } from '../entities/subscription.entity.js'; import { CreateSubscritpionTable1695740346003 } from '../migrations/CreateSubscriptionTable.js'; -import { AlterAPIKeyTableAddRevoked1695740346004 } from '../migrations/AlterAPIKeyTable.js'; +import { AlterAPIKeyTable1695740346004 } from '../migrations/AlterAPIKeyTable.js'; dotenv.config(); const { EXTERNAL_DB_CONNECTION_URL, EXTERNAL_DB_CERT } = process.env; @@ -108,7 +108,7 @@ export class Postgres implements AbstractDatabase { // Add subscription table CreateSubscritpionTable1695740346003, // Add revoked field to APIKey table - AlterAPIKeyTableAddRevoked1695740346004, + AlterAPIKeyTable1695740346004, ], entities: [ ...Entities, diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index 4c10144f..21ec9dae 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -48,6 +48,17 @@ export class AdminHandler extends BaseAuthHandler { this.registerRoute('/admin/product/list', 'GET', 'admin:product:list:mainnet', { skipNamespace: true }); this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:testnet', { skipNamespace: true }); this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:mainnet', { skipNamespace: true }); + // API Key + this.registerRoute('/admin/api-key/create', 'POST', 'admin:api-key:create:testnet', { skipNamespace: true }); + this.registerRoute('/admin/api-key/create', 'POST', 'admin:api-key:create:mainnet', { skipNamespace: true }); + this.registerRoute('/admin/api-key/update', 'POST', 'admin:api-key:update:testnet', { skipNamespace: true }); + this.registerRoute('/admin/api-key/update', 'POST', 'admin:api-key:update:mainnet', { skipNamespace: true }); + this.registerRoute('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:testnet', { skipNamespace: true }); + this.registerRoute('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:mainnet', { skipNamespace: true }); + this.registerRoute('/admin/api-key/get', 'GET', 'admin:api-key:get:testnet', { skipNamespace: true }); + this.registerRoute('/admin/api-key/get', 'GET', 'admin:api-key:get:mainnet', { skipNamespace: true }); + this.registerRoute('/admin/api-key/list', 'GET', 'admin:api-key:list:testnet', { skipNamespace: true }); + this.registerRoute('/admin/api-key/list', 'GET', 'admin:api-key:list:mainnet', { skipNamespace: true }); } public async handle(request: Request, response: Response): Promise { if (!request.path.includes('/admin/')) { diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 227510c1..695be967 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -61,7 +61,7 @@ export class Authentication { new PresentationAuthHandler(), new AuthInfoHandler(), new AdminHandler(), - ]) + ]); this.isSetup = true; } diff --git a/src/services/admin/api-key.ts b/src/services/admin/api-key.ts new file mode 100644 index 00000000..185ec1c7 --- /dev/null +++ b/src/services/admin/api-key.ts @@ -0,0 +1,160 @@ +import type { Repository } from 'typeorm'; +import { decodeJWT } from 'did-jwt'; +import { randomBytes } from 'crypto'; +import { Connection } from '../../database/connection/connection.js'; + +import * as dotenv from 'dotenv'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; +import { APIKeyEntity } from '../../database/entities/api.key.entity.js'; +import type { UserEntity } from '../../database/entities/user.entity.js'; +import { SecretBox } from '@veramo/kms-local'; +import { API_KEY_LENGTH, API_KEY_PREFIX, API_KEY_EXPIRATION } from '../../types/constants.js'; +import pkg from 'js-sha3'; +dotenv.config(); + +const { sha3_512 } = pkg; + +// Returns the decrypted API key +export function decryptAPIKey(target: any, key: string, descriptor: PropertyDescriptor | undefined) { + // save a reference to the original method this way we keep the values currently in the + // descriptor and don't overwrite what another decorator might have done to the descriptor. + if (descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(target, key) as PropertyDescriptor; + } + + const originalMethod = descriptor.value; + + //editing the descriptor/value parameter + descriptor.value = async function (...args: any[]) { + const decryptOne = async (entity: APIKeyEntity) => { + if (entity && entity.apiKey) { + entity.apiKey = await APIKeyService.instance.decryptAPIKey(entity.apiKey); + } + return entity; + }; + const entity = await originalMethod.apply(this, args); + if (Array.isArray(entity)) { + for (const apiKey of entity) { + await decryptOne(apiKey); + } + } else { + await decryptOne(entity); + } + return entity; + }; + + // return edited descriptor as opposed to overwriting the descriptor + return descriptor; +} + +export class APIKeyService { + public apiKeyRepository: Repository; + private secretBox: SecretBox; + + public static instance = new APIKeyService(); + + constructor() { + this.apiKeyRepository = Connection.instance.dbConnection.getRepository(APIKeyEntity); + this.secretBox = new SecretBox(process.env.EXTERNAL_DB_ENCRYPTION_KEY); + } + + // ToDo: Maybe we also need to store not the API key but the hash of it? + // But in that case the API key will be shown once and then it will be lost. + @decryptAPIKey + public async create(apiKey: string, user: UserEntity, expiresAt?: Date, revoked = false): Promise { + if (!apiKey) { + throw new Error('API key is not specified'); + } + if (!user) { + throw new Error('API key user is not specified'); + } + if (!expiresAt) { + expiresAt = new Date(); + expiresAt.setMonth(expiresAt.getMonth() + API_KEY_EXPIRATION); + } + const apiKeyHash = APIKeyService.hashAPIKey(apiKey); + const encryptedAPIKey = await this.secretBox.encrypt(apiKey); + // Create entity + const apiKeyEntity = new APIKeyEntity(apiKeyHash, encryptedAPIKey, expiresAt, user.customer, user, revoked); + const apiKeyRecord = (await this.apiKeyRepository.insert(apiKeyEntity)).identifiers[0]; + if (!apiKeyRecord) throw new Error(`Cannot create a new API key`); + return apiKeyEntity; + } + + @decryptAPIKey + public async update( + apiKey: string, + expiresAt?: Date, + revoked?: boolean, + customer?: CustomerEntity, + user?: UserEntity + ) { + const apiKeyHash = APIKeyService.hashAPIKey(apiKey); + const existingAPIKey = await this.apiKeyRepository.findOneBy({ apiKeyHash }); + if (!existingAPIKey) { + throw new Error(`API with key id ${apiKey} not found`); + } + if (expiresAt) { + existingAPIKey.expiresAt = expiresAt; + } + if (customer) { + existingAPIKey.customer = customer; + } + if (user) { + existingAPIKey.user = user; + } + if (revoked) { + existingAPIKey.revoked = revoked; + } + + const entity = await this.apiKeyRepository.save(existingAPIKey); + return entity; + } + + public async revoke(apiKey: string) { + return this.update(apiKey, undefined, true); + } + + public async decryptAPIKey(apiKey: string) { + return await this.secretBox.decrypt(apiKey); + } + + @decryptAPIKey + public async get(apiKey: string) { + const apiKeyHash = APIKeyService.hashAPIKey(apiKey); + const apiKeyEntity = await this.apiKeyRepository.findOne({ + where: { apiKeyHash: apiKeyHash }, + relations: ['customer', 'user'], + }); + return apiKeyEntity; + } + + @decryptAPIKey + public async find(where: Record, order?: Record) { + try { + const apiKeyList = await this.apiKeyRepository.find({ + where: where, + relations: ['customer', 'user'], + order: order, + }); + return apiKeyList; + } catch { + return []; + } + } + + // Utils + + public generateAPIKey(): string { + return `${API_KEY_PREFIX}_${randomBytes(API_KEY_LENGTH).toString('hex')}`; + } + + public async getExpiryDate(apiKey: string): Promise { + const decrypted = await decodeJWT(apiKey); + return new Date(decrypted.payload.exp ? decrypted.payload.exp * 1000 : 0); + } + + public static hashAPIKey(apiKey: string): string { + return sha3_512(apiKey); + } +} diff --git a/src/services/admin/stripe.ts b/src/services/admin/stripe.ts index cb27205c..bc859b2f 100644 --- a/src/services/admin/stripe.ts +++ b/src/services/admin/stripe.ts @@ -12,24 +12,23 @@ import type { NextFunction } from 'express'; dotenv.config(); export class StripeService { + submitter: SubscriptionSubmitter; + private isFullySynced = false; - submitter: SubscriptionSubmitter; - private isFullySynced = false; - - constructor() { - this.submitter = new SubscriptionSubmitter(eventTracker.getEmitter()); - } + constructor() { + this.submitter = new SubscriptionSubmitter(eventTracker.getEmitter()); + } - async syncAll(next: NextFunction): Promise { - if (!this.isFullySynced) { - await this.syncFull(); - this.isFullySynced = true; - } - next() - } + async syncAll(next: NextFunction): Promise { + if (!this.isFullySynced) { + await this.syncFull(); + this.isFullySynced = true; + } + next(); + } async syncFull(): Promise { - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // Sync all subscriptions for await (const subscription of stripe.subscriptions.list({ status: 'all', @@ -54,7 +53,7 @@ export class StripeService { // Sync all the subscriptions for current customer async syncCustomer(customer: CustomerEntity): Promise { - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); for await (const subscription of stripe.subscriptions.list({ customer: customer.paymentProviderId, status: 'all', @@ -70,66 +69,68 @@ export class StripeService { } } - async syncOne(customer: CustomerEntity): Promise { - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); - - const local = await SubscriptionService.instance.findCurrent(customer) - if (!local) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Active subscription not found for customer with id ${customer.customerId}`, - 'Subscription syncronization' - ), - severity: 'debug', - }); - const activeSubs = await stripe.subscriptions.list({ - customer: customer.paymentProviderId, - status: 'active', - }); - const trialSubs = await stripe.subscriptions.list({ - customer: customer.paymentProviderId, - status: 'trialing', - }); - const subs = [...activeSubs.data, ...trialSubs.data]; - if (subs.length > 1) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Multiple active subscriptions found for customer with id ${customer.customerId}`, - 'Subscription syncronization' - ), - severity: 'error', - }); - return; - } - if (subs.length > 0) { - await this.createSubscription(subs[0], customer); - } - return; - } - const subscriptionId = local.subscriptionId; - const remote = await stripe.subscriptions.retrieve(subscriptionId); - if (!remote) { - eventTracker.notify({ - message: EventTracker.compileBasicNotification( - `Subscription with id ${subscriptionId} could not be retrieved from Stripe`, - 'Subscription syncronization' - ), - severity: 'error', - }); - return; - } - const current = await SubscriptionService.instance.subscriptionRepository.findOne({ - where: { subscriptionId: remote.id }, - }); - if (current) { - await this.updateSubscription(remote, current); - } else { - await this.createSubscription(remote); - } - } + async syncOne(customer: CustomerEntity): Promise { + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + + const local = await SubscriptionService.instance.findCurrent(customer); + if (!local) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Active subscription not found for customer with id ${customer.customerId}`, + 'Subscription syncronization' + ), + severity: 'debug', + }); + const activeSubs = await stripe.subscriptions.list({ + customer: customer.paymentProviderId, + status: 'active', + }); + const trialSubs = await stripe.subscriptions.list({ + customer: customer.paymentProviderId, + status: 'trialing', + }); + const subs = [...activeSubs.data, ...trialSubs.data]; + if (subs.length > 1) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Multiple active subscriptions found for customer with id ${customer.customerId}`, + 'Subscription syncronization' + ), + severity: 'error', + }); + return; + } + if (subs.length > 0) { + await this.createSubscription(subs[0], customer); + } + return; + } + const subscriptionId = local.subscriptionId; + const remote = await stripe.subscriptions.retrieve(subscriptionId); + if (!remote) { + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Subscription with id ${subscriptionId} could not be retrieved from Stripe`, + 'Subscription syncronization' + ), + severity: 'error', + }); + return; + } + const current = await SubscriptionService.instance.subscriptionRepository.findOne({ + where: { subscriptionId: remote.id }, + }); + if (current) { + await this.updateSubscription(remote, current); + } else { + await this.createSubscription(remote); + } + } async createSubscription(subscription: Stripe.Subscription, customer?: CustomerEntity): Promise { - await this.submitter.submitSubscriptionCreate(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CREATE, {customer: customer})); + await this.submitter.submitSubscriptionCreate( + builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CREATE, { customer: customer }) + ); } async updateSubscription(subscription: Stripe.Subscription, current: SubscriptionEntity): Promise { @@ -144,7 +145,9 @@ export class StripeService { }); return; } - await this.submitter.submitSubscriptionUpdate(builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE)); + await this.submitter.submitSubscriptionUpdate( + builSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE) + ); } } diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts index 0d8ca45f..6e4b8eb3 100644 --- a/src/services/admin/subscription.ts +++ b/src/services/admin/subscription.ts @@ -75,7 +75,10 @@ export class SubscriptionService { public async findCurrent(customer: CustomerEntity) { return await this.subscriptionRepository.findOne({ - where: [{ customer: customer, status: 'active' }, { customer: customer, status: 'trialing' }], + where: [ + { customer: customer, status: 'active' }, + { customer: customer, status: 'trialing' }, + ], relations: ['customer'], }); } diff --git a/src/services/api/api-key.ts b/src/services/api/api-key.ts deleted file mode 100644 index d8e24773..00000000 --- a/src/services/api/api-key.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { Repository } from 'typeorm'; -import { decodeJWT } from 'did-jwt'; -import { Connection } from '../../database/connection/connection.js'; - -import * as dotenv from 'dotenv'; -import type { CustomerEntity } from '../../database/entities/customer.entity.js'; -import { APIKeyEntity } from '../../database/entities/api.key.entity.js'; -import type { UserEntity } from '../../database/entities/user.entity.js'; -import { v4 } from 'uuid'; -import { SecretBox } from '@veramo/kms-local'; -dotenv.config(); - -export class APIKeyService { - public apiKeyRepository: Repository; - private secretBox: SecretBox; - - public static instance = new APIKeyService(); - - constructor() { - this.apiKeyRepository = Connection.instance.dbConnection.getRepository(APIKeyEntity); - this.secretBox = new SecretBox(process.env.EXTERNAL_DB_ENCRYPTION_KEY) - } - - public async create(apiKey: string, customer: CustomerEntity, user: UserEntity, revoked = false): Promise { - const apiKeyId = v4(); - if (!apiKey) { - throw new Error('API key is not specified'); - } - if (!customer) { - throw new Error('API key customer is not specified'); - } - if (!user) { - throw new Error('API key user is not specified'); - } - if (!customer) { - throw new Error('Customer id is not specified'); - } - const encryptedAPIKey = await this.secretBox.encrypt(apiKey); - const expiresAt = await this.getExpiryDate(apiKey); - const apiKeyEntity = new APIKeyEntity(encryptedAPIKey, expiresAt, customer, user, revoked); - const apiKeyRecord = (await this.apiKeyRepository.insert(apiKeyEntity)).identifiers[0]; - if (!apiKeyRecord) throw new Error(`Cannot create a new API key`); - return apiKeyEntity; - } - - public async update( - apiKey: string, - expiresAt?: Date, - customer?: CustomerEntity, - user?: UserEntity, - revoked?: boolean - ) { - const existingAPIKey = await this.apiKeyRepository.findOneBy({ apiKey }); - if (!existingAPIKey) { - throw new Error(`API with key id ${apiKey} not found`); - } - if (expiresAt) { - existingAPIKey.expiresAt = expiresAt; - } - if (customer) { - existingAPIKey.customer = customer; - } - if (user) { - existingAPIKey.user = user; - } - if (revoked) { - existingAPIKey.revoked = revoked; - } - - return await this.apiKeyRepository.save(existingAPIKey); - } - - public async get(apiKey: string) { - const apiKeyEntity = await this.apiKeyRepository.findOne({ - where: { apiKey }, - relations: ['customer', 'user'], - }); - if (!apiKeyEntity) { - throw new Error(`API key ${apiKey} not found`); - } - - if (this.secretBox && apiKeyEntity.apiKey) { - apiKeyEntity.apiKey = await this.secretBox.decrypt(apiKeyEntity.apiKey); - } - return apiKeyEntity; - } - - public async find(where: Record) { - try { - const apiKeyList = await this.apiKeyRepository.find({ - where: where, - relations: ['customer', 'user'], - }); - // decrypt the API keys - if (this.secretBox) { - for (const apiKey of apiKeyList) { - apiKey.apiKey = await this.secretBox.decrypt(apiKey.apiKey); - } - } - return apiKeyList; - - } catch { - return []; - } - } - - public async getExpiryDate(apiKey: string): Promise { - const decrypted = await decodeJWT(apiKey); - return new Date(decrypted.payload.exp ? decrypted.payload.exp * 1000 : 0); - } -} diff --git a/src/services/identity/abstract.ts b/src/services/identity/abstract.ts index 5d1ee489..4b5fd4c6 100644 --- a/src/services/identity/abstract.ts +++ b/src/services/identity/abstract.ts @@ -221,6 +221,7 @@ export abstract class AbstractIdentityService implements IIdentityService { ): Promise { throw new Error(`Not supported`); } + // ToDo: All the next functions should be removed after adding new API key mechanism setAPIKey(apiKey: string, customer: CustomerEntity, user: UserEntity): Promise { throw new Error(`Not supported`); } @@ -230,4 +231,7 @@ export abstract class AbstractIdentityService implements IIdentityService { getAPIKey(customer: CustomerEntity, user: UserEntity): Promise { throw new Error(`Not supported`); } + decryptAPIKey(apiKey: string): Promise { + throw new Error(`Not supported`); + } } diff --git a/src/services/identity/index.ts b/src/services/identity/index.ts index de6bd310..7cb54abe 100644 --- a/src/services/identity/index.ts +++ b/src/services/identity/index.ts @@ -171,6 +171,7 @@ export interface IIdentityService { setAPIKey(apiKey: string, customer: CustomerEntity, user: UserEntity): Promise; updateAPIKey(apiKey: APIKeyEntity, newApiKey: string): Promise; getAPIKey(customer: CustomerEntity, user: UserEntity): Promise; + decryptAPIKey(apiKey: string): Promise; } export class IdentityServiceStrategySetup { diff --git a/src/services/identity/postgres.ts b/src/services/identity/postgres.ts index cd433ba5..8c35727b 100644 --- a/src/services/identity/postgres.ts +++ b/src/services/identity/postgres.ts @@ -48,7 +48,7 @@ import { CheqdNetwork } from '@cheqd/sdk'; import { IdentifierService } from '../api/identifier.js'; import type { KeyEntity } from '../../database/entities/key.entity.js'; import type { UserEntity } from '../../database/entities/user.entity.js'; -import { APIKeyService } from '../api/api-key.js'; +import { APIKeyService } from '../admin/api-key.js'; import type { APIKeyEntity } from '../../database/entities/api.key.entity.js'; import { KeyDIDProvider } from '@veramo/did-provider-key'; import type { AbstractIdentifierProvider } from '@veramo/did-manager'; @@ -524,35 +524,36 @@ export class PostgresIdentityService extends DefaultIdentityService { } async setAPIKey(apiKey: string, customer: CustomerEntity, user: UserEntity): Promise { - const keys = await APIKeyService.instance.find({ customer: customer, user: user }); + const keys = await APIKeyService.instance.find({ customer: customer, user: user, revoked: false }); if (keys.length > 0) { throw new Error(`API key for customer ${customer.customerId} and user ${user.logToId} already exists`); } - const apiKeyEntity = await APIKeyService.instance.create(apiKey, customer, user); + const apiKeyEntity = await APIKeyService.instance.create(apiKey, user); if (!apiKeyEntity) { throw new Error(`Cannot create API key for customer ${customer.customerId} and user ${user.logToId}`); } + apiKeyEntity.apiKey = await this.decryptAPIKey(apiKeyEntity.apiKey); return apiKeyEntity; } async updateAPIKey(apiKey: APIKeyEntity, newApiKey: string): Promise { - const key = await APIKeyService.instance.get(apiKey.apiKeyId); + const key = await APIKeyService.instance.get(apiKey.apiKey); if (!key) { - throw new Error(`API key with id ${apiKey.apiKeyId} not found`); + throw new Error(`API key not found`); } const apiKeyEntity = await APIKeyService.instance.update( - key.apiKeyId, newApiKey, await APIKeyService.instance.getExpiryDate(newApiKey) ); if (!apiKeyEntity) { - throw new Error(`Cannot update API key with id ${apiKey.apiKeyId}`); + throw new Error(`Cannot update API key`); } + apiKeyEntity.apiKey = await this.decryptAPIKey(apiKeyEntity.apiKey); return apiKeyEntity; } async getAPIKey(customer: CustomerEntity, user: UserEntity): Promise { - const keys = await APIKeyService.instance.find({ customer: customer, user: user }); + const keys = await APIKeyService.instance.find({ customer: customer, user: user, revoked: false }); if (keys.length > 1) { throw new Error( `For the customer with customer id ${customer.customerId} and user with logToId ${user.logToId} there more then 1 API key` @@ -563,4 +564,8 @@ export class PostgresIdentityService extends DefaultIdentityService { } return keys[0]; } + + async decryptAPIKey(apiKey: string): Promise { + return await APIKeyService.instance.decryptAPIKey(apiKey); + } } diff --git a/src/services/track/helpers.ts b/src/services/track/helpers.ts index 1a25fb3b..c88d7a12 100644 --- a/src/services/track/helpers.ts +++ b/src/services/track/helpers.ts @@ -35,7 +35,7 @@ export function toCoin(amount: bigint, denom = MINIMAL_DENOM): Coin { return coin(amount.toString(), denom); } -export function builSubmitOperation (subscription: Stripe.Subscription, name: string, options?: ISubmitOptions) { +export function builSubmitOperation(subscription: Stripe.Subscription, name: string, options?: ISubmitOptions) { return { operation: name, data: { @@ -49,4 +49,4 @@ export function builSubmitOperation (subscription: Stripe.Subscription, name: st } satisfies ISubmitData, options, } satisfies ISubmitOperation; -}; +} diff --git a/src/services/track/submitter.ts b/src/services/track/submitter.ts index 01796f73..207660e2 100644 --- a/src/services/track/submitter.ts +++ b/src/services/track/submitter.ts @@ -1,4 +1,4 @@ -import type { ISubmitOptions } from "./types"; +import type { ISubmitOptions } from './types'; // Type: Interface export type ISubmitData = ISubmitStripeCustomerCreateData | ISubmitSubscriptionData; diff --git a/src/static/swagger-admin-options.json b/src/static/swagger-admin-options.json index b3382775..1b041cf1 100644 --- a/src/static/swagger-admin-options.json +++ b/src/static/swagger-admin-options.json @@ -21,14 +21,11 @@ { "name": "Price" }, - { - "name": "Customer" - }, { "name": "Subscription" }, { - "name": "Checkout" + "name": "API Key" } ] } diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 8b44d945..fccdefcd 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -1,662 +1,1020 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service admin API for cheqd network", - "version": "2.0.0", - "description": "Admin API which handles users subscriptions and payments", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Product" - }, - { - "name": "Price" - }, - { - "name": "Customer" - }, - { - "name": "Subscription" - }, - { - "name": "Checkout" - } - ], - "paths": { - "/admin/price/list": { - "get": { - "summary": "Get a list of prices", - "description": "Get a list of prices", - "tags": [ - "Price" - ], - "parameters": [ - { - "in": "query", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id. If passed - returns filtered by this product list of prices.", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of prices", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PriceListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/list": { - "get": { - "summary": "Get a list of products", - "description": "Get a list of products which are on a Stripe side", - "tags": [ - "Product" - ], - "parameters": [ - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the list of products with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of products", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/get/{productId}": { - "get": { - "summary": "Get a product", - "description": "Get a product by id", - "tags": [ - "Product" - ], - "parameters": [ - { - "in": "path", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id which identifies the product in Stripe" - }, - "required": true - }, - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the product with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A product", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/create": { - "post": { - "summary": "Create a subscription", - "description": "Creates a new subscription for an existing customer", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateRequestBody" - } - } - } - }, - "responses": { - "201": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/update": { - "post": { - "summary": "Update a subscription", - "description": "Updates an existing subscription", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/list": { - "get": { - "summary": "Get a list of subscriptions", - "description": "Get a list of subscriptions", - "tags": [ - "Subscription" - ], - "parameters": [ - { - "in": "query", - "name": "paymentProviderId", - "schema": { - "type": "string", - "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." - } - } - ], - "responses": { - "200": { - "description": "A list of subscriptions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/get": { - "get": { - "summary": "Get a subscription", - "description": "Get a subscription", - "tags": [ - "Subscription" - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/cancel": { - "post": { - "summary": "Cancel a subscription", - "description": "Cancels an existing subscription", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/resume": {}, - "post": { - "summary": "Resume a subscription", - "description": "Resumes an existing subscription", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "components": { - "schemas": { - "PriceListResponseBody": { - "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", - "type": "object", - "properties": { - "prices": { - "type": "array", - "items": { - "type": "object", - "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" - } - } - } - }, - "ProductListResponseBody": { - "type": "object", - "properties": { - "products": { - "type": "array", - "items": { - "type": "object", - "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" - } - } - } - }, - "ProductGetResponseBody": { - "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", - "type": "object" - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - }, - "SubscriptionCreateRequestBody": { - "description": "The request body for creating a subscription", - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - }, - "successURL": { - "type": "string", - "description": "The URL to redirect to after the customer sucessfully completes the checkout", - "example": "https://example.com/success" - }, - "cancelURL": { - "type": "string", - "description": "The URL to redirect to after the customer cancels the checkout", - "example": "https://example.com/cancel" - }, - "quantity": { - "type": "number", - "description": "The quantity of the product", - "example": 1 - }, - "trialPeriodDays": { - "type": "number", - "description": "The number of days the customer has to pay for the product", - "example": 7 - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionCreateResponseBody": { - "description": "The response body for creating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", - "properties": { - "sessionURL": { - "type": "string", - "description": "URL which user should follow to manage subscription" - } - } - } - } - }, - "SubscriptionUpdateRequestBody": { - "description": "The request body for updating a subscription", - "type": "object", - "properties": { - "returnURL": { - "type": "string", - "description": "URL which is used to redirect to the page with ability to update the subscription" - } - } - }, - "SubscriptionUpdateResponseBody": { - "description": "The response body for updating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "Object with redirect url inside", - "properties": { - "sessionURL": { - "type": "string", - "description": "URL with session URL rediect to" - } - } - } - } - }, - "SubscriptionGetRequestBody": { - "description": "The request body for getting a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionGetResponseBody": { - "description": "The response body for getting a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionListRequestBody": { - "description": "The request body for listing subscriptions", - "type": "object", - "properties": { - "customerId": { - "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" - } - } - }, - "SubscriptionListResponseBody": { - "description": "The response body for listing subscriptions", - "type": "object", - "properties": { - "subscriptions": { - "type": "array", - "items": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - } - }, - "SubscriptionCancelRequestBody": { - "description": "The request body for canceling a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionCancelResponseBody": { - "description": "The response body for canceling a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionResumeRequestBody": { - "description": "The request body for resuming a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionResumeResponseBody": { - "description": "The response body for resuming a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - }, - "NotFoundError": { - "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Not Found Error" - } - } - } - } - } -} \ No newline at end of file + "openapi": "3.0.0", + "info": { + "title": "Credential Service admin API for cheqd network", + "version": "2.0.0", + "description": "Admin API which handles users subscriptions and payments", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Product" + }, + { + "name": "Price" + }, + { + "name": "Subscription" + }, + { + "name": "API Key" + } + ], + "paths": { + "/admin/api-key/create": { + "post": { + "summary": "Create a new API key", + "description": "Create a new API key", + "tags": ["API Key"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "A new API key has been created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyCreateUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/update": { + "post": { + "summary": "Update an existing API key", + "description": "Update an existing API key", + "tags": ["API Key"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The API key has been updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyUpdateUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/revoke": { + "delete": { + "summary": "Revoke an existing API key", + "description": "Revoke an existing API key", + "tags": ["API Key"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyRevokeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The API key has been revoked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyRevokeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyRevokeUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/list": { + "get": { + "summary": "List all API keys", + "description": "List all API keys", + "tags": ["API Key"], + "responses": { + "200": { + "description": "A list of API keys", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/get": { + "get": { + "summary": "Get an API key", + "description": "Get an API key. If the API key is not provided, the latest not revoked API key it returns.", + "tags": ["API Key"], + "parameters": [ + { + "name": "apiKey", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/price/list": { + "get": { + "summary": "Get a list of prices", + "description": "Get a list of prices", + "tags": ["Price"], + "parameters": [ + { + "in": "query", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id. If passed - returns filtered by this product list of prices.", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of prices", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriceListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/list": { + "get": { + "summary": "Get a list of products", + "description": "Get a list of products which are on a Stripe side", + "tags": ["Product"], + "parameters": [ + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the list of products with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of products", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/get/{productId}": { + "get": { + "summary": "Get a product", + "description": "Get a product by id", + "tags": ["Product"], + "parameters": [ + { + "in": "path", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id which identifies the product in Stripe" + }, + "required": true + }, + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the product with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/create": { + "post": { + "summary": "Create a subscription", + "description": "Creates a new subscription for an existing customer", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/update": { + "post": { + "summary": "Update a subscription", + "description": "Updates an existing subscription", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/list": { + "get": { + "summary": "Get a list of subscriptions", + "description": "Get a list of subscriptions", + "tags": ["Subscription"], + "parameters": [ + { + "in": "query", + "name": "paymentProviderId", + "schema": { + "type": "string", + "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." + } + } + ], + "responses": { + "200": { + "description": "A list of subscriptions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/get": { + "get": { + "summary": "Get a subscription", + "description": "Get a subscription", + "tags": ["Subscription"], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/cancel": { + "post": { + "summary": "Cancel a subscription", + "description": "Cancels an existing subscription", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/resume": {}, + "post": { + "summary": "Resume a subscription", + "description": "Resumes an existing subscription", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "components": { + "schemas": { + "PriceListResponseBody": { + "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", + "type": "object", + "properties": { + "prices": { + "type": "array", + "items": { + "type": "object", + "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" + } + } + } + }, + "ProductListResponseBody": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "type": "object", + "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" + } + } + } + }, + "ProductGetResponseBody": { + "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", + "type": "object" + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + }, + "SubscriptionCreateRequestBody": { + "description": "The request body for creating a subscription", + "type": "object", + "properties": { + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + }, + "successURL": { + "type": "string", + "description": "The URL to redirect to after the customer sucessfully completes the checkout", + "example": "https://example.com/success" + }, + "cancelURL": { + "type": "string", + "description": "The URL to redirect to after the customer cancels the checkout", + "example": "https://example.com/cancel" + }, + "quantity": { + "type": "number", + "description": "The quantity of the product", + "example": 1 + }, + "trialPeriodDays": { + "type": "number", + "description": "The number of days the customer has to pay for the product", + "example": 7 + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionCreateResponseBody": { + "description": "The response body for creating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", + "properties": { + "sessionURL": { + "type": "string", + "description": "URL which user should follow to manage subscription" + } + } + } + } + }, + "SubscriptionUpdateRequestBody": { + "description": "The request body for updating a subscription", + "type": "object", + "properties": { + "returnURL": { + "type": "string", + "description": "URL which is used to redirect to the page with ability to update the subscription" + } + } + }, + "SubscriptionUpdateResponseBody": { + "description": "The response body for updating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "Object with redirect url inside", + "properties": { + "sessionURL": { + "type": "string", + "description": "URL with session URL rediect to" + } + } + } + } + }, + "SubscriptionGetRequestBody": { + "description": "The request body for getting a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionGetResponseBody": { + "description": "The response body for getting a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionListRequestBody": { + "description": "The request body for listing subscriptions", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + } + } + }, + "SubscriptionListResponseBody": { + "description": "The response body for listing subscriptions", + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + } + }, + "SubscriptionCancelRequestBody": { + "description": "The request body for canceling a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionCancelResponseBody": { + "description": "The response body for canceling a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeRequestBody": { + "description": "The request body for resuming a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeResponseBody": { + "description": "The response body for resuming a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + }, + "APIKeyCreateRequestBody": { + "description": "The request body for creating an API key", + "type": "object", + "properties": { + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date", + "required": false + } + } + }, + "APIKeyCreateResponseBody": { + "description": "The response body for creating an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false + } + } + }, + "APIKeyCreateUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key creation", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key creation unsuccessful" + } + } + }, + "APIKeyUpdateRequestBody": { + "description": "The request body for updating an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz", + "required": true + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date", + "required": false + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false, + "required": false, + "default": false + } + } + }, + "APIKeyUpdateResponseBody": { + "description": "The response body for an unsuccessful API key update", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false + } + } + }, + "APIKeyRevokeRequestBody": { + "description": "The request body for revoking an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz", + "required": true + } + } + }, + "APIKeyRevokeResponseBody": { + "description": "The response body for revoking an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz", + "required": true + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": true + } + } + }, + "APIKeyRevokeUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key revocation", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key revocation unsuccessful" + } + } + }, + "APIKeyListResponseBody": { + "description": "The response body for listing API keys", + "type": "object", + "properties": { + "apiKeys": { + "type": "array", + "items": { + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false + } + } + } + } + } + }, + "APIKeyGetRequestBody": { + "description": "The request body for getting an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz", + "required": false + } + } + }, + "APIKeyGetResponseBody": { + "description": "The response body for getting an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false + } + } + }, + "NotFoundError": { + "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Not Found Error" + } + } + } + } + } +} diff --git a/src/static/swagger-api.json b/src/static/swagger-api.json index d8d26cfe..b2cf4b99 100644 --- a/src/static/swagger-api.json +++ b/src/static/swagger-api.json @@ -1,3549 +1,3334 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service API for cheqd network", - "version": "2.0.0", - "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Account" - }, - { - "name": "Key" - }, - { - "name": "DID" - }, - { - "name": "Resource" - }, - { - "name": "Credential" - }, - { - "name": "Presentation" - }, - { - "name": "Credential Status" - } - ], - "externalDocs": { - "description": "Credential Service API Documentation", - "url": "https://docs.cheqd.io/identity" - }, - "paths": { - "/account": { - "get": { - "tags": [ - "Account" - ], - "summary": "Fetch custodian-mode client details.", - "description": "This endpoint returns the custodian-mode client details for authenticated users.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Customer" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/idtoken": { - "get": { - "tags": [ - "Account" - ], - "summary": "Fetch IdToken.", - "description": "This endpoint returns IdToken as JWT with list of user roles inside", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/create": { - "post": { - "tags": [ - "Account" - ], - "summary": "Create an client for an authenticated user.", - "description": "This endpoint creates a client in the custodian-mode for an authenticated user", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/unencrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Create an unencrypted StatusList2021 credential status list.", - "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/encrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Create an encrypted StatusList2021 credential status list.", - "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/unencrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Update an existing unencrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revoke", - "suspend", - "reinstate" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/encrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Update an existing encrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revoke", - "suspend", - "reinstate" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/check": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Check a StatusList2021 index for a given Verifiable Credential.", - "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/search": { - "get": { - "tags": [ - "Credential Status" - ], - "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", - "parameters": [ - { - "in": "query", - "name": "did", - "description": "The DID of the issuer of the status list.", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - }, - { - "in": "query", - "name": "statusListName", - "description": "The name of the StatusList2021 DID-Linked Resource.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusListSearchResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/issue": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Issue a Verifiable Credential", - "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Credential" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/verify": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Verify a Verifiable Credential.", - "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "verify", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyCredentialResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/revoke": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Revoke a Verifiable Credential.", - "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", - "operationId": "revoke", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RevocationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/suspend": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Suspend a Verifiable Credential.", - "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "suspend", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/reinstate": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Reinstate a suspended Verifiable Credential.", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "operationId": "reinstate", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnsuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/create": { - "post": { - "tags": [ - "DID" - ], - "summary": "Create a DID Document.", - "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestFormBased" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestJson" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/update": { - "post": { - "tags": [ - "DID" - ], - "summary": "Update a DID Document.", - "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateResponse" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/import": { - "post": { - "tags": [ - "DID" - ], - "summary": "Import a DID Document.", - "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/deactivate/{did}": { - "post": { - "tags": [ - "DID" - ], - "summary": "Deactivate a DID Document.", - "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to deactivate.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeactivatedDidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/list": { - "get": { - "tags": [ - "DID" - ], - "summary": "Fetch DIDs associated with an account.", - "description": "This endpoint returns the list of DIDs controlled by the account.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/search/{did}": { - "get": { - "tags": [ - "DID" - ], - "summary": "Resolve a DID Document.", - "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to resolve.", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "metadata", - "description": "Return only metadata of DID Document instead of actual DID Document.", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "versionId", - "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "versionTime", - "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "transformKeys", - "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", - "schema": { - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "Ed25519VerificationKey2020", - "JsonWebKey2020" - ] - } - }, - { - "in": "query", - "name": "service", - "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", - "schema": { - "type": "string" - }, - "example": "service-1" - }, - { - "in": "query", - "name": "relativeRef", - "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", - "schema": { - "type": "string" - }, - "example": "/path/to/file" - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/key/create": { - "post": { - "tags": [ - "Key" - ], - "summary": "Create an identity key pair.", - "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/import": { - "post": { - "tags": [ - "Key" - ], - "summary": "Import an identity key pair.", - "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/read/{kid}": { - "get": { - "tags": [ - "Key" - ], - "summary": "Fetch an identity key pair.", - "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", - "parameters": [ - { - "name": "kid", - "description": "Key ID of the identity key pair to fetch.", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/presentation/create": { - "post": { - "tags": [ - "Presentation" - ], - "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", - "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/presentation/verify": { - "post": { - "tags": [ - "Presentation" - ], - "summary": "Verify a Verifiable Presentation generated from credential(s).", - "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyPresentationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/create/{did}": { - "post": { - "tags": [ - "Resource" - ], - "summary": "Create a DID-Linked Resource.", - "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to link the resource to.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/search/{did}": { - "get": { - "tags": [ - "Resource" - ], - "summary": "Get a DID-Linked Resource.", - "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "resourceId", - "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "resourceName", - "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "cheqd-issuer-logo" - }, - { - "in": "query", - "name": "resourceType", - "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "CredentialArtwork" - }, - { - "in": "query", - "name": "resourceVersion", - "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "v1" - }, - { - "in": "query", - "name": "resourceVersionTime", - "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "checksum", - "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" - }, - { - "in": "query", - "name": "resourceMetadata", - "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "any": { - "schema": { - "type": "object" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - } - }, - "components": { - "schemas": { - "AlsoKnownAs": { - "type": "object", - "properties": { - "alsoKnownAs": { - "type": "array", - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "format": "uri", - "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" - }, - "description": { - "type": "string", - "description": "Optional description of the URI." - } - } - } - } - } - }, - "CredentialRequest": { - "description": "Input fields for the creating a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "issuerDid": { - "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "subjectDid": { - "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", - "type": "string", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - }, - "attributes": { - "description": "JSON object containing the attributes to be included in the credential.", - "type": "object", - "example": { - "name": "Bob", - "gender": "male" - } - }, - "@context": { - "description": "Optional properties to be included in the `@context` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://schema.org/schema.jsonld", - "https://veramo.io/contexts/profile/v1" - ] - }, - "type": { - "description": "Optional properties to be included in the `type` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "Person" - ] - }, - "expirationDate": { - "description": "Optional expiration date according to the VC Data Model specification.", - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "format": { - "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", - "type": "string", - "enum": [ - "jwt", - "jsonld" - ], - "example": "jwt" - }, - "credentialStatus": { - "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", - "type": "object", - "required": [ - "statusPurpose", - "statusListName" - ], - "properties": { - "statusPurpose": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - }, - "statusListName": { - "type": "string" - }, - "statusListIndex": { - "type": "number" - }, - "statusListVersion": { - "type": "string", - "format": "date-time" - }, - "statusListRangeStart": { - "type": "number" - }, - "statusListRangeEnd": { - "type": "number" - }, - "indexNotIn": { - "type": "number" - } - }, - "example": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials" - } - } - }, - "required": [ - "issuerDid", - "subjectDid", - "attributes" - ], - "example": { - "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "attributes": { - "gender": "male", - "name": "Bob" - }, - "@context": [ - "https://schema.org" - ], - "type": [ - "Person" - ], - "format": "jwt", - "credentialStatus": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials", - "statusListIndex": 10 - } - } - }, - "Credential": { - "description": "Input fields for revoking/suspending a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ] - }, - "type": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "VerifiableCredential", - "Person" - ] - }, - "expirationDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "issuer": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - } - } - }, - "credentialSubject": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - } - } - }, - "credentialStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" - }, - "statusListIndex": { - "type": "number", - "example": 20 - }, - "statusPurpose": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ], - "example": "suspension" - }, - "type": { - "type": "string", - "enum": [ - "StatusList2021Entry" - ] - } - } - }, - "issuanceDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "proof": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "jwt": { - "type": "string" - } - }, - "example": { - "type": "JwtProof2020", - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ], - "credentialSubject": { - "gender": "male", - "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "name": "Bob" - }, - "credentialStatus": { - "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", - "statusIndex": 20, - "statusPurpose": "suspension", - "type": "StatusList2021Entry" - }, - "issuanceDate": "2023-06-08T13:49:28.000Z", - "issuer": { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "proof": { - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", - "type": "JwtProof2020" - }, - "type": [ - "VerifiableCredential", - "Person" - ] - } - }, - "CredentialRevokeRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", - "oneOf": [ - { - "type": "object" - }, - { - "type": "string" - } - ] - }, - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", - "type": "string" - } - } - }, - "RevocationResult": { - "properties": { - "revoked": { - "type": "boolean", - "example": true - } - } - }, - "SuspensionResult": { - "properties": { - "suspended": { - "type": "boolean", - "example": true - } - } - }, - "UnsuspensionResult": { - "properties": { - "unsuspended": { - "type": "boolean", - "example": true - } - } - }, - "CredentialVerifyRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", - "type": "object" - }, - "policies": { - "description": "Custom verification policies to execute when verifying credential.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "VerifyPresentationResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - } - }, - "VerifyCredentialResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - }, - "example": { - "verified": true, - "polices": {}, - "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "signer": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - } - }, - "PresentationCreateRequest": { - "type": "object", - "required": [ - "credentials" - ], - "properties": { - "credentials": { - "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", - "type": "array", - "items": { - "type": "object" - } - }, - "holderDid": { - "description": "DID of holder", - "type": "string" - }, - "verifierDid": { - "description": "DID of verifier", - "type": "string" - } - } - }, - "PresentationVerifyRequest": { - "type": "object", - "required": [ - "presentation" - ], - "properties": { - "presentation": { - "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", - "type": "object" - }, - "verifierDid": { - "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", - "type": "string" - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": false - }, - "policies": { - "description": "Custom verification policies to execute when verifying presentation.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "CredentialStatusCreateBody": { - "allOf": [ - { - "type": "object", - "required": [ - "did", - "statusListName" - ], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be created.", - "type": "string" - }, - "length": { - "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true, - "default": 140000 - }, - "encoding": { - "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", - "type": "string", - "default": "base64url", - "enum": [ - "base64url", - "base64", - "hex" - ] - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/AlsoKnownAs" - } - ] - }, - "CredentialStatusCreateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "length": 140000, - "encoding": "base64url" - } - }, - "CredentialStatusUnencryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": false - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-employee-credentials", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "1.0.0", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - } - } - }, - "CredentialStatusCreateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusEncryptedPaymentConditionsBody": { - "type": "object", - "properties": { - "feePaymentAddress": { - "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 20 - }, - "feePaymentWindow": { - "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 10 - } - } - }, - "CredentialStatusEncryptedPaymentConditionsJson": { - "type": "object", - "properties": { - "paymentConditions": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - } - } - }, - "CredentialStatusCreateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - }, - { - "type": "object", - "required": [ - "feePaymentAddress", - "feePaymentAmount", - "feePaymentWindow" - ] - } - ] - }, - "CredentialStatusCreateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - }, - { - "type": "object", - "required": [ - "paymentConditions" - ] - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "paymentConditions": [ - { - "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", - "feePaymentAmount": 20, - "feePaymentWindow": 10 - } - ] - } - }, - "CredentialStatusEncryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": true - }, - "encryptedSymmetricKey": { - "type": "string", - "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" - }, - "paymentConditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feePaymentAddress": { - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "type": "string", - "example": "20000000000ncheq" - }, - "intervalInSeconds": { - "type": "number", - "example": 600 - }, - "type": { - "type": "string", - "example": "timelockPayment" - } - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-revocation-encrypted-1", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "2023-06-26T11:45:19.349Z", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - }, - "symmetricKey": { - "type": "string", - "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - } - } - } - }, - "CredentialStatusCreateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusUpdateBody": { - "type": "object", - "required": [ - "did", - "statusListName", - "indices" - ], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", - "type": "string" - }, - "indices": { - "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - } - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - "CredentialStatusUpdateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "indices": [ - 10, - 3199, - 12109, - 130999 - ] - } - }, - "CredentialStatusUpdateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusUpdateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": [ - "symmetricKey" - ], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - }, - "CredentialStatusUpdateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": [ - "symmetricKey" - ], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "indices": [ - 10, - 3199, - 12109, - 130999 - ], - "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - }, - "CredentialStatusUpdateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusCheckRequest": { - "type": "object", - "required": [ - "did", - "statusListName", - "index" - ], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", - "type": "string" - }, - "index": { - "description": "Credential status index to be checked for revocation or suspension.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": true - } - } - }, - "CredentialStatusCheckResult": { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" - } - ] - }, - "CredentialStatusCheckRevocationResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "revoked": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusCheckSuspensionResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "suspended": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusListSearchResult": { - "allOf": [ - { - "type": "object", - "properties": { - "found": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - } - ] - }, - "KeyImportRequest": { - "type": "object", - "properties": { - "alias": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "Ed25519", - "Secp256k1" - ] - }, - "privateKeyHex": { - "type": "string" - } - } - }, - "KeyResult": { - "type": "object", - "properties": { - "kid": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "Ed25519", - "Secp256k1" - ] - }, - "publicKeyHex": { - "type": "string" - } - } - }, - "DidDocument": { - "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "verificationMethod": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/ns/did/v1" - ], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - ], - "verificationMethod": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "type": "Ed25519VerificationKey2018", - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" - } - ], - "authentication": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" - ], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": [ - "https://example.com" - ] - } - ] - } - }, - "DidDocumentWithoutVerificationMethod": { - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/ns/did/v1" - ], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - ], - "authentication": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" - ], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": [ - "https://example.com" - ] - } - ] - } - }, - "DidCreateRequestFormBased": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": [ - "testnet", - "mainnet" - ] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": [ - "uuid", - "base58btc" - ] - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "JsonWebKey2020", - "Ed25519VerificationKey2020" - ] - }, - "service": { - "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", - "type": "array", - "items": { - "type": "object", - "properties": { - "idFragment": { - "type": "string" - }, - "type": { - "type": "string" - }, - "serviceEndpoint": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "example": [ - { - "idFragment": "service-1", - "type": "LinkedDomains", - "serviceEndpoint": [ - "https://example.com" - ] - } - ] - }, - "key": { - "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", - "type": "string" - }, - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://www.w3.org/ns/did/v1" - ] - } - } - }, - "DidCreateRequestJson": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": [ - "testnet", - "mainnet" - ] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": [ - "uuid", - "base58btc" - ] - }, - "assertionMethod": { - "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", - "type": "boolean", - "default": true - }, - "options": { - "type": "object", - "properties": { - "key": { - "type": "string", - "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "JsonWebKey2020", - "Ed25519VerificationKey2020" - ] - } - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" - } - } - }, - "DidImportRequest": { - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "DID to be imported", - "format": "uri", - "required": true - }, - "keys": { - "type": "array", - "description": "List of keys required to import the DID", - "required": true, - "items": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "PresentationCreateResult": { - "type": "object", - "properties": { - "vp": { - "type": "object", - "description": "Verifiable Presentation which could be provided to the verifier." - }, - "nbf": { - "type": "integer", - "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." - }, - "iss": { - "type": "string", - "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" - }, - "aud": { - "type": "array", - "items": { - "type": "string" - }, - "description": "DID of the verifier of the Verifiable Presentation." - } - }, - "example": { - "vp": { - "@context": [ - "https://www.w3.org/2018/credentials/v1" - ], - "type": [ - "VerifiablePresentation" - ], - "verifiableCredential": [ - "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" - ] - }, - "nbf": 1700744275, - "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", - "aud": [ - "did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22" - ] - } - }, - "DidResult": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string" - }, - "keys": { - "type": "array", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - } - } - }, - "DidUpdateResponse": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string", - "description": "The default key id of which is the key associated with the first verificationMethod" - }, - "keys": { - "type": "array", - "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "controllerKeyRefs": { - "type": "array", - "description": "The list of keyRefs which were used for signing the transaction", - "items": { - "type": "string" - } - }, - "controllerKeys": { - "type": "array", - "description": "The list of all possible keys, inlcuding all controller's keys", - "items": { - "type": "string" - } - } - } - }, - "VerificationMethod": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - }, - "controller": { - "type": "string" - }, - "publicKeyMultibase": { - "type": "string" - }, - "publicKeyJwk": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - }, - "Service": { - "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", - "type": "object", - "properties": { - "id": { - "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", - "type": "string", - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" - }, - "type": { - "description": "Service type as defined in DID Specification Registries.", - "type": "string", - "example": "LinkedDomains" - }, - "serviceEndpoint": { - "description": "Service endpoint as defined in DID Core Specification.", - "type": "array", - "items": { - "type": "string", - "example": "https://example.com" - } - } - } - }, - "DidUpdateRequest": { - "type": "object", - "properties": { - "did": { - "description": "DID identifier to be updated.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "service": { - "type": "array", - "description": "Service section of the DID Document.", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "verificationMethod": { - "type": "array", - "description": "Verification Method section of the DID Document.", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "authentication": { - "description": "Authentication section of the DID Document.", - "type": "array", - "items": { - "type": "string" - } - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - } - } - }, - "DidDeactivateRequest": { - "type": "object", - "properties": { - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "CreateResourceRequest": { - "description": "Input fields for DID-Linked Resource creation.", - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "type", - "data", - "encoding" - ], - "properties": { - "data": { - "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", - "type": "string" - }, - "encoding": { - "description": "Encoding format used to encode the data.", - "type": "string", - "enum": [ - "base64url", - "base64", - "hex" - ] - }, - "name": { - "description": "Name of DID-Linked Resource.", - "type": "string" - }, - "type": { - "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", - "type": "string" - }, - "alsoKnownAs": { - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "type": "array", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "description": { - "type": "string" - } - } - } - }, - "version": { - "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", - "type": "string" - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "data": "SGVsbG8gV29ybGQ=", - "encoding": "base64url", - "name": "ResourceName", - "type": "TextDocument" - } - }, - "ResourceList": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "contentMetadata": { - "type": "object" - }, - "contentStream": { - "type": "object" - }, - "dereferencingMetadata": { - "$ref": "#/components/schemas/DereferencingMetadata" - } - } - }, - "DereferencingMetadata": { - "type": "object", - "properties": { - "contentType": { - "type": "string", - "example": "application/did+ld+json" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - } - } - }, - "DidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DidDocumentMetadata" - } - } - }, - "DeactivatedDidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" - } - } - }, - "DidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": false - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "DeactivatedDidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": true - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "ResourceMetadata": { - "type": "object", - "properties": { - "resourceURI": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceCollectionId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "resourceId": { - "type": "string", - "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceName": { - "type": "string", - "example": "cheqd-issuer-logo" - }, - "resourceType": { - "type": "string", - "example": "CredentialArtwork" - }, - "mediaType": { - "type": "string", - "example": "image/png" - }, - "resourceVersion": { - "type": "string", - "example": "1.0" - }, - "checksum": { - "type": "string", - "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" - }, - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "nextVersionId": { - "type": "string", - "example": "d4829ac7-4566-478c-a408-b44767eddadc" - }, - "previousVersionId": { - "type": "string", - "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" - } - } - }, - "DidResolutionMetadata": { - "type": "object", - "properties": { - "contentType": { - "allOf": [ - { - "$ref": "#/components/schemas/ContentType" - } - ], - "example": "application/did+ld+json" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - } - } - }, - "ContentType": { - "type": "string", - "enum": [ - "application/did+json", - "application/did+ld+json", - "application/ld+json", - "application/json" - ] - }, - "DidProperties": { - "type": "object", - "properties": { - "didString": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "method": { - "type": "string", - "example": "cheqd" - }, - "methodSpecificId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - } - } - }, - "Customer": { - "type": "object", - "properties": { - "customerId": { - "type": "string", - "example": "6w5drpiiwhhs" - }, - "address": { - "type": "string", - "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" - } - } - }, - "AccountCreateRequest": { - "type": "object", - "properties": { - "user": { - "type": "object", - "properties": { - "primaryEmail": { - "type": "string" - } - } - } - } - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - } - } - } -} \ No newline at end of file + "openapi": "3.0.0", + "info": { + "title": "Credential Service API for cheqd network", + "version": "2.0.0", + "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Account" + }, + { + "name": "Key" + }, + { + "name": "DID" + }, + { + "name": "Resource" + }, + { + "name": "Credential" + }, + { + "name": "Presentation" + }, + { + "name": "Credential Status" + } + ], + "externalDocs": { + "description": "Credential Service API Documentation", + "url": "https://docs.cheqd.io/identity" + }, + "paths": { + "/account": { + "get": { + "tags": ["Account"], + "summary": "Fetch custodian-mode client details.", + "description": "This endpoint returns the custodian-mode client details for authenticated users.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/idtoken": { + "get": { + "tags": ["Account"], + "summary": "Fetch IdToken.", + "description": "This endpoint returns IdToken as JWT with list of user roles inside", + "deprecated": true, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/create": { + "post": { + "tags": ["Account"], + "summary": "Create an client for an authenticated user.", + "description": "This endpoint creates a client in the custodian-mode for an authenticated user", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/unencrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Create an unencrypted StatusList2021 credential status list.", + "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/encrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Create an encrypted StatusList2021 credential status list.", + "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/unencrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Update an existing unencrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": ["revoke", "suspend", "reinstate"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/encrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Update an existing encrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": ["revoke", "suspend", "reinstate"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/check": { + "post": { + "tags": ["Credential Status"], + "summary": "Check a StatusList2021 index for a given Verifiable Credential.", + "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "required": true, + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/search": { + "get": { + "tags": ["Credential Status"], + "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", + "parameters": [ + { + "in": "query", + "name": "did", + "description": "The DID of the issuer of the status list.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + }, + { + "in": "query", + "name": "statusListName", + "description": "The name of the StatusList2021 DID-Linked Resource.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusListSearchResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/issue": { + "post": { + "tags": ["Credential"], + "summary": "Issue a Verifiable Credential", + "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Credential" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/verify": { + "post": { + "tags": ["Credential"], + "summary": "Verify a Verifiable Credential.", + "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "verify", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCredentialResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/revoke": { + "post": { + "tags": ["Credential"], + "summary": "Revoke a Verifiable Credential.", + "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", + "operationId": "revoke", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RevocationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/suspend": { + "post": { + "tags": ["Credential"], + "summary": "Suspend a Verifiable Credential.", + "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "suspend", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/reinstate": { + "post": { + "tags": ["Credential"], + "summary": "Reinstate a suspended Verifiable Credential.", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "operationId": "reinstate", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnsuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/create": { + "post": { + "tags": ["DID"], + "summary": "Create a DID Document.", + "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestFormBased" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestJson" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/update": { + "post": { + "tags": ["DID"], + "summary": "Update a DID Document.", + "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateResponse" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/import": { + "post": { + "tags": ["DID"], + "summary": "Import a DID Document.", + "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/deactivate/{did}": { + "post": { + "tags": ["DID"], + "summary": "Deactivate a DID Document.", + "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to deactivate.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeactivatedDidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/list": { + "get": { + "tags": ["DID"], + "summary": "Fetch DIDs associated with an account.", + "description": "This endpoint returns the list of DIDs controlled by the account.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/search/{did}": { + "get": { + "tags": ["DID"], + "summary": "Resolve a DID Document.", + "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to resolve.", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "metadata", + "description": "Return only metadata of DID Document instead of actual DID Document.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "versionId", + "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "versionTime", + "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "transformKeys", + "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", + "schema": { + "type": "string", + "enum": ["Ed25519VerificationKey2018", "Ed25519VerificationKey2020", "JsonWebKey2020"] + } + }, + { + "in": "query", + "name": "service", + "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", + "schema": { + "type": "string" + }, + "example": "service-1" + }, + { + "in": "query", + "name": "relativeRef", + "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", + "schema": { + "type": "string" + }, + "example": "/path/to/file" + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/key/create": { + "post": { + "tags": ["Key"], + "summary": "Create an identity key pair.", + "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/import": { + "post": { + "tags": ["Key"], + "summary": "Import an identity key pair.", + "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/read/{kid}": { + "get": { + "tags": ["Key"], + "summary": "Fetch an identity key pair.", + "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", + "parameters": [ + { + "name": "kid", + "description": "Key ID of the identity key pair to fetch.", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/presentation/create": { + "post": { + "tags": ["Presentation"], + "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", + "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/presentation/verify": { + "post": { + "tags": ["Presentation"], + "summary": "Verify a Verifiable Presentation generated from credential(s).", + "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPresentationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/create/{did}": { + "post": { + "tags": ["Resource"], + "summary": "Create a DID-Linked Resource.", + "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to link the resource to.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/search/{did}": { + "get": { + "tags": ["Resource"], + "summary": "Get a DID-Linked Resource.", + "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "resourceId", + "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "resourceName", + "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "cheqd-issuer-logo" + }, + { + "in": "query", + "name": "resourceType", + "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "CredentialArtwork" + }, + { + "in": "query", + "name": "resourceVersion", + "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "v1" + }, + { + "in": "query", + "name": "resourceVersionTime", + "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "checksum", + "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" + }, + { + "in": "query", + "name": "resourceMetadata", + "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "any": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + } + }, + "components": { + "schemas": { + "AlsoKnownAs": { + "type": "object", + "properties": { + "alsoKnownAs": { + "type": "array", + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "uri", + "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" + }, + "description": { + "type": "string", + "description": "Optional description of the URI." + } + } + } + } + } + }, + "CredentialRequest": { + "description": "Input fields for the creating a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "issuerDid": { + "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "subjectDid": { + "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", + "type": "string", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + }, + "attributes": { + "description": "JSON object containing the attributes to be included in the credential.", + "type": "object", + "example": { + "name": "Bob", + "gender": "male" + } + }, + "@context": { + "description": "Optional properties to be included in the `@context` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": ["https://schema.org/schema.jsonld", "https://veramo.io/contexts/profile/v1"] + }, + "type": { + "description": "Optional properties to be included in the `type` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": ["Person"] + }, + "expirationDate": { + "description": "Optional expiration date according to the VC Data Model specification.", + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "format": { + "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", + "type": "string", + "enum": ["jwt", "jsonld"], + "example": "jwt" + }, + "credentialStatus": { + "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", + "type": "object", + "required": ["statusPurpose", "statusListName"], + "properties": { + "statusPurpose": { + "type": "string", + "enum": ["revocation", "suspension"] + }, + "statusListName": { + "type": "string" + }, + "statusListIndex": { + "type": "number" + }, + "statusListVersion": { + "type": "string", + "format": "date-time" + }, + "statusListRangeStart": { + "type": "number" + }, + "statusListRangeEnd": { + "type": "number" + }, + "indexNotIn": { + "type": "number" + } + }, + "example": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials" + } + } + }, + "required": ["issuerDid", "subjectDid", "attributes"], + "example": { + "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "attributes": { + "gender": "male", + "name": "Bob" + }, + "@context": ["https://schema.org"], + "type": ["Person"], + "format": "jwt", + "credentialStatus": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials", + "statusListIndex": 10 + } + } + }, + "Credential": { + "description": "Input fields for revoking/suspending a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ] + }, + "type": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["VerifiableCredential", "Person"] + }, + "expirationDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "issuer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + } + } + }, + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + } + } + }, + "credentialStatus": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" + }, + "statusListIndex": { + "type": "number", + "example": 20 + }, + "statusPurpose": { + "type": "string", + "enum": ["revocation", "suspension"], + "example": "suspension" + }, + "type": { + "type": "string", + "enum": ["StatusList2021Entry"] + } + } + }, + "issuanceDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "proof": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "jwt": { + "type": "string" + } + }, + "example": { + "type": "JwtProof2020", + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ], + "credentialSubject": { + "gender": "male", + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "name": "Bob" + }, + "credentialStatus": { + "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", + "statusIndex": 20, + "statusPurpose": "suspension", + "type": "StatusList2021Entry" + }, + "issuanceDate": "2023-06-08T13:49:28.000Z", + "issuer": { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "proof": { + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", + "type": "JwtProof2020" + }, + "type": ["VerifiableCredential", "Person"] + } + }, + "CredentialRevokeRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", + "oneOf": [ + { + "type": "object" + }, + { + "type": "string" + } + ] + }, + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", + "type": "string" + } + } + }, + "RevocationResult": { + "properties": { + "revoked": { + "type": "boolean", + "example": true + } + } + }, + "SuspensionResult": { + "properties": { + "suspended": { + "type": "boolean", + "example": true + } + } + }, + "UnsuspensionResult": { + "properties": { + "unsuspended": { + "type": "boolean", + "example": true + } + } + }, + "CredentialVerifyRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", + "type": "object" + }, + "policies": { + "description": "Custom verification policies to execute when verifying credential.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "VerifyPresentationResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + } + }, + "VerifyCredentialResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + }, + "example": { + "verified": true, + "polices": {}, + "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "signer": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + } + }, + "PresentationCreateRequest": { + "type": "object", + "required": ["credentials"], + "properties": { + "credentials": { + "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", + "type": "array", + "items": { + "type": "object" + } + }, + "holderDid": { + "description": "DID of holder", + "type": "string" + }, + "verifierDid": { + "description": "DID of verifier", + "type": "string" + } + } + }, + "PresentationVerifyRequest": { + "type": "object", + "required": ["presentation"], + "properties": { + "presentation": { + "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", + "type": "object" + }, + "verifierDid": { + "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", + "type": "string" + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": false + }, + "policies": { + "description": "Custom verification policies to execute when verifying presentation.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "CredentialStatusCreateBody": { + "allOf": [ + { + "type": "object", + "required": ["did", "statusListName"], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be created.", + "type": "string" + }, + "length": { + "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "default": 140000 + }, + "encoding": { + "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", + "type": "string", + "default": "base64url", + "enum": ["base64url", "base64", "hex"] + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/AlsoKnownAs" + } + ] + }, + "CredentialStatusCreateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "length": 140000, + "encoding": "base64url" + } + }, + "CredentialStatusUnencryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": false + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-employee-credentials", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "1.0.0", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + } + } + }, + "CredentialStatusCreateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusEncryptedPaymentConditionsBody": { + "type": "object", + "properties": { + "feePaymentAddress": { + "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 20 + }, + "feePaymentWindow": { + "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 10 + } + } + }, + "CredentialStatusEncryptedPaymentConditionsJson": { + "type": "object", + "properties": { + "paymentConditions": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + } + } + }, + "CredentialStatusCreateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + }, + { + "type": "object", + "required": ["feePaymentAddress", "feePaymentAmount", "feePaymentWindow"] + } + ] + }, + "CredentialStatusCreateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + }, + { + "type": "object", + "required": ["paymentConditions"] + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "paymentConditions": [ + { + "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", + "feePaymentAmount": 20, + "feePaymentWindow": 10 + } + ] + } + }, + "CredentialStatusEncryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": true + }, + "encryptedSymmetricKey": { + "type": "string", + "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" + }, + "paymentConditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feePaymentAddress": { + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "type": "string", + "example": "20000000000ncheq" + }, + "intervalInSeconds": { + "type": "number", + "example": 600 + }, + "type": { + "type": "string", + "example": "timelockPayment" + } + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-revocation-encrypted-1", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "2023-06-26T11:45:19.349Z", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + }, + "symmetricKey": { + "type": "string", + "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + } + } + } + }, + "CredentialStatusCreateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusUpdateBody": { + "type": "object", + "required": ["did", "statusListName", "indices"], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", + "type": "string" + }, + "indices": { + "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + } + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + "CredentialStatusUpdateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "indices": [10, 3199, 12109, 130999] + } + }, + "CredentialStatusUpdateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusUpdateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": ["symmetricKey"], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + }, + "CredentialStatusUpdateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": ["symmetricKey"], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "indices": [10, 3199, 12109, 130999], + "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + }, + "CredentialStatusUpdateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusCheckRequest": { + "type": "object", + "required": ["did", "statusListName", "index"], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", + "type": "string" + }, + "index": { + "description": "Credential status index to be checked for revocation or suspension.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": true + } + } + }, + "CredentialStatusCheckResult": { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" + } + ] + }, + "CredentialStatusCheckRevocationResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "revoked": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusCheckSuspensionResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusListSearchResult": { + "allOf": [ + { + "type": "object", + "properties": { + "found": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + } + ] + }, + "KeyImportRequest": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Ed25519", "Secp256k1"] + }, + "privateKeyHex": { + "type": "string" + } + } + }, + "KeyResult": { + "type": "object", + "properties": { + "kid": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Ed25519", "Secp256k1"] + }, + "publicKeyHex": { + "type": "string" + } + } + }, + "DidDocument": { + "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationMethod": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": ["https://www.w3.org/ns/did/v1"], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], + "verificationMethod": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" + } + ], + "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": ["https://example.com"] + } + ] + } + }, + "DidDocumentWithoutVerificationMethod": { + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": ["https://www.w3.org/ns/did/v1"], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], + "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": ["https://example.com"] + } + ] + } + }, + "DidCreateRequestFormBased": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": ["testnet", "mainnet"] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": ["uuid", "base58btc"] + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] + }, + "service": { + "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", + "type": "array", + "items": { + "type": "object", + "properties": { + "idFragment": { + "type": "string" + }, + "type": { + "type": "string" + }, + "serviceEndpoint": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "example": [ + { + "idFragment": "service-1", + "type": "LinkedDomains", + "serviceEndpoint": ["https://example.com"] + } + ] + }, + "key": { + "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["https://www.w3.org/ns/did/v1"] + } + } + }, + "DidCreateRequestJson": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": ["testnet", "mainnet"] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": ["uuid", "base58btc"] + }, + "assertionMethod": { + "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] + } + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" + } + } + }, + "DidImportRequest": { + "type": "object", + "properties": { + "did": { + "type": "string", + "description": "DID to be imported", + "format": "uri", + "required": true + }, + "keys": { + "type": "array", + "description": "List of keys required to import the DID", + "required": true, + "items": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "PresentationCreateResult": { + "type": "object", + "properties": { + "vp": { + "type": "object", + "description": "Verifiable Presentation which could be provided to the verifier." + }, + "nbf": { + "type": "integer", + "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." + }, + "iss": { + "type": "string", + "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" + }, + "aud": { + "type": "array", + "items": { + "type": "string" + }, + "description": "DID of the verifier of the Verifiable Presentation." + } + }, + "example": { + "vp": { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [ + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" + ] + }, + "nbf": 1700744275, + "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", + "aud": ["did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22"] + } + }, + "DidResult": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string" + }, + "keys": { + "type": "array", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + } + } + }, + "DidUpdateResponse": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string", + "description": "The default key id of which is the key associated with the first verificationMethod" + }, + "keys": { + "type": "array", + "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "controllerKeyRefs": { + "type": "array", + "description": "The list of keyRefs which were used for signing the transaction", + "items": { + "type": "string" + } + }, + "controllerKeys": { + "type": "array", + "description": "The list of all possible keys, inlcuding all controller's keys", + "items": { + "type": "string" + } + } + } + }, + "VerificationMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "controller": { + "type": "string" + }, + "publicKeyMultibase": { + "type": "string" + }, + "publicKeyJwk": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + }, + "Service": { + "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", + "type": "object", + "properties": { + "id": { + "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", + "type": "string", + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" + }, + "type": { + "description": "Service type as defined in DID Specification Registries.", + "type": "string", + "example": "LinkedDomains" + }, + "serviceEndpoint": { + "description": "Service endpoint as defined in DID Core Specification.", + "type": "array", + "items": { + "type": "string", + "example": "https://example.com" + } + } + } + }, + "DidUpdateRequest": { + "type": "object", + "properties": { + "did": { + "description": "DID identifier to be updated.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "service": { + "type": "array", + "description": "Service section of the DID Document.", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "verificationMethod": { + "type": "array", + "description": "Verification Method section of the DID Document.", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "authentication": { + "description": "Authentication section of the DID Document.", + "type": "array", + "items": { + "type": "string" + } + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + } + } + }, + "DidDeactivateRequest": { + "type": "object", + "properties": { + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CreateResourceRequest": { + "description": "Input fields for DID-Linked Resource creation.", + "type": "object", + "additionalProperties": false, + "required": ["name", "type", "data", "encoding"], + "properties": { + "data": { + "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", + "type": "string" + }, + "encoding": { + "description": "Encoding format used to encode the data.", + "type": "string", + "enum": ["base64url", "base64", "hex"] + }, + "name": { + "description": "Name of DID-Linked Resource.", + "type": "string" + }, + "type": { + "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", + "type": "string" + }, + "alsoKnownAs": { + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "type": "array", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "version": { + "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", + "type": "string" + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "data": "SGVsbG8gV29ybGQ=", + "encoding": "base64url", + "name": "ResourceName", + "type": "TextDocument" + } + }, + "ResourceList": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "contentMetadata": { + "type": "object" + }, + "contentStream": { + "type": "object" + }, + "dereferencingMetadata": { + "$ref": "#/components/schemas/DereferencingMetadata" + } + } + }, + "DereferencingMetadata": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "example": "application/did+ld+json" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + } + } + }, + "DidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DidDocumentMetadata" + } + } + }, + "DeactivatedDidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" + } + } + }, + "DidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": false + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "DeactivatedDidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": true + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "ResourceMetadata": { + "type": "object", + "properties": { + "resourceURI": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceCollectionId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "resourceId": { + "type": "string", + "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceName": { + "type": "string", + "example": "cheqd-issuer-logo" + }, + "resourceType": { + "type": "string", + "example": "CredentialArtwork" + }, + "mediaType": { + "type": "string", + "example": "image/png" + }, + "resourceVersion": { + "type": "string", + "example": "1.0" + }, + "checksum": { + "type": "string", + "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" + }, + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "nextVersionId": { + "type": "string", + "example": "d4829ac7-4566-478c-a408-b44767eddadc" + }, + "previousVersionId": { + "type": "string", + "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" + } + } + }, + "DidResolutionMetadata": { + "type": "object", + "properties": { + "contentType": { + "allOf": [ + { + "$ref": "#/components/schemas/ContentType" + } + ], + "example": "application/did+ld+json" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + } + } + }, + "ContentType": { + "type": "string", + "enum": ["application/did+json", "application/did+ld+json", "application/ld+json", "application/json"] + }, + "DidProperties": { + "type": "object", + "properties": { + "didString": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "method": { + "type": "string", + "example": "cheqd" + }, + "methodSpecificId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + } + } + }, + "Customer": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "example": "6w5drpiiwhhs" + }, + "address": { + "type": "string", + "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" + } + } + }, + "AccountCreateRequest": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "primaryEmail": { + "type": "string" + } + } + } + } + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + } + } + } +} diff --git a/src/types/constants.ts b/src/types/constants.ts index e694965f..2b0546cb 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -10,6 +10,9 @@ export const HEADERS = { // Application constants export const APPLICATION_BASE_URL = process.env.APPLICATION_BASE_URL || 'http://localhost:3000'; export const CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || APPLICATION_BASE_URL; +export const API_KEY_PREFIX = process.env.API_KEY_PREFIX || 'caas'; +export const API_KEY_LENGTH = process.env.API_KEY_LENGTH || 32; +export const API_KEY_EXPIRATION = process.env.API_KEY_EXPIRATION || 1; // By default we don't send events to datadog export const ENABLE_DATADOG = process.env.ENABLE_DATADOG === 'true' ? true : false; // Possible cases 'trace' 'debug' 'info' 'warn' 'error'; @@ -75,6 +78,7 @@ export enum OperationCategoryNameEnum { PRESENTATION = 'presentation', KEY = 'key', SUBSCRIPTION = 'subscription', + API_KEY = 'api-key', } export enum OperationDefaultFeeEnum { @@ -128,6 +132,13 @@ export enum OperationNameEnum { SUBSCRIPTION_UPDATE = 'subscription-update', SUBSCRIPTION_TRIAL_WILL_END = 'subscription-trial-will-end', + // API key operations + API_KEY_CREATE = 'api-key-create', + API_KEY_UPDATE = 'api-key-update', + API_KEY_REVOKE = 'api-key-revoke', + API_KEY_GET = 'api-key-get', + API_KEY_LIST = 'api-key-list', + // Stripe operations STRIPE_ACCOUNT_CREATE = 'stripe-account-create', } diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index abd5ffae..60aee325 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -20,6 +20,9 @@ declare global { EXTERNAL_DB_CONNECTION_URL: string; EXTERNAL_DB_ENCRYPTION_KEY: string; EXTERNAL_DB_CERT: string | undefined; + API_KEY_PREFIX: string; + API_KEY_LENGTH: number; + API_KEY_EXPIRATION: number; // LogTo LOGTO_ENDPOINT: string; diff --git a/src/types/portal.ts b/src/types/portal.ts index 2e4fcaa7..4cb4ee9f 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -94,6 +94,54 @@ export type SubscriptionResumeUnsuccessfulResponseBody = UnsuccessfulResponseBod export type PortalCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; +// API key +// Create +export type APIKeyCreateRequestBody = { + expiresAt?: Date; +}; +export type APIKeyCreateResponseBody = { + apiKey: string; + expiresAt: string; + revoked: boolean; +}; +export type APIKeyCreateUnsuccessfulResponseBody = UnsuccessfulResponseBody; + +// Update +export type APIKeyUpdateRequestBody = { + apiKey: string; + expiresAt?: Date; + revoked?: boolean; +}; +export type APIKeyUpdateResponseBody = { + apiKey: string; + expiresAt: string; + revoked: boolean; +}; +export type APIKeyUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; + +// Revoke +export type APIKeyRevokeRequestBody = { + apiKey: string; +}; +export type APIKeyRevokeResponseBody = { + apiKey: string; + revoked: boolean; +}; +export type APIKeyRevokeUnsuccessfulResponseBody = UnsuccessfulResponseBody; + +// List +export type APIKeyListResponseBody = { + apiKeys: APIKeyCreateResponseBody[]; +}; +export type APIKeyListUnsuccessfulResponseBody = UnsuccessfulResponseBody; + +// Get +export type APIKeyGetRequestBody = { + apiKey: string; +}; +export type APIKeyGetResponseBody = APIKeyCreateResponseBody; +export type APIKeyGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; + // Utils export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 33249863..f7511105 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -171,6 +171,155 @@ * subscription: * type: object * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] + * APIKeyCreateRequestBody: + * description: The request body for creating an API key + * type: object + * properties: + * expiresAt: + * type: string + * description: The expiration date of the API key + * example: 2000-10-31T01:30:00.000-05:00 + * format: date + * required: false + * APIKeyCreateResponseBody: + * description: The response body for creating an API key + * type: object + * properties: + * apiKey: + * type: string + * description: The API key + * example: abcdefghijklmnopqrstuvwxyz + * expiresAt: + * type: string + * description: The expiration date of the API key + * example: 2000-10-31T01:30:00.000-05:00 + * format: date + * revoked: + * type: boolean + * description: The status of the API key + * example: false + * APIKeyCreateUnsuccessfulResponseBody: + * description: The response body for an unsuccessful API key creation + * type: object + * properties: + * error: + * type: string + * example: API key creation unsuccessful + * APIKeyUpdateRequestBody: + * description: The request body for updating an API key + * type: object + * properties: + * apiKey: + * type: string + * description: The API key + * example: abcdefghijklmnopqrstuvwxyz + * required: true + * expiresAt: + * type: string + * description: The expiration date of the API key + * example: 2000-10-31T01:30:00.000-05:00 + * format: date + * required: false + * revoked: + * type: boolean + * description: The status of the API key + * example: false + * required: false + * default: false + * APIKeyUpdateResponseBody: + * description: The response body for an unsuccessful API key update + * type: object + * properties: + * apiKey: + * type: string + * description: The API key + * example: abcdefghijklmnopqrstuvwxyz + * expiresAt: + * type: string + * description: The expiration date of the API key + * example: 2000-10-31T01:30:00.000-05:00 + * format: date + * revoked: + * type: boolean + * description: The status of the API key + * example: false + * APIKeyRevokeRequestBody: + * description: The request body for revoking an API key + * type: object + * properties: + * apiKey: + * type: string + * description: The API key + * example: abcdefghijklmnopqrstuvwxyz + * required: true + * APIKeyRevokeResponseBody: + * description: The response body for revoking an API key + * type: object + * properties: + * apiKey: + * type: string + * description: The API key + * example: abcdefghijklmnopqrstuvwxyz + * required: true + * revoked: + * type: boolean + * description: The status of the API key + * example: true + * APIKeyRevokeUnsuccessfulResponseBody: + * description: The response body for an unsuccessful API key revocation + * type: object + * properties: + * error: + * type: string + * example: API key revocation unsuccessful + * APIKeyListResponseBody: + * description: The response body for listing API keys + * type: object + * properties: + * apiKeys: + * type: array + * items: + * type: object + * properties: + * apiKey: + * type: string + * description: The API key + * example: abcdefghijklmnopqrstuvwxyz + * expiresAt: + * type: string + * description: The expiration date of the API key + * example: 2000-10-31T01:30:00.000-05:00 + * format: date + * revoked: + * type: boolean + * description: The status of the API key + * example: false + * APIKeyGetRequestBody: + * description: The request body for getting an API key + * type: object + * properties: + * apiKey: + * type: string + * description: The API key + * example: abcdefghijklmnopqrstuvwxyz + * required: false + * APIKeyGetResponseBody: + * description: The response body for getting an API key + * type: object + * properties: + * apiKey: + * type: string + * description: The API key + * example: abcdefghijklmnopqrstuvwxyz + * expiresAt: + * type: string + * description: The expiration date of the API key + * example: 2000-10-31T01:30:00.000-05:00 + * format: date + * revoked: + * type: boolean + * description: The status of the API key + * example: false * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. * type: object From 91957b674af492a3c15ea19a40fb30e07fc191a6 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Mon, 1 Apr 2024 18:49:40 +0200 Subject: [PATCH 22/49] MAkes linter happy --- docker/Dockerfile | 10 ++++++++++ src/middleware/auth/base-auth-handler.ts | 2 +- src/types/swagger-admin-types.ts | 19 +++++++++++++------ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index db80d375..f8e1ba09 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -71,6 +71,11 @@ ARG LOGTO_WEBHOOK_SECRET ARG ENABLE_DATADOG=false ARG LOG_LEVEL=info +# API generation +ARG API_KEY_PREFIX="caas" +ARG API_KEY_LENGTH=32 +ARG API_KEY_EXPIRATION=1 + # Verida connector: build-time ARG ENABLE_VERIDA_CONNECTOR=false ARG VERIDA_NETWORK=testnet @@ -119,6 +124,11 @@ ENV LOGTO_WEBHOOK_SECRET ${LOGTO_WEBHOOK_SECRET} ENV ENABLE_DATADOG ${ENABLE_DATADOG} ENV LOG_LEVEL ${LOG_LEVEL} +# API generatioin +ENV API_KEY_PREFIX ${API_KEY_PREFIX} +ENV API_KEY_LENGTH ${API_KEY_LENGTH} +ENV API_KEY_EXPIRATION ${API_KEY_EXPIRATION} + # Faucet setup ENV ENABLE_ACCOUNT_TOPUP ${ENABLE_ACCOUNT_TOPUP} ENV FAUCET_URI ${FAUCET_URI} diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index 89c59454..927f98c1 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -26,7 +26,7 @@ export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { if (!rule) { return this.returnError( StatusCodes.BAD_REQUEST, - `Bad Request. No atuh rules for handling such request: ${request.method} ${request.path}` + `Bad Request. No auth rules for handling such request: ${request.method} ${request.path}` ); } // If the rule is not found - skip the auth check diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index f7511105..ff59f11a 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -178,7 +178,7 @@ * expiresAt: * type: string * description: The expiration date of the API key - * example: 2000-10-31T01:30:00.000-05:00 + * example: "2000-10-31T01:30:00.000-05:00" * format: date * required: false * APIKeyCreateResponseBody: @@ -192,7 +192,7 @@ * expiresAt: * type: string * description: The expiration date of the API key - * example: 2000-10-31T01:30:00.000-05:00 + * example: "2000-10-31T01:30:00.000-05:00" * format: date * revoked: * type: boolean @@ -217,7 +217,7 @@ * expiresAt: * type: string * description: The expiration date of the API key - * example: 2000-10-31T01:30:00.000-05:00 + * example: "2000-10-31T01:30:00.000-05:00" * format: date * required: false * revoked: @@ -237,12 +237,19 @@ * expiresAt: * type: string * description: The expiration date of the API key - * example: 2000-10-31T01:30:00.000-05:00 + * example: "2000-10-31T01:30:00.000-05:00" * format: date * revoked: * type: boolean * description: The status of the API key * example: false + * APIKeyUpdateUnsuccessfulResponseBody: + * description: The response body for an unsuccessful API key update + * type: object + * properties: + * error: + * type: string + * example: API key update unsuccessful * APIKeyRevokeRequestBody: * description: The request body for revoking an API key * type: object @@ -288,7 +295,7 @@ * expiresAt: * type: string * description: The expiration date of the API key - * example: 2000-10-31T01:30:00.000-05:00 + * example: "2000-10-31T01:30:00.000-05:00" * format: date * revoked: * type: boolean @@ -314,7 +321,7 @@ * expiresAt: * type: string * description: The expiration date of the API key - * example: 2000-10-31T01:30:00.000-05:00 + * example: "2000-10-31T01:30:00.000-05:00" * format: date * revoked: * type: boolean From 3df21b2d31ebfc2510a9b909820752342de22878 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Mon, 8 Apr 2024 17:33:59 +0200 Subject: [PATCH 23/49] Merge remote-tracking branch 'origin/develop' into DEV-3793 --- src/controllers/admin/api-key.ts | 25 +- src/database/entities/api.key.entity.ts | 8 + src/database/migrations/AlterAPIKeyTable.ts | 9 + src/services/admin/api-key.ts | 13 +- src/services/identity/postgres.ts | 3 +- src/static/swagger-admin.json | 2026 +++--- src/static/swagger-api.json | 6882 ++++++++++--------- src/types/portal.ts | 29 +- src/types/swagger-admin-types.ts | 106 +- 9 files changed, 4653 insertions(+), 4448 deletions(-) diff --git a/src/controllers/admin/api-key.ts b/src/controllers/admin/api-key.ts index 3341e079..2e6df3bf 100644 --- a/src/controllers/admin/api-key.ts +++ b/src/controllers/admin/api-key.ts @@ -12,6 +12,7 @@ import type { APIKeyGetUnsuccessfulResponseBody, APIKeyListResponseBody, APIKeyListUnsuccessfulResponseBody, + APIKeyResponseBody, APIKeyRevokeRequestBody, APIKeyRevokeResponseBody, APIKeyRevokeUnsuccessfulResponseBody, @@ -39,6 +40,7 @@ export class APIKeyController { }) .toDate() .bail(), + check('name').exists().withMessage('Name is not specified').bail().isString().withMessage('Invalid name').bail(), ]; static apiKeyUpdateValidator = [ check('apiKey') @@ -62,6 +64,7 @@ export class APIKeyController { .toDate() .bail(), check('revoked').optional().isBoolean().withMessage('Invalid boolean value').bail(), + check('name').optional().isString().withMessage('Invalid name').bail(), ]; static apiKeyRevokeValidator = [ check('apiKey') @@ -107,10 +110,10 @@ export class APIKeyController { @validate public async create(request: Request, response: Response) { try { - const { expiresAt } = request.body satisfies APIKeyCreateRequestBody; + const { name, expiresAt } = request.body satisfies APIKeyCreateRequestBody; const apiKey = APIKeyService.instance.generateAPIKey(); - const apiKeyEntity = await APIKeyService.instance.create(apiKey, response.locals.user, expiresAt); + const apiKeyEntity = await APIKeyService.instance.create(apiKey, name, response.locals.user, expiresAt); if (!apiKeyEntity) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: 'Cannot create a new API key', @@ -125,6 +128,8 @@ export class APIKeyController { }); return response.status(StatusCodes.CREATED).json({ apiKey: apiKeyEntity.apiKey, + name: apiKeyEntity.name, + createdAt: apiKeyEntity.createdAt.toISOString(), expiresAt: apiKeyEntity.expiresAt.toISOString(), revoked: apiKeyEntity.revoked, } satisfies APIKeyCreateResponseBody); @@ -166,8 +171,8 @@ export class APIKeyController { @validate public async update(request: Request, response: Response) { try { - const { apiKey, expiresAt, revoked } = request.body satisfies APIKeyUpdateRequestBody; - const apiKeyEntity = await APIKeyService.instance.update(apiKey, expiresAt, revoked); + const { apiKey, name, expiresAt, revoked } = request.body satisfies APIKeyUpdateRequestBody; + const apiKeyEntity = await APIKeyService.instance.update(apiKey, name, expiresAt, revoked); if (!apiKeyEntity) { return response.status(StatusCodes.NOT_FOUND).json({ error: 'Cannot update API key cause it\'s not found', @@ -183,6 +188,8 @@ export class APIKeyController { }); return response.status(StatusCodes.OK).json({ apiKey: apiKeyEntity.apiKey, + name: apiKeyEntity.name, + createdAt: apiKeyEntity.createdAt.toISOString(), expiresAt: apiKeyEntity.expiresAt.toISOString(), revoked: apiKeyEntity.revoked, } satisfies APIKeyUpdateResponseBody); @@ -283,9 +290,11 @@ export class APIKeyController { const keys = apiKeyList.map((apiKey) => { return { apiKey: apiKey.apiKey, + name: apiKey.name, + createdAt: apiKey.createdAt.toISOString(), expiresAt: apiKey.expiresAt.toISOString(), revoked: apiKey.revoked, - }; + } satisfies APIKeyResponseBody; }); return response.status(StatusCodes.OK).json({ apiKeys: keys, @@ -341,9 +350,11 @@ export class APIKeyController { } return response.status(StatusCodes.OK).json({ apiKey: apiKeyEntity.apiKey, + name: apiKeyEntity.name, + createdAt: apiKeyEntity.createdAt.toISOString(), expiresAt: apiKeyEntity.expiresAt.toISOString(), revoked: apiKeyEntity.revoked, - }); + } satisfies APIKeyGetResponseBody); } // Otherwise try to get the latest not revoked API key const keys = await APIKeyService.instance.find( @@ -362,6 +373,8 @@ export class APIKeyController { } return response.status(StatusCodes.OK).json({ apiKey: keys[0].apiKey, + name: keys[0].name, + createdAt: keys[0].createdAt.toISOString(), expiresAt: keys[0].expiresAt.toISOString(), revoked: keys[0].revoked, } satisfies APIKeyGetResponseBody); diff --git a/src/database/entities/api.key.entity.ts b/src/database/entities/api.key.entity.ts index 161f0ed7..26e69a0f 100644 --- a/src/database/entities/api.key.entity.ts +++ b/src/database/entities/api.key.entity.ts @@ -20,6 +20,12 @@ export class APIKeyEntity { }) apiKey!: string; + @Column({ + type: 'text', + nullable: false, + }) + name!: string; + @Column({ type: 'boolean', nullable: false, @@ -69,6 +75,7 @@ export class APIKeyEntity { constructor( apiKeyHash: string, apiKey: string, + name: string, expiresAt: Date, customer: CustomerEntity, user: UserEntity, @@ -76,6 +83,7 @@ export class APIKeyEntity { ) { this.apiKeyHash = apiKeyHash; this.apiKey = apiKey; + this.name = name; this.expiresAt = expiresAt; this.customer = customer; this.user = user; diff --git a/src/database/migrations/AlterAPIKeyTable.ts b/src/database/migrations/AlterAPIKeyTable.ts index 23e4f4b7..72fb2520 100644 --- a/src/database/migrations/AlterAPIKeyTable.ts +++ b/src/database/migrations/AlterAPIKeyTable.ts @@ -15,6 +15,15 @@ export class AlterAPIKeyTable1695740346004 implements MigrationInterface { }) ); + await queryRunner.addColumn( + table_name, + new TableColumn({ + name: 'name', + type: 'text', + isNullable: false, + }) + ); + // Remove unused apiKeyId column await queryRunner.dropColumn(table_name, 'apiKeyId'); // Add column apiKeyHash diff --git a/src/services/admin/api-key.ts b/src/services/admin/api-key.ts index 185ec1c7..655734cb 100644 --- a/src/services/admin/api-key.ts +++ b/src/services/admin/api-key.ts @@ -61,10 +61,13 @@ export class APIKeyService { // ToDo: Maybe we also need to store not the API key but the hash of it? // But in that case the API key will be shown once and then it will be lost. @decryptAPIKey - public async create(apiKey: string, user: UserEntity, expiresAt?: Date, revoked = false): Promise { + public async create(apiKey: string, name: string, user: UserEntity, expiresAt?: Date, revoked = false): Promise { if (!apiKey) { throw new Error('API key is not specified'); } + if (!name) { + throw new Error('API key name is not specified'); + } if (!user) { throw new Error('API key user is not specified'); } @@ -75,7 +78,7 @@ export class APIKeyService { const apiKeyHash = APIKeyService.hashAPIKey(apiKey); const encryptedAPIKey = await this.secretBox.encrypt(apiKey); // Create entity - const apiKeyEntity = new APIKeyEntity(apiKeyHash, encryptedAPIKey, expiresAt, user.customer, user, revoked); + const apiKeyEntity = new APIKeyEntity(apiKeyHash, encryptedAPIKey, name, expiresAt, user.customer, user, revoked); const apiKeyRecord = (await this.apiKeyRepository.insert(apiKeyEntity)).identifiers[0]; if (!apiKeyRecord) throw new Error(`Cannot create a new API key`); return apiKeyEntity; @@ -84,6 +87,7 @@ export class APIKeyService { @decryptAPIKey public async update( apiKey: string, + name?: string, expiresAt?: Date, revoked?: boolean, customer?: CustomerEntity, @@ -94,6 +98,9 @@ export class APIKeyService { if (!existingAPIKey) { throw new Error(`API with key id ${apiKey} not found`); } + if (name) { + existingAPIKey.name = name; + } if (expiresAt) { existingAPIKey.expiresAt = expiresAt; } @@ -112,7 +119,7 @@ export class APIKeyService { } public async revoke(apiKey: string) { - return this.update(apiKey, undefined, true); + return this.update(apiKey, undefined, undefined, true); } public async decryptAPIKey(apiKey: string) { diff --git a/src/services/identity/postgres.ts b/src/services/identity/postgres.ts index 8c35727b..811f5951 100644 --- a/src/services/identity/postgres.ts +++ b/src/services/identity/postgres.ts @@ -528,7 +528,7 @@ export class PostgresIdentityService extends DefaultIdentityService { if (keys.length > 0) { throw new Error(`API key for customer ${customer.customerId} and user ${user.logToId} already exists`); } - const apiKeyEntity = await APIKeyService.instance.create(apiKey, user); + const apiKeyEntity = await APIKeyService.instance.create(apiKey, "idToken", user); if (!apiKeyEntity) { throw new Error(`Cannot create API key for customer ${customer.customerId} and user ${user.logToId}`); } @@ -543,6 +543,7 @@ export class PostgresIdentityService extends DefaultIdentityService { } const apiKeyEntity = await APIKeyService.instance.update( newApiKey, + undefined, await APIKeyService.instance.getExpiryDate(newApiKey) ); if (!apiKeyEntity) { diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index fccdefcd..362f5444 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -1,1020 +1,1008 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service admin API for cheqd network", - "version": "2.0.0", - "description": "Admin API which handles users subscriptions and payments", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Product" - }, - { - "name": "Price" - }, - { - "name": "Subscription" - }, - { - "name": "API Key" - } - ], - "paths": { - "/admin/api-key/create": { - "post": { - "summary": "Create a new API key", - "description": "Create a new API key", - "tags": ["API Key"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyCreateRequestBody" - } - } - } - }, - "responses": { - "201": { - "description": "A new API key has been created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyCreateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/APIKeyCreateUnsuccessfulResponseBody" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/update": { - "post": { - "summary": "Update an existing API key", - "description": "Update an existing API key", - "tags": ["API Key"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyUpdateRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The API key has been updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyUpdateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/APIKeyUpdateUnsuccessfulResponseBody" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/revoke": { - "delete": { - "summary": "Revoke an existing API key", - "description": "Revoke an existing API key", - "tags": ["API Key"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyRevokeRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The API key has been revoked", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyRevokeResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/APIKeyRevokeUnsuccessfulResponseBody" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/list": { - "get": { - "summary": "List all API keys", - "description": "List all API keys", - "tags": ["API Key"], - "responses": { - "200": { - "description": "A list of API keys", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/get": { - "get": { - "summary": "Get an API key", - "description": "Get an API key. If the API key is not provided, the latest not revoked API key it returns.", - "tags": ["API Key"], - "parameters": [ - { - "name": "apiKey", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/price/list": { - "get": { - "summary": "Get a list of prices", - "description": "Get a list of prices", - "tags": ["Price"], - "parameters": [ - { - "in": "query", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id. If passed - returns filtered by this product list of prices.", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of prices", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PriceListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/list": { - "get": { - "summary": "Get a list of products", - "description": "Get a list of products which are on a Stripe side", - "tags": ["Product"], - "parameters": [ - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the list of products with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of products", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/get/{productId}": { - "get": { - "summary": "Get a product", - "description": "Get a product by id", - "tags": ["Product"], - "parameters": [ - { - "in": "path", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id which identifies the product in Stripe" - }, - "required": true - }, - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the product with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A product", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/create": { - "post": { - "summary": "Create a subscription", - "description": "Creates a new subscription for an existing customer", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateRequestBody" - } - } - } - }, - "responses": { - "201": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/update": { - "post": { - "summary": "Update a subscription", - "description": "Updates an existing subscription", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/list": { - "get": { - "summary": "Get a list of subscriptions", - "description": "Get a list of subscriptions", - "tags": ["Subscription"], - "parameters": [ - { - "in": "query", - "name": "paymentProviderId", - "schema": { - "type": "string", - "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." - } - } - ], - "responses": { - "200": { - "description": "A list of subscriptions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/get": { - "get": { - "summary": "Get a subscription", - "description": "Get a subscription", - "tags": ["Subscription"], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/cancel": { - "post": { - "summary": "Cancel a subscription", - "description": "Cancels an existing subscription", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/resume": {}, - "post": { - "summary": "Resume a subscription", - "description": "Resumes an existing subscription", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "components": { - "schemas": { - "PriceListResponseBody": { - "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", - "type": "object", - "properties": { - "prices": { - "type": "array", - "items": { - "type": "object", - "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" - } - } - } - }, - "ProductListResponseBody": { - "type": "object", - "properties": { - "products": { - "type": "array", - "items": { - "type": "object", - "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" - } - } - } - }, - "ProductGetResponseBody": { - "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", - "type": "object" - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - }, - "SubscriptionCreateRequestBody": { - "description": "The request body for creating a subscription", - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - }, - "successURL": { - "type": "string", - "description": "The URL to redirect to after the customer sucessfully completes the checkout", - "example": "https://example.com/success" - }, - "cancelURL": { - "type": "string", - "description": "The URL to redirect to after the customer cancels the checkout", - "example": "https://example.com/cancel" - }, - "quantity": { - "type": "number", - "description": "The quantity of the product", - "example": 1 - }, - "trialPeriodDays": { - "type": "number", - "description": "The number of days the customer has to pay for the product", - "example": 7 - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionCreateResponseBody": { - "description": "The response body for creating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", - "properties": { - "sessionURL": { - "type": "string", - "description": "URL which user should follow to manage subscription" - } - } - } - } - }, - "SubscriptionUpdateRequestBody": { - "description": "The request body for updating a subscription", - "type": "object", - "properties": { - "returnURL": { - "type": "string", - "description": "URL which is used to redirect to the page with ability to update the subscription" - } - } - }, - "SubscriptionUpdateResponseBody": { - "description": "The response body for updating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "Object with redirect url inside", - "properties": { - "sessionURL": { - "type": "string", - "description": "URL with session URL rediect to" - } - } - } - } - }, - "SubscriptionGetRequestBody": { - "description": "The request body for getting a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionGetResponseBody": { - "description": "The response body for getting a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionListRequestBody": { - "description": "The request body for listing subscriptions", - "type": "object", - "properties": { - "customerId": { - "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" - } - } - }, - "SubscriptionListResponseBody": { - "description": "The response body for listing subscriptions", - "type": "object", - "properties": { - "subscriptions": { - "type": "array", - "items": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - } - }, - "SubscriptionCancelRequestBody": { - "description": "The request body for canceling a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionCancelResponseBody": { - "description": "The response body for canceling a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionResumeRequestBody": { - "description": "The request body for resuming a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionResumeResponseBody": { - "description": "The response body for resuming a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - }, - "APIKeyCreateRequestBody": { - "description": "The request body for creating an API key", - "type": "object", - "properties": { - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", - "format": "date", - "required": false - } - } - }, - "APIKeyCreateResponseBody": { - "description": "The response body for creating an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", - "format": "date" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": false - } - } - }, - "APIKeyCreateUnsuccessfulResponseBody": { - "description": "The response body for an unsuccessful API key creation", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "API key creation unsuccessful" - } - } - }, - "APIKeyUpdateRequestBody": { - "description": "The request body for updating an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz", - "required": true - }, - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", - "format": "date", - "required": false - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": false, - "required": false, - "default": false - } - } - }, - "APIKeyUpdateResponseBody": { - "description": "The response body for an unsuccessful API key update", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", - "format": "date" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": false - } - } - }, - "APIKeyRevokeRequestBody": { - "description": "The request body for revoking an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz", - "required": true - } - } - }, - "APIKeyRevokeResponseBody": { - "description": "The response body for revoking an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz", - "required": true - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": true - } - } - }, - "APIKeyRevokeUnsuccessfulResponseBody": { - "description": "The response body for an unsuccessful API key revocation", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "API key revocation unsuccessful" - } - } - }, - "APIKeyListResponseBody": { - "description": "The response body for listing API keys", - "type": "object", - "properties": { - "apiKeys": { - "type": "array", - "items": { - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", - "format": "date" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": false - } - } - } - } - } - }, - "APIKeyGetRequestBody": { - "description": "The request body for getting an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz", - "required": false - } - } - }, - "APIKeyGetResponseBody": { - "description": "The response body for getting an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", - "format": "date" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": false - } - } - }, - "NotFoundError": { - "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Not Found Error" - } - } - } - } - } -} + "openapi": "3.0.0", + "info": { + "title": "Credential Service admin API for cheqd network", + "version": "2.0.0", + "description": "Admin API which handles users subscriptions and payments", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Product" + }, + { + "name": "Price" + }, + { + "name": "Subscription" + }, + { + "name": "API Key" + } + ], + "paths": { + "/admin/api-key/create": { + "post": { + "summary": "Create a new API key", + "description": "Create a new API key", + "tags": [ + "API Key" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "A new API key has been created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyCreateUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/update": { + "post": { + "summary": "Update an existing API key", + "description": "Update an existing API key", + "tags": [ + "API Key" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The API key has been updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyUpdateUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/revoke": { + "delete": { + "summary": "Revoke an existing API key", + "description": "Revoke an existing API key", + "tags": [ + "API Key" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyRevokeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The API key has been revoked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyRevokeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyRevokeUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/list": { + "get": { + "summary": "List all API keys", + "description": "List all API keys", + "tags": [ + "API Key" + ], + "responses": { + "200": { + "description": "A list of API keys", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/get": { + "get": { + "summary": "Get an API key", + "description": "Get an API key. If the API key is not provided, the latest not revoked API key it returns.", + "tags": [ + "API Key" + ], + "parameters": [ + { + "name": "apiKey", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/price/list": { + "get": { + "summary": "Get a list of prices", + "description": "Get a list of prices", + "tags": [ + "Price" + ], + "parameters": [ + { + "in": "query", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id. If passed - returns filtered by this product list of prices.", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of prices", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriceListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/list": { + "get": { + "summary": "Get a list of products", + "description": "Get a list of products which are on a Stripe side", + "tags": [ + "Product" + ], + "parameters": [ + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the list of products with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of products", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/get/{productId}": { + "get": { + "summary": "Get a product", + "description": "Get a product by id", + "tags": [ + "Product" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id which identifies the product in Stripe" + }, + "required": true + }, + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the product with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/create": { + "post": { + "summary": "Create a subscription", + "description": "Creates a new subscription for an existing customer", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/update": { + "post": { + "summary": "Update a subscription", + "description": "Updates an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/list": { + "get": { + "summary": "Get a list of subscriptions", + "description": "Get a list of subscriptions", + "tags": [ + "Subscription" + ], + "parameters": [ + { + "in": "query", + "name": "paymentProviderId", + "schema": { + "type": "string", + "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." + } + } + ], + "responses": { + "200": { + "description": "A list of subscriptions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/get": { + "get": { + "summary": "Get a subscription", + "description": "Get a subscription", + "tags": [ + "Subscription" + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/cancel": { + "post": { + "summary": "Cancel a subscription", + "description": "Cancels an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/resume": {}, + "post": { + "summary": "Resume a subscription", + "description": "Resumes an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "components": { + "schemas": { + "PriceListResponseBody": { + "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", + "type": "object", + "properties": { + "prices": { + "type": "array", + "items": { + "type": "object", + "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" + } + } + } + }, + "ProductListResponseBody": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "type": "object", + "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" + } + } + } + }, + "ProductGetResponseBody": { + "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", + "type": "object" + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + }, + "SubscriptionCreateRequestBody": { + "description": "The request body for creating a subscription", + "type": "object", + "properties": { + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + }, + "successURL": { + "type": "string", + "description": "The URL to redirect to after the customer sucessfully completes the checkout", + "example": "https://example.com/success" + }, + "cancelURL": { + "type": "string", + "description": "The URL to redirect to after the customer cancels the checkout", + "example": "https://example.com/cancel" + }, + "quantity": { + "type": "number", + "description": "The quantity of the product", + "example": 1 + }, + "trialPeriodDays": { + "type": "number", + "description": "The number of days the customer has to pay for the product", + "example": 7 + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionCreateResponseBody": { + "description": "The response body for creating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", + "properties": { + "sessionURL": { + "type": "string", + "description": "URL which user should follow to manage subscription" + } + } + } + } + }, + "SubscriptionUpdateRequestBody": { + "description": "The request body for updating a subscription", + "type": "object", + "properties": { + "returnURL": { + "type": "string", + "description": "URL which is used to redirect to the page with ability to update the subscription" + } + } + }, + "SubscriptionUpdateResponseBody": { + "description": "The response body for updating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "Object with redirect url inside", + "properties": { + "sessionURL": { + "type": "string", + "description": "URL with session URL rediect to" + } + } + } + } + }, + "SubscriptionGetRequestBody": { + "description": "The request body for getting a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionGetResponseBody": { + "description": "The response body for getting a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionListRequestBody": { + "description": "The request body for listing subscriptions", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + } + } + }, + "SubscriptionListResponseBody": { + "description": "The response body for listing subscriptions", + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + } + }, + "SubscriptionCancelRequestBody": { + "description": "The request body for canceling a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionCancelResponseBody": { + "description": "The response body for canceling a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeRequestBody": { + "description": "The request body for resuming a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeResponseBody": { + "description": "The response body for resuming a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + }, + "APIKeyResponse": { + "description": "The general view for API key in response", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "createdAt": { + "type": "string", + "description": "The creation date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date" + }, + "name": { + "type": "string", + "description": "The name of the API key", + "example": "My API Key" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false + } + } + }, + "APIKeyCreateRequestBody": { + "description": "The request body for creating an API key", + "type": "object", + "properties": { + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date", + "required": false + }, + "name": { + "type": "string", + "description": "The name of the API key", + "example": "My API Key", + "required": true + } + } + }, + "APIKeyCreateResponseBody": { + "description": "The response body for creating an API key", + "type": "object", + "ref": "#/components/schemas/APIKeyResponse" + }, + "APIKeyCreateUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key creation", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key creation unsuccessful" + } + } + }, + "APIKeyUpdateRequestBody": { + "description": "The request body for updating an API key", + "type": "object", + "ref": "#/components/schemas/APIKeyResponse" + }, + "APIKeyUpdateResponseBody": { + "description": "The response body for an unsuccessful API key update", + "type": "object", + "ref": "#/components/schemas/APIKeyResponse" + }, + "APIKeyUpdateUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key update", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key update unsuccessful" + } + } + }, + "APIKeyRevokeRequestBody": { + "description": "The request body for revoking an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz", + "required": true + } + } + }, + "APIKeyRevokeResponseBody": { + "description": "The response body for revoking an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz", + "required": true + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": true + } + } + }, + "APIKeyRevokeUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key revocation", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key revocation unsuccessful" + } + } + }, + "APIKeyListResponseBody": { + "description": "The response body for listing API keys", + "type": "object", + "properties": { + "apiKeys": { + "type": "array", + "items": { + "type": "object", + "ref": "#/components/schemas/APIKeyResponse" + } + } + } + }, + "APIKeyGetRequestBody": { + "description": "The request body for getting an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz", + "required": false + } + } + }, + "APIKeyGetResponseBody": { + "description": "The response body for getting an API key", + "type": "object", + "ref": "#/components/schemas/APIKeyResponse" + }, + "NotFoundError": { + "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Not Found Error" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/static/swagger-api.json b/src/static/swagger-api.json index b2cf4b99..a42f421f 100644 --- a/src/static/swagger-api.json +++ b/src/static/swagger-api.json @@ -1,3334 +1,3550 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service API for cheqd network", - "version": "2.0.0", - "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Account" - }, - { - "name": "Key" - }, - { - "name": "DID" - }, - { - "name": "Resource" - }, - { - "name": "Credential" - }, - { - "name": "Presentation" - }, - { - "name": "Credential Status" - } - ], - "externalDocs": { - "description": "Credential Service API Documentation", - "url": "https://docs.cheqd.io/identity" - }, - "paths": { - "/account": { - "get": { - "tags": ["Account"], - "summary": "Fetch custodian-mode client details.", - "description": "This endpoint returns the custodian-mode client details for authenticated users.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Customer" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/idtoken": { - "get": { - "tags": ["Account"], - "summary": "Fetch IdToken.", - "description": "This endpoint returns IdToken as JWT with list of user roles inside", - "deprecated": true, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/create": { - "post": { - "tags": ["Account"], - "summary": "Create an client for an authenticated user.", - "description": "This endpoint creates a client in the custodian-mode for an authenticated user", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/unencrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Create an unencrypted StatusList2021 credential status list.", - "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/encrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Create an encrypted StatusList2021 credential status list.", - "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/unencrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Update an existing unencrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": ["revoke", "suspend", "reinstate"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/encrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Update an existing encrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": ["revoke", "suspend", "reinstate"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/check": { - "post": { - "tags": ["Credential Status"], - "summary": "Check a StatusList2021 index for a given Verifiable Credential.", - "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "required": true, - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/search": { - "get": { - "tags": ["Credential Status"], - "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", - "parameters": [ - { - "in": "query", - "name": "did", - "description": "The DID of the issuer of the status list.", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - }, - { - "in": "query", - "name": "statusListName", - "description": "The name of the StatusList2021 DID-Linked Resource.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusListSearchResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/issue": { - "post": { - "tags": ["Credential"], - "summary": "Issue a Verifiable Credential", - "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Credential" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/verify": { - "post": { - "tags": ["Credential"], - "summary": "Verify a Verifiable Credential.", - "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "verify", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyCredentialResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/revoke": { - "post": { - "tags": ["Credential"], - "summary": "Revoke a Verifiable Credential.", - "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", - "operationId": "revoke", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RevocationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/suspend": { - "post": { - "tags": ["Credential"], - "summary": "Suspend a Verifiable Credential.", - "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "suspend", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/reinstate": { - "post": { - "tags": ["Credential"], - "summary": "Reinstate a suspended Verifiable Credential.", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "operationId": "reinstate", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnsuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/create": { - "post": { - "tags": ["DID"], - "summary": "Create a DID Document.", - "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestFormBased" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestJson" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/update": { - "post": { - "tags": ["DID"], - "summary": "Update a DID Document.", - "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateResponse" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/import": { - "post": { - "tags": ["DID"], - "summary": "Import a DID Document.", - "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/deactivate/{did}": { - "post": { - "tags": ["DID"], - "summary": "Deactivate a DID Document.", - "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to deactivate.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeactivatedDidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/list": { - "get": { - "tags": ["DID"], - "summary": "Fetch DIDs associated with an account.", - "description": "This endpoint returns the list of DIDs controlled by the account.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/search/{did}": { - "get": { - "tags": ["DID"], - "summary": "Resolve a DID Document.", - "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to resolve.", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "metadata", - "description": "Return only metadata of DID Document instead of actual DID Document.", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "versionId", - "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "versionTime", - "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "transformKeys", - "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", - "schema": { - "type": "string", - "enum": ["Ed25519VerificationKey2018", "Ed25519VerificationKey2020", "JsonWebKey2020"] - } - }, - { - "in": "query", - "name": "service", - "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", - "schema": { - "type": "string" - }, - "example": "service-1" - }, - { - "in": "query", - "name": "relativeRef", - "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", - "schema": { - "type": "string" - }, - "example": "/path/to/file" - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/key/create": { - "post": { - "tags": ["Key"], - "summary": "Create an identity key pair.", - "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/import": { - "post": { - "tags": ["Key"], - "summary": "Import an identity key pair.", - "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/read/{kid}": { - "get": { - "tags": ["Key"], - "summary": "Fetch an identity key pair.", - "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", - "parameters": [ - { - "name": "kid", - "description": "Key ID of the identity key pair to fetch.", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/presentation/create": { - "post": { - "tags": ["Presentation"], - "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", - "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/presentation/verify": { - "post": { - "tags": ["Presentation"], - "summary": "Verify a Verifiable Presentation generated from credential(s).", - "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyPresentationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/create/{did}": { - "post": { - "tags": ["Resource"], - "summary": "Create a DID-Linked Resource.", - "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to link the resource to.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/search/{did}": { - "get": { - "tags": ["Resource"], - "summary": "Get a DID-Linked Resource.", - "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "resourceId", - "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "resourceName", - "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "cheqd-issuer-logo" - }, - { - "in": "query", - "name": "resourceType", - "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "CredentialArtwork" - }, - { - "in": "query", - "name": "resourceVersion", - "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "v1" - }, - { - "in": "query", - "name": "resourceVersionTime", - "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "checksum", - "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" - }, - { - "in": "query", - "name": "resourceMetadata", - "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "any": { - "schema": { - "type": "object" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - } - }, - "components": { - "schemas": { - "AlsoKnownAs": { - "type": "object", - "properties": { - "alsoKnownAs": { - "type": "array", - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "format": "uri", - "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" - }, - "description": { - "type": "string", - "description": "Optional description of the URI." - } - } - } - } - } - }, - "CredentialRequest": { - "description": "Input fields for the creating a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "issuerDid": { - "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "subjectDid": { - "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", - "type": "string", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - }, - "attributes": { - "description": "JSON object containing the attributes to be included in the credential.", - "type": "object", - "example": { - "name": "Bob", - "gender": "male" - } - }, - "@context": { - "description": "Optional properties to be included in the `@context` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": ["https://schema.org/schema.jsonld", "https://veramo.io/contexts/profile/v1"] - }, - "type": { - "description": "Optional properties to be included in the `type` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": ["Person"] - }, - "expirationDate": { - "description": "Optional expiration date according to the VC Data Model specification.", - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "format": { - "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", - "type": "string", - "enum": ["jwt", "jsonld"], - "example": "jwt" - }, - "credentialStatus": { - "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", - "type": "object", - "required": ["statusPurpose", "statusListName"], - "properties": { - "statusPurpose": { - "type": "string", - "enum": ["revocation", "suspension"] - }, - "statusListName": { - "type": "string" - }, - "statusListIndex": { - "type": "number" - }, - "statusListVersion": { - "type": "string", - "format": "date-time" - }, - "statusListRangeStart": { - "type": "number" - }, - "statusListRangeEnd": { - "type": "number" - }, - "indexNotIn": { - "type": "number" - } - }, - "example": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials" - } - } - }, - "required": ["issuerDid", "subjectDid", "attributes"], - "example": { - "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "attributes": { - "gender": "male", - "name": "Bob" - }, - "@context": ["https://schema.org"], - "type": ["Person"], - "format": "jwt", - "credentialStatus": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials", - "statusListIndex": 10 - } - } - }, - "Credential": { - "description": "Input fields for revoking/suspending a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ] - }, - "type": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["VerifiableCredential", "Person"] - }, - "expirationDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "issuer": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - } - } - }, - "credentialSubject": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - } - } - }, - "credentialStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" - }, - "statusListIndex": { - "type": "number", - "example": 20 - }, - "statusPurpose": { - "type": "string", - "enum": ["revocation", "suspension"], - "example": "suspension" - }, - "type": { - "type": "string", - "enum": ["StatusList2021Entry"] - } - } - }, - "issuanceDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "proof": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "jwt": { - "type": "string" - } - }, - "example": { - "type": "JwtProof2020", - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ], - "credentialSubject": { - "gender": "male", - "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "name": "Bob" - }, - "credentialStatus": { - "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", - "statusIndex": 20, - "statusPurpose": "suspension", - "type": "StatusList2021Entry" - }, - "issuanceDate": "2023-06-08T13:49:28.000Z", - "issuer": { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "proof": { - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", - "type": "JwtProof2020" - }, - "type": ["VerifiableCredential", "Person"] - } - }, - "CredentialRevokeRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", - "oneOf": [ - { - "type": "object" - }, - { - "type": "string" - } - ] - }, - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", - "type": "string" - } - } - }, - "RevocationResult": { - "properties": { - "revoked": { - "type": "boolean", - "example": true - } - } - }, - "SuspensionResult": { - "properties": { - "suspended": { - "type": "boolean", - "example": true - } - } - }, - "UnsuspensionResult": { - "properties": { - "unsuspended": { - "type": "boolean", - "example": true - } - } - }, - "CredentialVerifyRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", - "type": "object" - }, - "policies": { - "description": "Custom verification policies to execute when verifying credential.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "VerifyPresentationResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - } - }, - "VerifyCredentialResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - }, - "example": { - "verified": true, - "polices": {}, - "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "signer": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - } - }, - "PresentationCreateRequest": { - "type": "object", - "required": ["credentials"], - "properties": { - "credentials": { - "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", - "type": "array", - "items": { - "type": "object" - } - }, - "holderDid": { - "description": "DID of holder", - "type": "string" - }, - "verifierDid": { - "description": "DID of verifier", - "type": "string" - } - } - }, - "PresentationVerifyRequest": { - "type": "object", - "required": ["presentation"], - "properties": { - "presentation": { - "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", - "type": "object" - }, - "verifierDid": { - "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", - "type": "string" - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": false - }, - "policies": { - "description": "Custom verification policies to execute when verifying presentation.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "CredentialStatusCreateBody": { - "allOf": [ - { - "type": "object", - "required": ["did", "statusListName"], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be created.", - "type": "string" - }, - "length": { - "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true, - "default": 140000 - }, - "encoding": { - "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", - "type": "string", - "default": "base64url", - "enum": ["base64url", "base64", "hex"] - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/AlsoKnownAs" - } - ] - }, - "CredentialStatusCreateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "length": 140000, - "encoding": "base64url" - } - }, - "CredentialStatusUnencryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": false - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-employee-credentials", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "1.0.0", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - } - } - }, - "CredentialStatusCreateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusEncryptedPaymentConditionsBody": { - "type": "object", - "properties": { - "feePaymentAddress": { - "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 20 - }, - "feePaymentWindow": { - "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 10 - } - } - }, - "CredentialStatusEncryptedPaymentConditionsJson": { - "type": "object", - "properties": { - "paymentConditions": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - } - } - }, - "CredentialStatusCreateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - }, - { - "type": "object", - "required": ["feePaymentAddress", "feePaymentAmount", "feePaymentWindow"] - } - ] - }, - "CredentialStatusCreateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - }, - { - "type": "object", - "required": ["paymentConditions"] - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "paymentConditions": [ - { - "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", - "feePaymentAmount": 20, - "feePaymentWindow": 10 - } - ] - } - }, - "CredentialStatusEncryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": true - }, - "encryptedSymmetricKey": { - "type": "string", - "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" - }, - "paymentConditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feePaymentAddress": { - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "type": "string", - "example": "20000000000ncheq" - }, - "intervalInSeconds": { - "type": "number", - "example": 600 - }, - "type": { - "type": "string", - "example": "timelockPayment" - } - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-revocation-encrypted-1", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "2023-06-26T11:45:19.349Z", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - }, - "symmetricKey": { - "type": "string", - "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - } - } - } - }, - "CredentialStatusCreateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusUpdateBody": { - "type": "object", - "required": ["did", "statusListName", "indices"], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", - "type": "string" - }, - "indices": { - "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - } - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - "CredentialStatusUpdateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "indices": [10, 3199, 12109, 130999] - } - }, - "CredentialStatusUpdateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusUpdateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": ["symmetricKey"], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - }, - "CredentialStatusUpdateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": ["symmetricKey"], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "indices": [10, 3199, 12109, 130999], - "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - }, - "CredentialStatusUpdateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusCheckRequest": { - "type": "object", - "required": ["did", "statusListName", "index"], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", - "type": "string" - }, - "index": { - "description": "Credential status index to be checked for revocation or suspension.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": true - } - } - }, - "CredentialStatusCheckResult": { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" - } - ] - }, - "CredentialStatusCheckRevocationResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "revoked": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusCheckSuspensionResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "suspended": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusListSearchResult": { - "allOf": [ - { - "type": "object", - "properties": { - "found": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - } - ] - }, - "KeyImportRequest": { - "type": "object", - "properties": { - "alias": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["Ed25519", "Secp256k1"] - }, - "privateKeyHex": { - "type": "string" - } - } - }, - "KeyResult": { - "type": "object", - "properties": { - "kid": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["Ed25519", "Secp256k1"] - }, - "publicKeyHex": { - "type": "string" - } - } - }, - "DidDocument": { - "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "verificationMethod": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": ["https://www.w3.org/ns/did/v1"], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], - "verificationMethod": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "type": "Ed25519VerificationKey2018", - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" - } - ], - "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": ["https://example.com"] - } - ] - } - }, - "DidDocumentWithoutVerificationMethod": { - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": ["https://www.w3.org/ns/did/v1"], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], - "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": ["https://example.com"] - } - ] - } - }, - "DidCreateRequestFormBased": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": ["testnet", "mainnet"] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": ["uuid", "base58btc"] - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] - }, - "service": { - "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", - "type": "array", - "items": { - "type": "object", - "properties": { - "idFragment": { - "type": "string" - }, - "type": { - "type": "string" - }, - "serviceEndpoint": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "example": [ - { - "idFragment": "service-1", - "type": "LinkedDomains", - "serviceEndpoint": ["https://example.com"] - } - ] - }, - "key": { - "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", - "type": "string" - }, - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["https://www.w3.org/ns/did/v1"] - } - } - }, - "DidCreateRequestJson": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": ["testnet", "mainnet"] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": ["uuid", "base58btc"] - }, - "assertionMethod": { - "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", - "type": "boolean", - "default": true - }, - "options": { - "type": "object", - "properties": { - "key": { - "type": "string", - "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] - } - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" - } - } - }, - "DidImportRequest": { - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "DID to be imported", - "format": "uri", - "required": true - }, - "keys": { - "type": "array", - "description": "List of keys required to import the DID", - "required": true, - "items": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "PresentationCreateResult": { - "type": "object", - "properties": { - "vp": { - "type": "object", - "description": "Verifiable Presentation which could be provided to the verifier." - }, - "nbf": { - "type": "integer", - "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." - }, - "iss": { - "type": "string", - "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" - }, - "aud": { - "type": "array", - "items": { - "type": "string" - }, - "description": "DID of the verifier of the Verifiable Presentation." - } - }, - "example": { - "vp": { - "@context": ["https://www.w3.org/2018/credentials/v1"], - "type": ["VerifiablePresentation"], - "verifiableCredential": [ - "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" - ] - }, - "nbf": 1700744275, - "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", - "aud": ["did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22"] - } - }, - "DidResult": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string" - }, - "keys": { - "type": "array", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - } - } - }, - "DidUpdateResponse": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string", - "description": "The default key id of which is the key associated with the first verificationMethod" - }, - "keys": { - "type": "array", - "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "controllerKeyRefs": { - "type": "array", - "description": "The list of keyRefs which were used for signing the transaction", - "items": { - "type": "string" - } - }, - "controllerKeys": { - "type": "array", - "description": "The list of all possible keys, inlcuding all controller's keys", - "items": { - "type": "string" - } - } - } - }, - "VerificationMethod": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - }, - "controller": { - "type": "string" - }, - "publicKeyMultibase": { - "type": "string" - }, - "publicKeyJwk": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - }, - "Service": { - "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", - "type": "object", - "properties": { - "id": { - "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", - "type": "string", - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" - }, - "type": { - "description": "Service type as defined in DID Specification Registries.", - "type": "string", - "example": "LinkedDomains" - }, - "serviceEndpoint": { - "description": "Service endpoint as defined in DID Core Specification.", - "type": "array", - "items": { - "type": "string", - "example": "https://example.com" - } - } - } - }, - "DidUpdateRequest": { - "type": "object", - "properties": { - "did": { - "description": "DID identifier to be updated.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "service": { - "type": "array", - "description": "Service section of the DID Document.", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "verificationMethod": { - "type": "array", - "description": "Verification Method section of the DID Document.", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "authentication": { - "description": "Authentication section of the DID Document.", - "type": "array", - "items": { - "type": "string" - } - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - } - } - }, - "DidDeactivateRequest": { - "type": "object", - "properties": { - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "CreateResourceRequest": { - "description": "Input fields for DID-Linked Resource creation.", - "type": "object", - "additionalProperties": false, - "required": ["name", "type", "data", "encoding"], - "properties": { - "data": { - "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", - "type": "string" - }, - "encoding": { - "description": "Encoding format used to encode the data.", - "type": "string", - "enum": ["base64url", "base64", "hex"] - }, - "name": { - "description": "Name of DID-Linked Resource.", - "type": "string" - }, - "type": { - "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", - "type": "string" - }, - "alsoKnownAs": { - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "type": "array", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "description": { - "type": "string" - } - } - } - }, - "version": { - "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", - "type": "string" - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "data": "SGVsbG8gV29ybGQ=", - "encoding": "base64url", - "name": "ResourceName", - "type": "TextDocument" - } - }, - "ResourceList": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "contentMetadata": { - "type": "object" - }, - "contentStream": { - "type": "object" - }, - "dereferencingMetadata": { - "$ref": "#/components/schemas/DereferencingMetadata" - } - } - }, - "DereferencingMetadata": { - "type": "object", - "properties": { - "contentType": { - "type": "string", - "example": "application/did+ld+json" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - } - } - }, - "DidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DidDocumentMetadata" - } - } - }, - "DeactivatedDidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" - } - } - }, - "DidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": false - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "DeactivatedDidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": true - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "ResourceMetadata": { - "type": "object", - "properties": { - "resourceURI": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceCollectionId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "resourceId": { - "type": "string", - "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceName": { - "type": "string", - "example": "cheqd-issuer-logo" - }, - "resourceType": { - "type": "string", - "example": "CredentialArtwork" - }, - "mediaType": { - "type": "string", - "example": "image/png" - }, - "resourceVersion": { - "type": "string", - "example": "1.0" - }, - "checksum": { - "type": "string", - "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" - }, - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "nextVersionId": { - "type": "string", - "example": "d4829ac7-4566-478c-a408-b44767eddadc" - }, - "previousVersionId": { - "type": "string", - "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" - } - } - }, - "DidResolutionMetadata": { - "type": "object", - "properties": { - "contentType": { - "allOf": [ - { - "$ref": "#/components/schemas/ContentType" - } - ], - "example": "application/did+ld+json" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - } - } - }, - "ContentType": { - "type": "string", - "enum": ["application/did+json", "application/did+ld+json", "application/ld+json", "application/json"] - }, - "DidProperties": { - "type": "object", - "properties": { - "didString": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "method": { - "type": "string", - "example": "cheqd" - }, - "methodSpecificId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - } - } - }, - "Customer": { - "type": "object", - "properties": { - "customerId": { - "type": "string", - "example": "6w5drpiiwhhs" - }, - "address": { - "type": "string", - "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" - } - } - }, - "AccountCreateRequest": { - "type": "object", - "properties": { - "user": { - "type": "object", - "properties": { - "primaryEmail": { - "type": "string" - } - } - } - } - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - } - } - } -} + "openapi": "3.0.0", + "info": { + "title": "Credential Service API for cheqd network", + "version": "2.0.0", + "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Account" + }, + { + "name": "Key" + }, + { + "name": "DID" + }, + { + "name": "Resource" + }, + { + "name": "Credential" + }, + { + "name": "Presentation" + }, + { + "name": "Credential Status" + } + ], + "externalDocs": { + "description": "Credential Service API Documentation", + "url": "https://docs.cheqd.io/identity" + }, + "paths": { + "/account": { + "get": { + "tags": [ + "Account" + ], + "summary": "Fetch custodian-mode client details.", + "description": "This endpoint returns the custodian-mode client details for authenticated users.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/idtoken": { + "get": { + "tags": [ + "Account" + ], + "summary": "Fetch IdToken.", + "description": "This endpoint returns IdToken as JWT with list of user roles inside", + "deprecated": true, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/create": { + "post": { + "tags": [ + "Account" + ], + "summary": "Create an client for an authenticated user.", + "description": "This endpoint creates a client in the custodian-mode for an authenticated user", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/unencrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Create an unencrypted StatusList2021 credential status list.", + "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/encrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Create an encrypted StatusList2021 credential status list.", + "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/unencrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Update an existing unencrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revoke", + "suspend", + "reinstate" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/encrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Update an existing encrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revoke", + "suspend", + "reinstate" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/check": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Check a StatusList2021 index for a given Verifiable Credential.", + "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/search": { + "get": { + "tags": [ + "Credential Status" + ], + "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", + "parameters": [ + { + "in": "query", + "name": "did", + "description": "The DID of the issuer of the status list.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + }, + { + "in": "query", + "name": "statusListName", + "description": "The name of the StatusList2021 DID-Linked Resource.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusListSearchResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/issue": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Issue a Verifiable Credential", + "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Credential" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/verify": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Verify a Verifiable Credential.", + "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "verify", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCredentialResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/revoke": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Revoke a Verifiable Credential.", + "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", + "operationId": "revoke", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RevocationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/suspend": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Suspend a Verifiable Credential.", + "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "suspend", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/reinstate": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Reinstate a suspended Verifiable Credential.", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "operationId": "reinstate", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnsuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/create": { + "post": { + "tags": [ + "DID" + ], + "summary": "Create a DID Document.", + "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestFormBased" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestJson" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/update": { + "post": { + "tags": [ + "DID" + ], + "summary": "Update a DID Document.", + "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateResponse" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/import": { + "post": { + "tags": [ + "DID" + ], + "summary": "Import a DID Document.", + "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/deactivate/{did}": { + "post": { + "tags": [ + "DID" + ], + "summary": "Deactivate a DID Document.", + "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to deactivate.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeactivatedDidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/list": { + "get": { + "tags": [ + "DID" + ], + "summary": "Fetch DIDs associated with an account.", + "description": "This endpoint returns the list of DIDs controlled by the account.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/search/{did}": { + "get": { + "tags": [ + "DID" + ], + "summary": "Resolve a DID Document.", + "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to resolve.", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "metadata", + "description": "Return only metadata of DID Document instead of actual DID Document.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "versionId", + "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "versionTime", + "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "transformKeys", + "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", + "schema": { + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "Ed25519VerificationKey2020", + "JsonWebKey2020" + ] + } + }, + { + "in": "query", + "name": "service", + "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", + "schema": { + "type": "string" + }, + "example": "service-1" + }, + { + "in": "query", + "name": "relativeRef", + "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", + "schema": { + "type": "string" + }, + "example": "/path/to/file" + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/key/create": { + "post": { + "tags": [ + "Key" + ], + "summary": "Create an identity key pair.", + "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/import": { + "post": { + "tags": [ + "Key" + ], + "summary": "Import an identity key pair.", + "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/read/{kid}": { + "get": { + "tags": [ + "Key" + ], + "summary": "Fetch an identity key pair.", + "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", + "parameters": [ + { + "name": "kid", + "description": "Key ID of the identity key pair to fetch.", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/presentation/create": { + "post": { + "tags": [ + "Presentation" + ], + "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", + "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/presentation/verify": { + "post": { + "tags": [ + "Presentation" + ], + "summary": "Verify a Verifiable Presentation generated from credential(s).", + "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPresentationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/create/{did}": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Create a DID-Linked Resource.", + "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to link the resource to.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/search/{did}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get a DID-Linked Resource.", + "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "resourceId", + "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "resourceName", + "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "cheqd-issuer-logo" + }, + { + "in": "query", + "name": "resourceType", + "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "CredentialArtwork" + }, + { + "in": "query", + "name": "resourceVersion", + "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "v1" + }, + { + "in": "query", + "name": "resourceVersionTime", + "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "checksum", + "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" + }, + { + "in": "query", + "name": "resourceMetadata", + "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "any": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + } + }, + "components": { + "schemas": { + "AlsoKnownAs": { + "type": "object", + "properties": { + "alsoKnownAs": { + "type": "array", + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "uri", + "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" + }, + "description": { + "type": "string", + "description": "Optional description of the URI." + } + } + } + } + } + }, + "CredentialRequest": { + "description": "Input fields for the creating a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "issuerDid": { + "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "subjectDid": { + "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", + "type": "string", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + }, + "attributes": { + "description": "JSON object containing the attributes to be included in the credential.", + "type": "object", + "example": { + "name": "Bob", + "gender": "male" + } + }, + "@context": { + "description": "Optional properties to be included in the `@context` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://schema.org/schema.jsonld", + "https://veramo.io/contexts/profile/v1" + ] + }, + "type": { + "description": "Optional properties to be included in the `type` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "Person" + ] + }, + "expirationDate": { + "description": "Optional expiration date according to the VC Data Model specification.", + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "format": { + "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", + "type": "string", + "enum": [ + "jwt", + "jsonld" + ], + "example": "jwt" + }, + "credentialStatus": { + "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", + "type": "object", + "required": [ + "statusPurpose", + "statusListName" + ], + "properties": { + "statusPurpose": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + }, + "statusListName": { + "type": "string" + }, + "statusListIndex": { + "type": "number" + }, + "statusListVersion": { + "type": "string", + "format": "date-time" + }, + "statusListRangeStart": { + "type": "number" + }, + "statusListRangeEnd": { + "type": "number" + }, + "indexNotIn": { + "type": "number" + } + }, + "example": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials" + } + } + }, + "required": [ + "issuerDid", + "subjectDid", + "attributes" + ], + "example": { + "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "attributes": { + "gender": "male", + "name": "Bob" + }, + "@context": [ + "https://schema.org" + ], + "type": [ + "Person" + ], + "format": "jwt", + "credentialStatus": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials", + "statusListIndex": 10 + } + } + }, + "Credential": { + "description": "Input fields for revoking/suspending a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ] + }, + "type": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "VerifiableCredential", + "Person" + ] + }, + "expirationDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "issuer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + } + } + }, + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + } + } + }, + "credentialStatus": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" + }, + "statusListIndex": { + "type": "number", + "example": 20 + }, + "statusPurpose": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ], + "example": "suspension" + }, + "type": { + "type": "string", + "enum": [ + "StatusList2021Entry" + ] + } + } + }, + "issuanceDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "proof": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "jwt": { + "type": "string" + } + }, + "example": { + "type": "JwtProof2020", + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ], + "credentialSubject": { + "gender": "male", + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "name": "Bob" + }, + "credentialStatus": { + "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", + "statusIndex": 20, + "statusPurpose": "suspension", + "type": "StatusList2021Entry" + }, + "issuanceDate": "2023-06-08T13:49:28.000Z", + "issuer": { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "proof": { + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", + "type": "JwtProof2020" + }, + "type": [ + "VerifiableCredential", + "Person" + ] + } + }, + "CredentialRevokeRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", + "oneOf": [ + { + "type": "object" + }, + { + "type": "string" + } + ] + }, + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", + "type": "string" + } + } + }, + "RevocationResult": { + "properties": { + "revoked": { + "type": "boolean", + "example": true + } + } + }, + "SuspensionResult": { + "properties": { + "suspended": { + "type": "boolean", + "example": true + } + } + }, + "UnsuspensionResult": { + "properties": { + "unsuspended": { + "type": "boolean", + "example": true + } + } + }, + "CredentialVerifyRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", + "type": "object" + }, + "policies": { + "description": "Custom verification policies to execute when verifying credential.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "VerifyPresentationResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + } + }, + "VerifyCredentialResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + }, + "example": { + "verified": true, + "polices": {}, + "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "signer": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + } + }, + "PresentationCreateRequest": { + "type": "object", + "required": [ + "credentials" + ], + "properties": { + "credentials": { + "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", + "type": "array", + "items": { + "type": "object" + } + }, + "holderDid": { + "description": "DID of holder", + "type": "string" + }, + "verifierDid": { + "description": "DID of verifier", + "type": "string" + } + } + }, + "PresentationVerifyRequest": { + "type": "object", + "required": [ + "presentation" + ], + "properties": { + "presentation": { + "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", + "type": "object" + }, + "verifierDid": { + "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", + "type": "string" + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": false + }, + "policies": { + "description": "Custom verification policies to execute when verifying presentation.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "CredentialStatusCreateBody": { + "allOf": [ + { + "type": "object", + "required": [ + "did", + "statusListName" + ], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be created.", + "type": "string" + }, + "length": { + "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "default": 140000 + }, + "encoding": { + "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", + "type": "string", + "default": "base64url", + "enum": [ + "base64url", + "base64", + "hex" + ] + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/AlsoKnownAs" + } + ] + }, + "CredentialStatusCreateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "length": 140000, + "encoding": "base64url" + } + }, + "CredentialStatusUnencryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": false + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-employee-credentials", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "1.0.0", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + } + } + }, + "CredentialStatusCreateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusEncryptedPaymentConditionsBody": { + "type": "object", + "properties": { + "feePaymentAddress": { + "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 20 + }, + "feePaymentWindow": { + "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 10 + } + } + }, + "CredentialStatusEncryptedPaymentConditionsJson": { + "type": "object", + "properties": { + "paymentConditions": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + } + } + }, + "CredentialStatusCreateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + }, + { + "type": "object", + "required": [ + "feePaymentAddress", + "feePaymentAmount", + "feePaymentWindow" + ] + } + ] + }, + "CredentialStatusCreateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + }, + { + "type": "object", + "required": [ + "paymentConditions" + ] + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "paymentConditions": [ + { + "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", + "feePaymentAmount": 20, + "feePaymentWindow": 10 + } + ] + } + }, + "CredentialStatusEncryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": true + }, + "encryptedSymmetricKey": { + "type": "string", + "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" + }, + "paymentConditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feePaymentAddress": { + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "type": "string", + "example": "20000000000ncheq" + }, + "intervalInSeconds": { + "type": "number", + "example": 600 + }, + "type": { + "type": "string", + "example": "timelockPayment" + } + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-revocation-encrypted-1", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "2023-06-26T11:45:19.349Z", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + }, + "symmetricKey": { + "type": "string", + "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + } + } + } + }, + "CredentialStatusCreateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusUpdateBody": { + "type": "object", + "required": [ + "did", + "statusListName", + "indices" + ], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", + "type": "string" + }, + "indices": { + "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + } + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + "CredentialStatusUpdateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "indices": [ + 10, + 3199, + 12109, + 130999 + ] + } + }, + "CredentialStatusUpdateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusUpdateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": [ + "symmetricKey" + ], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + }, + "CredentialStatusUpdateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": [ + "symmetricKey" + ], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "indices": [ + 10, + 3199, + 12109, + 130999 + ], + "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + }, + "CredentialStatusUpdateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusCheckRequest": { + "type": "object", + "required": [ + "did", + "statusListName", + "index" + ], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", + "type": "string" + }, + "index": { + "description": "Credential status index to be checked for revocation or suspension.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": true + } + } + }, + "CredentialStatusCheckResult": { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" + } + ] + }, + "CredentialStatusCheckRevocationResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "revoked": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusCheckSuspensionResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusListSearchResult": { + "allOf": [ + { + "type": "object", + "properties": { + "found": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + } + ] + }, + "KeyImportRequest": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Ed25519", + "Secp256k1" + ] + }, + "privateKeyHex": { + "type": "string" + } + } + }, + "KeyResult": { + "type": "object", + "properties": { + "kid": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Ed25519", + "Secp256k1" + ] + }, + "publicKeyHex": { + "type": "string" + } + } + }, + "DidDocument": { + "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationMethod": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + ], + "verificationMethod": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" + } + ], + "authentication": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" + ], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + } + }, + "DidDocumentWithoutVerificationMethod": { + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + ], + "authentication": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" + ], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + } + }, + "DidCreateRequestFormBased": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": [ + "testnet", + "mainnet" + ] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": [ + "uuid", + "base58btc" + ] + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "JsonWebKey2020", + "Ed25519VerificationKey2020" + ] + }, + "service": { + "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", + "type": "array", + "items": { + "type": "object", + "properties": { + "idFragment": { + "type": "string" + }, + "type": { + "type": "string" + }, + "serviceEndpoint": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "example": [ + { + "idFragment": "service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + }, + "key": { + "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/ns/did/v1" + ] + } + } + }, + "DidCreateRequestJson": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": [ + "testnet", + "mainnet" + ] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": [ + "uuid", + "base58btc" + ] + }, + "assertionMethod": { + "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "JsonWebKey2020", + "Ed25519VerificationKey2020" + ] + } + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" + } + } + }, + "DidImportRequest": { + "type": "object", + "properties": { + "did": { + "type": "string", + "description": "DID to be imported", + "format": "uri", + "required": true + }, + "keys": { + "type": "array", + "description": "List of keys required to import the DID", + "required": true, + "items": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "PresentationCreateResult": { + "type": "object", + "properties": { + "vp": { + "type": "object", + "description": "Verifiable Presentation which could be provided to the verifier." + }, + "nbf": { + "type": "integer", + "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." + }, + "iss": { + "type": "string", + "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" + }, + "aud": { + "type": "array", + "items": { + "type": "string" + }, + "description": "DID of the verifier of the Verifiable Presentation." + } + }, + "example": { + "vp": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" + ] + }, + "nbf": 1700744275, + "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", + "aud": [ + "did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22" + ] + } + }, + "DidResult": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string" + }, + "keys": { + "type": "array", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + } + } + }, + "DidUpdateResponse": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string", + "description": "The default key id of which is the key associated with the first verificationMethod" + }, + "keys": { + "type": "array", + "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "controllerKeyRefs": { + "type": "array", + "description": "The list of keyRefs which were used for signing the transaction", + "items": { + "type": "string" + } + }, + "controllerKeys": { + "type": "array", + "description": "The list of all possible keys, inlcuding all controller's keys", + "items": { + "type": "string" + } + } + } + }, + "VerificationMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "controller": { + "type": "string" + }, + "publicKeyMultibase": { + "type": "string" + }, + "publicKeyJwk": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + }, + "Service": { + "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", + "type": "object", + "properties": { + "id": { + "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", + "type": "string", + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" + }, + "type": { + "description": "Service type as defined in DID Specification Registries.", + "type": "string", + "example": "LinkedDomains" + }, + "serviceEndpoint": { + "description": "Service endpoint as defined in DID Core Specification.", + "type": "array", + "items": { + "type": "string", + "example": "https://example.com" + } + } + } + }, + "DidUpdateRequest": { + "type": "object", + "properties": { + "did": { + "description": "DID identifier to be updated.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "service": { + "type": "array", + "description": "Service section of the DID Document.", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "verificationMethod": { + "type": "array", + "description": "Verification Method section of the DID Document.", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "authentication": { + "description": "Authentication section of the DID Document.", + "type": "array", + "items": { + "type": "string" + } + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + } + } + }, + "DidDeactivateRequest": { + "type": "object", + "properties": { + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CreateResourceRequest": { + "description": "Input fields for DID-Linked Resource creation.", + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "type", + "data", + "encoding" + ], + "properties": { + "data": { + "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", + "type": "string" + }, + "encoding": { + "description": "Encoding format used to encode the data.", + "type": "string", + "enum": [ + "base64url", + "base64", + "hex" + ] + }, + "name": { + "description": "Name of DID-Linked Resource.", + "type": "string" + }, + "type": { + "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", + "type": "string" + }, + "alsoKnownAs": { + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "type": "array", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "version": { + "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", + "type": "string" + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "data": "SGVsbG8gV29ybGQ=", + "encoding": "base64url", + "name": "ResourceName", + "type": "TextDocument" + } + }, + "ResourceList": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "contentMetadata": { + "type": "object" + }, + "contentStream": { + "type": "object" + }, + "dereferencingMetadata": { + "$ref": "#/components/schemas/DereferencingMetadata" + } + } + }, + "DereferencingMetadata": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "example": "application/did+ld+json" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + } + } + }, + "DidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DidDocumentMetadata" + } + } + }, + "DeactivatedDidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" + } + } + }, + "DidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": false + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "DeactivatedDidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": true + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "ResourceMetadata": { + "type": "object", + "properties": { + "resourceURI": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceCollectionId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "resourceId": { + "type": "string", + "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceName": { + "type": "string", + "example": "cheqd-issuer-logo" + }, + "resourceType": { + "type": "string", + "example": "CredentialArtwork" + }, + "mediaType": { + "type": "string", + "example": "image/png" + }, + "resourceVersion": { + "type": "string", + "example": "1.0" + }, + "checksum": { + "type": "string", + "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" + }, + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "nextVersionId": { + "type": "string", + "example": "d4829ac7-4566-478c-a408-b44767eddadc" + }, + "previousVersionId": { + "type": "string", + "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" + } + } + }, + "DidResolutionMetadata": { + "type": "object", + "properties": { + "contentType": { + "allOf": [ + { + "$ref": "#/components/schemas/ContentType" + } + ], + "example": "application/did+ld+json" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + } + } + }, + "ContentType": { + "type": "string", + "enum": [ + "application/did+json", + "application/did+ld+json", + "application/ld+json", + "application/json" + ] + }, + "DidProperties": { + "type": "object", + "properties": { + "didString": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "method": { + "type": "string", + "example": "cheqd" + }, + "methodSpecificId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + } + } + }, + "Customer": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "example": "6w5drpiiwhhs" + }, + "address": { + "type": "string", + "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" + } + } + }, + "AccountCreateRequest": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "primaryEmail": { + "type": "string" + } + } + } + } + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/types/portal.ts b/src/types/portal.ts index 4cb4ee9f..e52e4989 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -95,28 +95,31 @@ export type SubscriptionResumeUnsuccessfulResponseBody = UnsuccessfulResponseBod export type PortalCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; // API key -// Create -export type APIKeyCreateRequestBody = { - expiresAt?: Date; -}; -export type APIKeyCreateResponseBody = { +export type APIKeyResponseBody = { apiKey: string; + name: string; + createdAt: string; expiresAt: string; revoked: boolean; }; +// Create +export type APIKeyCreateRequestBody = { + name: string, + expiresAt?: Date; +}; + +export type APIKeyCreateResponseBody = APIKeyResponseBody export type APIKeyCreateUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Update export type APIKeyUpdateRequestBody = { apiKey: string; + name?: string; expiresAt?: Date; revoked?: boolean; }; -export type APIKeyUpdateResponseBody = { - apiKey: string; - expiresAt: string; - revoked: boolean; -}; +export type APIKeyUpdateResponseBody = APIKeyResponseBody; + export type APIKeyUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Revoke @@ -131,14 +134,12 @@ export type APIKeyRevokeUnsuccessfulResponseBody = UnsuccessfulResponseBody; // List export type APIKeyListResponseBody = { - apiKeys: APIKeyCreateResponseBody[]; + apiKeys: APIKeyResponseBody[]; }; export type APIKeyListUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Get -export type APIKeyGetRequestBody = { - apiKey: string; -}; +export type APIKeyGetRequestBody = APIKeyResponseBody; export type APIKeyGetResponseBody = APIKeyCreateResponseBody; export type APIKeyGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index ff59f11a..98d70cb8 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -171,24 +171,23 @@ * subscription: * type: object * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] - * APIKeyCreateRequestBody: - * description: The request body for creating an API key - * type: object - * properties: - * expiresAt: - * type: string - * description: The expiration date of the API key - * example: "2000-10-31T01:30:00.000-05:00" - * format: date - * required: false - * APIKeyCreateResponseBody: - * description: The response body for creating an API key + * APIKeyResponse: + * description: The general view for API key in response * type: object * properties: * apiKey: * type: string * description: The API key * example: abcdefghijklmnopqrstuvwxyz + * createdAt: + * type: string + * description: The creation date of the API key + * example: "2000-10-31T01:30:00.000-05:00" + * format: date + * name: + * type: string + * description: The name of the API key + * example: My API Key * expiresAt: * type: string * description: The expiration date of the API key @@ -198,6 +197,25 @@ * type: boolean * description: The status of the API key * example: false + * APIKeyCreateRequestBody: + * description: The request body for creating an API key + * type: object + * properties: + * expiresAt: + * type: string + * description: The expiration date of the API key + * example: "2000-10-31T01:30:00.000-05:00" + * format: date + * required: false + * name: + * type: string + * description: The name of the API key + * example: My API Key + * required: true + * APIKeyCreateResponseBody: + * description: The response body for creating an API key + * type: object + * ref: '#/components/schemas/APIKeyResponse' * APIKeyCreateUnsuccessfulResponseBody: * description: The response body for an unsuccessful API key creation * type: object @@ -208,41 +226,11 @@ * APIKeyUpdateRequestBody: * description: The request body for updating an API key * type: object - * properties: - * apiKey: - * type: string - * description: The API key - * example: abcdefghijklmnopqrstuvwxyz - * required: true - * expiresAt: - * type: string - * description: The expiration date of the API key - * example: "2000-10-31T01:30:00.000-05:00" - * format: date - * required: false - * revoked: - * type: boolean - * description: The status of the API key - * example: false - * required: false - * default: false + * ref: '#/components/schemas/APIKeyResponse' * APIKeyUpdateResponseBody: * description: The response body for an unsuccessful API key update * type: object - * properties: - * apiKey: - * type: string - * description: The API key - * example: abcdefghijklmnopqrstuvwxyz - * expiresAt: - * type: string - * description: The expiration date of the API key - * example: "2000-10-31T01:30:00.000-05:00" - * format: date - * revoked: - * type: boolean - * description: The status of the API key - * example: false + * ref: '#/components/schemas/APIKeyResponse' * APIKeyUpdateUnsuccessfulResponseBody: * description: The response body for an unsuccessful API key update * type: object @@ -287,20 +275,7 @@ * type: array * items: * type: object - * properties: - * apiKey: - * type: string - * description: The API key - * example: abcdefghijklmnopqrstuvwxyz - * expiresAt: - * type: string - * description: The expiration date of the API key - * example: "2000-10-31T01:30:00.000-05:00" - * format: date - * revoked: - * type: boolean - * description: The status of the API key - * example: false + * ref: '#/components/schemas/APIKeyResponse' * APIKeyGetRequestBody: * description: The request body for getting an API key * type: object @@ -313,20 +288,7 @@ * APIKeyGetResponseBody: * description: The response body for getting an API key * type: object - * properties: - * apiKey: - * type: string - * description: The API key - * example: abcdefghijklmnopqrstuvwxyz - * expiresAt: - * type: string - * description: The expiration date of the API key - * example: "2000-10-31T01:30:00.000-05:00" - * format: date - * revoked: - * type: boolean - * description: The status of the API key - * example: false + * ref: '#/components/schemas/APIKeyResponse' * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. * type: object From 13be89fb553ec7811afe79409968faa128845d4b Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 9 Apr 2024 15:42:08 +0200 Subject: [PATCH 24/49] Get rid of apiKeyHash --- src/controllers/admin/api-key.ts | 44 +++++-- src/database/entities/api.key.entity.ts | 14 +- src/database/migrations/AlterAPIKeyTable.ts | 32 ----- src/middleware/auth/base-auth-handler.ts | 30 +++-- .../auth/user-info-fetcher/api-token.ts | 46 +++---- .../auth/user-info-fetcher/idtoken.ts | 51 ++++++++ src/services/admin/api-key.ts | 120 +++++++++--------- src/services/identity/postgres.ts | 22 ++-- src/static/swagger-admin.json | 28 +++- src/types/portal.ts | 6 + src/types/swagger-admin-types.ts | 23 +++- 11 files changed, 260 insertions(+), 156 deletions(-) create mode 100644 src/middleware/auth/user-info-fetcher/idtoken.ts diff --git a/src/controllers/admin/api-key.ts b/src/controllers/admin/api-key.ts index 2e6df3bf..211cbcf2 100644 --- a/src/controllers/admin/api-key.ts +++ b/src/controllers/admin/api-key.ts @@ -19,6 +19,7 @@ import type { APIKeyUpdateRequestBody, APIKeyUpdateResponseBody, APIKeyUpdateUnsuccessfulResponseBody, + APIServiceOptions, } from '../../types/portal.js'; import { EventTracker, eventTracker } from '../../services/track/tracker.js'; import { OperationNameEnum } from '../../types/constants.js'; @@ -109,11 +110,12 @@ export class APIKeyController { */ @validate public async create(request: Request, response: Response) { - try { - const { name, expiresAt } = request.body satisfies APIKeyCreateRequestBody; + const { name, expiresAt } = request.body satisfies APIKeyCreateRequestBody; + const options = { decryptionNeeded: true } satisfies APIServiceOptions + try { const apiKey = APIKeyService.instance.generateAPIKey(); - const apiKeyEntity = await APIKeyService.instance.create(apiKey, name, response.locals.user, expiresAt); + const apiKeyEntity = await APIKeyService.instance.create(apiKey, name, response.locals.user, expiresAt, false, options); if (!apiKeyEntity) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: 'Cannot create a new API key', @@ -170,9 +172,18 @@ export class APIKeyController { */ @validate public async update(request: Request, response: Response) { + const { apiKey, name, expiresAt, revoked } = request.body satisfies APIKeyUpdateRequestBody; + const options = { decryptionNeeded: true } satisfies APIServiceOptions try { - const { apiKey, name, expiresAt, revoked } = request.body satisfies APIKeyUpdateRequestBody; - const apiKeyEntity = await APIKeyService.instance.update(apiKey, name, expiresAt, revoked); + const apiKeyEntity = await APIKeyService.instance.update({ + customer: response.locals.customer, + apiKey, + name, + expiresAt, + revoked + }, + options + ); if (!apiKeyEntity) { return response.status(StatusCodes.NOT_FOUND).json({ error: 'Cannot update API key cause it\'s not found', @@ -230,9 +241,10 @@ export class APIKeyController { */ @validate public async revoke(request: Request, response: Response) { + const options = { decryptionNeeded: true } satisfies APIServiceOptions + const { apiKey } = request.body satisfies APIKeyRevokeRequestBody; try { - const { apiKey } = request.body satisfies APIKeyRevokeRequestBody; - const apiKeyEntity = await APIKeyService.instance.revoke(apiKey); + const apiKeyEntity = await APIKeyService.instance.revoke(apiKey, response.locals.customer, options); if (!apiKeyEntity) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: 'Cannot revoke API key', @@ -284,9 +296,13 @@ export class APIKeyController { @validate public async list(request: Request, response: Response) { try { + const options = { decryptionNeeded: true } satisfies APIServiceOptions const apiKeyList = await APIKeyService.instance.find({ - customer: response.locals.customer, - }); + customer: response.locals.customer, + }, + undefined, + options + ); const keys = apiKeyList.map((apiKey) => { return { apiKey: apiKey.apiKey, @@ -339,10 +355,13 @@ export class APIKeyController { */ @validate public async get(request: Request, response: Response) { + + const apiKey = request.query.apiKey as string; + const options = { decryptionNeeded: true } satisfies APIServiceOptions + try { - const apiKey = request.query.apiKey as string; if (apiKey) { - const apiKeyEntity = await APIKeyService.instance.get(apiKey); + const apiKeyEntity = await APIKeyService.instance.get(apiKey, response.locals.customer, options); if (!apiKeyEntity) { return response.status(StatusCodes.NOT_FOUND).json({ error: 'API key not found', @@ -364,7 +383,8 @@ export class APIKeyController { }, { createdAt: 'DESC', - } + }, + options ); if (keys.length == 0) { return response.status(StatusCodes.NOT_FOUND).json({ diff --git a/src/database/entities/api.key.entity.ts b/src/database/entities/api.key.entity.ts index 26e69a0f..ea6ef8a1 100644 --- a/src/database/entities/api.key.entity.ts +++ b/src/database/entities/api.key.entity.ts @@ -1,4 +1,4 @@ -import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import * as dotenv from 'dotenv'; import { CustomerEntity } from './customer.entity.js'; @@ -7,12 +7,8 @@ dotenv.config(); @Entity('apiKey') export class APIKeyEntity { - @Column({ - type: 'text', - nullable: false, - primary: true, - }) - apiKeyHash!: string; + @PrimaryGeneratedColumn('uuid') + apiKeyId!: string; @Column({ type: 'text', @@ -73,7 +69,7 @@ export class APIKeyEntity { } constructor( - apiKeyHash: string, + apiKeyId: string, apiKey: string, name: string, expiresAt: Date, @@ -81,7 +77,7 @@ export class APIKeyEntity { user: UserEntity, revoked = false ) { - this.apiKeyHash = apiKeyHash; + this.apiKeyId = apiKeyId; this.apiKey = apiKey; this.name = name; this.expiresAt = expiresAt; diff --git a/src/database/migrations/AlterAPIKeyTable.ts b/src/database/migrations/AlterAPIKeyTable.ts index 72fb2520..a0fa7d73 100644 --- a/src/database/migrations/AlterAPIKeyTable.ts +++ b/src/database/migrations/AlterAPIKeyTable.ts @@ -1,6 +1,4 @@ import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm'; -import pkg from 'js-sha3'; -const { sha3_512 } = pkg; export class AlterAPIKeyTable1695740346004 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { @@ -23,36 +21,6 @@ export class AlterAPIKeyTable1695740346004 implements MigrationInterface { isNullable: false, }) ); - - // Remove unused apiKeyId column - await queryRunner.dropColumn(table_name, 'apiKeyId'); - // Add column apiKeyHash - await queryRunner.addColumn( - table_name, - new TableColumn({ - name: 'apiKeyHash', - type: 'text', - isNullable: true, - }) - ); - // Data migration - // Make all current API keys as revoked cause we need to force API key recreation - for (const apiKey of await queryRunner.query(`SELECT * FROM "${table_name}"`)) { - await queryRunner.query( - `UPDATE "${table_name}" SET "apiKeyHash" = '${sha3_512(apiKey.apiKey)}', "revoked" = 'true' WHERE "apiKey" = '${apiKey.apiKey}'` - ); - } - // Make apiKeyHash not nullable - await queryRunner.changeColumn( - table_name, - 'apiKeyHash', - new TableColumn({ - name: 'apiKeyHash', - type: 'text', - isNullable: false, - isPrimary: true, - }) - ); } public async down(queryRunner: QueryRunner): Promise { diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index 927f98c1..e1b9dfbe 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -4,13 +4,14 @@ import type { IncomingHttpHeaders } from 'http'; import type { IOAuthProvider } from './oauth/base.js'; import { LogToProvider } from './oauth/logto-provider.js'; import { SwaggerUserInfoFetcher } from './user-info-fetcher/swagger-ui.js'; -import { APITokenUserInfoFetcher } from './user-info-fetcher/api-token.js'; +import { IdTokenUserInfoFetcher } from './user-info-fetcher/idtoken.js'; import type { IUserInfoFetcher } from './user-info-fetcher/base.js'; import { IAuthHandler, RuleRoutine, IAPIGuard } from './routine.js'; import type { IAuthResponse, MethodToScopeRule } from '../../types/authentication.js'; import { M2MCredsTokenUserInfoFetcher } from './user-info-fetcher/m2m-creds-token.js'; import { decodeJwt } from 'jose'; import { PortalUserInfoFetcher } from './user-info-fetcher/portal-token.js'; +import { APITokenUserInfoFetcher } from './user-info-fetcher/api-token.js'; export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { userInfoFetcher: IUserInfoFetcher = {} as IUserInfoFetcher; @@ -131,21 +132,30 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { private chooseUserFetcherStrategy(request: Request): void { const token = BaseAuthHandler.extractBearerTokenFromHeaders(request.headers) as string; + const apiKey = request.headers['x-api-key'] as string; const headerIdToken = request.headers['id-token'] as string; if (headerIdToken && token) { this.setUserInfoStrategy(new PortalUserInfoFetcher(token, headerIdToken)); - } else { - if (token) { - const payload = decodeJwt(token); - if (payload.aud === process.env.LOGTO_APP_ID) { - this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); - } else { - this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token)); - } + return; + } + + if (token) { + const payload = decodeJwt(token); + if (payload.aud === process.env.LOGTO_APP_ID) { + this.setUserInfoStrategy(new IdTokenUserInfoFetcher(token)); + return; } else { - this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); + this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token)); + return; } } + if (apiKey) { + this.setUserInfoStrategy(new APITokenUserInfoFetcher(apiKey)); + return; + } else { + this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); + } + } public setOAuthProvider(oauthProvider: IOAuthProvider): IAuthHandler { diff --git a/src/middleware/auth/user-info-fetcher/api-token.ts b/src/middleware/auth/user-info-fetcher/api-token.ts index 26f8b661..52588a40 100644 --- a/src/middleware/auth/user-info-fetcher/api-token.ts +++ b/src/middleware/auth/user-info-fetcher/api-token.ts @@ -4,9 +4,10 @@ import type { IAuthResponse } from '../../../types/authentication.js'; import { StatusCodes } from 'http-status-codes'; import type { IUserInfoFetcher } from './base.js'; import type { IOAuthProvider } from '../oauth/base.js'; -import { createRemoteJWKSet, jwtVerify } from 'jose'; import * as dotenv from 'dotenv'; +import { APIKeyService } from '../../../services/admin/api-key.js'; +import { UserService } from '../../../services/api/user.js'; dotenv.config(); export class APITokenUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { @@ -18,31 +19,30 @@ export class APITokenUserInfoFetcher extends AuthReturn implements IUserInfoFetc } async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { - return this.verifyJWTToken(this.token as string, oauthProvider); + return this.verifyToken(this.token as string, oauthProvider); } - public async verifyJWTToken(token: string, oauthProvider: IOAuthProvider): Promise { + public async verifyToken(token: string, oauthProvider: IOAuthProvider): Promise { try { - const { payload } = await jwtVerify( - token, // The raw Bearer Token extracted from the request header - createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server - { - // expected issuer of the token, should be issued by the Logto server - issuer: oauthProvider.endpoint_issuer, - // expected audience token, should be the resource indicator of the current API - audience: process.env.LOGTO_APP_ID, - } - ); - // Setup the scopes from the token - if (!payload.roles) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No roles found in the token.`); - } - const scopes = await oauthProvider.getScopesForRoles(payload.roles as string[]); - if (!scopes) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); - } - this.setScopes(scopes); - this.setUserId(payload.sub as string); + const apiEntity = await APIKeyService.instance.discoverAPIKey(token); + if (!apiEntity) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: API Key not found.`); + } + if (apiEntity.revoked) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: API Key is revoked.`); + } + const userEntity = await UserService.instance.findOne({ customer: apiEntity.customer}); + if (!userEntity) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: User not found.`); + } + const _resp = await oauthProvider.getUserScopes(userEntity.logToId as string); + if (_resp.status !== 200) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the user.`); + } + if (_resp.data) { + this.setScopes(_resp.data); + } + this.setCustomerId(apiEntity.customer.customerId); return this.returnOk(); } catch (error) { return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); diff --git a/src/middleware/auth/user-info-fetcher/idtoken.ts b/src/middleware/auth/user-info-fetcher/idtoken.ts new file mode 100644 index 00000000..93bff01d --- /dev/null +++ b/src/middleware/auth/user-info-fetcher/idtoken.ts @@ -0,0 +1,51 @@ +import type { Request } from 'express'; +import { AuthReturn } from '../routine.js'; +import type { IAuthResponse } from '../../../types/authentication.js'; +import { StatusCodes } from 'http-status-codes'; +import type { IUserInfoFetcher } from './base.js'; +import type { IOAuthProvider } from '../oauth/base.js'; +import { createRemoteJWKSet, jwtVerify } from 'jose'; + +import * as dotenv from 'dotenv'; +dotenv.config(); + +export class IdTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { + token: string; + + constructor(token: string) { + super(); + this.token = token; + } + + async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { + return this.verifyJWTToken(this.token as string, oauthProvider); + } + + public async verifyJWTToken(token: string, oauthProvider: IOAuthProvider): Promise { + try { + const { payload } = await jwtVerify( + token, // The raw Bearer Token extracted from the request header + createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + { + // expected issuer of the token, should be issued by the Logto server + issuer: oauthProvider.endpoint_issuer, + // expected audience token, should be the resource indicator of the current API + audience: process.env.LOGTO_APP_ID, + } + ); + // Setup the scopes from the token + if (!payload.roles) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No roles found in the token.`); + } + const scopes = await oauthProvider.getScopesForRoles(payload.roles as string[]); + if (!scopes) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); + } + this.setScopes(scopes); + this.setUserId(payload.sub as string); + return this.returnOk(); + } catch (error) { + return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + } + } +} diff --git a/src/services/admin/api-key.ts b/src/services/admin/api-key.ts index 655734cb..c1d4060c 100644 --- a/src/services/admin/api-key.ts +++ b/src/services/admin/api-key.ts @@ -10,43 +10,12 @@ import type { UserEntity } from '../../database/entities/user.entity.js'; import { SecretBox } from '@veramo/kms-local'; import { API_KEY_LENGTH, API_KEY_PREFIX, API_KEY_EXPIRATION } from '../../types/constants.js'; import pkg from 'js-sha3'; +import { v4 } from 'uuid'; +import type { APIServiceOptions } from '../../types/portal.js'; dotenv.config(); const { sha3_512 } = pkg; -// Returns the decrypted API key -export function decryptAPIKey(target: any, key: string, descriptor: PropertyDescriptor | undefined) { - // save a reference to the original method this way we keep the values currently in the - // descriptor and don't overwrite what another decorator might have done to the descriptor. - if (descriptor === undefined) { - descriptor = Object.getOwnPropertyDescriptor(target, key) as PropertyDescriptor; - } - - const originalMethod = descriptor.value; - - //editing the descriptor/value parameter - descriptor.value = async function (...args: any[]) { - const decryptOne = async (entity: APIKeyEntity) => { - if (entity && entity.apiKey) { - entity.apiKey = await APIKeyService.instance.decryptAPIKey(entity.apiKey); - } - return entity; - }; - const entity = await originalMethod.apply(this, args); - if (Array.isArray(entity)) { - for (const apiKey of entity) { - await decryptOne(apiKey); - } - } else { - await decryptOne(entity); - } - return entity; - }; - - // return edited descriptor as opposed to overwriting the descriptor - return descriptor; -} - export class APIKeyService { public apiKeyRepository: Repository; private secretBox: SecretBox; @@ -60,8 +29,10 @@ export class APIKeyService { // ToDo: Maybe we also need to store not the API key but the hash of it? // But in that case the API key will be shown once and then it will be lost. - @decryptAPIKey - public async create(apiKey: string, name: string, user: UserEntity, expiresAt?: Date, revoked = false): Promise { + + public async create(apiKey: string, name: string, user: UserEntity, expiresAt?: Date, revoked = false, options?: APIServiceOptions): Promise { + const apiKeyId = v4(); + const { decryptionNeeded } = options || {}; if (!apiKey) { throw new Error('API key is not specified'); } @@ -75,28 +46,36 @@ export class APIKeyService { expiresAt = new Date(); expiresAt.setMonth(expiresAt.getMonth() + API_KEY_EXPIRATION); } - const apiKeyHash = APIKeyService.hashAPIKey(apiKey); const encryptedAPIKey = await this.secretBox.encrypt(apiKey); // Create entity - const apiKeyEntity = new APIKeyEntity(apiKeyHash, encryptedAPIKey, name, expiresAt, user.customer, user, revoked); + const apiKeyEntity = new APIKeyEntity(apiKeyId, encryptedAPIKey, name, expiresAt, user.customer, user, revoked); const apiKeyRecord = (await this.apiKeyRepository.insert(apiKeyEntity)).identifiers[0]; if (!apiKeyRecord) throw new Error(`Cannot create a new API key`); + + if (decryptionNeeded) { + apiKeyEntity.apiKey = apiKey; + } return apiKeyEntity; } - @decryptAPIKey public async update( - apiKey: string, - name?: string, - expiresAt?: Date, - revoked?: boolean, - customer?: CustomerEntity, - user?: UserEntity + item: { + customer: CustomerEntity, + apiKey: string, + name?: string, + expiresAt?: Date, + revoked?: boolean, + user?: UserEntity + }, + options?: APIServiceOptions ) { - const apiKeyHash = APIKeyService.hashAPIKey(apiKey); - const existingAPIKey = await this.apiKeyRepository.findOneBy({ apiKeyHash }); + const { apiKey, name, expiresAt, revoked, customer, user } = item; + const { decryptionNeeded } = options || {}; + + const existingAPIKey = await this.discoverAPIKey(apiKey as string, customer); + if (!existingAPIKey) { - throw new Error(`API with key id ${apiKey} not found`); + throw new Error(`API key for customer ${customer.customerId} not found`); } if (name) { existingAPIKey.name = name; @@ -115,43 +94,66 @@ export class APIKeyService { } const entity = await this.apiKeyRepository.save(existingAPIKey); + if (entity && decryptionNeeded) { + entity.apiKey = await this.decryptAPIKey(existingAPIKey.apiKey); + } return entity; } - public async revoke(apiKey: string) { - return this.update(apiKey, undefined, undefined, true); + public async revoke(apiKey: string, customer: CustomerEntity, options?: APIServiceOptions) { + return this.update({ + customer, + apiKey: apiKey, + revoked: true + }, options); } public async decryptAPIKey(apiKey: string) { return await this.secretBox.decrypt(apiKey); } - @decryptAPIKey - public async get(apiKey: string) { - const apiKeyHash = APIKeyService.hashAPIKey(apiKey); - const apiKeyEntity = await this.apiKeyRepository.findOne({ - where: { apiKeyHash: apiKeyHash }, - relations: ['customer', 'user'], - }); + public async get(apiKey: string, customer: CustomerEntity, options?: APIServiceOptions) { + const { decryptionNeeded } = options || {}; + const apiKeyEntity = await this.discoverAPIKey(apiKey, customer); + + if (apiKeyEntity && decryptionNeeded) { + apiKeyEntity.apiKey = await this.decryptAPIKey(apiKeyEntity.apiKey); + } + return apiKeyEntity; } - @decryptAPIKey - public async find(where: Record, order?: Record) { + public async find(where: Record, order?: Record, options?: APIServiceOptions) { try { + const { decryptionNeeded } = options || {}; const apiKeyList = await this.apiKeyRepository.find({ where: where, relations: ['customer', 'user'], order: order, }); + if (decryptionNeeded) { + for (const apiKey of apiKeyList) { + apiKey.apiKey = await this.decryptAPIKey(apiKey.apiKey); + } + } return apiKeyList; } catch { return []; } } - // Utils + public async discoverAPIKey(apiKey: string, customer?: CustomerEntity) { + const where = customer ? { customer } : {}; + const keys = await this.find(where, { createdAt: 'DESC' }); + for (const key of keys) { + if (await this.decryptAPIKey(key.apiKey) === apiKey) { + return key; + } + } + return undefined; + } + // Utils public generateAPIKey(): string { return `${API_KEY_PREFIX}_${randomBytes(API_KEY_LENGTH).toString('hex')}`; } diff --git a/src/services/identity/postgres.ts b/src/services/identity/postgres.ts index 811f5951..736ad0af 100644 --- a/src/services/identity/postgres.ts +++ b/src/services/identity/postgres.ts @@ -55,6 +55,7 @@ import type { AbstractIdentifierProvider } from '@veramo/did-manager'; import type { CheqdProviderError } from '@cheqd/did-provider-cheqd'; import type { TPublicKeyEd25519 } from '@cheqd/did-provider-cheqd'; import { toTPublicKeyEd25519 } from '../helpers.js'; +import type { APIServiceOptions } from '../../types/portal.js'; dotenv.config(); @@ -524,7 +525,8 @@ export class PostgresIdentityService extends DefaultIdentityService { } async setAPIKey(apiKey: string, customer: CustomerEntity, user: UserEntity): Promise { - const keys = await APIKeyService.instance.find({ customer: customer, user: user, revoked: false }); + const options = { decryptionNeeded: true } satisfies APIServiceOptions + const keys = await APIKeyService.instance.find({ customer: customer, user: user, revoked: false }, undefined, options); if (keys.length > 0) { throw new Error(`API key for customer ${customer.customerId} and user ${user.logToId} already exists`); } @@ -532,29 +534,31 @@ export class PostgresIdentityService extends DefaultIdentityService { if (!apiKeyEntity) { throw new Error(`Cannot create API key for customer ${customer.customerId} and user ${user.logToId}`); } - apiKeyEntity.apiKey = await this.decryptAPIKey(apiKeyEntity.apiKey); return apiKeyEntity; } async updateAPIKey(apiKey: APIKeyEntity, newApiKey: string): Promise { - const key = await APIKeyService.instance.get(apiKey.apiKey); + const options = { decryptionNeeded: true } satisfies APIServiceOptions + const key = await APIKeyService.instance.get(apiKey.apiKey, apiKey.customer, options); if (!key) { throw new Error(`API key not found`); } - const apiKeyEntity = await APIKeyService.instance.update( - newApiKey, - undefined, - await APIKeyService.instance.getExpiryDate(newApiKey) + const apiKeyEntity = await APIKeyService.instance.update({ + customer: key.customer, + apiKey: newApiKey, + expiresAt: await APIKeyService.instance.getExpiryDate(newApiKey) + }, + options ); if (!apiKeyEntity) { throw new Error(`Cannot update API key`); } - apiKeyEntity.apiKey = await this.decryptAPIKey(apiKeyEntity.apiKey); return apiKeyEntity; } async getAPIKey(customer: CustomerEntity, user: UserEntity): Promise { - const keys = await APIKeyService.instance.find({ customer: customer, user: user, revoked: false }); + const options = { decryptionNeeded: true } satisfies APIServiceOptions + const keys = await APIKeyService.instance.find({ customer: customer, user: user, revoked: false }, undefined, options); if (keys.length > 1) { throw new Error( `For the customer with customer id ${customer.customerId} and user with logToId ${user.logToId} there more then 1 API key` diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 362f5444..d6dddf47 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -907,7 +907,33 @@ "APIKeyUpdateRequestBody": { "description": "The request body for updating an API key", "type": "object", - "ref": "#/components/schemas/APIKeyResponse" + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz", + "required": true + }, + "name": { + "type": "string", + "description": "The name of the API key", + "example": "My API Key" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:30:00.000-05:00", + "format": "date", + "required": false + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false, + "required": false, + "default": false + } + } }, "APIKeyUpdateResponseBody": { "description": "The response body for an unsuccessful API key update", diff --git a/src/types/portal.ts b/src/types/portal.ts index e52e4989..37c7cf32 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -5,6 +5,11 @@ export type ProductWithPrices = Stripe.Product & { prices?: Stripe.Price[]; }; + +export type APIServiceOptions = { + decryptionNeeded: boolean; +}; + export type ProductListUnsuccessfulResponseBody = UnsuccessfulResponseBody; export type ProductGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; @@ -146,3 +151,4 @@ export type APIKeyGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Utils export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; + diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 98d70cb8..5bdcdda9 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -226,7 +226,28 @@ * APIKeyUpdateRequestBody: * description: The request body for updating an API key * type: object - * ref: '#/components/schemas/APIKeyResponse' + * properties: + * apiKey: + * type: string + * description: The API key + * example: abcdefghijklmnopqrstuvwxyz + * required: true + * name: + * type: string + * description: The name of the API key + * example: My API Key + * expiresAt: + * type: string + * description: The expiration date of the API key + * example: 2000-10-31T01:30:00.000-05:00 + * format: date + * required: false + * revoked: + * type: boolean + * description: The status of the API key + * example: false + * required: false + * default: false * APIKeyUpdateResponseBody: * description: The response body for an unsuccessful API key update * type: object From e0fc23ebd880995a609be7be0c61e8c1441c5bb9 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 9 Apr 2024 16:41:28 +0200 Subject: [PATCH 25/49] Lint --- src/types/swagger-admin-types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 5bdcdda9..e46f23e4 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -182,7 +182,7 @@ * createdAt: * type: string * description: The creation date of the API key - * example: "2000-10-31T01:30:00.000-05:00" + * example: "2000-10-31T01:23:45Z" * format: date * name: * type: string @@ -191,7 +191,7 @@ * expiresAt: * type: string * description: The expiration date of the API key - * example: "2000-10-31T01:30:00.000-05:00" + * example: "2000-10-31T01:23:45Z" * format: date * revoked: * type: boolean @@ -204,7 +204,7 @@ * expiresAt: * type: string * description: The expiration date of the API key - * example: "2000-10-31T01:30:00.000-05:00" + * example: "2000-10-31T01:23:45Z" * format: date * required: false * name: @@ -239,7 +239,7 @@ * expiresAt: * type: string * description: The expiration date of the API key - * example: 2000-10-31T01:30:00.000-05:00 + * example: 2000-10-31T01:23:45Z * format: date * required: false * revoked: From a2fb347957c004fb21504667f768c3f5c64857c0 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 9 Apr 2024 16:45:32 +0200 Subject: [PATCH 26/49] Push swagger file --- src/static/swagger-admin.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index d6dddf47..52abacfc 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -849,7 +849,7 @@ "createdAt": { "type": "string", "description": "The creation date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", + "example": "2000-10-31T01:23:45Z", "format": "date" }, "name": { @@ -860,7 +860,7 @@ "expiresAt": { "type": "string", "description": "The expiration date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", + "example": "2000-10-31T01:23:45Z", "format": "date" }, "revoked": { @@ -877,7 +877,7 @@ "expiresAt": { "type": "string", "description": "The expiration date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", + "example": "2000-10-31T01:23:45Z", "format": "date", "required": false }, @@ -922,7 +922,7 @@ "expiresAt": { "type": "string", "description": "The expiration date of the API key", - "example": "2000-10-31T01:30:00.000-05:00", + "example": "2000-10-31T01:23:45Z", "format": "date", "required": false }, From 7b232eb921cd1d838b92cb38da9f6b2bed24fad2 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 9 Apr 2024 16:58:45 +0200 Subject: [PATCH 27/49] Fix required field in swagger --- src/static/swagger-admin.json | 60 ++++++++++++++++++++------------ src/types/swagger-admin-types.ts | 29 +++++++++------ 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 52abacfc..d93fc415 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -878,8 +878,7 @@ "type": "string", "description": "The expiration date of the API key", "example": "2000-10-31T01:23:45Z", - "format": "date", - "required": false + "format": "date" }, "name": { "type": "string", @@ -887,12 +886,17 @@ "example": "My API Key", "required": true } - } + }, + "required": [ + "name" + ] }, "APIKeyCreateResponseBody": { "description": "The response body for creating an API key", "type": "object", - "ref": "#/components/schemas/APIKeyResponse" + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } }, "APIKeyCreateUnsuccessfulResponseBody": { "description": "The response body for an unsuccessful API key creation", @@ -911,8 +915,7 @@ "apiKey": { "type": "string", "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz", - "required": true + "example": "abcdefghijklmnopqrstuvwxyz" }, "name": { "type": "string", @@ -923,22 +926,25 @@ "type": "string", "description": "The expiration date of the API key", "example": "2000-10-31T01:23:45Z", - "format": "date", - "required": false + "format": "date" }, "revoked": { "type": "boolean", "description": "The status of the API key", "example": false, - "required": false, "default": false } - } + }, + "required": [ + "apiKey" + ] }, "APIKeyUpdateResponseBody": { "description": "The response body for an unsuccessful API key update", "type": "object", - "ref": "#/components/schemas/APIKeyResponse" + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } }, "APIKeyUpdateUnsuccessfulResponseBody": { "description": "The response body for an unsuccessful API key update", @@ -957,10 +963,12 @@ "apiKey": { "type": "string", "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz", - "required": true + "example": "abcdefghijklmnopqrstuvwxyz" } - } + }, + "required": [ + "apiKey" + ] }, "APIKeyRevokeResponseBody": { "description": "The response body for revoking an API key", @@ -969,15 +977,17 @@ "apiKey": { "type": "string", "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz", - "required": true + "example": "abcdefghijklmnopqrstuvwxyz" }, "revoked": { "type": "boolean", "description": "The status of the API key", "example": true } - } + }, + "required": [ + "apiKey" + ] }, "APIKeyRevokeUnsuccessfulResponseBody": { "description": "The response body for an unsuccessful API key revocation", @@ -997,7 +1007,9 @@ "type": "array", "items": { "type": "object", - "ref": "#/components/schemas/APIKeyResponse" + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } } } } @@ -1009,15 +1021,19 @@ "apiKey": { "type": "string", "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz", - "required": false + "example": "abcdefghijklmnopqrstuvwxyz" } - } + }, + "required": [ + "apiKey" + ] }, "APIKeyGetResponseBody": { "description": "The response body for getting an API key", "type": "object", - "ref": "#/components/schemas/APIKeyResponse" + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } }, "NotFoundError": { "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index e46f23e4..5421a237 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -206,16 +206,18 @@ * description: The expiration date of the API key * example: "2000-10-31T01:23:45Z" * format: date - * required: false * name: * type: string * description: The name of the API key * example: My API Key * required: true + * required: + * - name * APIKeyCreateResponseBody: * description: The response body for creating an API key * type: object - * ref: '#/components/schemas/APIKeyResponse' + * schema: + * ref: '#/components/schemas/APIKeyResponse' * APIKeyCreateUnsuccessfulResponseBody: * description: The response body for an unsuccessful API key creation * type: object @@ -231,7 +233,6 @@ * type: string * description: The API key * example: abcdefghijklmnopqrstuvwxyz - * required: true * name: * type: string * description: The name of the API key @@ -241,17 +242,18 @@ * description: The expiration date of the API key * example: 2000-10-31T01:23:45Z * format: date - * required: false * revoked: * type: boolean * description: The status of the API key * example: false - * required: false * default: false + * required: + * - apiKey * APIKeyUpdateResponseBody: * description: The response body for an unsuccessful API key update * type: object - * ref: '#/components/schemas/APIKeyResponse' + * schema: + * ref: '#/components/schemas/APIKeyResponse' * APIKeyUpdateUnsuccessfulResponseBody: * description: The response body for an unsuccessful API key update * type: object @@ -267,7 +269,8 @@ * type: string * description: The API key * example: abcdefghijklmnopqrstuvwxyz - * required: true + * required: + * - apiKey * APIKeyRevokeResponseBody: * description: The response body for revoking an API key * type: object @@ -276,11 +279,12 @@ * type: string * description: The API key * example: abcdefghijklmnopqrstuvwxyz - * required: true * revoked: * type: boolean * description: The status of the API key * example: true + * required: + * - apiKey * APIKeyRevokeUnsuccessfulResponseBody: * description: The response body for an unsuccessful API key revocation * type: object @@ -296,7 +300,8 @@ * type: array * items: * type: object - * ref: '#/components/schemas/APIKeyResponse' + * schema: + * ref: '#/components/schemas/APIKeyResponse' * APIKeyGetRequestBody: * description: The request body for getting an API key * type: object @@ -305,11 +310,13 @@ * type: string * description: The API key * example: abcdefghijklmnopqrstuvwxyz - * required: false + * required: + * - apiKey * APIKeyGetResponseBody: * description: The response body for getting an API key * type: object - * ref: '#/components/schemas/APIKeyResponse' + * schema: + * ref: '#/components/schemas/APIKeyResponse' * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. * type: object From 2226a48306a9509f375ba031c972417b4634b4d9 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 9 Apr 2024 17:01:40 +0200 Subject: [PATCH 28/49] Fix swagger date-time --- src/static/swagger-admin.json | 8 ++++---- src/types/swagger-admin-types.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index d93fc415..af93bdc9 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -850,7 +850,7 @@ "type": "string", "description": "The creation date of the API key", "example": "2000-10-31T01:23:45Z", - "format": "date" + "format": "date-time" }, "name": { "type": "string", @@ -861,7 +861,7 @@ "type": "string", "description": "The expiration date of the API key", "example": "2000-10-31T01:23:45Z", - "format": "date" + "format": "date-time" }, "revoked": { "type": "boolean", @@ -878,7 +878,7 @@ "type": "string", "description": "The expiration date of the API key", "example": "2000-10-31T01:23:45Z", - "format": "date" + "format": "date-time" }, "name": { "type": "string", @@ -926,7 +926,7 @@ "type": "string", "description": "The expiration date of the API key", "example": "2000-10-31T01:23:45Z", - "format": "date" + "format": "date-time" }, "revoked": { "type": "boolean", diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 5421a237..dbada729 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -183,7 +183,7 @@ * type: string * description: The creation date of the API key * example: "2000-10-31T01:23:45Z" - * format: date + * format: date-time * name: * type: string * description: The name of the API key @@ -192,7 +192,7 @@ * type: string * description: The expiration date of the API key * example: "2000-10-31T01:23:45Z" - * format: date + * format: date-time * revoked: * type: boolean * description: The status of the API key @@ -205,7 +205,7 @@ * type: string * description: The expiration date of the API key * example: "2000-10-31T01:23:45Z" - * format: date + * format: date-time * name: * type: string * description: The name of the API key @@ -241,7 +241,7 @@ * type: string * description: The expiration date of the API key * example: 2000-10-31T01:23:45Z - * format: date + * format: date-time * revoked: * type: boolean * description: The status of the API key From 5a4fd53d7247378be01549212d5dafd31c8b6fa5 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 9 Apr 2024 18:14:21 +0200 Subject: [PATCH 29/49] Fix name field in APIKeyCreateRequestBody --- src/static/swagger-admin.json | 3 +-- src/types/swagger-admin-types.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index af93bdc9..f8d03627 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -883,8 +883,7 @@ "name": { "type": "string", "description": "The name of the API key", - "example": "My API Key", - "required": true + "example": "My API Key" } }, "required": [ diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index dbada729..8f320a75 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -210,7 +210,6 @@ * type: string * description: The name of the API key * example: My API Key - * required: true * required: * - name * APIKeyCreateResponseBody: From a0a953ae37e483e36353336ffc6b6bddccae9727 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Thu, 11 Apr 2024 13:40:17 +0200 Subject: [PATCH 30/49] Get rid of env variables and make them constants --- README.md | 4 +--- docker/Dockerfile | 6 +----- src/services/admin/api-key.ts | 2 +- src/types/constants.ts | 6 +++--- src/types/environment.d.ts | 2 -- 5 files changed, 6 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9473875a..e6070e0f 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,7 @@ By default, `ENABLE_AUTHENTICATION` is set to off/`false`. To enable external Ve 2. `LOGTO_WEBHOOK_SECRET`: Webhook secret to authenticate incoming webhook requests from LogTo. 5. **Miscellaneous** 1. `COOKIE_SECRET`: Secret for cookie encryption. - 2. `API_KEY_PREFIX` (optional): Prefix for API keys. (Default "caas") - 3. `API_KEY_LENGTH` (optional): Length of API keys. (Default 32) - 4. `API_KEY_EXPIRATION` (optional): Expiration time for API keys in month. (Default 1 month) + 2. `API_KEY_EXPIRATION` (optional): Expiration time for API keys in days. (Default 30 days) #### Faucet settings diff --git a/docker/Dockerfile b/docker/Dockerfile index 7d9a0ace..5eb787ed 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -72,9 +72,7 @@ ARG ENABLE_DATADOG=false ARG LOG_LEVEL=info # API generation -ARG API_KEY_PREFIX="caas" -ARG API_KEY_LENGTH=32 -ARG API_KEY_EXPIRATION=1 +ARG API_KEY_EXPIRATION=30 # Verida connector: build-time ARG ENABLE_VERIDA_CONNECTOR=false @@ -124,8 +122,6 @@ ENV ENABLE_DATADOG ${ENABLE_DATADOG} ENV LOG_LEVEL ${LOG_LEVEL} # API generatioin -ENV API_KEY_PREFIX ${API_KEY_PREFIX} -ENV API_KEY_LENGTH ${API_KEY_LENGTH} ENV API_KEY_EXPIRATION ${API_KEY_EXPIRATION} # Faucet setup diff --git a/src/services/admin/api-key.ts b/src/services/admin/api-key.ts index c1d4060c..49579680 100644 --- a/src/services/admin/api-key.ts +++ b/src/services/admin/api-key.ts @@ -44,7 +44,7 @@ export class APIKeyService { } if (!expiresAt) { expiresAt = new Date(); - expiresAt.setMonth(expiresAt.getMonth() + API_KEY_EXPIRATION); + expiresAt.setMonth(expiresAt.getDay() + API_KEY_EXPIRATION); } const encryptedAPIKey = await this.secretBox.encrypt(apiKey); // Create entity diff --git a/src/types/constants.ts b/src/types/constants.ts index a92d89f6..1a6fa86a 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -11,9 +11,9 @@ export const HEADERS = { // Application constants export const APPLICATION_BASE_URL = process.env.APPLICATION_BASE_URL || 'http://localhost:3000'; export const CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || APPLICATION_BASE_URL; -export const API_KEY_PREFIX = process.env.API_KEY_PREFIX || 'caas'; -export const API_KEY_LENGTH = process.env.API_KEY_LENGTH || 32; -export const API_KEY_EXPIRATION = process.env.API_KEY_EXPIRATION || 1; +export const API_KEY_PREFIX = 'caas'; +export const API_KEY_LENGTH = 64; +export const API_KEY_EXPIRATION = 30; // By default we don't send events to datadog export const ENABLE_DATADOG = process.env.ENABLE_DATADOG === 'true' ? true : false; // Possible cases 'trace' 'debug' 'info' 'warn' 'error'; diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index 3a32c77c..f5584c72 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -20,8 +20,6 @@ declare global { EXTERNAL_DB_CONNECTION_URL: string; EXTERNAL_DB_ENCRYPTION_KEY: string; EXTERNAL_DB_CERT: string | undefined; - API_KEY_PREFIX: string; - API_KEY_LENGTH: number; API_KEY_EXPIRATION: number; // LogTo From 180471779aaa3fcd91d410d1cd661bac038a3348 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sat, 13 Apr 2024 15:57:27 +0200 Subject: [PATCH 31/49] Move to bcrypt and HMAC API key generating --- package-lock.json | 173 +- package.json | 2 + src/controllers/admin/api-key.ts | 57 +- src/database/entities/api.key.entity.ts | 14 +- src/database/migrations/AlterAPIKeyTable.ts | 48 + src/middleware/auth/base-auth-handler.ts | 1 - .../auth/user-info-fetcher/api-token.ts | 36 +- src/services/admin/api-key.ts | 118 +- src/services/identity/postgres.ts | 36 +- src/static/swagger-admin.json | 2056 +++-- src/static/swagger-api.json | 6880 ++++++++--------- src/types/constants.ts | 2 +- src/types/portal.ts | 6 +- 13 files changed, 4708 insertions(+), 4721 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87177282..7006beb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@verida/encryption-utils": "^3.0.0", "@verida/types": "^3.0.0", "@verida/vda-did-resolver": "^3.0.1", + "bcrypt": "^5.1.1", "bs58": "^5.0.0", "cookie-parser": "^1.4.6", "copyfiles": "^2.4.1", @@ -69,6 +70,7 @@ "@semantic-release/github": "^9.2.6", "@semantic-release/npm": "^11.0.2", "@semantic-release/release-notes-generator": "^12.1.0", + "@types/bcrypt": "^5.0.2", "@types/bs58": "^4.0.4", "@types/cookie-parser": "^1.4.6", "@types/cors": "^2.8.17", @@ -8375,6 +8377,133 @@ "node-fetch": "^2.6.7" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@motionone/animation": { "version": "10.17.0", "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.17.0.tgz", @@ -11404,6 +11533,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -14386,8 +14524,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/abort-controller": { "version": "3.0.0", @@ -14727,8 +14864,7 @@ "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "optional": true + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, "node_modules/are-we-there-yet": { "version": "3.0.1", @@ -15257,6 +15393,19 @@ "node": ">=14" } }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -16295,7 +16444,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "optional": true, "bin": { "color-support": "bin.js" } @@ -16534,8 +16682,7 @@ "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "optional": true + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -17428,8 +17575,7 @@ "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "optional": true + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, "node_modules/denodeify": { "version": "1.2.1", @@ -19958,8 +20104,7 @@ "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, "node_modules/hash-base": { "version": "3.1.0", @@ -24966,7 +25111,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "optional": true, "dependencies": { "abbrev": "1" }, @@ -30590,7 +30734,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "devOptional": true, "dependencies": { "glob": "^7.1.3" }, @@ -31365,8 +31508,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/signale": { "version": "1.4.0", @@ -34143,7 +34285,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "optional": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } diff --git a/package.json b/package.json index 86e21932..88419af8 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@verida/encryption-utils": "^3.0.0", "@verida/types": "^3.0.0", "@verida/vda-did-resolver": "^3.0.1", + "bcrypt": "^5.1.1", "bs58": "^5.0.0", "cookie-parser": "^1.4.6", "copyfiles": "^2.4.1", @@ -112,6 +113,7 @@ "@semantic-release/github": "^9.2.6", "@semantic-release/npm": "^11.0.2", "@semantic-release/release-notes-generator": "^12.1.0", + "@types/bcrypt": "^5.0.2", "@types/bs58": "^4.0.4", "@types/cookie-parser": "^1.4.6", "@types/cors": "^2.8.17", diff --git a/src/controllers/admin/api-key.ts b/src/controllers/admin/api-key.ts index 211cbcf2..3786cc55 100644 --- a/src/controllers/admin/api-key.ts +++ b/src/controllers/admin/api-key.ts @@ -41,7 +41,13 @@ export class APIKeyController { }) .toDate() .bail(), - check('name').exists().withMessage('Name is not specified').bail().isString().withMessage('Invalid name').bail(), + check('name') + .exists() + .withMessage('Name is not specified') + .bail() + .isString() + .withMessage('Invalid name') + .bail(), ]; static apiKeyUpdateValidator = [ check('apiKey') @@ -111,11 +117,18 @@ export class APIKeyController { @validate public async create(request: Request, response: Response) { const { name, expiresAt } = request.body satisfies APIKeyCreateRequestBody; - const options = { decryptionNeeded: true } satisfies APIServiceOptions + const options = { decryptionNeeded: true } satisfies APIServiceOptions; try { - const apiKey = APIKeyService.instance.generateAPIKey(); - const apiKeyEntity = await APIKeyService.instance.create(apiKey, name, response.locals.user, expiresAt, false, options); + const apiKey = await APIKeyService.generateAPIKey(response.locals.user.logToId as string); + const apiKeyEntity = await APIKeyService.instance.create( + apiKey, + name, + response.locals.user, + expiresAt, + false, + options + ); if (!apiKeyEntity) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: 'Cannot create a new API key', @@ -173,20 +186,20 @@ export class APIKeyController { @validate public async update(request: Request, response: Response) { const { apiKey, name, expiresAt, revoked } = request.body satisfies APIKeyUpdateRequestBody; - const options = { decryptionNeeded: true } satisfies APIServiceOptions + const options = { decryptionNeeded: true } satisfies APIServiceOptions; try { - const apiKeyEntity = await APIKeyService.instance.update({ - customer: response.locals.customer, - apiKey, - name, - expiresAt, - revoked - }, + const apiKeyEntity = await APIKeyService.instance.update( + { + apiKey, + name, + expiresAt, + revoked, + }, options - ); + ); if (!apiKeyEntity) { return response.status(StatusCodes.NOT_FOUND).json({ - error: 'Cannot update API key cause it\'s not found', + error: "Cannot update API key cause it's not found", } satisfies APIKeyUpdateUnsuccessfulResponseBody); } @@ -241,10 +254,10 @@ export class APIKeyController { */ @validate public async revoke(request: Request, response: Response) { - const options = { decryptionNeeded: true } satisfies APIServiceOptions + const options = { decryptionNeeded: true } satisfies APIServiceOptions; const { apiKey } = request.body satisfies APIKeyRevokeRequestBody; try { - const apiKeyEntity = await APIKeyService.instance.revoke(apiKey, response.locals.customer, options); + const apiKeyEntity = await APIKeyService.instance.revoke(apiKey, options); if (!apiKeyEntity) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: 'Cannot revoke API key', @@ -296,8 +309,9 @@ export class APIKeyController { @validate public async list(request: Request, response: Response) { try { - const options = { decryptionNeeded: true } satisfies APIServiceOptions - const apiKeyList = await APIKeyService.instance.find({ + const options = { decryptionNeeded: true } satisfies APIServiceOptions; + const apiKeyList = await APIKeyService.instance.find( + { customer: response.locals.customer, }, undefined, @@ -355,13 +369,12 @@ export class APIKeyController { */ @validate public async get(request: Request, response: Response) { - const apiKey = request.query.apiKey as string; - const options = { decryptionNeeded: true } satisfies APIServiceOptions + const options = { decryptionNeeded: true } satisfies APIServiceOptions; try { if (apiKey) { - const apiKeyEntity = await APIKeyService.instance.get(apiKey, response.locals.customer, options); + const apiKeyEntity = await APIKeyService.instance.get(apiKey, options); if (!apiKeyEntity) { return response.status(StatusCodes.NOT_FOUND).json({ error: 'API key not found', @@ -375,7 +388,7 @@ export class APIKeyController { revoked: apiKeyEntity.revoked, } satisfies APIKeyGetResponseBody); } - // Otherwise try to get the latest not revoked API key + // Otherwise try to get the latest not revoked API key const keys = await APIKeyService.instance.find( { customer: response.locals.customer, diff --git a/src/database/entities/api.key.entity.ts b/src/database/entities/api.key.entity.ts index ea6ef8a1..4df2a688 100644 --- a/src/database/entities/api.key.entity.ts +++ b/src/database/entities/api.key.entity.ts @@ -1,4 +1,4 @@ -import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; import * as dotenv from 'dotenv'; import { CustomerEntity } from './customer.entity.js'; @@ -7,8 +7,12 @@ dotenv.config(); @Entity('apiKey') export class APIKeyEntity { - @PrimaryGeneratedColumn('uuid') - apiKeyId!: string; + @Column({ + type: 'text', + primary: true, + nullable: false, + }) + apiKeyHash!: string; @Column({ type: 'text', @@ -69,7 +73,7 @@ export class APIKeyEntity { } constructor( - apiKeyId: string, + apiKeyHash: string, apiKey: string, name: string, expiresAt: Date, @@ -77,7 +81,7 @@ export class APIKeyEntity { user: UserEntity, revoked = false ) { - this.apiKeyId = apiKeyId; + this.apiKeyHash = apiKeyHash; this.apiKey = apiKey; this.name = name; this.expiresAt = expiresAt; diff --git a/src/database/migrations/AlterAPIKeyTable.ts b/src/database/migrations/AlterAPIKeyTable.ts index a0fa7d73..c31c0b66 100644 --- a/src/database/migrations/AlterAPIKeyTable.ts +++ b/src/database/migrations/AlterAPIKeyTable.ts @@ -1,8 +1,11 @@ import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm'; +import bcrypt from 'bcrypt'; +import { SecretBox } from '@veramo/kms-local'; export class AlterAPIKeyTable1695740346004 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const table_name = 'apiKey'; + const secretBox = new SecretBox(process.env.EXTERNAL_DB_ENCRYPTION_KEY); await queryRunner.addColumn( table_name, @@ -15,6 +18,51 @@ export class AlterAPIKeyTable1695740346004 implements MigrationInterface { await queryRunner.addColumn( table_name, + new TableColumn({ + name: 'name', + type: 'text', + isNullable: true, + }) + ); + + await queryRunner.addColumn( + table_name, + new TableColumn({ + name: 'apiKeyHash', + type: 'varchar', + isNullable: true, + isUnique: true, + }) + ); + + for (const record of await queryRunner.query(`SELECT * FROM "${table_name}"`)) { + const hash = await bcrypt.hash(record.apiKey, 12); + const encryptedHash = await secretBox.encrypt(record.apiKey); + // All the previous idToken should be unique + const name = 'idToken'; + await queryRunner.query( + `UPDATE "${table_name}" SET "apiKeyHash" = '${hash}', "apiKey" = '${encryptedHash}', "name" = '${name}' WHERE "apiKeyId" = '${record.apiKeyId}'` + ); + } + + // Drop old id field + await queryRunner.dropColumn(table_name, 'apiKeyId'); + + // setup apiKeyHash column as primary + await queryRunner.changeColumn( + table_name, + 'apiKeyHash', + new TableColumn({ + name: 'apiKeyHash', + type: 'varchar', + isNullable: false, + isPrimary: true, + isUnique: true, + }) + ); + await queryRunner.changeColumn( + table_name, + 'name', new TableColumn({ name: 'name', type: 'text', diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index f928ebc8..a86a3f55 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -164,7 +164,6 @@ export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); } } - } public setOAuthProvider(oauthProvider: IOAuthProvider): IAuthHandler { diff --git a/src/middleware/auth/user-info-fetcher/api-token.ts b/src/middleware/auth/user-info-fetcher/api-token.ts index 52588a40..4964dc16 100644 --- a/src/middleware/auth/user-info-fetcher/api-token.ts +++ b/src/middleware/auth/user-info-fetcher/api-token.ts @@ -24,24 +24,24 @@ export class APITokenUserInfoFetcher extends AuthReturn implements IUserInfoFetc public async verifyToken(token: string, oauthProvider: IOAuthProvider): Promise { try { - const apiEntity = await APIKeyService.instance.discoverAPIKey(token); - if (!apiEntity) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: API Key not found.`); - } - if (apiEntity.revoked) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: API Key is revoked.`); - } - const userEntity = await UserService.instance.findOne({ customer: apiEntity.customer}); - if (!userEntity) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: User not found.`); - } - const _resp = await oauthProvider.getUserScopes(userEntity.logToId as string); - if (_resp.status !== 200) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the user.`); - } - if (_resp.data) { - this.setScopes(_resp.data); - } + const apiEntity = await APIKeyService.instance.get(token); + if (!apiEntity) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: API Key not found.`); + } + if (apiEntity.revoked) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: API Key is revoked.`); + } + const userEntity = await UserService.instance.findOne({ customer: apiEntity.customer }); + if (!userEntity) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: User not found.`); + } + const _resp = await oauthProvider.getUserScopes(userEntity.logToId as string); + if (_resp.status !== 200) { + return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the user.`); + } + if (_resp.data) { + this.setScopes(_resp.data); + } this.setCustomerId(apiEntity.customer.customerId); return this.returnOk(); } catch (error) { diff --git a/src/services/admin/api-key.ts b/src/services/admin/api-key.ts index 49579680..adc843b7 100644 --- a/src/services/admin/api-key.ts +++ b/src/services/admin/api-key.ts @@ -1,6 +1,7 @@ import type { Repository } from 'typeorm'; import { decodeJWT } from 'did-jwt'; -import { randomBytes } from 'crypto'; +import bcrypt from 'bcrypt'; +import { randomBytes, createHmac } from 'crypto'; import { Connection } from '../../database/connection/connection.js'; import * as dotenv from 'dotenv'; @@ -8,14 +9,10 @@ import type { CustomerEntity } from '../../database/entities/customer.entity.js' import { APIKeyEntity } from '../../database/entities/api.key.entity.js'; import type { UserEntity } from '../../database/entities/user.entity.js'; import { SecretBox } from '@veramo/kms-local'; -import { API_KEY_LENGTH, API_KEY_PREFIX, API_KEY_EXPIRATION } from '../../types/constants.js'; -import pkg from 'js-sha3'; -import { v4 } from 'uuid'; +import { API_SECRET_KEY_LENGTH, API_KEY_PREFIX, API_KEY_EXPIRATION } from '../../types/constants.js'; import type { APIServiceOptions } from '../../types/portal.js'; dotenv.config(); -const { sha3_512 } = pkg; - export class APIKeyService { public apiKeyRepository: Repository; private secretBox: SecretBox; @@ -30,8 +27,15 @@ export class APIKeyService { // ToDo: Maybe we also need to store not the API key but the hash of it? // But in that case the API key will be shown once and then it will be lost. - public async create(apiKey: string, name: string, user: UserEntity, expiresAt?: Date, revoked = false, options?: APIServiceOptions): Promise { - const apiKeyId = v4(); + public async create( + apiKey: string, + name: string, + user: UserEntity, + expiresAt?: Date, + revoked = false, + options?: APIServiceOptions + ): Promise { + const apiKeyHash = await APIKeyService.hashAPIKey(apiKey); const { decryptionNeeded } = options || {}; if (!apiKey) { throw new Error('API key is not specified'); @@ -46,9 +50,17 @@ export class APIKeyService { expiresAt = new Date(); expiresAt.setMonth(expiresAt.getDay() + API_KEY_EXPIRATION); } - const encryptedAPIKey = await this.secretBox.encrypt(apiKey); + const encryptedAPIKey = await this.encryptAPIKey(apiKey); // Create entity - const apiKeyEntity = new APIKeyEntity(apiKeyId, encryptedAPIKey, name, expiresAt, user.customer, user, revoked); + const apiKeyEntity = new APIKeyEntity( + apiKeyHash, + encryptedAPIKey, + name, + expiresAt, + user.customer, + user, + revoked + ); const apiKeyRecord = (await this.apiKeyRepository.insert(apiKeyEntity)).identifiers[0]; if (!apiKeyRecord) throw new Error(`Cannot create a new API key`); @@ -60,22 +72,22 @@ export class APIKeyService { public async update( item: { - customer: CustomerEntity, - apiKey: string, - name?: string, - expiresAt?: Date, - revoked?: boolean, - user?: UserEntity + apiKey: string; + name?: string; + expiresAt?: Date; + revoked?: boolean; + customer?: CustomerEntity; + user?: UserEntity; }, options?: APIServiceOptions ) { - const { apiKey, name, expiresAt, revoked, customer, user } = item; + const { apiKey, name, expiresAt, customer, revoked, user } = item; const { decryptionNeeded } = options || {}; - const existingAPIKey = await this.discoverAPIKey(apiKey as string, customer); + const existingAPIKey = await this.get(apiKey); if (!existingAPIKey) { - throw new Error(`API key for customer ${customer.customerId} not found`); + throw new Error(`API key not found`); } if (name) { existingAPIKey.name = name; @@ -100,30 +112,44 @@ export class APIKeyService { return entity; } - public async revoke(apiKey: string, customer: CustomerEntity, options?: APIServiceOptions) { - return this.update({ - customer, - apiKey: apiKey, - revoked: true - }, options); + public async revoke(apiKey: string, options?: APIServiceOptions) { + return this.update( + { + apiKey: apiKey, + revoked: true, + }, + options + ); } public async decryptAPIKey(apiKey: string) { return await this.secretBox.decrypt(apiKey); } - public async get(apiKey: string, customer: CustomerEntity, options?: APIServiceOptions) { + public async encryptAPIKey(apiKey: string) { + return await this.secretBox.encrypt(apiKey); + } + + public async get(apiKey: string, options?: APIServiceOptions) { const { decryptionNeeded } = options || {}; - const apiKeyEntity = await this.discoverAPIKey(apiKey, customer); - if (apiKeyEntity && decryptionNeeded) { - apiKeyEntity.apiKey = await this.decryptAPIKey(apiKeyEntity.apiKey); + // ToDo: possible bottleneck cause we are fetching all the keys + for (const record of await this.find({})) { + if (await APIKeyService.compareAPIKey(apiKey, record.apiKeyHash)) { + if (decryptionNeeded) { + record.apiKey = await this.decryptAPIKey(record.apiKey); + } + return record; + } } - - return apiKeyEntity; + return null; } - public async find(where: Record, order?: Record, options?: APIServiceOptions) { + public async find( + where: Record, + order?: Record, + options?: APIServiceOptions + ) { try { const { decryptionNeeded } = options || {}; const apiKeyList = await this.apiKeyRepository.find({ @@ -142,28 +168,22 @@ export class APIKeyService { } } - public async discoverAPIKey(apiKey: string, customer?: CustomerEntity) { - const where = customer ? { customer } : {}; - const keys = await this.find(where, { createdAt: 'DESC' }); - for (const key of keys) { - if (await this.decryptAPIKey(key.apiKey) === apiKey) { - return key; - } - } - return undefined; - } - // Utils - public generateAPIKey(): string { - return `${API_KEY_PREFIX}_${randomBytes(API_KEY_LENGTH).toString('hex')}`; + public static generateAPIKey(userId: string): string { + const apiKey = createHmac('sha512', randomBytes(API_SECRET_KEY_LENGTH)).update(userId).digest('hex'); + return `${API_KEY_PREFIX}_${apiKey}`; } - public async getExpiryDate(apiKey: string): Promise { - const decrypted = await decodeJWT(apiKey); + public static getExpiryDateJWT(apiKey: string): Date { + const decrypted = decodeJWT(apiKey); return new Date(decrypted.payload.exp ? decrypted.payload.exp * 1000 : 0); } - public static hashAPIKey(apiKey: string): string { - return sha3_512(apiKey); + public static async hashAPIKey(apiKey: string): Promise { + return bcrypt.hash(apiKey, 12); + } + + public static async compareAPIKey(apiKey: string, hash: string): Promise { + return bcrypt.compare(apiKey, hash); } } diff --git a/src/services/identity/postgres.ts b/src/services/identity/postgres.ts index 736ad0af..2b1c3fe9 100644 --- a/src/services/identity/postgres.ts +++ b/src/services/identity/postgres.ts @@ -525,12 +525,23 @@ export class PostgresIdentityService extends DefaultIdentityService { } async setAPIKey(apiKey: string, customer: CustomerEntity, user: UserEntity): Promise { - const options = { decryptionNeeded: true } satisfies APIServiceOptions - const keys = await APIKeyService.instance.find({ customer: customer, user: user, revoked: false }, undefined, options); + const options = { decryptionNeeded: true } satisfies APIServiceOptions; + const keys = await APIKeyService.instance.find( + { customer: customer, user: user, revoked: false }, + undefined, + options + ); if (keys.length > 0) { throw new Error(`API key for customer ${customer.customerId} and user ${user.logToId} already exists`); } - const apiKeyEntity = await APIKeyService.instance.create(apiKey, "idToken", user); + const apiKeyEntity = await APIKeyService.instance.create( + apiKey, + 'idToken', + user, + undefined, + undefined, + options + ); if (!apiKeyEntity) { throw new Error(`Cannot create API key for customer ${customer.customerId} and user ${user.logToId}`); } @@ -538,16 +549,17 @@ export class PostgresIdentityService extends DefaultIdentityService { } async updateAPIKey(apiKey: APIKeyEntity, newApiKey: string): Promise { - const options = { decryptionNeeded: true } satisfies APIServiceOptions - const key = await APIKeyService.instance.get(apiKey.apiKey, apiKey.customer, options); + const options = { decryptionNeeded: true } satisfies APIServiceOptions; + const key = await APIKeyService.instance.get(apiKey.apiKey, options); if (!key) { throw new Error(`API key not found`); } - const apiKeyEntity = await APIKeyService.instance.update({ + const apiKeyEntity = await APIKeyService.instance.update( + { customer: key.customer, apiKey: newApiKey, - expiresAt: await APIKeyService.instance.getExpiryDate(newApiKey) - }, + expiresAt: APIKeyService.getExpiryDateJWT(newApiKey), + }, options ); if (!apiKeyEntity) { @@ -557,8 +569,12 @@ export class PostgresIdentityService extends DefaultIdentityService { } async getAPIKey(customer: CustomerEntity, user: UserEntity): Promise { - const options = { decryptionNeeded: true } satisfies APIServiceOptions - const keys = await APIKeyService.instance.find({ customer: customer, user: user, revoked: false }, undefined, options); + const options = { decryptionNeeded: true } satisfies APIServiceOptions; + const keys = await APIKeyService.instance.find( + { customer: customer, user: user, revoked: false, name: 'idToken' }, + undefined, + options + ); if (keys.length > 1) { throw new Error( `For the customer with customer id ${customer.customerId} and user with logToId ${user.logToId} there more then 1 API key` diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 8106c31e..72021893 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -1,1049 +1,1011 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service admin API for cheqd network", - "version": "2.0.0", - "description": "Admin API which handles users subscriptions and payments", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Product" - }, - { - "name": "Price" - }, - { - "name": "Subscription" - }, - { - "name": "API Key" - } - ], - "paths": { - "/admin/api-key/create": { - "post": { - "summary": "Create a new API key", - "description": "Create a new API key", - "tags": [ - "API Key" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyCreateRequestBody" - } - } - } - }, - "responses": { - "201": { - "description": "A new API key has been created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyCreateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/APIKeyCreateUnsuccessfulResponseBody" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/update": { - "post": { - "summary": "Update an existing API key", - "description": "Update an existing API key", - "tags": [ - "API Key" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyUpdateRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The API key has been updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyUpdateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/APIKeyUpdateUnsuccessfulResponseBody" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/revoke": { - "delete": { - "summary": "Revoke an existing API key", - "description": "Revoke an existing API key", - "tags": [ - "API Key" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyRevokeRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The API key has been revoked", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyRevokeResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/APIKeyRevokeUnsuccessfulResponseBody" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/list": { - "get": { - "summary": "List all API keys", - "description": "List all API keys", - "tags": [ - "API Key" - ], - "responses": { - "200": { - "description": "A list of API keys", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/get": { - "get": { - "summary": "Get an API key", - "description": "Get an API key. If the API key is not provided, the latest not revoked API key it returns.", - "tags": [ - "API Key" - ], - "parameters": [ - { - "name": "apiKey", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/price/list": { - "get": { - "summary": "Get a list of prices", - "description": "Get a list of prices", - "tags": [ - "Price" - ], - "parameters": [ - { - "in": "query", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id. If passed - returns filtered by this product list of prices.", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of prices", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PriceListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/list": { - "get": { - "summary": "Get a list of products", - "description": "Get a list of products which are on a Stripe side", - "tags": [ - "Product" - ], - "parameters": [ - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the list of products with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of products", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/get/{productId}": { - "get": { - "summary": "Get a product", - "description": "Get a product by id", - "tags": [ - "Product" - ], - "parameters": [ - { - "in": "path", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id which identifies the product in Stripe" - }, - "required": true - }, - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the product with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A product", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/create": { - "post": { - "summary": "Create a subscription", - "description": "Creates a new subscription for an existing customer", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateRequestBody" - } - } - } - }, - "responses": { - "201": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/update": { - "post": { - "summary": "Update a subscription", - "description": "Updates an existing subscription", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/list": { - "get": { - "summary": "Get a list of subscriptions", - "description": "Get a list of subscriptions", - "tags": [ - "Subscription" - ], - "parameters": [ - { - "in": "query", - "name": "paymentProviderId", - "schema": { - "type": "string", - "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." - } - } - ], - "responses": { - "200": { - "description": "A list of subscriptions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/get": { - "get": { - "summary": "Get a subscription", - "description": "Get a subscription", - "tags": [ - "Subscription" - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/cancel": { - "post": { - "summary": "Cancel a subscription", - "description": "Cancels an existing subscription", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/resume": {}, - "post": { - "summary": "Resume a subscription", - "description": "Resumes an existing subscription", - "tags": [ - "Subscription" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "components": { - "schemas": { - "PriceListResponseBody": { - "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", - "type": "object", - "properties": { - "prices": { - "type": "array", - "items": { - "type": "object", - "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" - } - } - } - }, - "ProductListResponseBody": { - "type": "object", - "properties": { - "products": { - "type": "array", - "items": { - "type": "object", - "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" - } - } - } - }, - "ProductGetResponseBody": { - "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", - "type": "object" - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - }, - "SubscriptionCreateRequestBody": { - "description": "The request body for creating a subscription", - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - }, - "successURL": { - "type": "string", - "description": "The URL to redirect to after the customer sucessfully completes the checkout", - "example": "https://example.com/success" - }, - "cancelURL": { - "type": "string", - "description": "The URL to redirect to after the customer cancels the checkout", - "example": "https://example.com/cancel" - }, - "quantity": { - "type": "number", - "description": "The quantity of the product", - "example": 1 - }, - "trialPeriodDays": { - "type": "number", - "description": "The number of days the customer has to pay for the product", - "example": 7 - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionCreateResponseBody": { - "description": "The response body for creating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", - "properties": { - "sessionURL": { - "type": "string", - "description": "URL which user should follow to manage subscription" - } - } - } - } - }, - "SubscriptionUpdateRequestBody": { - "description": "The request body for updating a subscription", - "type": "object", - "properties": { - "returnURL": { - "type": "string", - "description": "URL which is used to redirect to the page with ability to update the subscription" - } - } - }, - "SubscriptionUpdateResponseBody": { - "description": "The response body for updating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "Object with redirect url inside", - "properties": { - "sessionURL": { - "type": "string", - "description": "URL with session URL rediect to" - } - } - } - } - }, - "SubscriptionGetRequestBody": { - "description": "The request body for getting a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionGetResponseBody": { - "description": "The response body for getting a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionListRequestBody": { - "description": "The request body for listing subscriptions", - "type": "object", - "properties": { - "customerId": { - "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" - } - } - }, - "SubscriptionListResponseBody": { - "description": "The response body for listing subscriptions", - "type": "object", - "properties": { - "subscriptions": { - "type": "array", - "items": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - } - }, - "SubscriptionCancelRequestBody": { - "description": "The request body for canceling a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionCancelResponseBody": { - "description": "The response body for canceling a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionResumeRequestBody": { - "description": "The request body for resuming a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionResumeResponseBody": { - "description": "The response body for resuming a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - }, - "APIKeyResponse": { - "description": "The general view for API key in response", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "createdAt": { - "type": "string", - "description": "The creation date of the API key", - "example": "2000-10-31T01:23:45Z", - "format": "date-time" - }, - "name": { - "type": "string", - "description": "The name of the API key", - "example": "My API Key" - }, - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:23:45Z", - "format": "date-time" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": false - } - } - }, - "APIKeyCreateRequestBody": { - "description": "The request body for creating an API key", - "type": "object", - "properties": { - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:23:45Z", - "format": "date-time" - }, - "name": { - "type": "string", - "description": "The name of the API key", - "example": "My API Key" - } - }, - "required": [ - "name" - ] - }, - "APIKeyCreateResponseBody": { - "description": "The response body for creating an API key", - "type": "object", - "schema": { - "ref": "#/components/schemas/APIKeyResponse" - } - }, - "APIKeyCreateUnsuccessfulResponseBody": { - "description": "The response body for an unsuccessful API key creation", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "API key creation unsuccessful" - } - } - }, - "APIKeyUpdateRequestBody": { - "description": "The request body for updating an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "name": { - "type": "string", - "description": "The name of the API key", - "example": "My API Key" - }, - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:23:45Z", - "format": "date-time" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": false, - "default": false - } - }, - "required": [ - "apiKey" - ] - }, - "APIKeyUpdateResponseBody": { - "description": "The response body for an unsuccessful API key update", - "type": "object", - "schema": { - "ref": "#/components/schemas/APIKeyResponse" - } - }, - "APIKeyUpdateUnsuccessfulResponseBody": { - "description": "The response body for an unsuccessful API key update", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "API key update unsuccessful" - } - } - }, - "APIKeyRevokeRequestBody": { - "description": "The request body for revoking an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - } - }, - "required": [ - "apiKey" - ] - }, - "APIKeyRevokeResponseBody": { - "description": "The response body for revoking an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": true - } - }, - "required": [ - "apiKey" - ] - }, - "APIKeyRevokeUnsuccessfulResponseBody": { - "description": "The response body for an unsuccessful API key revocation", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "API key revocation unsuccessful" - } - } - }, - "APIKeyListResponseBody": { - "description": "The response body for listing API keys", - "type": "object", - "properties": { - "apiKeys": { - "type": "array", - "items": { - "type": "object", - "schema": { - "ref": "#/components/schemas/APIKeyResponse" - } - } - } - } - }, - "APIKeyGetRequestBody": { - "description": "The request body for getting an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - } - }, - "required": [ - "apiKey" - ] - }, - "APIKeyGetResponseBody": { - "description": "The response body for getting an API key", - "type": "object", - "schema": { - "ref": "#/components/schemas/APIKeyResponse" - } - }, - "NotFoundError": { - "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Not Found Error" - } - } - } - } - } + "openapi": "3.0.0", + "info": { + "title": "Credential Service admin API for cheqd network", + "version": "2.0.0", + "description": "Admin API which handles users subscriptions and payments", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Product" + }, + { + "name": "Price" + }, + { + "name": "Subscription" + }, + { + "name": "API Key" + } + ], + "paths": { + "/admin/api-key/create": { + "post": { + "summary": "Create a new API key", + "description": "Create a new API key", + "tags": ["API Key"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "A new API key has been created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyCreateUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/update": { + "post": { + "summary": "Update an existing API key", + "description": "Update an existing API key", + "tags": ["API Key"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The API key has been updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyUpdateUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/revoke": { + "delete": { + "summary": "Revoke an existing API key", + "description": "Revoke an existing API key", + "tags": ["API Key"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyRevokeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The API key has been revoked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyRevokeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyRevokeUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/list": { + "get": { + "summary": "List all API keys", + "description": "List all API keys", + "tags": ["API Key"], + "responses": { + "200": { + "description": "A list of API keys", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/get": { + "get": { + "summary": "Get an API key", + "description": "Get an API key. If the API key is not provided, the latest not revoked API key it returns.", + "tags": ["API Key"], + "parameters": [ + { + "name": "apiKey", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/price/list": { + "get": { + "summary": "Get a list of prices", + "description": "Get a list of prices", + "tags": ["Price"], + "parameters": [ + { + "in": "query", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id. If passed - returns filtered by this product list of prices.", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of prices", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriceListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/list": { + "get": { + "summary": "Get a list of products", + "description": "Get a list of products which are on a Stripe side", + "tags": ["Product"], + "parameters": [ + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the list of products with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of products", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/get/{productId}": { + "get": { + "summary": "Get a product", + "description": "Get a product by id", + "tags": ["Product"], + "parameters": [ + { + "in": "path", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id which identifies the product in Stripe" + }, + "required": true + }, + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the product with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/create": { + "post": { + "summary": "Create a subscription", + "description": "Creates a new subscription for an existing customer", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/update": { + "post": { + "summary": "Update a subscription", + "description": "Updates an existing subscription", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/list": { + "get": { + "summary": "Get a list of subscriptions", + "description": "Get a list of subscriptions", + "tags": ["Subscription"], + "parameters": [ + { + "in": "query", + "name": "paymentProviderId", + "schema": { + "type": "string", + "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." + } + } + ], + "responses": { + "200": { + "description": "A list of subscriptions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/get": { + "get": { + "summary": "Get a subscription", + "description": "Get a subscription", + "tags": ["Subscription"], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/cancel": { + "post": { + "summary": "Cancel a subscription", + "description": "Cancels an existing subscription", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/resume": {}, + "post": { + "summary": "Resume a subscription", + "description": "Resumes an existing subscription", + "tags": ["Subscription"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "components": { + "schemas": { + "PriceListResponseBody": { + "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", + "type": "object", + "properties": { + "prices": { + "type": "array", + "items": { + "type": "object", + "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" + } + } + } + }, + "ProductListResponseBody": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "type": "object", + "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" + } + } + } + }, + "ProductGetResponseBody": { + "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", + "type": "object" + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + }, + "SubscriptionCreateRequestBody": { + "description": "The request body for creating a subscription", + "type": "object", + "properties": { + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + }, + "successURL": { + "type": "string", + "description": "The URL to redirect to after the customer sucessfully completes the checkout", + "example": "https://example.com/success" + }, + "cancelURL": { + "type": "string", + "description": "The URL to redirect to after the customer cancels the checkout", + "example": "https://example.com/cancel" + }, + "quantity": { + "type": "number", + "description": "The quantity of the product", + "example": 1 + }, + "trialPeriodDays": { + "type": "number", + "description": "The number of days the customer has to pay for the product", + "example": 7 + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionCreateResponseBody": { + "description": "The response body for creating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", + "properties": { + "sessionURL": { + "type": "string", + "description": "URL which user should follow to manage subscription" + } + } + } + } + }, + "SubscriptionUpdateRequestBody": { + "description": "The request body for updating a subscription", + "type": "object", + "properties": { + "returnURL": { + "type": "string", + "description": "URL which is used to redirect to the page with ability to update the subscription" + } + } + }, + "SubscriptionUpdateResponseBody": { + "description": "The response body for updating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "Object with redirect url inside", + "properties": { + "sessionURL": { + "type": "string", + "description": "URL with session URL rediect to" + } + } + } + } + }, + "SubscriptionGetRequestBody": { + "description": "The request body for getting a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionGetResponseBody": { + "description": "The response body for getting a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionListRequestBody": { + "description": "The request body for listing subscriptions", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + } + } + }, + "SubscriptionListResponseBody": { + "description": "The response body for listing subscriptions", + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + } + }, + "SubscriptionCancelRequestBody": { + "description": "The request body for canceling a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionCancelResponseBody": { + "description": "The response body for canceling a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeRequestBody": { + "description": "The request body for resuming a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeResponseBody": { + "description": "The response body for resuming a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + }, + "APIKeyResponse": { + "description": "The general view for API key in response", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "createdAt": { + "type": "string", + "description": "The creation date of the API key", + "example": "2000-10-31T01:23:45Z", + "format": "date-time" + }, + "name": { + "type": "string", + "description": "The name of the API key", + "example": "My API Key" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:23:45Z", + "format": "date-time" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false + } + } + }, + "APIKeyCreateRequestBody": { + "description": "The request body for creating an API key", + "type": "object", + "properties": { + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:23:45Z", + "format": "date-time" + }, + "name": { + "type": "string", + "description": "The name of the API key", + "example": "My API Key" + } + }, + "required": ["name"] + }, + "APIKeyCreateResponseBody": { + "description": "The response body for creating an API key", + "type": "object", + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } + }, + "APIKeyCreateUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key creation", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key creation unsuccessful" + } + } + }, + "APIKeyUpdateRequestBody": { + "description": "The request body for updating an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "name": { + "type": "string", + "description": "The name of the API key", + "example": "My API Key" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:23:45Z", + "format": "date-time" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false, + "default": false + } + }, + "required": ["apiKey"] + }, + "APIKeyUpdateResponseBody": { + "description": "The response body for an unsuccessful API key update", + "type": "object", + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } + }, + "APIKeyUpdateUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key update", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key update unsuccessful" + } + } + }, + "APIKeyRevokeRequestBody": { + "description": "The request body for revoking an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + } + }, + "required": ["apiKey"] + }, + "APIKeyRevokeResponseBody": { + "description": "The response body for revoking an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": true + } + }, + "required": ["apiKey"] + }, + "APIKeyRevokeUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key revocation", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key revocation unsuccessful" + } + } + }, + "APIKeyListResponseBody": { + "description": "The response body for listing API keys", + "type": "object", + "properties": { + "apiKeys": { + "type": "array", + "items": { + "type": "object", + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } + } + } + } + }, + "APIKeyGetRequestBody": { + "description": "The request body for getting an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + } + }, + "required": ["apiKey"] + }, + "APIKeyGetResponseBody": { + "description": "The response body for getting an API key", + "type": "object", + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } + }, + "NotFoundError": { + "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Not Found Error" + } + } + } + } + } } diff --git a/src/static/swagger-api.json b/src/static/swagger-api.json index 8edb8fb8..b2cf4b99 100644 --- a/src/static/swagger-api.json +++ b/src/static/swagger-api.json @@ -1,3550 +1,3334 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service API for cheqd network", - "version": "2.0.0", - "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Account" - }, - { - "name": "Key" - }, - { - "name": "DID" - }, - { - "name": "Resource" - }, - { - "name": "Credential" - }, - { - "name": "Presentation" - }, - { - "name": "Credential Status" - } - ], - "externalDocs": { - "description": "Credential Service API Documentation", - "url": "https://docs.cheqd.io/identity" - }, - "paths": { - "/account": { - "get": { - "tags": [ - "Account" - ], - "summary": "Fetch custodian-mode client details.", - "description": "This endpoint returns the custodian-mode client details for authenticated users.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Customer" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/idtoken": { - "get": { - "tags": [ - "Account" - ], - "summary": "Fetch IdToken.", - "description": "This endpoint returns IdToken as JWT with list of user roles inside", - "deprecated": true, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/create": { - "post": { - "tags": [ - "Account" - ], - "summary": "Create an client for an authenticated user.", - "description": "This endpoint creates a client in the custodian-mode for an authenticated user", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/unencrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Create an unencrypted StatusList2021 credential status list.", - "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/encrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Create an encrypted StatusList2021 credential status list.", - "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/unencrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Update an existing unencrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revoke", - "suspend", - "reinstate" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/encrypted": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Update an existing encrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revoke", - "suspend", - "reinstate" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/check": { - "post": { - "tags": [ - "Credential Status" - ], - "summary": "Check a StatusList2021 index for a given Verifiable Credential.", - "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "required": true, - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/search": { - "get": { - "tags": [ - "Credential Status" - ], - "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", - "parameters": [ - { - "in": "query", - "name": "did", - "description": "The DID of the issuer of the status list.", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "schema": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - } - }, - { - "in": "query", - "name": "statusListName", - "description": "The name of the StatusList2021 DID-Linked Resource.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusListSearchResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/issue": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Issue a Verifiable Credential", - "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Credential" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/verify": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Verify a Verifiable Credential.", - "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "verify", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyCredentialResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/revoke": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Revoke a Verifiable Credential.", - "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", - "operationId": "revoke", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RevocationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/suspend": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Suspend a Verifiable Credential.", - "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "suspend", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/reinstate": { - "post": { - "tags": [ - "Credential" - ], - "summary": "Reinstate a suspended Verifiable Credential.", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "operationId": "reinstate", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnsuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/create": { - "post": { - "tags": [ - "DID" - ], - "summary": "Create a DID Document.", - "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestFormBased" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestJson" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/update": { - "post": { - "tags": [ - "DID" - ], - "summary": "Update a DID Document.", - "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateResponse" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/import": { - "post": { - "tags": [ - "DID" - ], - "summary": "Import a DID Document.", - "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/deactivate/{did}": { - "post": { - "tags": [ - "DID" - ], - "summary": "Deactivate a DID Document.", - "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to deactivate.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeactivatedDidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/list": { - "get": { - "tags": [ - "DID" - ], - "summary": "Fetch DIDs associated with an account.", - "description": "This endpoint returns the list of DIDs controlled by the account.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/search/{did}": { - "get": { - "tags": [ - "DID" - ], - "summary": "Resolve a DID Document.", - "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to resolve.", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "metadata", - "description": "Return only metadata of DID Document instead of actual DID Document.", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "versionId", - "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "versionTime", - "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "transformKeys", - "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", - "schema": { - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "Ed25519VerificationKey2020", - "JsonWebKey2020" - ] - } - }, - { - "in": "query", - "name": "service", - "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", - "schema": { - "type": "string" - }, - "example": "service-1" - }, - { - "in": "query", - "name": "relativeRef", - "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", - "schema": { - "type": "string" - }, - "example": "/path/to/file" - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/key/create": { - "post": { - "tags": [ - "Key" - ], - "summary": "Create an identity key pair.", - "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/import": { - "post": { - "tags": [ - "Key" - ], - "summary": "Import an identity key pair.", - "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/read/{kid}": { - "get": { - "tags": [ - "Key" - ], - "summary": "Fetch an identity key pair.", - "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", - "parameters": [ - { - "name": "kid", - "description": "Key ID of the identity key pair to fetch.", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/presentation/create": { - "post": { - "tags": [ - "Presentation" - ], - "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", - "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/presentation/verify": { - "post": { - "tags": [ - "Presentation" - ], - "summary": "Verify a Verifiable Presentation generated from credential(s).", - "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyPresentationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/create/{did}": { - "post": { - "tags": [ - "Resource" - ], - "summary": "Create a DID-Linked Resource.", - "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to link the resource to.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/search/{did}": { - "get": { - "tags": [ - "Resource" - ], - "summary": "Get a DID-Linked Resource.", - "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "resourceId", - "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "resourceName", - "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "cheqd-issuer-logo" - }, - { - "in": "query", - "name": "resourceType", - "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "CredentialArtwork" - }, - { - "in": "query", - "name": "resourceVersion", - "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "v1" - }, - { - "in": "query", - "name": "resourceVersionTime", - "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "checksum", - "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" - }, - { - "in": "query", - "name": "resourceMetadata", - "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "any": { - "schema": { - "type": "object" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - } - }, - "components": { - "schemas": { - "AlsoKnownAs": { - "type": "object", - "properties": { - "alsoKnownAs": { - "type": "array", - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "format": "uri", - "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" - }, - "description": { - "type": "string", - "description": "Optional description of the URI." - } - } - } - } - } - }, - "CredentialRequest": { - "description": "Input fields for the creating a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "issuerDid": { - "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "subjectDid": { - "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", - "type": "string", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - }, - "attributes": { - "description": "JSON object containing the attributes to be included in the credential.", - "type": "object", - "example": { - "name": "Bob", - "gender": "male" - } - }, - "@context": { - "description": "Optional properties to be included in the `@context` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://schema.org/schema.jsonld", - "https://veramo.io/contexts/profile/v1" - ] - }, - "type": { - "description": "Optional properties to be included in the `type` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "Person" - ] - }, - "expirationDate": { - "description": "Optional expiration date according to the VC Data Model specification.", - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "format": { - "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", - "type": "string", - "enum": [ - "jwt", - "jsonld" - ], - "example": "jwt" - }, - "credentialStatus": { - "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", - "type": "object", - "required": [ - "statusPurpose", - "statusListName" - ], - "properties": { - "statusPurpose": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ] - }, - "statusListName": { - "type": "string" - }, - "statusListIndex": { - "type": "number" - }, - "statusListVersion": { - "type": "string", - "format": "date-time" - }, - "statusListRangeStart": { - "type": "number" - }, - "statusListRangeEnd": { - "type": "number" - }, - "indexNotIn": { - "type": "number" - } - }, - "example": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials" - } - } - }, - "required": [ - "issuerDid", - "subjectDid", - "attributes" - ], - "example": { - "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "attributes": { - "gender": "male", - "name": "Bob" - }, - "@context": [ - "https://schema.org" - ], - "type": [ - "Person" - ], - "format": "jwt", - "credentialStatus": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials", - "statusListIndex": 10 - } - } - }, - "Credential": { - "description": "Input fields for revoking/suspending a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ] - }, - "type": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "VerifiableCredential", - "Person" - ] - }, - "expirationDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "issuer": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - } - } - }, - "credentialSubject": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - } - } - }, - "credentialStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" - }, - "statusListIndex": { - "type": "number", - "example": 20 - }, - "statusPurpose": { - "type": "string", - "enum": [ - "revocation", - "suspension" - ], - "example": "suspension" - }, - "type": { - "type": "string", - "enum": [ - "StatusList2021Entry" - ] - } - } - }, - "issuanceDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "proof": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "jwt": { - "type": "string" - } - }, - "example": { - "type": "JwtProof2020", - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ], - "credentialSubject": { - "gender": "male", - "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "name": "Bob" - }, - "credentialStatus": { - "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", - "statusIndex": 20, - "statusPurpose": "suspension", - "type": "StatusList2021Entry" - }, - "issuanceDate": "2023-06-08T13:49:28.000Z", - "issuer": { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "proof": { - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", - "type": "JwtProof2020" - }, - "type": [ - "VerifiableCredential", - "Person" - ] - } - }, - "CredentialRevokeRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", - "oneOf": [ - { - "type": "object" - }, - { - "type": "string" - } - ] - }, - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", - "type": "string" - } - } - }, - "RevocationResult": { - "properties": { - "revoked": { - "type": "boolean", - "example": true - } - } - }, - "SuspensionResult": { - "properties": { - "suspended": { - "type": "boolean", - "example": true - } - } - }, - "UnsuspensionResult": { - "properties": { - "unsuspended": { - "type": "boolean", - "example": true - } - } - }, - "CredentialVerifyRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", - "type": "object" - }, - "policies": { - "description": "Custom verification policies to execute when verifying credential.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "VerifyPresentationResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - } - }, - "VerifyCredentialResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - }, - "example": { - "verified": true, - "polices": {}, - "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "signer": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - } - }, - "PresentationCreateRequest": { - "type": "object", - "required": [ - "credentials" - ], - "properties": { - "credentials": { - "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", - "type": "array", - "items": { - "type": "object" - } - }, - "holderDid": { - "description": "DID of holder", - "type": "string" - }, - "verifierDid": { - "description": "DID of verifier", - "type": "string" - } - } - }, - "PresentationVerifyRequest": { - "type": "object", - "required": [ - "presentation" - ], - "properties": { - "presentation": { - "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", - "type": "object" - }, - "verifierDid": { - "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", - "type": "string" - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": false - }, - "policies": { - "description": "Custom verification policies to execute when verifying presentation.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "CredentialStatusCreateBody": { - "allOf": [ - { - "type": "object", - "required": [ - "did", - "statusListName" - ], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be created.", - "type": "string" - }, - "length": { - "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true, - "default": 140000 - }, - "encoding": { - "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", - "type": "string", - "default": "base64url", - "enum": [ - "base64url", - "base64", - "hex" - ] - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/AlsoKnownAs" - } - ] - }, - "CredentialStatusCreateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "length": 140000, - "encoding": "base64url" - } - }, - "CredentialStatusUnencryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": false - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-employee-credentials", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "1.0.0", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - } - } - }, - "CredentialStatusCreateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusEncryptedPaymentConditionsBody": { - "type": "object", - "properties": { - "feePaymentAddress": { - "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 20 - }, - "feePaymentWindow": { - "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 10 - } - } - }, - "CredentialStatusEncryptedPaymentConditionsJson": { - "type": "object", - "properties": { - "paymentConditions": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - } - } - }, - "CredentialStatusCreateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - }, - { - "type": "object", - "required": [ - "feePaymentAddress", - "feePaymentAmount", - "feePaymentWindow" - ] - } - ] - }, - "CredentialStatusCreateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - }, - { - "type": "object", - "required": [ - "paymentConditions" - ] - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "paymentConditions": [ - { - "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", - "feePaymentAmount": 20, - "feePaymentWindow": 10 - } - ] - } - }, - "CredentialStatusEncryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": true - }, - "encryptedSymmetricKey": { - "type": "string", - "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" - }, - "paymentConditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feePaymentAddress": { - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "type": "string", - "example": "20000000000ncheq" - }, - "intervalInSeconds": { - "type": "number", - "example": 600 - }, - "type": { - "type": "string", - "example": "timelockPayment" - } - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-revocation-encrypted-1", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "2023-06-26T11:45:19.349Z", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - }, - "symmetricKey": { - "type": "string", - "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - } - } - } - }, - "CredentialStatusCreateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusUpdateBody": { - "type": "object", - "required": [ - "did", - "statusListName", - "indices" - ], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", - "type": "string" - }, - "indices": { - "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - } - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - "CredentialStatusUpdateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "indices": [ - 10, - 3199, - 12109, - 130999 - ] - } - }, - "CredentialStatusUpdateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusUpdateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": [ - "symmetricKey" - ], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - }, - "CredentialStatusUpdateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": [ - "symmetricKey" - ], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "indices": [ - 10, - 3199, - 12109, - 130999 - ], - "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - }, - "CredentialStatusUpdateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusCheckRequest": { - "type": "object", - "required": [ - "did", - "statusListName", - "index" - ], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", - "type": "string" - }, - "index": { - "description": "Credential status index to be checked for revocation or suspension.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": true - } - } - }, - "CredentialStatusCheckResult": { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" - } - ] - }, - "CredentialStatusCheckRevocationResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "revoked": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusCheckSuspensionResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "suspended": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusListSearchResult": { - "allOf": [ - { - "type": "object", - "properties": { - "found": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - } - ] - }, - "KeyImportRequest": { - "type": "object", - "properties": { - "alias": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "Ed25519", - "Secp256k1" - ] - }, - "privateKeyHex": { - "type": "string" - } - } - }, - "KeyResult": { - "type": "object", - "properties": { - "kid": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "Ed25519", - "Secp256k1" - ] - }, - "publicKeyHex": { - "type": "string" - } - } - }, - "DidDocument": { - "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "verificationMethod": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/ns/did/v1" - ], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - ], - "verificationMethod": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "type": "Ed25519VerificationKey2018", - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" - } - ], - "authentication": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" - ], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": [ - "https://example.com" - ] - } - ] - } - }, - "DidDocumentWithoutVerificationMethod": { - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/ns/did/v1" - ], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - ], - "authentication": [ - "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" - ], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": [ - "https://example.com" - ] - } - ] - } - }, - "DidCreateRequestFormBased": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": [ - "testnet", - "mainnet" - ] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": [ - "uuid", - "base58btc" - ] - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "JsonWebKey2020", - "Ed25519VerificationKey2020" - ] - }, - "service": { - "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", - "type": "array", - "items": { - "type": "object", - "properties": { - "idFragment": { - "type": "string" - }, - "type": { - "type": "string" - }, - "serviceEndpoint": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "example": [ - { - "idFragment": "service-1", - "type": "LinkedDomains", - "serviceEndpoint": [ - "https://example.com" - ] - } - ] - }, - "key": { - "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", - "type": "string" - }, - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://www.w3.org/ns/did/v1" - ] - } - } - }, - "DidCreateRequestJson": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": [ - "testnet", - "mainnet" - ] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": [ - "uuid", - "base58btc" - ] - }, - "assertionMethod": { - "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", - "type": "boolean", - "default": true - }, - "options": { - "type": "object", - "properties": { - "key": { - "type": "string", - "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "JsonWebKey2020", - "Ed25519VerificationKey2020" - ] - } - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" - } - } - }, - "DidImportRequest": { - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "DID to be imported", - "format": "uri", - "required": true - }, - "keys": { - "type": "array", - "description": "List of keys required to import the DID", - "required": true, - "items": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "PresentationCreateResult": { - "type": "object", - "properties": { - "vp": { - "type": "object", - "description": "Verifiable Presentation which could be provided to the verifier." - }, - "nbf": { - "type": "integer", - "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." - }, - "iss": { - "type": "string", - "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" - }, - "aud": { - "type": "array", - "items": { - "type": "string" - }, - "description": "DID of the verifier of the Verifiable Presentation." - } - }, - "example": { - "vp": { - "@context": [ - "https://www.w3.org/2018/credentials/v1" - ], - "type": [ - "VerifiablePresentation" - ], - "verifiableCredential": [ - "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" - ] - }, - "nbf": 1700744275, - "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", - "aud": [ - "did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22" - ] - } - }, - "DidResult": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string" - }, - "keys": { - "type": "array", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - } - } - }, - "DidUpdateResponse": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string", - "description": "The default key id of which is the key associated with the first verificationMethod" - }, - "keys": { - "type": "array", - "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "controllerKeyRefs": { - "type": "array", - "description": "The list of keyRefs which were used for signing the transaction", - "items": { - "type": "string" - } - }, - "controllerKeys": { - "type": "array", - "description": "The list of all possible keys, inlcuding all controller's keys", - "items": { - "type": "string" - } - } - } - }, - "VerificationMethod": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - }, - "controller": { - "type": "string" - }, - "publicKeyMultibase": { - "type": "string" - }, - "publicKeyJwk": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - }, - "Service": { - "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", - "type": "object", - "properties": { - "id": { - "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", - "type": "string", - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" - }, - "type": { - "description": "Service type as defined in DID Specification Registries.", - "type": "string", - "example": "LinkedDomains" - }, - "serviceEndpoint": { - "description": "Service endpoint as defined in DID Core Specification.", - "type": "array", - "items": { - "type": "string", - "example": "https://example.com" - } - } - } - }, - "DidUpdateRequest": { - "type": "object", - "properties": { - "did": { - "description": "DID identifier to be updated.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "service": { - "type": "array", - "description": "Service section of the DID Document.", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "verificationMethod": { - "type": "array", - "description": "Verification Method section of the DID Document.", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "authentication": { - "description": "Authentication section of the DID Document.", - "type": "array", - "items": { - "type": "string" - } - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - } - } - }, - "DidDeactivateRequest": { - "type": "object", - "properties": { - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "CreateResourceRequest": { - "description": "Input fields for DID-Linked Resource creation.", - "type": "object", - "additionalProperties": false, - "required": [ - "name", - "type", - "data", - "encoding" - ], - "properties": { - "data": { - "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", - "type": "string" - }, - "encoding": { - "description": "Encoding format used to encode the data.", - "type": "string", - "enum": [ - "base64url", - "base64", - "hex" - ] - }, - "name": { - "description": "Name of DID-Linked Resource.", - "type": "string" - }, - "type": { - "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", - "type": "string" - }, - "alsoKnownAs": { - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "type": "array", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "description": { - "type": "string" - } - } - } - }, - "version": { - "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", - "type": "string" - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "data": "SGVsbG8gV29ybGQ=", - "encoding": "base64url", - "name": "ResourceName", - "type": "TextDocument" - } - }, - "ResourceList": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "contentMetadata": { - "type": "object" - }, - "contentStream": { - "type": "object" - }, - "dereferencingMetadata": { - "$ref": "#/components/schemas/DereferencingMetadata" - } - } - }, - "DereferencingMetadata": { - "type": "object", - "properties": { - "contentType": { - "type": "string", - "example": "application/did+ld+json" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - } - } - }, - "DidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DidDocumentMetadata" - } - } - }, - "DeactivatedDidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" - } - } - }, - "DidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": false - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "DeactivatedDidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": true - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "ResourceMetadata": { - "type": "object", - "properties": { - "resourceURI": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceCollectionId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "resourceId": { - "type": "string", - "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceName": { - "type": "string", - "example": "cheqd-issuer-logo" - }, - "resourceType": { - "type": "string", - "example": "CredentialArtwork" - }, - "mediaType": { - "type": "string", - "example": "image/png" - }, - "resourceVersion": { - "type": "string", - "example": "1.0" - }, - "checksum": { - "type": "string", - "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" - }, - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "nextVersionId": { - "type": "string", - "example": "d4829ac7-4566-478c-a408-b44767eddadc" - }, - "previousVersionId": { - "type": "string", - "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" - } - } - }, - "DidResolutionMetadata": { - "type": "object", - "properties": { - "contentType": { - "allOf": [ - { - "$ref": "#/components/schemas/ContentType" - } - ], - "example": "application/did+ld+json" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - } - } - }, - "ContentType": { - "type": "string", - "enum": [ - "application/did+json", - "application/did+ld+json", - "application/ld+json", - "application/json" - ] - }, - "DidProperties": { - "type": "object", - "properties": { - "didString": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "method": { - "type": "string", - "example": "cheqd" - }, - "methodSpecificId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - } - } - }, - "Customer": { - "type": "object", - "properties": { - "customerId": { - "type": "string", - "example": "6w5drpiiwhhs" - }, - "address": { - "type": "string", - "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" - } - } - }, - "AccountCreateRequest": { - "type": "object", - "properties": { - "user": { - "type": "object", - "properties": { - "primaryEmail": { - "type": "string" - } - } - } - } - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - } - } - } + "openapi": "3.0.0", + "info": { + "title": "Credential Service API for cheqd network", + "version": "2.0.0", + "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Account" + }, + { + "name": "Key" + }, + { + "name": "DID" + }, + { + "name": "Resource" + }, + { + "name": "Credential" + }, + { + "name": "Presentation" + }, + { + "name": "Credential Status" + } + ], + "externalDocs": { + "description": "Credential Service API Documentation", + "url": "https://docs.cheqd.io/identity" + }, + "paths": { + "/account": { + "get": { + "tags": ["Account"], + "summary": "Fetch custodian-mode client details.", + "description": "This endpoint returns the custodian-mode client details for authenticated users.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/idtoken": { + "get": { + "tags": ["Account"], + "summary": "Fetch IdToken.", + "description": "This endpoint returns IdToken as JWT with list of user roles inside", + "deprecated": true, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/create": { + "post": { + "tags": ["Account"], + "summary": "Create an client for an authenticated user.", + "description": "This endpoint creates a client in the custodian-mode for an authenticated user", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/unencrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Create an unencrypted StatusList2021 credential status list.", + "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/encrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Create an encrypted StatusList2021 credential status list.", + "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/unencrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Update an existing unencrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": ["revoke", "suspend", "reinstate"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/encrypted": { + "post": { + "tags": ["Credential Status"], + "summary": "Update an existing encrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": ["revoke", "suspend", "reinstate"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/check": { + "post": { + "tags": ["Credential Status"], + "summary": "Check a StatusList2021 index for a given Verifiable Credential.", + "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "required": true, + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/search": { + "get": { + "tags": ["Credential Status"], + "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", + "parameters": [ + { + "in": "query", + "name": "did", + "description": "The DID of the issuer of the status list.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "schema": { + "type": "string", + "enum": ["revocation", "suspension"] + } + }, + { + "in": "query", + "name": "statusListName", + "description": "The name of the StatusList2021 DID-Linked Resource.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusListSearchResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/issue": { + "post": { + "tags": ["Credential"], + "summary": "Issue a Verifiable Credential", + "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Credential" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/verify": { + "post": { + "tags": ["Credential"], + "summary": "Verify a Verifiable Credential.", + "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "verify", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCredentialResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/revoke": { + "post": { + "tags": ["Credential"], + "summary": "Revoke a Verifiable Credential.", + "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", + "operationId": "revoke", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RevocationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/suspend": { + "post": { + "tags": ["Credential"], + "summary": "Suspend a Verifiable Credential.", + "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "suspend", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/reinstate": { + "post": { + "tags": ["Credential"], + "summary": "Reinstate a suspended Verifiable Credential.", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "operationId": "reinstate", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnsuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/create": { + "post": { + "tags": ["DID"], + "summary": "Create a DID Document.", + "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestFormBased" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestJson" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/update": { + "post": { + "tags": ["DID"], + "summary": "Update a DID Document.", + "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateResponse" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/import": { + "post": { + "tags": ["DID"], + "summary": "Import a DID Document.", + "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/deactivate/{did}": { + "post": { + "tags": ["DID"], + "summary": "Deactivate a DID Document.", + "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to deactivate.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeactivatedDidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/list": { + "get": { + "tags": ["DID"], + "summary": "Fetch DIDs associated with an account.", + "description": "This endpoint returns the list of DIDs controlled by the account.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/search/{did}": { + "get": { + "tags": ["DID"], + "summary": "Resolve a DID Document.", + "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to resolve.", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "metadata", + "description": "Return only metadata of DID Document instead of actual DID Document.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "versionId", + "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "versionTime", + "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "transformKeys", + "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", + "schema": { + "type": "string", + "enum": ["Ed25519VerificationKey2018", "Ed25519VerificationKey2020", "JsonWebKey2020"] + } + }, + { + "in": "query", + "name": "service", + "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", + "schema": { + "type": "string" + }, + "example": "service-1" + }, + { + "in": "query", + "name": "relativeRef", + "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", + "schema": { + "type": "string" + }, + "example": "/path/to/file" + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/key/create": { + "post": { + "tags": ["Key"], + "summary": "Create an identity key pair.", + "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/import": { + "post": { + "tags": ["Key"], + "summary": "Import an identity key pair.", + "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/read/{kid}": { + "get": { + "tags": ["Key"], + "summary": "Fetch an identity key pair.", + "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", + "parameters": [ + { + "name": "kid", + "description": "Key ID of the identity key pair to fetch.", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/presentation/create": { + "post": { + "tags": ["Presentation"], + "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", + "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/presentation/verify": { + "post": { + "tags": ["Presentation"], + "summary": "Verify a Verifiable Presentation generated from credential(s).", + "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPresentationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/create/{did}": { + "post": { + "tags": ["Resource"], + "summary": "Create a DID-Linked Resource.", + "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to link the resource to.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/search/{did}": { + "get": { + "tags": ["Resource"], + "summary": "Get a DID-Linked Resource.", + "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "resourceId", + "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "resourceName", + "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "cheqd-issuer-logo" + }, + { + "in": "query", + "name": "resourceType", + "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "CredentialArtwork" + }, + { + "in": "query", + "name": "resourceVersion", + "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "v1" + }, + { + "in": "query", + "name": "resourceVersionTime", + "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "checksum", + "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" + }, + { + "in": "query", + "name": "resourceMetadata", + "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "any": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + } + }, + "components": { + "schemas": { + "AlsoKnownAs": { + "type": "object", + "properties": { + "alsoKnownAs": { + "type": "array", + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "uri", + "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" + }, + "description": { + "type": "string", + "description": "Optional description of the URI." + } + } + } + } + } + }, + "CredentialRequest": { + "description": "Input fields for the creating a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "issuerDid": { + "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "subjectDid": { + "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", + "type": "string", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + }, + "attributes": { + "description": "JSON object containing the attributes to be included in the credential.", + "type": "object", + "example": { + "name": "Bob", + "gender": "male" + } + }, + "@context": { + "description": "Optional properties to be included in the `@context` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": ["https://schema.org/schema.jsonld", "https://veramo.io/contexts/profile/v1"] + }, + "type": { + "description": "Optional properties to be included in the `type` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": ["Person"] + }, + "expirationDate": { + "description": "Optional expiration date according to the VC Data Model specification.", + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "format": { + "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", + "type": "string", + "enum": ["jwt", "jsonld"], + "example": "jwt" + }, + "credentialStatus": { + "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", + "type": "object", + "required": ["statusPurpose", "statusListName"], + "properties": { + "statusPurpose": { + "type": "string", + "enum": ["revocation", "suspension"] + }, + "statusListName": { + "type": "string" + }, + "statusListIndex": { + "type": "number" + }, + "statusListVersion": { + "type": "string", + "format": "date-time" + }, + "statusListRangeStart": { + "type": "number" + }, + "statusListRangeEnd": { + "type": "number" + }, + "indexNotIn": { + "type": "number" + } + }, + "example": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials" + } + } + }, + "required": ["issuerDid", "subjectDid", "attributes"], + "example": { + "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "attributes": { + "gender": "male", + "name": "Bob" + }, + "@context": ["https://schema.org"], + "type": ["Person"], + "format": "jwt", + "credentialStatus": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials", + "statusListIndex": 10 + } + } + }, + "Credential": { + "description": "Input fields for revoking/suspending a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ] + }, + "type": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["VerifiableCredential", "Person"] + }, + "expirationDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "issuer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + } + } + }, + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + } + } + }, + "credentialStatus": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" + }, + "statusListIndex": { + "type": "number", + "example": 20 + }, + "statusPurpose": { + "type": "string", + "enum": ["revocation", "suspension"], + "example": "suspension" + }, + "type": { + "type": "string", + "enum": ["StatusList2021Entry"] + } + } + }, + "issuanceDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "proof": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "jwt": { + "type": "string" + } + }, + "example": { + "type": "JwtProof2020", + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ], + "credentialSubject": { + "gender": "male", + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "name": "Bob" + }, + "credentialStatus": { + "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", + "statusIndex": 20, + "statusPurpose": "suspension", + "type": "StatusList2021Entry" + }, + "issuanceDate": "2023-06-08T13:49:28.000Z", + "issuer": { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "proof": { + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", + "type": "JwtProof2020" + }, + "type": ["VerifiableCredential", "Person"] + } + }, + "CredentialRevokeRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", + "oneOf": [ + { + "type": "object" + }, + { + "type": "string" + } + ] + }, + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", + "type": "string" + } + } + }, + "RevocationResult": { + "properties": { + "revoked": { + "type": "boolean", + "example": true + } + } + }, + "SuspensionResult": { + "properties": { + "suspended": { + "type": "boolean", + "example": true + } + } + }, + "UnsuspensionResult": { + "properties": { + "unsuspended": { + "type": "boolean", + "example": true + } + } + }, + "CredentialVerifyRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", + "type": "object" + }, + "policies": { + "description": "Custom verification policies to execute when verifying credential.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "VerifyPresentationResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + } + }, + "VerifyCredentialResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + }, + "example": { + "verified": true, + "polices": {}, + "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "signer": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + } + }, + "PresentationCreateRequest": { + "type": "object", + "required": ["credentials"], + "properties": { + "credentials": { + "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", + "type": "array", + "items": { + "type": "object" + } + }, + "holderDid": { + "description": "DID of holder", + "type": "string" + }, + "verifierDid": { + "description": "DID of verifier", + "type": "string" + } + } + }, + "PresentationVerifyRequest": { + "type": "object", + "required": ["presentation"], + "properties": { + "presentation": { + "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", + "type": "object" + }, + "verifierDid": { + "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", + "type": "string" + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": false + }, + "policies": { + "description": "Custom verification policies to execute when verifying presentation.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "CredentialStatusCreateBody": { + "allOf": [ + { + "type": "object", + "required": ["did", "statusListName"], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be created.", + "type": "string" + }, + "length": { + "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "default": 140000 + }, + "encoding": { + "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", + "type": "string", + "default": "base64url", + "enum": ["base64url", "base64", "hex"] + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/AlsoKnownAs" + } + ] + }, + "CredentialStatusCreateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "length": 140000, + "encoding": "base64url" + } + }, + "CredentialStatusUnencryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": false + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-employee-credentials", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "1.0.0", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + } + } + }, + "CredentialStatusCreateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusEncryptedPaymentConditionsBody": { + "type": "object", + "properties": { + "feePaymentAddress": { + "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 20 + }, + "feePaymentWindow": { + "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 10 + } + } + }, + "CredentialStatusEncryptedPaymentConditionsJson": { + "type": "object", + "properties": { + "paymentConditions": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + } + } + }, + "CredentialStatusCreateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + }, + { + "type": "object", + "required": ["feePaymentAddress", "feePaymentAmount", "feePaymentWindow"] + } + ] + }, + "CredentialStatusCreateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + }, + { + "type": "object", + "required": ["paymentConditions"] + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "paymentConditions": [ + { + "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", + "feePaymentAmount": 20, + "feePaymentWindow": 10 + } + ] + } + }, + "CredentialStatusEncryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": true + }, + "encryptedSymmetricKey": { + "type": "string", + "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" + }, + "paymentConditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feePaymentAddress": { + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "type": "string", + "example": "20000000000ncheq" + }, + "intervalInSeconds": { + "type": "number", + "example": 600 + }, + "type": { + "type": "string", + "example": "timelockPayment" + } + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-revocation-encrypted-1", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "2023-06-26T11:45:19.349Z", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + }, + "symmetricKey": { + "type": "string", + "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + } + } + } + }, + "CredentialStatusCreateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusUpdateBody": { + "type": "object", + "required": ["did", "statusListName", "indices"], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", + "type": "string" + }, + "indices": { + "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + } + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + "CredentialStatusUpdateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "indices": [10, 3199, 12109, 130999] + } + }, + "CredentialStatusUpdateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusUpdateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": ["symmetricKey"], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + }, + "CredentialStatusUpdateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": ["symmetricKey"], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "indices": [10, 3199, 12109, 130999], + "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + }, + "CredentialStatusUpdateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusCheckRequest": { + "type": "object", + "required": ["did", "statusListName", "index"], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", + "type": "string" + }, + "index": { + "description": "Credential status index to be checked for revocation or suspension.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": true + } + } + }, + "CredentialStatusCheckResult": { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" + } + ] + }, + "CredentialStatusCheckRevocationResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "revoked": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusCheckSuspensionResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusListSearchResult": { + "allOf": [ + { + "type": "object", + "properties": { + "found": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + } + ] + }, + "KeyImportRequest": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Ed25519", "Secp256k1"] + }, + "privateKeyHex": { + "type": "string" + } + } + }, + "KeyResult": { + "type": "object", + "properties": { + "kid": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Ed25519", "Secp256k1"] + }, + "publicKeyHex": { + "type": "string" + } + } + }, + "DidDocument": { + "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationMethod": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": ["https://www.w3.org/ns/did/v1"], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], + "verificationMethod": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" + } + ], + "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": ["https://example.com"] + } + ] + } + }, + "DidDocumentWithoutVerificationMethod": { + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": ["https://www.w3.org/ns/did/v1"], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], + "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": ["https://example.com"] + } + ] + } + }, + "DidCreateRequestFormBased": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": ["testnet", "mainnet"] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": ["uuid", "base58btc"] + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] + }, + "service": { + "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", + "type": "array", + "items": { + "type": "object", + "properties": { + "idFragment": { + "type": "string" + }, + "type": { + "type": "string" + }, + "serviceEndpoint": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "example": [ + { + "idFragment": "service-1", + "type": "LinkedDomains", + "serviceEndpoint": ["https://example.com"] + } + ] + }, + "key": { + "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": ["https://www.w3.org/ns/did/v1"] + } + } + }, + "DidCreateRequestJson": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": ["testnet", "mainnet"] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": ["uuid", "base58btc"] + }, + "assertionMethod": { + "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] + } + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" + } + } + }, + "DidImportRequest": { + "type": "object", + "properties": { + "did": { + "type": "string", + "description": "DID to be imported", + "format": "uri", + "required": true + }, + "keys": { + "type": "array", + "description": "List of keys required to import the DID", + "required": true, + "items": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "PresentationCreateResult": { + "type": "object", + "properties": { + "vp": { + "type": "object", + "description": "Verifiable Presentation which could be provided to the verifier." + }, + "nbf": { + "type": "integer", + "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." + }, + "iss": { + "type": "string", + "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" + }, + "aud": { + "type": "array", + "items": { + "type": "string" + }, + "description": "DID of the verifier of the Verifiable Presentation." + } + }, + "example": { + "vp": { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [ + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" + ] + }, + "nbf": 1700744275, + "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", + "aud": ["did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22"] + } + }, + "DidResult": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string" + }, + "keys": { + "type": "array", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + } + } + }, + "DidUpdateResponse": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string", + "description": "The default key id of which is the key associated with the first verificationMethod" + }, + "keys": { + "type": "array", + "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "controllerKeyRefs": { + "type": "array", + "description": "The list of keyRefs which were used for signing the transaction", + "items": { + "type": "string" + } + }, + "controllerKeys": { + "type": "array", + "description": "The list of all possible keys, inlcuding all controller's keys", + "items": { + "type": "string" + } + } + } + }, + "VerificationMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "controller": { + "type": "string" + }, + "publicKeyMultibase": { + "type": "string" + }, + "publicKeyJwk": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + }, + "Service": { + "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", + "type": "object", + "properties": { + "id": { + "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", + "type": "string", + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" + }, + "type": { + "description": "Service type as defined in DID Specification Registries.", + "type": "string", + "example": "LinkedDomains" + }, + "serviceEndpoint": { + "description": "Service endpoint as defined in DID Core Specification.", + "type": "array", + "items": { + "type": "string", + "example": "https://example.com" + } + } + } + }, + "DidUpdateRequest": { + "type": "object", + "properties": { + "did": { + "description": "DID identifier to be updated.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "service": { + "type": "array", + "description": "Service section of the DID Document.", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "verificationMethod": { + "type": "array", + "description": "Verification Method section of the DID Document.", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "authentication": { + "description": "Authentication section of the DID Document.", + "type": "array", + "items": { + "type": "string" + } + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + } + } + }, + "DidDeactivateRequest": { + "type": "object", + "properties": { + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CreateResourceRequest": { + "description": "Input fields for DID-Linked Resource creation.", + "type": "object", + "additionalProperties": false, + "required": ["name", "type", "data", "encoding"], + "properties": { + "data": { + "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", + "type": "string" + }, + "encoding": { + "description": "Encoding format used to encode the data.", + "type": "string", + "enum": ["base64url", "base64", "hex"] + }, + "name": { + "description": "Name of DID-Linked Resource.", + "type": "string" + }, + "type": { + "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", + "type": "string" + }, + "alsoKnownAs": { + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "type": "array", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "version": { + "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", + "type": "string" + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "data": "SGVsbG8gV29ybGQ=", + "encoding": "base64url", + "name": "ResourceName", + "type": "TextDocument" + } + }, + "ResourceList": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "contentMetadata": { + "type": "object" + }, + "contentStream": { + "type": "object" + }, + "dereferencingMetadata": { + "$ref": "#/components/schemas/DereferencingMetadata" + } + } + }, + "DereferencingMetadata": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "example": "application/did+ld+json" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + } + } + }, + "DidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DidDocumentMetadata" + } + } + }, + "DeactivatedDidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" + } + } + }, + "DidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": false + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "DeactivatedDidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": true + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "ResourceMetadata": { + "type": "object", + "properties": { + "resourceURI": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceCollectionId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "resourceId": { + "type": "string", + "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceName": { + "type": "string", + "example": "cheqd-issuer-logo" + }, + "resourceType": { + "type": "string", + "example": "CredentialArtwork" + }, + "mediaType": { + "type": "string", + "example": "image/png" + }, + "resourceVersion": { + "type": "string", + "example": "1.0" + }, + "checksum": { + "type": "string", + "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" + }, + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "nextVersionId": { + "type": "string", + "example": "d4829ac7-4566-478c-a408-b44767eddadc" + }, + "previousVersionId": { + "type": "string", + "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" + } + } + }, + "DidResolutionMetadata": { + "type": "object", + "properties": { + "contentType": { + "allOf": [ + { + "$ref": "#/components/schemas/ContentType" + } + ], + "example": "application/did+ld+json" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + } + } + }, + "ContentType": { + "type": "string", + "enum": ["application/did+json", "application/did+ld+json", "application/ld+json", "application/json"] + }, + "DidProperties": { + "type": "object", + "properties": { + "didString": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "method": { + "type": "string", + "example": "cheqd" + }, + "methodSpecificId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + } + } + }, + "Customer": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "example": "6w5drpiiwhhs" + }, + "address": { + "type": "string", + "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" + } + } + }, + "AccountCreateRequest": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "primaryEmail": { + "type": "string" + } + } + } + } + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + } + } + } } diff --git a/src/types/constants.ts b/src/types/constants.ts index 1a6fa86a..34c30e24 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -12,7 +12,7 @@ export const HEADERS = { export const APPLICATION_BASE_URL = process.env.APPLICATION_BASE_URL || 'http://localhost:3000'; export const CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || APPLICATION_BASE_URL; export const API_KEY_PREFIX = 'caas'; -export const API_KEY_LENGTH = 64; +export const API_SECRET_KEY_LENGTH = 64; export const API_KEY_EXPIRATION = 30; // By default we don't send events to datadog export const ENABLE_DATADOG = process.env.ENABLE_DATADOG === 'true' ? true : false; diff --git a/src/types/portal.ts b/src/types/portal.ts index 37c7cf32..8ab6f0ba 100644 --- a/src/types/portal.ts +++ b/src/types/portal.ts @@ -5,7 +5,6 @@ export type ProductWithPrices = Stripe.Product & { prices?: Stripe.Price[]; }; - export type APIServiceOptions = { decryptionNeeded: boolean; }; @@ -109,11 +108,11 @@ export type APIKeyResponseBody = { }; // Create export type APIKeyCreateRequestBody = { - name: string, + name: string; expiresAt?: Date; }; -export type APIKeyCreateResponseBody = APIKeyResponseBody +export type APIKeyCreateResponseBody = APIKeyResponseBody; export type APIKeyCreateUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Update @@ -151,4 +150,3 @@ export type APIKeyGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Utils export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; - From 18fa100326a2b392fc65c08eb14781d7cb8fc188 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sat, 13 Apr 2024 22:16:23 +0200 Subject: [PATCH 32/49] Add error message for Stripe account creation failure --- src/services/track/admin/account-submitter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/track/admin/account-submitter.ts b/src/services/track/admin/account-submitter.ts index de5346fa..6cab1568 100644 --- a/src/services/track/admin/account-submitter.ts +++ b/src/services/track/admin/account-submitter.ts @@ -57,7 +57,7 @@ export class PortalAccountCreateSubmitter implements IObserver { } catch (error) { await this.notify({ message: EventTracker.compileBasicNotification( - `Failed to create Stripe account with name: ${data.name as string}.`, + `Failed to create Stripe account with name: ${data.name as string}. Error: ${error}`, operation.operation ), severity: 'error', From 0623db88ec963afea07f0a69607ff59f08082756 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 14 Apr 2024 14:03:27 +0200 Subject: [PATCH 33/49] Rename module to abstract --- src/middleware/auth/base-auth-handler.ts | 2 +- src/middleware/auth/logto-helper.ts | 4 ++-- src/middleware/auth/oauth/{base.ts => abstract.ts} | 0 src/middleware/auth/oauth/logto-provider.ts | 2 +- src/middleware/auth/routes/admin/admin-auth.ts | 3 +++ src/middleware/auth/routine.ts | 2 +- src/middleware/auth/user-info-fetcher/api-token.ts | 2 +- src/middleware/auth/user-info-fetcher/base.ts | 2 +- src/middleware/auth/user-info-fetcher/idtoken.ts | 2 +- src/middleware/auth/user-info-fetcher/m2m-creds-token.ts | 2 +- src/middleware/auth/user-info-fetcher/portal-token.ts | 2 +- src/middleware/auth/user-info-fetcher/swagger-ui.ts | 2 +- 12 files changed, 14 insertions(+), 11 deletions(-) rename src/middleware/auth/oauth/{base.ts => abstract.ts} (100%) diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts index a86a3f55..74b3ab2f 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -1,7 +1,7 @@ import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; import type { IncomingHttpHeaders } from 'http'; -import type { IOAuthProvider } from './oauth/base.js'; +import type { IOAuthProvider } from './oauth/abstract.js'; import { LogToProvider } from './oauth/logto-provider.js'; import { SwaggerUserInfoFetcher } from './user-info-fetcher/swagger-ui.js'; import { IdTokenUserInfoFetcher } from './user-info-fetcher/idtoken.js'; diff --git a/src/middleware/auth/logto-helper.ts b/src/middleware/auth/logto-helper.ts index 87bd48d5..22aa1e07 100644 --- a/src/middleware/auth/logto-helper.ts +++ b/src/middleware/auth/logto-helper.ts @@ -2,8 +2,8 @@ import type { ICommonErrorResponse } from '../../types/authentication'; import { StatusCodes } from 'http-status-codes'; import jwt from 'jsonwebtoken'; import * as dotenv from 'dotenv'; -import type { IOAuthProvider } from './oauth/base.js'; -import { OAuthProvider } from './oauth/base.js'; +import type { IOAuthProvider } from './oauth/abstract.js'; +import { OAuthProvider } from './oauth/abstract.js'; dotenv.config(); export class LogToHelper extends OAuthProvider implements IOAuthProvider { diff --git a/src/middleware/auth/oauth/base.ts b/src/middleware/auth/oauth/abstract.ts similarity index 100% rename from src/middleware/auth/oauth/base.ts rename to src/middleware/auth/oauth/abstract.ts diff --git a/src/middleware/auth/oauth/logto-provider.ts b/src/middleware/auth/oauth/logto-provider.ts index 8db5e812..4458cb24 100644 --- a/src/middleware/auth/oauth/logto-provider.ts +++ b/src/middleware/auth/oauth/logto-provider.ts @@ -1,6 +1,6 @@ import type { ICommonErrorResponse } from '../../../types/authentication.js'; import { LogToHelper } from '../logto-helper.js'; -import { IOAuthProvider, OAuthProvider } from './base.js'; +import { IOAuthProvider, OAuthProvider } from './abstract.js'; export class LogToProvider extends OAuthProvider implements IOAuthProvider { private logToHelper: LogToHelper; diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index 21ec9dae..164e5c62 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -5,6 +5,9 @@ import type { IAuthResponse } from '../../../../types/authentication.js'; export class AdminHandler extends BaseAuthHandler { constructor() { super(); + // Main swagger route + this.registerRoute('/admin/swagger', 'GET', 'admin:swagger:testnet', { skipNamespace: true }); + this.registerRoute('/admin/swagger', 'GET', 'admin:swagger:mainnet', { skipNamespace: true }); // Subscriptions // skipNamespace is set to true cause we don't have the information about the namespace in the request this.registerRoute('/admin/subscription/create', 'POST', 'admin:subscription:create:testnet', { diff --git a/src/middleware/auth/routine.ts b/src/middleware/auth/routine.ts index 9f6291bc..4555c865 100644 --- a/src/middleware/auth/routine.ts +++ b/src/middleware/auth/routine.ts @@ -6,7 +6,7 @@ import { DefaultNetworkPattern } from '../../types/shared.js'; import { MethodToScopeRule, Namespaces, IAuthResponse, ICommonErrorResponse } from '../../types/authentication.js'; import { InvalidTokenError } from 'jwt-decode'; import { jwtDecode } from 'jwt-decode'; -import type { IOAuthProvider } from './oauth/base.js'; +import type { IOAuthProvider } from './oauth/abstract.js'; import type { IUserInfoFetcher } from './user-info-fetcher/base.js'; dotenv.config(); diff --git a/src/middleware/auth/user-info-fetcher/api-token.ts b/src/middleware/auth/user-info-fetcher/api-token.ts index 4964dc16..316caa9b 100644 --- a/src/middleware/auth/user-info-fetcher/api-token.ts +++ b/src/middleware/auth/user-info-fetcher/api-token.ts @@ -3,7 +3,7 @@ import { AuthReturn } from '../routine.js'; import type { IAuthResponse } from '../../../types/authentication.js'; import { StatusCodes } from 'http-status-codes'; import type { IUserInfoFetcher } from './base.js'; -import type { IOAuthProvider } from '../oauth/base.js'; +import type { IOAuthProvider } from '../oauth/abstract.js'; import * as dotenv from 'dotenv'; import { APIKeyService } from '../../../services/admin/api-key.js'; diff --git a/src/middleware/auth/user-info-fetcher/base.ts b/src/middleware/auth/user-info-fetcher/base.ts index ad373936..ee2875c3 100644 --- a/src/middleware/auth/user-info-fetcher/base.ts +++ b/src/middleware/auth/user-info-fetcher/base.ts @@ -1,6 +1,6 @@ import type { Request } from 'express'; import type { IAuthResponse } from '../../../types/authentication.js'; -import type { IOAuthProvider } from '../oauth/base.js'; +import type { IOAuthProvider } from '../oauth/abstract.js'; export interface IUserInfoOptions { [key: string]: any; diff --git a/src/middleware/auth/user-info-fetcher/idtoken.ts b/src/middleware/auth/user-info-fetcher/idtoken.ts index 93bff01d..c26e3fd4 100644 --- a/src/middleware/auth/user-info-fetcher/idtoken.ts +++ b/src/middleware/auth/user-info-fetcher/idtoken.ts @@ -3,7 +3,7 @@ import { AuthReturn } from '../routine.js'; import type { IAuthResponse } from '../../../types/authentication.js'; import { StatusCodes } from 'http-status-codes'; import type { IUserInfoFetcher } from './base.js'; -import type { IOAuthProvider } from '../oauth/base.js'; +import type { IOAuthProvider } from '../oauth/abstract.js'; import { createRemoteJWKSet, jwtVerify } from 'jose'; import * as dotenv from 'dotenv'; diff --git a/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts b/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts index f07a9cac..8aa9d4e2 100644 --- a/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts +++ b/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts @@ -3,7 +3,7 @@ import { AuthReturn } from '../routine.js'; import type { IAuthResponse } from '../../../types/authentication.js'; import { StatusCodes } from 'http-status-codes'; import type { IUserInfoFetcher } from './base.js'; -import type { IOAuthProvider } from '../oauth/base.js'; +import type { IOAuthProvider } from '../oauth/abstract.js'; import { createRemoteJWKSet, jwtVerify } from 'jose'; import * as dotenv from 'dotenv'; diff --git a/src/middleware/auth/user-info-fetcher/portal-token.ts b/src/middleware/auth/user-info-fetcher/portal-token.ts index 2eaf358f..dca00198 100644 --- a/src/middleware/auth/user-info-fetcher/portal-token.ts +++ b/src/middleware/auth/user-info-fetcher/portal-token.ts @@ -3,7 +3,7 @@ import { AuthReturn } from '../routine.js'; import type { IAuthResponse } from '../../../types/authentication.js'; import { StatusCodes } from 'http-status-codes'; import type { IUserInfoFetcher } from './base.js'; -import type { IOAuthProvider } from '../oauth/base.js'; +import type { IOAuthProvider } from '../oauth/abstract.js'; import { createRemoteJWKSet, jwtVerify } from 'jose'; import * as dotenv from 'dotenv'; diff --git a/src/middleware/auth/user-info-fetcher/swagger-ui.ts b/src/middleware/auth/user-info-fetcher/swagger-ui.ts index a01a6ec7..02950239 100644 --- a/src/middleware/auth/user-info-fetcher/swagger-ui.ts +++ b/src/middleware/auth/user-info-fetcher/swagger-ui.ts @@ -1,6 +1,6 @@ import type { Request } from 'express'; import { AuthReturn } from '../routine.js'; -import type { IOAuthProvider } from '../oauth/base.js'; +import type { IOAuthProvider } from '../oauth/abstract.js'; import type { IAuthResponse } from '../../../types/authentication.js'; import { StatusCodes } from 'http-status-codes'; import type { IUserInfoFetcher } from './base.js'; From f55588347752a699456854775be3257ceda392eb Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Sun, 14 Apr 2024 20:29:35 +0200 Subject: [PATCH 34/49] Move routes to api folder --- src/middleware/auth/logto-helper.ts | 34 +++++++++++++++---- .../auth/routes/{ => api}/account-auth.ts | 4 +-- .../auth/routes/{ => api}/auth-user-info.ts | 4 +-- .../auth/routes/{ => api}/credential-auth.ts | 4 +-- .../{ => api}/credential-status-auth.ts | 4 +-- .../auth/routes/{ => api}/did-auth.ts | 4 +-- .../auth/routes/{ => api}/key-auth.ts | 4 +-- .../routes/{ => api}/presentation-auth.ts | 4 +-- .../auth/routes/{ => api}/resource-auth.ts | 4 +-- src/middleware/auth/routine.ts | 3 ++ src/middleware/authentication.ts | 16 ++++----- 11 files changed, 55 insertions(+), 30 deletions(-) rename src/middleware/auth/routes/{ => api}/account-auth.ts (79%) rename src/middleware/auth/routes/{ => api}/auth-user-info.ts (77%) rename src/middleware/auth/routes/{ => api}/credential-auth.ts (89%) rename src/middleware/auth/routes/{ => api}/credential-status-auth.ts (92%) rename src/middleware/auth/routes/{ => api}/did-auth.ts (89%) rename src/middleware/auth/routes/{ => api}/key-auth.ts (83%) rename src/middleware/auth/routes/{ => api}/presentation-auth.ts (81%) rename src/middleware/auth/routes/{ => api}/resource-auth.ts (82%) diff --git a/src/middleware/auth/logto-helper.ts b/src/middleware/auth/logto-helper.ts index 22aa1e07..771ab653 100644 --- a/src/middleware/auth/logto-helper.ts +++ b/src/middleware/auth/logto-helper.ts @@ -4,6 +4,7 @@ import jwt from 'jsonwebtoken'; import * as dotenv from 'dotenv'; import type { IOAuthProvider } from './oauth/abstract.js'; import { OAuthProvider } from './oauth/abstract.js'; +import { EventTracker, eventTracker } from '../../services/track/tracker'; dotenv.config(); export class LogToHelper extends OAuthProvider implements IOAuthProvider { @@ -11,6 +12,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { private allScopes: string[]; private allResourceWithNames: string[]; public defaultScopes: string[]; + private m2mGetTokenAttempts = 5; constructor() { super(); @@ -42,8 +44,21 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { } private async getM2MToken(): Promise { - if (this.m2mToken === '' || this.isTokenExpired(this.m2mToken)) { - await this.setM2MToken(); + if (this.isTokenExpired(this.m2mToken)) { + for (let i = 0; i < this.m2mGetTokenAttempts; i++) { + const response = await this.setM2MToken(); + if (response.status === StatusCodes.OK) { + return this.m2mToken; + } + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + 'Failed to get M2M token, Attempt ' + i + ' of ' + this.m2mGetTokenAttempts, + 'M2M token issuing' + ), + severity: 'info' + }) + } + throw new Error('Failed to get M2M token after ' + this.m2mGetTokenAttempts + ' attempts'); } return this.m2mToken; } @@ -77,7 +92,6 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return this.returnOk(roles.data); } } - // Assign a default role to a user return await this.assignDefaultRoleForUser(userId, process.env.LOGTO_DEFAULT_ROLE_ID); } @@ -268,6 +282,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return this.returnError(StatusCodes.BAD_GATEWAY, `getRolesForUser ${err}`); } } + private async getRoleInfo(roleId: string): Promise { const uri = new URL(`/api/roles/${roleId}`, process.env.LOGTO_ENDPOINT); try { @@ -276,6 +291,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return this.returnError(StatusCodes.BAD_GATEWAY, `getRoleInfo ${err}`); } } + private async assignDefaultRoleForUser(userId: string, roleId: string): Promise { const userInfo = await this.getUserInfo(userId); const uri = new URL(`/api/users/${userId}/roles`, process.env.LOGTO_ENDPOINT); @@ -294,8 +310,8 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { const role = await this.getRoleInfo(roleId); if (role.status !== StatusCodes.OK) { return this.returnError( - StatusCodes.INTERNAL_SERVER_ERROR, - `Could not fetch the info about user with userId ${userId}` + StatusCodes.BAD_GATEWAY, + `Could not fetch the info about user with userId ${userId} because of error from authority server: ${role.error}` ); } // Such role exists @@ -355,6 +371,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return this.returnError(500, `updateCustomData ${err}`); } } + private async getUserInfo(userId: string): Promise { const uri = new URL(`/api/users/${userId}`, process.env.LOGTO_ENDPOINT); try { @@ -363,6 +380,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return this.returnError(StatusCodes.BAD_GATEWAY, `getUserInfo ${err}`); } } + public async getCustomData(userId: string): Promise { const uri = new URL(`/api/users/${userId}/custom-data`, process.env.LOGTO_ENDPOINT); try { @@ -378,7 +396,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { if (allResources.status !== StatusCodes.OK) { return this.returnError( StatusCodes.BAD_GATEWAY, - `setAllResourcesWithNames: Error while getting all resources` + `setAllResourcesWithNames: Error while getting all resources. Error: ${allResources.error}` ); } for (const resource of allResources.data) { @@ -386,6 +404,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { } return this.returnOk({}); } + public async getAllResources(): Promise { const uri = new URL(`/api/resources`, process.env.LOGTO_ENDPOINT); @@ -412,6 +431,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { } return this.returnOk({}); } + private async postToLogto(uri: URL, body: any, headers: any = {}): Promise { const response = await fetch(uri, { headers: { @@ -427,6 +447,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { } return this.returnOk({}); } + private async getToLogto(uri: URL, headers: any = {}): Promise { const response = await fetch(uri, { headers: { @@ -442,6 +463,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { const metadata = await response.json(); return this.returnOk(metadata); } + private async setM2MToken(): Promise { const searchParams = new URLSearchParams({ grant_type: 'client_credentials', diff --git a/src/middleware/auth/routes/account-auth.ts b/src/middleware/auth/routes/api/account-auth.ts similarity index 79% rename from src/middleware/auth/routes/account-auth.ts rename to src/middleware/auth/routes/api/account-auth.ts index 830f32d2..96cf1f35 100644 --- a/src/middleware/auth/routes/account-auth.ts +++ b/src/middleware/auth/routes/api/account-auth.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../base-auth-handler.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import { BaseAuthHandler } from '../../base-auth-handler.js'; +import type { IAuthResponse } from '../../../../types/authentication.js'; export class AccountAuthHandler extends BaseAuthHandler { constructor() { diff --git a/src/middleware/auth/routes/auth-user-info.ts b/src/middleware/auth/routes/api/auth-user-info.ts similarity index 77% rename from src/middleware/auth/routes/auth-user-info.ts rename to src/middleware/auth/routes/api/auth-user-info.ts index 0d8f72fb..88d7426c 100644 --- a/src/middleware/auth/routes/auth-user-info.ts +++ b/src/middleware/auth/routes/api/auth-user-info.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../base-auth-handler.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import { BaseAuthHandler } from '../../base-auth-handler.js'; +import type { IAuthResponse } from '../../../../types/authentication.js'; export class AuthInfoHandler extends BaseAuthHandler { constructor() { diff --git a/src/middleware/auth/routes/credential-auth.ts b/src/middleware/auth/routes/api/credential-auth.ts similarity index 89% rename from src/middleware/auth/routes/credential-auth.ts rename to src/middleware/auth/routes/api/credential-auth.ts index 73b0f94f..b2cb70f9 100644 --- a/src/middleware/auth/routes/credential-auth.ts +++ b/src/middleware/auth/routes/api/credential-auth.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../base-auth-handler.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import { BaseAuthHandler } from '../../base-auth-handler.js'; +import type { IAuthResponse } from '../../../../types/authentication.js'; export class CredentialAuthHandler extends BaseAuthHandler { constructor() { diff --git a/src/middleware/auth/routes/credential-status-auth.ts b/src/middleware/auth/routes/api/credential-status-auth.ts similarity index 92% rename from src/middleware/auth/routes/credential-status-auth.ts rename to src/middleware/auth/routes/api/credential-status-auth.ts index 3504953e..c05b31c3 100644 --- a/src/middleware/auth/routes/credential-status-auth.ts +++ b/src/middleware/auth/routes/api/credential-status-auth.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../base-auth-handler.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import { BaseAuthHandler } from '../../base-auth-handler.js'; +import type { IAuthResponse } from '../../../../types/authentication.js'; export class CredentialStatusAuthHandler extends BaseAuthHandler { constructor() { diff --git a/src/middleware/auth/routes/did-auth.ts b/src/middleware/auth/routes/api/did-auth.ts similarity index 89% rename from src/middleware/auth/routes/did-auth.ts rename to src/middleware/auth/routes/api/did-auth.ts index f0fe4a97..e3d8df14 100644 --- a/src/middleware/auth/routes/did-auth.ts +++ b/src/middleware/auth/routes/api/did-auth.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../base-auth-handler.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import { BaseAuthHandler } from '../../base-auth-handler.js'; +import type { IAuthResponse } from '../../../../types/authentication.js'; export class DidAuthHandler extends BaseAuthHandler { constructor() { diff --git a/src/middleware/auth/routes/key-auth.ts b/src/middleware/auth/routes/api/key-auth.ts similarity index 83% rename from src/middleware/auth/routes/key-auth.ts rename to src/middleware/auth/routes/api/key-auth.ts index 8c6e1a7e..2c84cd3b 100644 --- a/src/middleware/auth/routes/key-auth.ts +++ b/src/middleware/auth/routes/api/key-auth.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../base-auth-handler.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import { BaseAuthHandler } from '../../base-auth-handler.js'; +import type { IAuthResponse } from '../../../../types/authentication.js'; export class KeyAuthHandler extends BaseAuthHandler { constructor() { diff --git a/src/middleware/auth/routes/presentation-auth.ts b/src/middleware/auth/routes/api/presentation-auth.ts similarity index 81% rename from src/middleware/auth/routes/presentation-auth.ts rename to src/middleware/auth/routes/api/presentation-auth.ts index b7d23fd8..f68dcab5 100644 --- a/src/middleware/auth/routes/presentation-auth.ts +++ b/src/middleware/auth/routes/api/presentation-auth.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../base-auth-handler.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import { BaseAuthHandler } from '../../base-auth-handler.js'; +import type { IAuthResponse } from '../../../../types/authentication.js'; export class PresentationAuthHandler extends BaseAuthHandler { constructor() { diff --git a/src/middleware/auth/routes/resource-auth.ts b/src/middleware/auth/routes/api/resource-auth.ts similarity index 82% rename from src/middleware/auth/routes/resource-auth.ts rename to src/middleware/auth/routes/api/resource-auth.ts index 9182b853..a4ee1080 100644 --- a/src/middleware/auth/routes/resource-auth.ts +++ b/src/middleware/auth/routes/api/resource-auth.ts @@ -1,6 +1,6 @@ import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../base-auth-handler.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import { BaseAuthHandler } from '../../base-auth-handler.js'; +import type { IAuthResponse } from '../../../../types/authentication.js'; export class ResourceAuthHandler extends BaseAuthHandler { constructor() { diff --git a/src/middleware/auth/routine.ts b/src/middleware/auth/routine.ts index 4555c865..2a843ce9 100644 --- a/src/middleware/auth/routine.ts +++ b/src/middleware/auth/routine.ts @@ -203,6 +203,7 @@ export class RuleRoutine extends AuthReturn implements IReturn { return null; } + protected findNetworkInBody(body: string): string | null { const matches = body.match(DefaultNetworkPattern); if (matches && matches.length > 0) { @@ -210,6 +211,7 @@ export class RuleRoutine extends AuthReturn implements IReturn { } return null; } + protected switchNetwork(network: string): Namespaces | null { switch (network) { case 'testnet': { @@ -223,6 +225,7 @@ export class RuleRoutine extends AuthReturn implements IReturn { } } } + protected findRule(route: string, method: string, namespace = Namespaces.Testnet): MethodToScopeRule | undefined { for (const rule of this.getRouteToScopeList()) { if (rule.doesRuleMatches(route, method, namespace)) { diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 695be967..4bb8cdbb 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -2,20 +2,20 @@ import { Request, Response, NextFunction, response } from 'express'; import { StatusCodes } from 'http-status-codes'; import * as dotenv from 'dotenv'; -import { AccountAuthHandler } from './auth/routes/account-auth.js'; -import { CredentialAuthHandler } from './auth/routes/credential-auth.js'; -import { DidAuthHandler } from './auth/routes/did-auth.js'; -import { KeyAuthHandler } from './auth/routes/key-auth.js'; -import { CredentialStatusAuthHandler } from './auth/routes/credential-status-auth.js'; -import { ResourceAuthHandler } from './auth/routes/resource-auth.js'; +import { AccountAuthHandler } from './auth/routes/api/account-auth.js'; +import { CredentialAuthHandler } from './auth/routes/api/credential-auth.js'; +import { DidAuthHandler } from './auth/routes/api/did-auth.js'; +import { KeyAuthHandler } from './auth/routes/api/key-auth.js'; +import { CredentialStatusAuthHandler } from './auth/routes/api/credential-status-auth.js'; +import { ResourceAuthHandler } from './auth/routes/api/resource-auth.js'; import type { BaseAuthHandler } from './auth/base-auth-handler.js'; import { LogToHelper } from './auth/logto-helper.js'; -import { PresentationAuthHandler } from './auth/routes/presentation-auth.js'; +import { PresentationAuthHandler } from './auth/routes/api/presentation-auth.js'; import { UserService } from '../services/api/user.js'; import { configLogToExpress } from '../types/constants.js'; import { handleAuthRoutes, withLogto } from '@logto/express'; import { LogToProvider } from './auth/oauth/logto-provider.js'; -import { AuthInfoHandler } from './auth/routes/auth-user-info.js'; +import { AuthInfoHandler } from './auth/routes/api/auth-user-info.js'; import { CustomerService } from '../services/api/customer.js'; import { AdminHandler } from './auth/routes/admin/admin-auth.js'; From f41d5e585f96e73bd88397eadea42ad6899ba307 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Mon, 15 Apr 2024 23:30:33 +0200 Subject: [PATCH 35/49] Refactor in API guarding --- src/controllers/api/account.ts | 16 +- src/middleware/auth/auth-gaurd.ts | 144 + src/middleware/auth/auth-rule-provider.ts | 78 + src/middleware/auth/base-auth-handler.ts | 205 - src/middleware/auth/logto-helper.ts | 6 +- .../auth/routes/admin/admin-auth.ts | 75 +- .../auth/routes/api/account-auth.ts | 16 +- .../auth/routes/api/auth-user-info.ts | 14 +- .../auth/routes/api/credential-auth.ts | 31 +- .../auth/routes/api/credential-status-auth.ts | 36 +- src/middleware/auth/routes/api/did-auth.ts | 35 +- src/middleware/auth/routes/api/key-auth.ts | 21 +- .../auth/routes/api/presentation-auth.ts | 16 +- .../auth/routes/api/resource-auth.ts | 18 +- src/middleware/auth/routine.ts | 234 +- .../auth/user-info-fetcher/api-token.ts | 50 +- src/middleware/auth/user-info-fetcher/base.ts | 45 +- .../auth/user-info-fetcher/idtoken.ts | 47 +- .../auth/user-info-fetcher/m2m-creds-token.ts | 45 +- .../auth/user-info-fetcher/portal-token.ts | 67 +- .../auth/user-info-fetcher/swagger-ui.ts | 70 +- src/middleware/authentication.ts | 130 +- src/static/swagger-admin.json | 2058 ++--- src/static/swagger-api.json | 6882 +++++++++-------- src/types/authentication.ts | 165 +- 25 files changed, 5294 insertions(+), 5210 deletions(-) create mode 100644 src/middleware/auth/auth-gaurd.ts create mode 100644 src/middleware/auth/auth-rule-provider.ts delete mode 100644 src/middleware/auth/base-auth-handler.ts diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 9fc99d35..16af974a 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -23,6 +23,8 @@ import type { UnsuccessfulResponseBody } from '../../types/shared.js'; import { check, validationResult } from 'express-validator'; import { eventTracker } from '../../services/track/tracker.js'; import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from '../../services/track/submitter.js'; +import * as dotenv from 'dotenv'; +dotenv.config(); export class AccountController { public static createValidator = [ @@ -306,7 +308,7 @@ export class AccountController { } } // 8. Add the Stripe account to the Customer - if (customer.paymentProviderId === null) { + if (process.env.STRIPE_ENABLED === 'true' && customer.paymentProviderId === null) { eventTracker.submit({ operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { @@ -369,7 +371,7 @@ export class AccountController { return response.status(StatusCodes.BAD_REQUEST).json({ error: result.array().pop()?.msg }); } - const username = request.body.username; + const { username } = request.body; try { // 2. Check if the customer exists @@ -425,6 +427,16 @@ export class AccountController { } } } + // 5. Setup stripe account + if (process.env.STRIPE_ENABLED === 'true' && customer.paymentProviderId === null) { + eventTracker.submit({ + operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, + data: { + name: customer.name, + customerId: customer.customerId, + } satisfies ISubmitStripeCustomerCreateData, + } satisfies ISubmitOperation); + } return response.status(StatusCodes.CREATED).json(customer); } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ diff --git a/src/middleware/auth/auth-gaurd.ts b/src/middleware/auth/auth-gaurd.ts new file mode 100644 index 00000000..0fb5143f --- /dev/null +++ b/src/middleware/auth/auth-gaurd.ts @@ -0,0 +1,144 @@ +import { StatusCodes } from "http-status-codes"; +import type { AuthRuleRepository } from "./auth-rule-provider.js"; +import type { NextFunction, Request, Response } from "express"; +import type { ValidationErrorResponseBody } from "../../types/shared.js"; +import type { IUserInfoFetcher } from "./user-info-fetcher/base.js"; +import { SwaggerUserInfoFetcher } from "./user-info-fetcher/swagger-ui.js"; +import type { IOAuthProvider } from "./oauth/abstract.js"; +import type { IncomingHttpHeaders } from "http"; +import { PortalUserInfoFetcher } from "./user-info-fetcher/portal-token.js"; +import { IdTokenUserInfoFetcher } from "./user-info-fetcher/idtoken.js"; +import { M2MCredsTokenUserInfoFetcher } from "./user-info-fetcher/m2m-creds-token.js"; +import { APITokenUserInfoFetcher } from "./user-info-fetcher/api-token.js"; +import { decodeJwt } from 'jose'; + +export class APIGuard { + private authRuleRepository: AuthRuleRepository; + private userInfoFetcher: IUserInfoFetcher; + private oauthProvider: IOAuthProvider; + private static bearerTokenIdentifier = 'Bearer'; + private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap', '/admin/webhook', '/admin/swagger']; + + constructor(authRuleRepository: AuthRuleRepository, oauthProvider: IOAuthProvider) { + this.authRuleRepository = authRuleRepository; + this.oauthProvider = oauthProvider + this.userInfoFetcher = new SwaggerUserInfoFetcher(this.oauthProvider); + } + + + /** + * Executes the authentication guard for incoming requests. + * + * @param {Request} request - The incoming request object. + * @param {Response} response - The outgoing response object. + * @param {NextFunction} next - The next middleware function in the chain. + * @return {void} + */ + public async guard(request: Request, response: Response, next: NextFunction) { + const authRule = this.authRuleRepository.match(request); + if (!authRule) { + return response.status(StatusCodes.BAD_REQUEST).send({ + error: `Bad Request. No auth rules for handling such request: ${request.method} ${request.path} or please check that namespace scpecified correctly.` + } satisfies ValidationErrorResponseBody); + } + + if (authRule.isAllowedUnauthorized()) { + return next(); + } + + // Set user info fetcher + this.chooseUserFetcherStrategy(request); + + // Get User info. scopes and user id maybe placed in M2M, API token or using Swagger UI + const resp = await this.userInfoFetcher.fetch(request, response, this.oauthProvider) + if (resp) { + return resp + } + + // Checks if the list of scopes from user enough to make an action + if (!authRule.areValidScopes(response.locals.scopes)) { + return response.status(StatusCodes.FORBIDDEN).send({ + error: `Forbidden. Your account is not authorized to carry out this action.` + } satisfies ValidationErrorResponseBody); + } + + next() + } + + /** + * Chooses the appropriate user fetcher strategy based on the request headers. + * + * @param {Request} request - The request object containing the headers. + * @return {void} This function does not return a value. + */ + private chooseUserFetcherStrategy(request: Request): void { + const authorizationHeader = request.headers.authorization as string; + const apiKeyHeader = request.headers['x-api-key'] as string; + + if (authorizationHeader && apiKeyHeader) { + const token = authorizationHeader.replace('Bearer ', ''); + this.setUserInfoStrategy(new PortalUserInfoFetcher(token, apiKeyHeader, this.oauthProvider)); + return; + } + + if (authorizationHeader) { + const token = authorizationHeader.replace('Bearer ', ''); + const payload = decodeJwt(token); + + if (payload.aud === process.env.LOGTO_APP_ID) { + this.setUserInfoStrategy(new IdTokenUserInfoFetcher(token, this.oauthProvider)); + } else { + this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token, this.oauthProvider)); + } + + return; + } + + if (apiKeyHeader) { + this.setUserInfoStrategy(new APITokenUserInfoFetcher(apiKeyHeader, this.oauthProvider)); + return; + } + + this.setUserInfoStrategy(new SwaggerUserInfoFetcher(this.oauthProvider)); + } + + + /** + * Sets the user info strategy for the API guard. + * + * @param {IUserInfoFetcher} strategy - The strategy to set as the user info fetcher. + * @return {void} This function does not return anything. + */ + public setUserInfoStrategy(strategy: IUserInfoFetcher): void { + this.userInfoFetcher = strategy; + } + + /** + * Extracts the bearer token from the incoming HTTP headers. + * + * @param {IncomingHttpHeaders} headers - The incoming HTTP headers + * @return {string | unknown} The extracted bearer token + */ + public static extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders): string | unknown { + if (authorization && authorization.startsWith(this.bearerTokenIdentifier)) { + return authorization.slice(this.bearerTokenIdentifier.length + 1); + } + return undefined; + } + + /** + * Checks if the given path should be skipped based on the list of paths to skip. + * + * @param {string} path - The path to check. + * @return {boolean} True if the path should be skipped, false otherwise. + */ + public skipPath(path: string): boolean { + for (const ps of this.pathSkip) { + if (path === '/' || path.startsWith(ps)) { + return true; + } + } + return false; + } +} + diff --git a/src/middleware/auth/auth-rule-provider.ts b/src/middleware/auth/auth-rule-provider.ts new file mode 100644 index 00000000..0849dda0 --- /dev/null +++ b/src/middleware/auth/auth-rule-provider.ts @@ -0,0 +1,78 @@ +import { AuthRule, AuthRuleOptions } from "../../types/authentication.js"; +import type { Request } from "express"; + +export interface IAuthRuleProvider { + push(methodToScopeRule: AuthRule): void; + match(request: Request): AuthRule | null; +} + +export class AuthRuleProvider implements IAuthRuleProvider { + + protected rules: AuthRule[] = []; + + /** + * Adds a new methodToScopeRule to the ruleList. + * + * @param {AuthRule} methodToScopeRule - The methodToScopeRule to push. + * @return {void} + */ + public push(methodToScopeRule: AuthRule): void { + this.rules.push(methodToScopeRule); + } + + /** + * Registers a new route with the specified method, scope, and options. + * + * @param {string} route - The route to register. + * @param {string} method - The HTTP method for the route. + * @param {string} scope - The scope associated with the route. + * @param {Object} options - (Optional) Additional options for the route registration. + */ + protected registerRule(route: string, method: string, scope: string, options?: AuthRuleOptions): void { + this.push(new AuthRule(route, method, scope, options)); + } + + /** + * Matches the request against the rules and returns the matching rule, if found. + * + * @param {Request} request - The request to match against the rules. + * @return {AuthRule | null} The matching rule, if found; otherwise, null. + */ + public match(request: Request): AuthRule | null { + for (const rule of this.rules) { + if (rule.match(request)) { + return rule; + } + } + return null; + } +} + +export class AuthRuleRepository { + private providers: AuthRuleProvider[] = []; + /** + * Adds a new provider to the providers list. + * + * @param {AuthRuleProvider} provider - The provider to push. + * @return {void} + */ + public push(provider: AuthRuleProvider): void { + this.providers.push(provider); + } + + /** + * Matches the request against the rules and returns the matching rule, if found. + * + * @param {Request} request - The request to match against the rules. + * @return {AuthRule | null} The matching rule, if found; otherwise, null. + */ + public match(request: Request): AuthRule | null { + for (const provider of this.providers) { + const rule = provider.match(request); + if (rule) { + return rule; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/middleware/auth/base-auth-handler.ts b/src/middleware/auth/base-auth-handler.ts deleted file mode 100644 index 74b3ab2f..00000000 --- a/src/middleware/auth/base-auth-handler.ts +++ /dev/null @@ -1,205 +0,0 @@ -import type { Request, Response } from 'express'; -import { StatusCodes } from 'http-status-codes'; -import type { IncomingHttpHeaders } from 'http'; -import type { IOAuthProvider } from './oauth/abstract.js'; -import { LogToProvider } from './oauth/logto-provider.js'; -import { SwaggerUserInfoFetcher } from './user-info-fetcher/swagger-ui.js'; -import { IdTokenUserInfoFetcher } from './user-info-fetcher/idtoken.js'; -import type { IUserInfoFetcher } from './user-info-fetcher/base.js'; -import { IAuthHandler, RuleRoutine, IAPIGuard } from './routine.js'; -import type { IAuthResponse, MethodToScopeRule } from '../../types/authentication.js'; -import { M2MCredsTokenUserInfoFetcher } from './user-info-fetcher/m2m-creds-token.js'; -import { decodeJwt } from 'jose'; -import { PortalUserInfoFetcher } from './user-info-fetcher/portal-token.js'; -import { APITokenUserInfoFetcher } from './user-info-fetcher/api-token.js'; - -export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { - userInfoFetcher: IUserInfoFetcher = {} as IUserInfoFetcher; - - async guardAPI(request: Request, oauthProvider: IOAuthProvider): Promise { - // Reset all variables - this.reset(); - // Preps - this.preps(request); - // Firstly - try to find the rule for the request - const rule = this.findRule(request.path, request.method, this.getNamespace()); - - if (!rule) { - return this.returnError( - StatusCodes.BAD_REQUEST, - `Bad Request. No auth rules for handling such request: ${request.method} ${request.path}` - ); - } - // If the rule is not found - skip the auth check - if (!rule.isEmpty()) { - this.setRule(rule); - // If the rule is found and it allows unauthorized - return ok - this.setIsAllowedUnauthorized(rule.isAllowedUnauthorized()); - } - // Rule can has allowedUnauthorised to true - it means that it's allowed for all - // Rule can has empty namespace - it means that it's allowed for all namespaces - let _res = this.applyDefaults(request); - if (_res) { - return _res; - } - // Set userId. Usually it will be the LogTo userId - // Here we could get UserId from API Key or from user's token - _res = await this.userInfoFetcher.fetchUserInfo(request, oauthProvider); - if (_res.error) { - return _res; - } - this.setScopes(_res.data.scopes); - this.setUserId(_res.data.userId); - this.setCustomerId(_res.data.customerId); - // Checks if the list of scopes from user enough to make an action - if (!this.areValidScopes(this.getRule(), this.getScopes())) { - return this.returnError( - StatusCodes.FORBIDDEN, - `Unauthorized error: Your account is not authorized to carry out this action.` - ); - } - return this.returnOk(); - } - - protected preps(request: Request) { - // Setup the namespace - // Here we just trying to get the network value from the request - // The validation depends on the rule for the request - const namespace = this.getNamespaceFromRequest(request); - if (namespace) { - this.setNamespace(namespace); - } - } - - protected applyDefaults(request: Request): IAuthResponse | void { - const rule = this.getRule(); - const namespace = this.getNamespace(); - - if (rule.isEmpty()) { - return this.returnError( - StatusCodes.INTERNAL_SERVER_ERROR, - `Internal error: Issue with finding the rule for the path ${request.path}` - ); - } - // If the rule allows unauthorized - return ok - if (rule.isAllowedUnauthorized()) { - return this.returnOk(); - } - // Namespace should be testnet or mainnet or '' if isSkipNamespace is true - // Otherwise - raise an error. - if (!namespace && !rule.isSkipNamespace()) { - return this.returnError( - StatusCodes.INTERNAL_SERVER_ERROR, - 'Seems like there is no information about the network in the request.' - ); - } - } - - public isValidScope(rule: MethodToScopeRule, scope: string): boolean { - return rule.validate(scope); - } - - public areValidScopes(rule: MethodToScopeRule, scopes: string[]): boolean { - for (const scope of scopes) { - if (this.isValidScope(rule, scope)) { - return true; - } - } - return false; - } -} - -export class BaseAuthHandler extends BaseAPIGuard implements IAuthHandler { - private nextHandler: IAuthHandler; - oauthProvider: IOAuthProvider; - private static bearerTokenIdentifier = 'Bearer'; - private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap', '/admin/webhook', '/admin/swagger']; - - constructor() { - super(); - this.userInfoFetcher = new SwaggerUserInfoFetcher(); - // For now we use only one provider - LogTo - this.oauthProvider = new LogToProvider(); - this.nextHandler = {} as IAuthHandler; - } - - public static extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders): string | unknown { - if (authorization && authorization.startsWith(this.bearerTokenIdentifier)) { - return authorization.slice(this.bearerTokenIdentifier.length + 1); - } - return undefined; - } - - private chooseUserFetcherStrategy(request: Request): void { - const token = BaseAuthHandler.extractBearerTokenFromHeaders(request.headers) as string; - const apiKey = request.headers['x-api-key'] as string; - const headerIdToken = request.headers['id-token'] as string; - if (headerIdToken && token) { - this.setUserInfoStrategy(new PortalUserInfoFetcher(token, headerIdToken)); - return; - } - - if (token) { - const payload = decodeJwt(token); - if (payload.aud === process.env.LOGTO_APP_ID) { - this.setUserInfoStrategy(new IdTokenUserInfoFetcher(token)); - return; - } else { - this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token)); - return; - } - } - if (apiKey) { - this.setUserInfoStrategy(new APITokenUserInfoFetcher(apiKey)); - return; - } else { - if (token) { - const payload = decodeJwt(token); - if (payload.aud === process.env.LOGTO_APP_ID) { - this.setUserInfoStrategy(new APITokenUserInfoFetcher(token)); - } else { - this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token)); - } - } else { - this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); - } - } - } - - public setOAuthProvider(oauthProvider: IOAuthProvider): IAuthHandler { - this.oauthProvider = oauthProvider; - return this; - } - - public setUserInfoStrategy(strategy: IUserInfoFetcher): void { - this.userInfoFetcher = strategy; - } - - // interface implementation - async guardAPI(request: Request): Promise { - this.chooseUserFetcherStrategy(request); - return super.guardAPI(request, this.oauthProvider); - } - - public setNext(handler: IAuthHandler): IAuthHandler { - this.nextHandler = handler; - return handler; - } - - public async handle(request: Request, response: Response): Promise { - if (Object.keys(this.nextHandler).length !== 0) { - return this.nextHandler.handle(request, response); - } - // If request.path was not registered in the routeToScope, then skip the auth check - return this.returnOk(); - } - - public skipPath(path: string): boolean { - for (const ps of this.pathSkip) { - if (path === '/' || path.startsWith(ps)) { - return true; - } - } - return false; - } -} diff --git a/src/middleware/auth/logto-helper.ts b/src/middleware/auth/logto-helper.ts index 771ab653..e20de9a0 100644 --- a/src/middleware/auth/logto-helper.ts +++ b/src/middleware/auth/logto-helper.ts @@ -1,10 +1,10 @@ -import type { ICommonErrorResponse } from '../../types/authentication'; +import type { ICommonErrorResponse } from '../../types/authentication.js'; import { StatusCodes } from 'http-status-codes'; import jwt from 'jsonwebtoken'; import * as dotenv from 'dotenv'; import type { IOAuthProvider } from './oauth/abstract.js'; import { OAuthProvider } from './oauth/abstract.js'; -import { EventTracker, eventTracker } from '../../services/track/tracker'; +import { EventTracker, eventTracker } from '../../services/track/tracker.js'; dotenv.config(); export class LogToHelper extends OAuthProvider implements IOAuthProvider { @@ -44,7 +44,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { } private async getM2MToken(): Promise { - if (this.isTokenExpired(this.m2mToken)) { + if (!this.m2mToken || this.isTokenExpired(this.m2mToken)) { for (let i = 0; i < this.m2mGetTokenAttempts; i++) { const response = await this.setM2MToken(); if (response.status === StatusCodes.OK) { diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index 164e5c62..a9e4d7e0 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -1,72 +1,67 @@ -import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../../base-auth-handler.js'; -import type { IAuthResponse } from '../../../../types/authentication.js'; +import { AuthRuleProvider } from '../../auth-rule-provider.js'; -export class AdminHandler extends BaseAuthHandler { +export class AdminAuthRuleProvider extends AuthRuleProvider { + /** + * Constructor for the AdminHandler class. Registers various routes related to admin functionalities like swagger, subscriptions, prices, products, and API keys for different environments. + */ constructor() { super(); // Main swagger route - this.registerRoute('/admin/swagger', 'GET', 'admin:swagger:testnet', { skipNamespace: true }); - this.registerRoute('/admin/swagger', 'GET', 'admin:swagger:mainnet', { skipNamespace: true }); + this.registerRule('/admin/swagger', 'GET', 'admin:swagger:testnet', { skipNamespace: true }); + this.registerRule('/admin/swagger', 'GET', 'admin:swagger:mainnet', { skipNamespace: true }); // Subscriptions // skipNamespace is set to true cause we don't have the information about the namespace in the request - this.registerRoute('/admin/subscription/create', 'POST', 'admin:subscription:create:testnet', { + this.registerRule('/admin/subscription/create', 'POST', 'admin:subscription:create:testnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/create', 'POST', 'admin:subscription:create:mainnet', { + this.registerRule('/admin/subscription/create', 'POST', 'admin:subscription:create:mainnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { + this.registerRule('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/list', 'GET', 'admin:subscription:list:mainnet', { + this.registerRule('/admin/subscription/list', 'GET', 'admin:subscription:list:mainnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/update', 'POST', 'admin:subscription:update:testnet', { + this.registerRule('/admin/subscription/update', 'POST', 'admin:subscription:update:testnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/update', 'POST', 'admin:subscription:update:mainnet', { + this.registerRule('/admin/subscription/update', 'POST', 'admin:subscription:update:mainnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:testnet', { + this.registerRule('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:testnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:mainnet', { + this.registerRule('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:mainnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/resume', 'POST', 'admin:subscription:resume:testnet', { + this.registerRule('/admin/subscription/resume', 'POST', 'admin:subscription:resume:testnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/resume', 'POST', 'admin:subscription:resume:mainnet', { + this.registerRule('/admin/subscription/resume', 'POST', 'admin:subscription:resume:mainnet', { skipNamespace: true, }); - this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:testnet', { skipNamespace: true }); - this.registerRoute('/admin/subscription/get', 'GET', 'admin:subscription:get:mainnet', { skipNamespace: true }); + this.registerRule('/admin/subscription/get', 'GET', 'admin:subscription:get:testnet', { skipNamespace: true }); + this.registerRule('/admin/subscription/get', 'GET', 'admin:subscription:get:mainnet', { skipNamespace: true }); // Prices - this.registerRoute('/admin/price/list', 'GET', 'admin:price:list:testnet', { skipNamespace: true }); - this.registerRoute('/admin/price/list', 'GET', 'admin:price:list:mainnet', { skipNamespace: true }); + this.registerRule('/admin/price/list', 'GET', 'admin:price:list:testnet', { skipNamespace: true }); + this.registerRule('/admin/price/list', 'GET', 'admin:price:list:mainnet', { skipNamespace: true }); // Products - this.registerRoute('/admin/product/list', 'GET', 'admin:product:list:testnet', { skipNamespace: true }); - this.registerRoute('/admin/product/list', 'GET', 'admin:product:list:mainnet', { skipNamespace: true }); - this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:testnet', { skipNamespace: true }); - this.registerRoute('/admin/product/get', 'GET', 'admin:product:get:mainnet', { skipNamespace: true }); + this.registerRule('/admin/product/list', 'GET', 'admin:product:list:testnet', { skipNamespace: true }); + this.registerRule('/admin/product/list', 'GET', 'admin:product:list:mainnet', { skipNamespace: true }); + this.registerRule('/admin/product/get', 'GET', 'admin:product:get:testnet', { skipNamespace: true }); + this.registerRule('/admin/product/get', 'GET', 'admin:product:get:mainnet', { skipNamespace: true }); // API Key - this.registerRoute('/admin/api-key/create', 'POST', 'admin:api-key:create:testnet', { skipNamespace: true }); - this.registerRoute('/admin/api-key/create', 'POST', 'admin:api-key:create:mainnet', { skipNamespace: true }); - this.registerRoute('/admin/api-key/update', 'POST', 'admin:api-key:update:testnet', { skipNamespace: true }); - this.registerRoute('/admin/api-key/update', 'POST', 'admin:api-key:update:mainnet', { skipNamespace: true }); - this.registerRoute('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:testnet', { skipNamespace: true }); - this.registerRoute('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:mainnet', { skipNamespace: true }); - this.registerRoute('/admin/api-key/get', 'GET', 'admin:api-key:get:testnet', { skipNamespace: true }); - this.registerRoute('/admin/api-key/get', 'GET', 'admin:api-key:get:mainnet', { skipNamespace: true }); - this.registerRoute('/admin/api-key/list', 'GET', 'admin:api-key:list:testnet', { skipNamespace: true }); - this.registerRoute('/admin/api-key/list', 'GET', 'admin:api-key:list:mainnet', { skipNamespace: true }); - } - public async handle(request: Request, response: Response): Promise { - if (!request.path.includes('/admin/')) { - return super.handle(request, response); - } - return this.guardAPI(request); + this.registerRule('/admin/api-key/create', 'POST', 'admin:api-key:create:testnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/create', 'POST', 'admin:api-key:create:mainnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/update', 'POST', 'admin:api-key:update:testnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/update', 'POST', 'admin:api-key:update:mainnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:testnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:mainnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get:testnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get:mainnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list:testnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list:mainnet', { skipNamespace: true }); } } diff --git a/src/middleware/auth/routes/api/account-auth.ts b/src/middleware/auth/routes/api/account-auth.ts index 96cf1f35..74c7dd90 100644 --- a/src/middleware/auth/routes/api/account-auth.ts +++ b/src/middleware/auth/routes/api/account-auth.ts @@ -1,17 +1,9 @@ -import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../../base-auth-handler.js'; -import type { IAuthResponse } from '../../../../types/authentication.js'; +import { AuthRuleProvider } from '../../auth-rule-provider.js'; -export class AccountAuthHandler extends BaseAuthHandler { +export class AccountAuthProvider extends AuthRuleProvider { constructor() { super(); - this.registerRoute('/account', 'GET', 'read:account', { skipNamespace: true }); - this.registerRoute('/account', 'POST', 'create:account', { skipNamespace: true }); - } - public async handle(request: Request, response: Response): Promise { - if (!request.path.includes('/account')) { - return super.handle(request, response); - } - return this.guardAPI(request); + this.registerRule('/account', 'GET', 'read:account', { skipNamespace: true }); + this.registerRule('/account', 'POST', 'create:account', { skipNamespace: true }); } } diff --git a/src/middleware/auth/routes/api/auth-user-info.ts b/src/middleware/auth/routes/api/auth-user-info.ts index 88d7426c..e662c923 100644 --- a/src/middleware/auth/routes/api/auth-user-info.ts +++ b/src/middleware/auth/routes/api/auth-user-info.ts @@ -1,16 +1,8 @@ -import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../../base-auth-handler.js'; -import type { IAuthResponse } from '../../../../types/authentication.js'; +import { AuthRuleProvider } from '../../auth-rule-provider.js'; -export class AuthInfoHandler extends BaseAuthHandler { +export class AuthInfoProvider extends AuthRuleProvider { constructor() { super(); - this.registerRoute('/auth/user-info', 'GET', '', { skipNamespace: true, allowUnauthorized: true }); - } - public async handle(request: Request, response: Response): Promise { - if (!request.path.includes('/auth/user-info')) { - return super.handle(request, response); - } - return this.guardAPI(request); + this.registerRule('/auth/user-info', 'GET', '', { skipNamespace: true, allowUnauthorized: true }); } } diff --git a/src/middleware/auth/routes/api/credential-auth.ts b/src/middleware/auth/routes/api/credential-auth.ts index b2cb70f9..a5a74b9c 100644 --- a/src/middleware/auth/routes/api/credential-auth.ts +++ b/src/middleware/auth/routes/api/credential-auth.ts @@ -1,26 +1,17 @@ -import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../../base-auth-handler.js'; -import type { IAuthResponse } from '../../../../types/authentication.js'; +import { AuthRuleProvider } from "../../auth-rule-provider.js"; -export class CredentialAuthHandler extends BaseAuthHandler { +export class CredentialAuthRuleProvider extends AuthRuleProvider { constructor() { super(); - this.registerRoute('/credential/issue', 'POST', 'issue:credential:testnet'); - this.registerRoute('/credential/issue', 'POST', 'issue:credential:mainnet'); - this.registerRoute('/credential/revoke', 'POST', 'revoke:credential:testnet'); - this.registerRoute('/credential/revoke', 'POST', 'revoke:credential:mainnet'); - this.registerRoute('/credential/suspend', 'POST', 'suspend:credential:testnet'); - this.registerRoute('/credential/suspend', 'POST', 'suspend:credential:mainnet'); - this.registerRoute('/credential/reinstate', 'POST', 'reinstate:credential:testnet'); - this.registerRoute('/credential/reinstate', 'POST', 'reinstate:credential:mainnet'); + this.registerRule('/credential/issue', 'POST', 'issue:credential:testnet'); + this.registerRule('/credential/issue', 'POST', 'issue:credential:mainnet'); + this.registerRule('/credential/revoke', 'POST', 'revoke:credential:testnet'); + this.registerRule('/credential/revoke', 'POST', 'revoke:credential:mainnet'); + this.registerRule('/credential/suspend', 'POST', 'suspend:credential:testnet'); + this.registerRule('/credential/suspend', 'POST', 'suspend:credential:mainnet'); + this.registerRule('/credential/reinstate', 'POST', 'reinstate:credential:testnet'); + this.registerRule('/credential/reinstate', 'POST', 'reinstate:credential:mainnet'); // Unauthorized routes - this.registerRoute('/credential/verify', 'POST', '', { allowUnauthorized: true, skipNamespace: true }); - } - - public async handle(request: Request, response: Response): Promise { - if (!request.path.includes('/credential/')) { - return super.handle(request, response); - } - return this.guardAPI(request); + this.registerRule('/credential/verify', 'POST', '', { allowUnauthorized: true, skipNamespace: true }); } } diff --git a/src/middleware/auth/routes/api/credential-status-auth.ts b/src/middleware/auth/routes/api/credential-status-auth.ts index c05b31c3..2e03a3b0 100644 --- a/src/middleware/auth/routes/api/credential-status-auth.ts +++ b/src/middleware/auth/routes/api/credential-status-auth.ts @@ -1,44 +1,36 @@ -import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../../base-auth-handler.js'; -import type { IAuthResponse } from '../../../../types/authentication.js'; +import { AuthRuleProvider } from "../../auth-rule-provider.js"; -export class CredentialStatusAuthHandler extends BaseAuthHandler { +export class CredentialStatusAuthRuleProvider extends AuthRuleProvider { constructor() { super(); - this.registerRoute('/credential-status/create/encrypted', 'POST', 'create-encrypted:credential-status:testnet'); - this.registerRoute('/credential-status/create/encrypted', 'POST', 'create-encrypted:credential-status:mainnet'); - this.registerRoute( + this.registerRule('/credential-status/create/encrypted', 'POST', 'create-encrypted:credential-status:testnet'); + this.registerRule('/credential-status/create/encrypted', 'POST', 'create-encrypted:credential-status:mainnet'); + this.registerRule( '/credential-status/create/unencrypted', 'POST', 'create-unencrypted:credential-status:testnet' ); - this.registerRoute( + this.registerRule( '/credential-status/create/unencrypted', 'POST', 'create-unencrypted:credential-status:mainnet' ); - this.registerRoute('/credential-status/publish', 'POST', 'publish:credential-status:testnet'); - this.registerRoute('/credential-status/publish', 'POST', 'publish:credential-status:mainnet'); - this.registerRoute('/credential-status/update/encrypted', 'POST', 'update-encrypted:credential-status:testnet'); - this.registerRoute('/credential-status/update/encrypted', 'POST', 'update-encrypted:credential-status:mainnet'); - this.registerRoute( + this.registerRule('/credential-status/publish', 'POST', 'publish:credential-status:testnet'); + this.registerRule('/credential-status/publish', 'POST', 'publish:credential-status:mainnet'); + this.registerRule('/credential-status/update/encrypted', 'POST', 'update-encrypted:credential-status:testnet'); + this.registerRule('/credential-status/update/encrypted', 'POST', 'update-encrypted:credential-status:mainnet'); + this.registerRule( '/credential-status/update/unencrypted', 'POST', 'update-unencrypted:credential-status:testnet' ); - this.registerRoute( + this.registerRule( '/credential-status/update/unencrypted', 'POST', 'update-unencrypted:credential-status:mainnet' ); // Unauthorized routes - this.registerRoute('/credential-status/search', 'GET', '', { allowUnauthorized: true, skipNamespace: true }); - this.registerRoute('/credential-status/check', 'POST', 'check:credential-status', { skipNamespace: true }); - } - public async handle(request: Request, response: Response): Promise { - if (!request.path.includes('/credential-status/')) { - return super.handle(request, response); - } - return this.guardAPI(request); + this.registerRule('/credential-status/search', 'GET', '', { allowUnauthorized: true, skipNamespace: true }); + this.registerRule('/credential-status/check', 'POST', 'check:credential-status', { skipNamespace: true }); } } diff --git a/src/middleware/auth/routes/api/did-auth.ts b/src/middleware/auth/routes/api/did-auth.ts index e3d8df14..af24927a 100644 --- a/src/middleware/auth/routes/api/did-auth.ts +++ b/src/middleware/auth/routes/api/did-auth.ts @@ -1,28 +1,19 @@ -import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../../base-auth-handler.js'; -import type { IAuthResponse } from '../../../../types/authentication.js'; +import { AuthRuleProvider } from "../../auth-rule-provider.js"; -export class DidAuthHandler extends BaseAuthHandler { +export class DidAuthRuleProvider extends AuthRuleProvider { constructor() { super(); - this.registerRoute('/did/create', 'POST', 'create:did:testnet'); - this.registerRoute('/did/create', 'POST', 'create:did:mainnet'); - this.registerRoute('/did/list', 'GET', 'list:did:testnet', { skipNamespace: true }); - this.registerRoute('/did/list', 'GET', 'list:did:mainnet', { skipNamespace: true }); - this.registerRoute('/did/update', 'POST', 'update:did:testnet'); - this.registerRoute('/did/update', 'POST', 'update:did:mainnet'); - this.registerRoute('/did/deactivate', 'POST', 'deactivate:did:testnet'); - this.registerRoute('/did/deactivate', 'POST', 'deactivate:did:mainnet'); - this.registerRoute('/did/import', 'POST', 'import:did:testnet'); - this.registerRoute('/did/import', 'POST', 'import:did:mainnet'); + this.registerRule('/did/create', 'POST', 'create:did:testnet'); + this.registerRule('/did/create', 'POST', 'create:did:mainnet'); + this.registerRule('/did/list', 'GET', 'list:did:testnet', { skipNamespace: true }); + this.registerRule('/did/list', 'GET', 'list:did:mainnet', { skipNamespace: true }); + this.registerRule('/did/update', 'POST', 'update:did:testnet'); + this.registerRule('/did/update', 'POST', 'update:did:mainnet'); + this.registerRule('/did/deactivate', 'POST', 'deactivate:did:testnet'); + this.registerRule('/did/deactivate', 'POST', 'deactivate:did:mainnet'); + this.registerRule('/did/import', 'POST', 'import:did:testnet'); + this.registerRule('/did/import', 'POST', 'import:did:mainnet'); // Unauthorized routes - this.registerRoute('/did/search/(.*)', 'GET', '', { allowUnauthorized: true, skipNamespace: true }); - } - - public async handle(request: Request, response: Response): Promise { - if (!request.path.includes('/did/')) { - return super.handle(request, response); - } - return this.guardAPI(request); + this.registerRule('/did/search/(.*)', 'GET', '', { allowUnauthorized: true, skipNamespace: true }); } } diff --git a/src/middleware/auth/routes/api/key-auth.ts b/src/middleware/auth/routes/api/key-auth.ts index 2c84cd3b..79a5acf1 100644 --- a/src/middleware/auth/routes/api/key-auth.ts +++ b/src/middleware/auth/routes/api/key-auth.ts @@ -1,20 +1,11 @@ -import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../../base-auth-handler.js'; -import type { IAuthResponse } from '../../../../types/authentication.js'; +import { AuthRuleProvider } from "../../auth-rule-provider.js"; -export class KeyAuthHandler extends BaseAuthHandler { +export class KeyAuthProvider extends AuthRuleProvider { constructor() { super(); - this.registerRoute('/key/create', 'POST', 'create:key', { skipNamespace: true }); - this.registerRoute('/key/import', 'POST', 'import:key', { skipNamespace: true }); - this.registerRoute('/key/read/(.*)', 'GET', 'read:key', { skipNamespace: true }); - this.registerRoute('/key/list', 'GET', 'list:key', { skipNamespace: true }); - } - - public async handle(request: Request, response: Response): Promise { - if (!request.path.includes('/key/')) { - return super.handle(request, response); - } - return this.guardAPI(request); + this.registerRule('/key/create', 'POST', 'create:key', { skipNamespace: true }); + this.registerRule('/key/import', 'POST', 'import:key', { skipNamespace: true }); + this.registerRule('/key/read/(.*)', 'GET', 'read:key', { skipNamespace: true }); + this.registerRule('/key/list', 'GET', 'list:key', { skipNamespace: true }); } } diff --git a/src/middleware/auth/routes/api/presentation-auth.ts b/src/middleware/auth/routes/api/presentation-auth.ts index f68dcab5..9ca96ea1 100644 --- a/src/middleware/auth/routes/api/presentation-auth.ts +++ b/src/middleware/auth/routes/api/presentation-auth.ts @@ -1,18 +1,10 @@ -import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../../base-auth-handler.js'; -import type { IAuthResponse } from '../../../../types/authentication.js'; +import { AuthRuleProvider } from "../../auth-rule-provider.js"; -export class PresentationAuthHandler extends BaseAuthHandler { +export class PresentationAuthRuleProvider extends AuthRuleProvider { constructor() { super(); // Unauthorized routes - this.registerRoute('/presentation/verify', 'POST', 'verify:presentation', { skipNamespace: true }); - this.registerRoute('/presentation/create', 'POST', 'create:presentation', { skipNamespace: true }); - } - public async handle(request: Request, response: Response): Promise { - if (!request.path.includes('/presentation/')) { - return super.handle(request, response); - } - return this.guardAPI(request); + this.registerRule('/presentation/verify', 'POST', 'verify:presentation', { skipNamespace: true }); + this.registerRule('/presentation/create', 'POST', 'create:presentation', { skipNamespace: true }); } } diff --git a/src/middleware/auth/routes/api/resource-auth.ts b/src/middleware/auth/routes/api/resource-auth.ts index a4ee1080..eaf23ae7 100644 --- a/src/middleware/auth/routes/api/resource-auth.ts +++ b/src/middleware/auth/routes/api/resource-auth.ts @@ -1,19 +1,11 @@ -import type { Request, Response } from 'express'; -import { BaseAuthHandler } from '../../base-auth-handler.js'; -import type { IAuthResponse } from '../../../../types/authentication.js'; +import { AuthRuleProvider } from "../../auth-rule-provider.js"; -export class ResourceAuthHandler extends BaseAuthHandler { +export class ResourceAuthRuleProvider extends AuthRuleProvider { constructor() { super(); - this.registerRoute('/resource/create', 'POST', 'create:resource:testnet'); - this.registerRoute('/resource/create', 'POST', 'create:resource:mainnet'); + this.registerRule('/resource/create', 'POST', 'create:resource:testnet'); + this.registerRule('/resource/create', 'POST', 'create:resource:mainnet'); // Unauthorized routes - this.registerRoute('/resource/search/(.*)', 'GET', '', { allowUnauthorized: true, skipNamespace: true }); - } - public async handle(request: Request, response: Response): Promise { - if (!request.path.includes('/resource/')) { - return super.handle(request, response); - } - return this.guardAPI(request); + this.registerRule('/resource/search/(.*)', 'GET', '', { allowUnauthorized: true, skipNamespace: true }); } } diff --git a/src/middleware/auth/routine.ts b/src/middleware/auth/routine.ts index 2a843ce9..6e3c6a71 100644 --- a/src/middleware/auth/routine.ts +++ b/src/middleware/auth/routine.ts @@ -1,241 +1,9 @@ -import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import { StatusCodes } from 'http-status-codes'; -import stringify from 'json-stringify-safe'; -import { DefaultNetworkPattern } from '../../types/shared.js'; -import { MethodToScopeRule, Namespaces, IAuthResponse, ICommonErrorResponse } from '../../types/authentication.js'; -import { InvalidTokenError } from 'jwt-decode'; -import { jwtDecode } from 'jwt-decode'; -import type { IOAuthProvider } from './oauth/abstract.js'; -import type { IUserInfoFetcher } from './user-info-fetcher/base.js'; +import type { ICommonErrorResponse } from '../../types/authentication.js'; dotenv.config(); -export interface IAPIGuard { - guardAPI(request: Request, oauthProvider: IOAuthProvider): Promise; -} - -export interface IAuthParams { - reset(): void; - // Getters - getUserId(): string; - getCustomerId(): string; - getScopes(): string[]; - getNamespace(): Namespaces; - getIsAllowedUnauthorized(): boolean; - getRule(): MethodToScopeRule; - getRouteToScopeList(): MethodToScopeRule[]; - // Setters - setUserId(userId: string): void; - setScopes(scopes: string[]): void; - setNamespace(namespace: Namespaces): void; - setIsAllowedUnauthorized(isAllowedUnauthorized: boolean): void; - setRule(MethodToScopeRule: MethodToScopeRule): void; - pushToRuleList(MethodToScopeRule: MethodToScopeRule): void; -} - -export class AuthParams implements IAuthParams { - userId: string; - customerId: string; - scopes: string[]; - namespace: Namespaces; - isAllowedUnauthorized: boolean; - rule: MethodToScopeRule; - routeToScoupeList: MethodToScopeRule[] = []; - - constructor() { - this.namespace = '' as Namespaces; - this.scopes = []; - this.userId = '' as string; - this.customerId = '' as string; - this.isAllowedUnauthorized = false; - this.rule = {} as MethodToScopeRule; - this.routeToScoupeList = []; - } - - // Getters - public getUserId(): string { - return this.userId; - } - public getCustomerId(): string { - return this.customerId; - } - public getScopes(): string[] { - return this.scopes; - } - public getNamespace(): Namespaces { - return this.namespace; - } - public getIsAllowedUnauthorized(): boolean { - return this.isAllowedUnauthorized; - } - public getRule(): MethodToScopeRule { - return this.rule; - } - public getRouteToScopeList(): MethodToScopeRule[] { - return this.routeToScoupeList; - } - - //Setters - public setUserId(userId: string): void { - this.userId = userId; - } - public setCustomerId(customerId: string): void { - this.customerId = customerId; - } - public setScopes(scopes: string[]): void { - this.scopes = scopes; - } - public setNamespace(namespace: Namespaces): void { - this.namespace = namespace; - } - public setIsAllowedUnauthorized(isAllowedUnauthorized: boolean): void { - this.isAllowedUnauthorized = isAllowedUnauthorized; - } - public setRule(methodToScopeRule: MethodToScopeRule): void { - this.rule = methodToScopeRule; - } - public pushToRuleList(methodToScopeRule: MethodToScopeRule): void { - this.routeToScoupeList.push(methodToScopeRule); - } - - // Reset - reset() { - this.namespace = '' as Namespaces; - this.scopes = []; - this.userId = '' as string; - this.isAllowedUnauthorized = false; - this.rule = {} as MethodToScopeRule; - } -} - // Simple interface for building the response/result export interface IReturn { returnOk(): ICommonErrorResponse; returnError(status: number, error: string): ICommonErrorResponse; } - -export class AuthReturn extends AuthParams implements IReturn { - returnOk(): IAuthResponse { - return { - status: StatusCodes.OK, - error: '', - data: { - userId: this.getUserId(), - customerId: this.getCustomerId(), - scopes: this.getScopes() as string[], - namespace: this.getNamespace(), - isAllowedUnauthorized: this.getIsAllowedUnauthorized(), - }, - }; - } - - returnError(status: number, error: string): IAuthResponse { - return { - status: status, - error: error, - data: { - userId: '', - customerId: '', - scopes: [], - namespace: this.getNamespace(), - isAllowedUnauthorized: this.getIsAllowedUnauthorized(), - }, - }; - } -} - -export interface IAuthHandler extends IAPIGuard, IReturn, IAuthParams { - oauthProvider: IOAuthProvider; - setOAuthProvider(oauthProvider: IOAuthProvider): void; - setUserInfoStrategy(strategy: IUserInfoFetcher): void; - setNext(handler: IAuthHandler): IAuthHandler; - handle(request: Request, response: Response): Promise; -} - -export class RuleRoutine extends AuthReturn implements IReturn { - protected getNamespaceFromRequest(req: Request): Namespaces | null { - let network: string | null = ''; - - if (req && req.body && req.body.credential) { - const { credential } = req.body; - let decoded = ''; - let issuerDid = ''; - // Try to get issuer DID - if (credential && credential.issuer) { - issuerDid = credential.issuer.id; - } - network = this.findNetworkInBody(issuerDid); - if (network) { - return this.switchNetwork(network); - } - try { - decoded = jwtDecode(req.body.credential); - } catch (e) { - // If it's not a JWT - just skip it - if (!(e instanceof InvalidTokenError)) { - throw e; - } - } - // if not - try to search for decoded credential - network = this.findNetworkInBody(stringify(decoded)); - if (network) { - return this.switchNetwork(network); - } - } - // Try to search in request body - if (req && req.body) { - network = this.findNetworkInBody(stringify(req.body)); - if (network) { - return this.switchNetwork(network); - } - } - // Try to search in request path - if (req && req.path) { - network = this.findNetworkInBody(decodeURIComponent(req.path)); - if (network) { - return this.switchNetwork(network); - } - } - // For DID create we specify it as a separate parameter in body - if (req.body && req.body.network) { - return this.switchNetwork(req.body.network); - } - - return null; - } - - protected findNetworkInBody(body: string): string | null { - const matches = body.match(DefaultNetworkPattern); - if (matches && matches.length > 0) { - return matches[1]; - } - return null; - } - - protected switchNetwork(network: string): Namespaces | null { - switch (network) { - case 'testnet': { - return Namespaces.Testnet; - } - case 'mainnet': { - return Namespaces.Mainnet; - } - default: { - return null; - } - } - } - - protected findRule(route: string, method: string, namespace = Namespaces.Testnet): MethodToScopeRule | undefined { - for (const rule of this.getRouteToScopeList()) { - if (rule.doesRuleMatches(route, method, namespace)) { - return rule; - } - } - return undefined; - } - - protected registerRoute(route: string, method: string, scope: string, options = {}): void { - this.pushToRuleList(new MethodToScopeRule(route, method, scope, options)); - } -} diff --git a/src/middleware/auth/user-info-fetcher/api-token.ts b/src/middleware/auth/user-info-fetcher/api-token.ts index 316caa9b..c366abe9 100644 --- a/src/middleware/auth/user-info-fetcher/api-token.ts +++ b/src/middleware/auth/user-info-fetcher/api-token.ts @@ -1,51 +1,61 @@ -import type { Request } from 'express'; -import { AuthReturn } from '../routine.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import type { IUserInfoFetcher } from './base.js'; +import { UserInfoHelper, type IUserInfoFetcher } from './base.js'; import type { IOAuthProvider } from '../oauth/abstract.js'; import * as dotenv from 'dotenv'; import { APIKeyService } from '../../../services/admin/api-key.js'; import { UserService } from '../../../services/api/user.js'; +import type { UnsuccessfulResponseBody } from '../../../types/shared.js'; dotenv.config(); -export class APITokenUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { +export class APITokenUserInfoFetcher extends UserInfoHelper implements IUserInfoFetcher { token: string; + private oauthProvider: IOAuthProvider; - constructor(token: string) { + constructor(token: string, oauthProvider: IOAuthProvider) { super(); this.token = token; + this.oauthProvider = oauthProvider; } - async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { - return this.verifyToken(this.token as string, oauthProvider); + async fetch(request: Request, response: Response): Promise { + return this.verifyToken(this.token as string, response); } - public async verifyToken(token: string, oauthProvider: IOAuthProvider): Promise { + public async verifyToken(token: string, response: Response): Promise { try { const apiEntity = await APIKeyService.instance.get(token); if (!apiEntity) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: API Key not found.`); + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: API Key not found.` + } satisfies UnsuccessfulResponseBody); } if (apiEntity.revoked) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: API Key is revoked.`); + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: API Key is revoked.` + } satisfies UnsuccessfulResponseBody); } const userEntity = await UserService.instance.findOne({ customer: apiEntity.customer }); if (!userEntity) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: User not found.`); + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: User not found.` + } satisfies UnsuccessfulResponseBody); } - const _resp = await oauthProvider.getUserScopes(userEntity.logToId as string); + const _resp = await this.oauthProvider.getUserScopes(userEntity.logToId as string); if (_resp.status !== 200) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the user.`); + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: No scopes found for the user: ${userEntity.logToId}` + } satisfies UnsuccessfulResponseBody); } - if (_resp.data) { - this.setScopes(_resp.data); - } - this.setCustomerId(apiEntity.customer.customerId); - return this.returnOk(); + // Set global context + this.setScopes(_resp.data, response); + return await this.setCustomerEntity(apiEntity.customer.customerId, response); + } catch (error) { - return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Unexpected error: While verifying API key: ${error}` + } satisfies UnsuccessfulResponseBody); } } } diff --git a/src/middleware/auth/user-info-fetcher/base.ts b/src/middleware/auth/user-info-fetcher/base.ts index ee2875c3..e34456bd 100644 --- a/src/middleware/auth/user-info-fetcher/base.ts +++ b/src/middleware/auth/user-info-fetcher/base.ts @@ -1,11 +1,50 @@ -import type { Request } from 'express'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import type { Request, Response } from 'express'; import type { IOAuthProvider } from '../oauth/abstract.js'; +import { CustomerService } from '../../../services/api/customer.js'; +import { StatusCodes } from 'http-status-codes'; +import { UserService } from '../../../services/api/user.js'; export interface IUserInfoOptions { [key: string]: any; } export interface IUserInfoFetcher { - fetchUserInfo(request: Request, oauthProvider: IOAuthProvider, options?: IUserInfoOptions): Promise; + fetch(request: Request, response: Response, oauthProvider: IOAuthProvider, options?: IUserInfoOptions): Promise; } + +export class UserInfoHelper { + + setScopes(scopes: string[], response: Response) { + response.locals.scopes = scopes; + return; + } + async setCustomerEntity(customerId: string, response: Response): Promise { + const customerEntity = await CustomerService.instance.get(customerId); + if (!customerEntity) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Unexpected error: Customer entity for handling such request is not found in internal storage. CustomerId: ${customerId}`, + }) + } + const userEntity = await UserService.instance.findOne({ customer: customerEntity }); + if (!userEntity) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Unexpected error: User entity for handling such request is not found in internal storage. CustomerId: ${customerId}`, + }) + } + response.locals.customer = customerEntity; + response.locals.user = userEntity; + return; + } + + async setUserEntity(logToId: string, response: Response): Promise { + const entity = await UserService.instance.get(logToId); + if (!entity) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Unexpected error: User entity for handling such request is not found in internal storage`, + }) + } + response.locals.user = entity; + response.locals.customer = entity.customer; + return; + } +} \ No newline at end of file diff --git a/src/middleware/auth/user-info-fetcher/idtoken.ts b/src/middleware/auth/user-info-fetcher/idtoken.ts index c26e3fd4..cd4c7552 100644 --- a/src/middleware/auth/user-info-fetcher/idtoken.ts +++ b/src/middleware/auth/user-info-fetcher/idtoken.ts @@ -1,51 +1,58 @@ -import type { Request } from 'express'; -import { AuthReturn } from '../routine.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import type { IUserInfoFetcher } from './base.js'; +import { UserInfoHelper, type IUserInfoFetcher } from './base.js'; import type { IOAuthProvider } from '../oauth/abstract.js'; import { createRemoteJWKSet, jwtVerify } from 'jose'; import * as dotenv from 'dotenv'; +import type { UnsuccessfulResponseBody } from '../../../types/shared.js'; dotenv.config(); -export class IdTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { +export class IdTokenUserInfoFetcher extends UserInfoHelper implements IUserInfoFetcher { token: string; + private oauthProvider: IOAuthProvider; - constructor(token: string) { - super(); + constructor(token: string, oauthProvider: IOAuthProvider) { + super() this.token = token; + this.oauthProvider = oauthProvider; } - async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { - return this.verifyJWTToken(this.token as string, oauthProvider); + async fetch(request: Request, response: Response) { + return this.verifyJWTToken(request, response); } - public async verifyJWTToken(token: string, oauthProvider: IOAuthProvider): Promise { + public async verifyJWTToken(request: Request, response: Response) { try { const { payload } = await jwtVerify( - token, // The raw Bearer Token extracted from the request header - createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + this.token, // The raw Bearer Token extracted from the request header + createRemoteJWKSet(new URL(this.oauthProvider.endpoint_jwks)), { // expected issuer of the token, should be issued by the Logto server - issuer: oauthProvider.endpoint_issuer, + issuer: this.oauthProvider.endpoint_issuer, // expected audience token, should be the resource indicator of the current API audience: process.env.LOGTO_APP_ID, } ); // Setup the scopes from the token if (!payload.roles) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No roles found in the token.`); + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: No roles found in the token.` + } satisfies UnsuccessfulResponseBody); } - const scopes = await oauthProvider.getScopesForRoles(payload.roles as string[]); + const scopes = await this.oauthProvider.getScopesForRoles(payload.roles as string[]); if (!scopes) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No scopes found for the roles.`); + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: No scopes found for the roles: ${payload.roles}` + } satisfies UnsuccessfulResponseBody); } - this.setScopes(scopes); - this.setUserId(payload.sub as string); - return this.returnOk(); + // Set global context + this.setScopes(scopes, response); + return await this.setUserEntity(payload.sub as string, response); } catch (error) { - return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Unexpected error: While verifying ID token: ${error}` + } satisfies UnsuccessfulResponseBody); } } } diff --git a/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts b/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts index 8aa9d4e2..672bc973 100644 --- a/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts +++ b/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts @@ -1,50 +1,53 @@ -import type { Request } from 'express'; -import { AuthReturn } from '../routine.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import type { IUserInfoFetcher } from './base.js'; +import { UserInfoHelper, type IUserInfoFetcher } from './base.js'; import type { IOAuthProvider } from '../oauth/abstract.js'; import { createRemoteJWKSet, jwtVerify } from 'jose'; - import * as dotenv from 'dotenv'; +import type { UnsuccessfulResponseBody } from '../../../types/shared.js'; dotenv.config(); -export class M2MCredsTokenUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { +export class M2MCredsTokenUserInfoFetcher extends UserInfoHelper implements IUserInfoFetcher { token: string; + private oauthProvider: IOAuthProvider; - constructor(token: string) { + constructor(token: string, oauthProvider: IOAuthProvider) { super(); this.token = token; + this.oauthProvider = oauthProvider; } - async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { - // Get customerId from header - const customerId = request.headers['customer-id']; - if (typeof customerId === 'string') this.setCustomerId(customerId); + async fetch(request: Request, response: Response) { // Verify M2M token - return this.verifyJWTToken(this.token as string, oauthProvider); + return this.verifyJWTToken(request, response); } - public async verifyJWTToken(token: string, oauthProvider: IOAuthProvider): Promise { + public async verifyJWTToken(request: Request, response: Response) { + // Get customerId from header + const customerId = request.headers['customer-id']; try { const { payload } = await jwtVerify( - token, // The raw Bearer Token extracted from the request header - createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + this.token, // The raw Bearer Token extracted from the request header + createRemoteJWKSet(new URL(this.oauthProvider.endpoint_jwks)), { // expected issuer of the token, should be issued by the Logto server - issuer: oauthProvider.endpoint_issuer, + issuer: this.oauthProvider.endpoint_issuer, } ); // Setup the scopes from the token if (!payload.sub) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No sub found in the token.`); + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: No sub found in the token.` + } satisfies UnsuccessfulResponseBody); } + // Set global context const scopes = payload.scope ? (payload.scope as string).split(' ') : []; - this.setScopes(scopes); - return this.returnOk(); + this.setScopes(scopes, response); + return await this.setCustomerEntity(customerId as string, response); } catch (error) { - console.error(error); - return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Unexpected error: While verifying M2M token: ${error}` + } satisfies UnsuccessfulResponseBody); } } } diff --git a/src/middleware/auth/user-info-fetcher/portal-token.ts b/src/middleware/auth/user-info-fetcher/portal-token.ts index dca00198..c56ceb33 100644 --- a/src/middleware/auth/user-info-fetcher/portal-token.ts +++ b/src/middleware/auth/user-info-fetcher/portal-token.ts @@ -1,84 +1,83 @@ -import type { Request } from 'express'; -import { AuthReturn } from '../routine.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; +import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import type { IUserInfoFetcher } from './base.js'; +import { UserInfoHelper, type IUserInfoFetcher } from './base.js'; import type { IOAuthProvider } from '../oauth/abstract.js'; import { createRemoteJWKSet, jwtVerify } from 'jose'; import * as dotenv from 'dotenv'; +import type { UnsuccessfulResponseBody } from '../../../types/shared.js'; dotenv.config(); -export class PortalUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { +export class PortalUserInfoFetcher extends UserInfoHelper implements IUserInfoFetcher { private m2mToken: string; private idToken; + private oauthProvider: IOAuthProvider; - constructor(m2mToken: string, idToken: string) { + constructor(m2mToken: string, idToken: string, oauthProvider: IOAuthProvider) { super(); this.m2mToken = m2mToken; this.idToken = idToken; + this.oauthProvider = oauthProvider; } - async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { - // Get customerId from header - if (!this.idToken) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No idToken found in the header.`); - } - + async fetch(request: Request, response: Response) { // Check the idToken, provided in header - const idTokenVerification = await this.verifyIdToken(oauthProvider); - if (idTokenVerification.error) { - return idTokenVerification; + const errorResponse = await this.verifyIdToken(request, response); + if (errorResponse) { + return errorResponse; } - return this.verifyM2MToken(oauthProvider); + return this.verifyM2MToken(request, response); } - public async verifyIdToken(oauthProvider: IOAuthProvider): Promise { + public async verifyIdToken(request: Request, response: Response) { try { const { payload } = await jwtVerify( this.idToken, // The raw Bearer Token extracted from the request header - createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + createRemoteJWKSet(new URL(this.oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server { // expected issuer of the token, should be issued by the Logto server - issuer: oauthProvider.endpoint_issuer, + issuer: this.oauthProvider.endpoint_issuer, } ); // Setup the scopes from the token if (!payload.sub) { - return this.returnError( - StatusCodes.UNAUTHORIZED, - `Unauthorized error: No sub found in the token. Cannot set customerId.` - ); + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: No sub found in the token. Cannot set customerId.` + } satisfies UnsuccessfulResponseBody); } - this.setUserId(payload.sub); - return this.returnOk(); + return await this.setUserEntity(payload.sub, response); } catch (error) { console.error(error); - return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error:`Unexpected error: While verifying ID token for Portal: ${error}` + } satisfies UnsuccessfulResponseBody); } } - public async verifyM2MToken(oauthProvider: IOAuthProvider): Promise { + public async verifyM2MToken(request: Request, response: Response) { try { const { payload } = await jwtVerify( this.m2mToken, // The raw Bearer Token extracted from the request header - createRemoteJWKSet(new URL(oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server + createRemoteJWKSet(new URL(this.oauthProvider.endpoint_jwks)), // generate a jwks using jwks_uri inquired from Logto server { // expected issuer of the token, should be issued by the Logto server - issuer: oauthProvider.endpoint_issuer, + issuer: this.oauthProvider.endpoint_issuer, } ); // Setup the scopes from the token if (!payload.sub) { - return this.returnError(StatusCodes.UNAUTHORIZED, `Unauthorized error: No sub found in the token.`); + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: No sub found in the token.` + } satisfies UnsuccessfulResponseBody); } const scopes = payload.scope ? (payload.scope as string).split(' ') : []; - this.setScopes(scopes); - return this.returnOk(); + this.setScopes(scopes, response); + return; } catch (error) { - console.error(error); - return this.returnError(StatusCodes.INTERNAL_SERVER_ERROR, `Unexpected error: ${error}`); + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Unexpected error: While verifying M2M token for Portal: ${error}` + } satisfies UnsuccessfulResponseBody); } } } diff --git a/src/middleware/auth/user-info-fetcher/swagger-ui.ts b/src/middleware/auth/user-info-fetcher/swagger-ui.ts index 02950239..719bfa17 100644 --- a/src/middleware/auth/user-info-fetcher/swagger-ui.ts +++ b/src/middleware/auth/user-info-fetcher/swagger-ui.ts @@ -1,36 +1,46 @@ -import type { Request } from 'express'; -import { AuthReturn } from '../routine.js'; +import type { Response, Request } from 'express'; import type { IOAuthProvider } from '../oauth/abstract.js'; -import type { IAuthResponse } from '../../../types/authentication.js'; import { StatusCodes } from 'http-status-codes'; -import type { IUserInfoFetcher } from './base.js'; +import { UserInfoHelper, type IUserInfoFetcher } from './base.js'; +import type { UnsuccessfulResponseBody } from '../../../types/shared.js'; -export class SwaggerUserInfoFetcher extends AuthReturn implements IUserInfoFetcher { - async fetchUserInfo(request: Request, oauthProvider: IOAuthProvider): Promise { - // If the user is not authenticated - return error - if (!request.user.isAuthenticated) { - return this.returnError( - StatusCodes.UNAUTHORIZED, - "Unauthorized error: Seems like you are not authenticated. Please follow the authentication process using 'LogIn' button" - ); - } - // Tries to get customerId from the logTo user structure - if (request.user && request.user.claims) { - this.setUserId(request.user.claims.sub); - } else { - return this.returnError( - StatusCodes.BAD_GATEWAY, - 'Internal error: Seems like authentication process was corrupted and there are problems with getting customerId' - ); - } - // Tries to get scopes for current user and check that required scopes are present - const _resp = await oauthProvider.getUserScopes(this.getUserId()); - if (_resp.status !== 200) { - return _resp; - } - if (_resp.data) { - this.setScopes(_resp.data); +export class SwaggerUserInfoFetcher extends UserInfoHelper implements IUserInfoFetcher { + private oauthProvider: IOAuthProvider; + + constructor(oauthProvider: IOAuthProvider) { + super(); + this.oauthProvider = oauthProvider; + } + + async fetch(request: Request, response: Response) { + try { + // If the user is not authenticated - return error + if (!request.user.isAuthenticated) { + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: "Unauthorized error: Seems like you are not authenticated. Please follow the authentication process using 'LogIn' button" + } satisfies UnsuccessfulResponseBody); + } + // Tries to get customerId from the logTo user structure + if (!request.user || !request.user.claims || !request.user.claims.sub) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Internal error: Seems like authentication process was corrupted and there are problems with getting customerId' + } satisfies UnsuccessfulResponseBody); + } + const userId = request.user.claims.sub; + // Tries to get scopes for current user and check that required scopes are present + const _resp = await this.oauthProvider.getUserScopes(userId); + if (_resp.status !== 200) { + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: `Unauthorized error: No scopes found for the user: ${userId}.` + } satisfies UnsuccessfulResponseBody); + } + this.setScopes(_resp.data, response); + return await this.setUserEntity(userId, response); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Unexpected error: While verifying API key: ${error}` + } satisfies UnsuccessfulResponseBody); } - return this.returnOk(); + } } diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 4bb8cdbb..5cc118ff 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -2,35 +2,53 @@ import { Request, Response, NextFunction, response } from 'express'; import { StatusCodes } from 'http-status-codes'; import * as dotenv from 'dotenv'; -import { AccountAuthHandler } from './auth/routes/api/account-auth.js'; -import { CredentialAuthHandler } from './auth/routes/api/credential-auth.js'; -import { DidAuthHandler } from './auth/routes/api/did-auth.js'; -import { KeyAuthHandler } from './auth/routes/api/key-auth.js'; -import { CredentialStatusAuthHandler } from './auth/routes/api/credential-status-auth.js'; -import { ResourceAuthHandler } from './auth/routes/api/resource-auth.js'; -import type { BaseAuthHandler } from './auth/base-auth-handler.js'; +import { AccountAuthProvider } from './auth/routes/api/account-auth.js'; +import { KeyAuthProvider } from './auth/routes/api/key-auth.js'; import { LogToHelper } from './auth/logto-helper.js'; -import { PresentationAuthHandler } from './auth/routes/api/presentation-auth.js'; -import { UserService } from '../services/api/user.js'; import { configLogToExpress } from '../types/constants.js'; import { handleAuthRoutes, withLogto } from '@logto/express'; import { LogToProvider } from './auth/oauth/logto-provider.js'; -import { AuthInfoHandler } from './auth/routes/api/auth-user-info.js'; -import { CustomerService } from '../services/api/customer.js'; -import { AdminHandler } from './auth/routes/admin/admin-auth.js'; +import { AdminAuthRuleProvider } from './auth/routes/admin/admin-auth.js'; +import { APIGuard } from './auth/auth-gaurd.js'; +import { AuthRuleRepository } from './auth/auth-rule-provider.js'; +import type { IOAuthProvider } from './auth/oauth/abstract.js'; +import { DidAuthRuleProvider } from './auth/routes/api/did-auth.js'; +import { PresentationAuthRuleProvider } from './auth/routes/api/presentation-auth.js'; +import { ResourceAuthRuleProvider } from './auth/routes/api/resource-auth.js'; +import { CredentialAuthRuleProvider } from './auth/routes/api/credential-auth.js'; +import { CredentialStatusAuthRuleProvider } from './auth/routes/api/credential-status-auth.js'; +import { AuthInfoProvider } from './auth/routes/api/auth-user-info.js'; +import type { UnsuccessfulResponseBody } from '../types/shared.js'; dotenv.config(); const { ENABLE_EXTERNAL_DB } = process.env; export class Authentication { - private initHandler: BaseAuthHandler; + private apiGuardian: APIGuard; private isSetup = false; private logToHelper: LogToHelper; + private oauthProvider: IOAuthProvider; constructor() { + this.oauthProvider = new LogToProvider(); + const authRuleRepository = new AuthRuleRepository(); + + authRuleRepository.push(new AuthInfoProvider()); + + authRuleRepository.push(new AccountAuthProvider()); + authRuleRepository.push(new KeyAuthProvider()); + + authRuleRepository.push(new DidAuthRuleProvider()); + authRuleRepository.push(new ResourceAuthRuleProvider()) + authRuleRepository.push(new CredentialAuthRuleProvider()); + authRuleRepository.push(new CredentialStatusAuthRuleProvider()); + authRuleRepository.push(new PresentationAuthRuleProvider()); + + authRuleRepository.push(new AdminAuthRuleProvider()); + + this.apiGuardian = new APIGuard(authRuleRepository, this.oauthProvider); // Initial auth handler - this.initHandler = new AccountAuthHandler(); this.logToHelper = new LogToHelper(); } @@ -42,26 +60,6 @@ export class Authentication { error: _r.error, }); } - const fillChain = async (handler: BaseAuthHandler[]) => { - const oauthProvider = new LogToProvider(); - oauthProvider.setHelper(this.logToHelper); - for (let i = 0; i < handler.length - 1; i++) { - handler[i].setOAuthProvider(oauthProvider); - handler[i].setNext(handler[i + 1]); - } - }; - - fillChain([ - this.initHandler, - new DidAuthHandler(), - new KeyAuthHandler(), - new CredentialAuthHandler(), - new CredentialStatusAuthHandler(), - new ResourceAuthHandler(), - new PresentationAuthHandler(), - new AuthInfoHandler(), - new AdminHandler(), - ]); this.isSetup = true; } @@ -78,7 +76,7 @@ export class Authentication { } public async accessControl(request: Request, response: Response, next: NextFunction) { - if (this.initHandler.skipPath(request.path)) return next(); + if (this.apiGuardian.skipPath(request.path)) return next(); // ToDo: Make it more readable if (ENABLE_EXTERNAL_DB === 'false') { @@ -101,7 +99,7 @@ export class Authentication { } public async withLogtoWrapper(request: Request, response: Response, next: NextFunction) { - if (this.initHandler.skipPath(request.path)) return next(); + if (this.apiGuardian.skipPath(request.path)) return next(); try { return withLogto({ ...configLogToExpress, scopes: ['roles'] })(request, response, next); } catch (err) { @@ -114,65 +112,19 @@ export class Authentication { } // ToDo: refactor it or keep for the moment of setting up the admin panel - private isBootstrapping(request: Request) { - return ['/account/create'].includes(request.path); - } + // private isBootstrapping(request: Request) { + // return ['/account/create'].includes(request.path); + // } public async guard(request: Request, response: Response, next: NextFunction) { - const { provider } = request.body as { claim: string; provider: string }; - if (this.initHandler.skipPath(request.path)) return next(); + if (this.apiGuardian.skipPath(request.path)) return next(); try { - // If response got back that means error was raised - const _resp = await this.initHandler.handle(request, response); - if (_resp.status !== StatusCodes.OK) { - return response.status(_resp.status).json({ - error: _resp.error, - }); - } - // Only for rules when it's not allowed for unauthorized users - // we need to find customer or user and assign them to the response.locals - if (!_resp.data.isAllowedUnauthorized) { - let customer; - let user; - if (_resp.data.userId !== '') { - user = await UserService.instance.get(_resp.data.userId); - if (!user) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: `Looks like user with logToId ${_resp.data.userId} is not found`, - }); - } - if (user && !user.customer) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: `Looks like user with logToId ${_resp.data.userId} is not assigned to any CredentialService customer`, - }); - } - customer = user.customer; - } - if (_resp.data.customerId !== '' && !customer) { - customer = await CustomerService.instance.get(_resp.data.customerId); - if (!customer) { - return response.status(StatusCodes.NOT_FOUND).json({ - error: `Looks like customer with id ${_resp.data.customerId} is not found`, - }); - } - } - if (!customer && !user && !this.isBootstrapping(request)) { - return response.status(StatusCodes.UNAUTHORIZED).json({ - error: `Looks like customer and user are not found in the system or they are not registered yet. Please contact administrator.`, - }); - } - response.locals.customer = customer; - response.locals.user = user; - } - next(); + return await this.apiGuardian.guard(request, response, next); } catch (err) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ - authenticated: false, - error: `${err}`, - customerId: null, - provider, - }); + error: `Unexpected error: While guarding API request ${err}` + } satisfies UnsuccessfulResponseBody); } } } diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 72021893..f8d03627 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -1,1011 +1,1049 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service admin API for cheqd network", - "version": "2.0.0", - "description": "Admin API which handles users subscriptions and payments", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Product" - }, - { - "name": "Price" - }, - { - "name": "Subscription" - }, - { - "name": "API Key" - } - ], - "paths": { - "/admin/api-key/create": { - "post": { - "summary": "Create a new API key", - "description": "Create a new API key", - "tags": ["API Key"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyCreateRequestBody" - } - } - } - }, - "responses": { - "201": { - "description": "A new API key has been created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyCreateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/APIKeyCreateUnsuccessfulResponseBody" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/update": { - "post": { - "summary": "Update an existing API key", - "description": "Update an existing API key", - "tags": ["API Key"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyUpdateRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The API key has been updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyUpdateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/APIKeyUpdateUnsuccessfulResponseBody" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/revoke": { - "delete": { - "summary": "Revoke an existing API key", - "description": "Revoke an existing API key", - "tags": ["API Key"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyRevokeRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The API key has been revoked", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyRevokeResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/APIKeyRevokeUnsuccessfulResponseBody" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/list": { - "get": { - "summary": "List all API keys", - "description": "List all API keys", - "tags": ["API Key"], - "responses": { - "200": { - "description": "A list of API keys", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/api-key/get": { - "get": { - "summary": "Get an API key", - "description": "Get an API key. If the API key is not provided, the latest not revoked API key it returns.", - "tags": ["API Key"], - "parameters": [ - { - "name": "apiKey", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The API key", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/APIKeyGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/price/list": { - "get": { - "summary": "Get a list of prices", - "description": "Get a list of prices", - "tags": ["Price"], - "parameters": [ - { - "in": "query", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id. If passed - returns filtered by this product list of prices.", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of prices", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PriceListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/list": { - "get": { - "summary": "Get a list of products", - "description": "Get a list of products which are on a Stripe side", - "tags": ["Product"], - "parameters": [ - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the list of products with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A list of products", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/product/get/{productId}": { - "get": { - "summary": "Get a product", - "description": "Get a product by id", - "tags": ["Product"], - "parameters": [ - { - "in": "path", - "name": "productId", - "schema": { - "type": "string", - "description": "The product id which identifies the product in Stripe" - }, - "required": true - }, - { - "in": "query", - "name": "prices", - "schema": { - "type": "boolean", - "description": "If setup to true - returns the product with prices inside. Default - true", - "required": false - } - } - ], - "responses": { - "200": { - "description": "A product", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/create": { - "post": { - "summary": "Create a subscription", - "description": "Creates a new subscription for an existing customer", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateRequestBody" - } - } - } - }, - "responses": { - "201": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCreateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/update": { - "post": { - "summary": "Update a subscription", - "description": "Updates an existing subscription", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/list": { - "get": { - "summary": "Get a list of subscriptions", - "description": "Get a list of subscriptions", - "tags": ["Subscription"], - "parameters": [ - { - "in": "query", - "name": "paymentProviderId", - "schema": { - "type": "string", - "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." - } - } - ], - "responses": { - "200": { - "description": "A list of subscriptions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionListResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/get": { - "get": { - "summary": "Get a subscription", - "description": "Get a subscription", - "tags": ["Subscription"], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionGetResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/cancel": { - "post": { - "summary": "Cancel a subscription", - "description": "Cancels an existing subscription", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionCancelResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/admin/subscription/resume": {}, - "post": { - "summary": "Resume a subscription", - "description": "Resumes an existing subscription", - "tags": ["Subscription"], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeRequestBody" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscriptionResumeResponseBody" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "$ref": "#/components/schemas/NotFoundError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "components": { - "schemas": { - "PriceListResponseBody": { - "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", - "type": "object", - "properties": { - "prices": { - "type": "array", - "items": { - "type": "object", - "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" - } - } - } - }, - "ProductListResponseBody": { - "type": "object", - "properties": { - "products": { - "type": "array", - "items": { - "type": "object", - "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" - } - } - } - }, - "ProductGetResponseBody": { - "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", - "type": "object" - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - }, - "SubscriptionCreateRequestBody": { - "description": "The request body for creating a subscription", - "type": "object", - "properties": { - "price": { - "type": "string", - "description": "The price id", - "example": "price_1234567890" - }, - "successURL": { - "type": "string", - "description": "The URL to redirect to after the customer sucessfully completes the checkout", - "example": "https://example.com/success" - }, - "cancelURL": { - "type": "string", - "description": "The URL to redirect to after the customer cancels the checkout", - "example": "https://example.com/cancel" - }, - "quantity": { - "type": "number", - "description": "The quantity of the product", - "example": 1 - }, - "trialPeriodDays": { - "type": "number", - "description": "The number of days the customer has to pay for the product", - "example": 7 - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionCreateResponseBody": { - "description": "The response body for creating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", - "properties": { - "sessionURL": { - "type": "string", - "description": "URL which user should follow to manage subscription" - } - } - } - } - }, - "SubscriptionUpdateRequestBody": { - "description": "The request body for updating a subscription", - "type": "object", - "properties": { - "returnURL": { - "type": "string", - "description": "URL which is used to redirect to the page with ability to update the subscription" - } - } - }, - "SubscriptionUpdateResponseBody": { - "description": "The response body for updating a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "Object with redirect url inside", - "properties": { - "sessionURL": { - "type": "string", - "description": "URL with session URL rediect to" - } - } - } - } - }, - "SubscriptionGetRequestBody": { - "description": "The request body for getting a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionGetResponseBody": { - "description": "The response body for getting a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" - } - } - }, - "SubscriptionListRequestBody": { - "description": "The request body for listing subscriptions", - "type": "object", - "properties": { - "customerId": { - "type": "string", - "description": "The Stripe customer id", - "example": "cus_1234567890" - } - } - }, - "SubscriptionListResponseBody": { - "description": "The response body for listing subscriptions", - "type": "object", - "properties": { - "subscriptions": { - "type": "array", - "items": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - } - }, - "SubscriptionCancelRequestBody": { - "description": "The request body for canceling a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - } - } - }, - "SubscriptionCancelResponseBody": { - "description": "The response body for canceling a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionResumeRequestBody": { - "description": "The request body for resuming a subscription", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "The subscription id", - "example": "sub_1234567890" - }, - "idempotencyKey": { - "type": "string", - "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", - "example": "abcdefghijklmnopqrstuvwxyz" - } - } - }, - "SubscriptionResumeResponseBody": { - "description": "The response body for resuming a subscription", - "type": "object", - "properties": { - "subscription": { - "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" - } - } - }, - "APIKeyResponse": { - "description": "The general view for API key in response", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "createdAt": { - "type": "string", - "description": "The creation date of the API key", - "example": "2000-10-31T01:23:45Z", - "format": "date-time" - }, - "name": { - "type": "string", - "description": "The name of the API key", - "example": "My API Key" - }, - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:23:45Z", - "format": "date-time" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": false - } - } - }, - "APIKeyCreateRequestBody": { - "description": "The request body for creating an API key", - "type": "object", - "properties": { - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:23:45Z", - "format": "date-time" - }, - "name": { - "type": "string", - "description": "The name of the API key", - "example": "My API Key" - } - }, - "required": ["name"] - }, - "APIKeyCreateResponseBody": { - "description": "The response body for creating an API key", - "type": "object", - "schema": { - "ref": "#/components/schemas/APIKeyResponse" - } - }, - "APIKeyCreateUnsuccessfulResponseBody": { - "description": "The response body for an unsuccessful API key creation", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "API key creation unsuccessful" - } - } - }, - "APIKeyUpdateRequestBody": { - "description": "The request body for updating an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "name": { - "type": "string", - "description": "The name of the API key", - "example": "My API Key" - }, - "expiresAt": { - "type": "string", - "description": "The expiration date of the API key", - "example": "2000-10-31T01:23:45Z", - "format": "date-time" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": false, - "default": false - } - }, - "required": ["apiKey"] - }, - "APIKeyUpdateResponseBody": { - "description": "The response body for an unsuccessful API key update", - "type": "object", - "schema": { - "ref": "#/components/schemas/APIKeyResponse" - } - }, - "APIKeyUpdateUnsuccessfulResponseBody": { - "description": "The response body for an unsuccessful API key update", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "API key update unsuccessful" - } - } - }, - "APIKeyRevokeRequestBody": { - "description": "The request body for revoking an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - } - }, - "required": ["apiKey"] - }, - "APIKeyRevokeResponseBody": { - "description": "The response body for revoking an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - }, - "revoked": { - "type": "boolean", - "description": "The status of the API key", - "example": true - } - }, - "required": ["apiKey"] - }, - "APIKeyRevokeUnsuccessfulResponseBody": { - "description": "The response body for an unsuccessful API key revocation", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "API key revocation unsuccessful" - } - } - }, - "APIKeyListResponseBody": { - "description": "The response body for listing API keys", - "type": "object", - "properties": { - "apiKeys": { - "type": "array", - "items": { - "type": "object", - "schema": { - "ref": "#/components/schemas/APIKeyResponse" - } - } - } - } - }, - "APIKeyGetRequestBody": { - "description": "The request body for getting an API key", - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "description": "The API key", - "example": "abcdefghijklmnopqrstuvwxyz" - } - }, - "required": ["apiKey"] - }, - "APIKeyGetResponseBody": { - "description": "The response body for getting an API key", - "type": "object", - "schema": { - "ref": "#/components/schemas/APIKeyResponse" - } - }, - "NotFoundError": { - "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Not Found Error" - } - } - } - } - } -} + "openapi": "3.0.0", + "info": { + "title": "Credential Service admin API for cheqd network", + "version": "2.0.0", + "description": "Admin API which handles users subscriptions and payments", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Product" + }, + { + "name": "Price" + }, + { + "name": "Subscription" + }, + { + "name": "API Key" + } + ], + "paths": { + "/admin/api-key/create": { + "post": { + "summary": "Create a new API key", + "description": "Create a new API key", + "tags": [ + "API Key" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "A new API key has been created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyCreateUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/update": { + "post": { + "summary": "Update an existing API key", + "description": "Update an existing API key", + "tags": [ + "API Key" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The API key has been updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyUpdateUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/revoke": { + "delete": { + "summary": "Revoke an existing API key", + "description": "Revoke an existing API key", + "tags": [ + "API Key" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyRevokeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The API key has been revoked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyRevokeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/APIKeyRevokeUnsuccessfulResponseBody" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/list": { + "get": { + "summary": "List all API keys", + "description": "List all API keys", + "tags": [ + "API Key" + ], + "responses": { + "200": { + "description": "A list of API keys", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/api-key/get": { + "get": { + "summary": "Get an API key", + "description": "Get an API key. If the API key is not provided, the latest not revoked API key it returns.", + "tags": [ + "API Key" + ], + "parameters": [ + { + "name": "apiKey", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/APIKeyGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/price/list": { + "get": { + "summary": "Get a list of prices", + "description": "Get a list of prices", + "tags": [ + "Price" + ], + "parameters": [ + { + "in": "query", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id. If passed - returns filtered by this product list of prices.", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of prices", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PriceListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/list": { + "get": { + "summary": "Get a list of products", + "description": "Get a list of products which are on a Stripe side", + "tags": [ + "Product" + ], + "parameters": [ + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the list of products with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A list of products", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/product/get/{productId}": { + "get": { + "summary": "Get a product", + "description": "Get a product by id", + "tags": [ + "Product" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "schema": { + "type": "string", + "description": "The product id which identifies the product in Stripe" + }, + "required": true + }, + { + "in": "query", + "name": "prices", + "schema": { + "type": "boolean", + "description": "If setup to true - returns the product with prices inside. Default - true", + "required": false + } + } + ], + "responses": { + "200": { + "description": "A product", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/create": { + "post": { + "summary": "Create a subscription", + "description": "Creates a new subscription for an existing customer", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateRequestBody" + } + } + } + }, + "responses": { + "201": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCreateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/update": { + "post": { + "summary": "Update a subscription", + "description": "Updates an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionUpdateResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/list": { + "get": { + "summary": "Get a list of subscriptions", + "description": "Get a list of subscriptions", + "tags": [ + "Subscription" + ], + "parameters": [ + { + "in": "query", + "name": "paymentProviderId", + "schema": { + "type": "string", + "description": "The customer id. If passed - returns filtered by this customer list of subscriptions." + } + } + ], + "responses": { + "200": { + "description": "A list of subscriptions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionListResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/get": { + "get": { + "summary": "Get a subscription", + "description": "Get a subscription", + "tags": [ + "Subscription" + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/cancel": { + "post": { + "summary": "Cancel a subscription", + "description": "Cancels an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionCancelResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/subscription/resume": {}, + "post": { + "summary": "Resume a subscription", + "description": "Resumes an existing subscription", + "tags": [ + "Subscription" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeRequestBody" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResumeResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "components": { + "schemas": { + "PriceListResponseBody": { + "description": "A list of active prcies from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/list)", + "type": "object", + "properties": { + "prices": { + "type": "array", + "items": { + "type": "object", + "description": "A price object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/prices/object)" + } + } + } + }, + "ProductListResponseBody": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "type": "object", + "description": "A product object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/object)" + } + } + } + }, + "ProductGetResponseBody": { + "description": "A product with or without prices inside. For more information see the [Stripe API documentation](https://docs.stripe.com/api/products/retrieve)", + "type": "object" + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + }, + "SubscriptionCreateRequestBody": { + "description": "The request body for creating a subscription", + "type": "object", + "properties": { + "price": { + "type": "string", + "description": "The price id", + "example": "price_1234567890" + }, + "successURL": { + "type": "string", + "description": "The URL to redirect to after the customer sucessfully completes the checkout", + "example": "https://example.com/success" + }, + "cancelURL": { + "type": "string", + "description": "The URL to redirect to after the customer cancels the checkout", + "example": "https://example.com/cancel" + }, + "quantity": { + "type": "number", + "description": "The quantity of the product", + "example": 1 + }, + "trialPeriodDays": { + "type": "number", + "description": "The number of days the customer has to pay for the product", + "example": 7 + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionCreateResponseBody": { + "description": "The response body for creating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "An object with link to checkout session. For more information see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)", + "properties": { + "sessionURL": { + "type": "string", + "description": "URL which user should follow to manage subscription" + } + } + } + } + }, + "SubscriptionUpdateRequestBody": { + "description": "The request body for updating a subscription", + "type": "object", + "properties": { + "returnURL": { + "type": "string", + "description": "URL which is used to redirect to the page with ability to update the subscription" + } + } + }, + "SubscriptionUpdateResponseBody": { + "description": "The response body for updating a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "Object with redirect url inside", + "properties": { + "sessionURL": { + "type": "string", + "description": "URL with session URL rediect to" + } + } + } + } + }, + "SubscriptionGetRequestBody": { + "description": "The request body for getting a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionGetResponseBody": { + "description": "The response body for getting a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "SubscriptionListRequestBody": { + "description": "The request body for listing subscriptions", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "description": "The Stripe customer id", + "example": "cus_1234567890" + } + } + }, + "SubscriptionListResponseBody": { + "description": "The response body for listing subscriptions", + "type": "object", + "properties": { + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + } + }, + "SubscriptionCancelRequestBody": { + "description": "The request body for canceling a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + } + } + }, + "SubscriptionCancelResponseBody": { + "description": "The response body for canceling a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeRequestBody": { + "description": "The request body for resuming a subscription", + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "The subscription id", + "example": "sub_1234567890" + }, + "idempotencyKey": { + "type": "string", + "description": "The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request.", + "example": "abcdefghijklmnopqrstuvwxyz" + } + } + }, + "SubscriptionResumeResponseBody": { + "description": "The response body for resuming a subscription", + "type": "object", + "properties": { + "subscription": { + "type": "object", + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + } + } + }, + "APIKeyResponse": { + "description": "The general view for API key in response", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "createdAt": { + "type": "string", + "description": "The creation date of the API key", + "example": "2000-10-31T01:23:45Z", + "format": "date-time" + }, + "name": { + "type": "string", + "description": "The name of the API key", + "example": "My API Key" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:23:45Z", + "format": "date-time" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false + } + } + }, + "APIKeyCreateRequestBody": { + "description": "The request body for creating an API key", + "type": "object", + "properties": { + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:23:45Z", + "format": "date-time" + }, + "name": { + "type": "string", + "description": "The name of the API key", + "example": "My API Key" + } + }, + "required": [ + "name" + ] + }, + "APIKeyCreateResponseBody": { + "description": "The response body for creating an API key", + "type": "object", + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } + }, + "APIKeyCreateUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key creation", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key creation unsuccessful" + } + } + }, + "APIKeyUpdateRequestBody": { + "description": "The request body for updating an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "name": { + "type": "string", + "description": "The name of the API key", + "example": "My API Key" + }, + "expiresAt": { + "type": "string", + "description": "The expiration date of the API key", + "example": "2000-10-31T01:23:45Z", + "format": "date-time" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": false, + "default": false + } + }, + "required": [ + "apiKey" + ] + }, + "APIKeyUpdateResponseBody": { + "description": "The response body for an unsuccessful API key update", + "type": "object", + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } + }, + "APIKeyUpdateUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key update", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key update unsuccessful" + } + } + }, + "APIKeyRevokeRequestBody": { + "description": "The request body for revoking an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + } + }, + "required": [ + "apiKey" + ] + }, + "APIKeyRevokeResponseBody": { + "description": "The response body for revoking an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + }, + "revoked": { + "type": "boolean", + "description": "The status of the API key", + "example": true + } + }, + "required": [ + "apiKey" + ] + }, + "APIKeyRevokeUnsuccessfulResponseBody": { + "description": "The response body for an unsuccessful API key revocation", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "API key revocation unsuccessful" + } + } + }, + "APIKeyListResponseBody": { + "description": "The response body for listing API keys", + "type": "object", + "properties": { + "apiKeys": { + "type": "array", + "items": { + "type": "object", + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } + } + } + } + }, + "APIKeyGetRequestBody": { + "description": "The request body for getting an API key", + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "The API key", + "example": "abcdefghijklmnopqrstuvwxyz" + } + }, + "required": [ + "apiKey" + ] + }, + "APIKeyGetResponseBody": { + "description": "The response body for getting an API key", + "type": "object", + "schema": { + "ref": "#/components/schemas/APIKeyResponse" + } + }, + "NotFoundError": { + "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Not Found Error" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/static/swagger-api.json b/src/static/swagger-api.json index b2cf4b99..a42f421f 100644 --- a/src/static/swagger-api.json +++ b/src/static/swagger-api.json @@ -1,3334 +1,3550 @@ { - "openapi": "3.0.0", - "info": { - "title": "Credential Service API for cheqd network", - "version": "2.0.0", - "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", - "contact": { - "name": "Cheqd Foundation Limited", - "url": "https://github.com/cheqd/credential-service", - "email": "support-github@cheqd.io" - }, - "license": { - "name": "Apache 2.0", - "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" - } - }, - "tags": [ - { - "name": "Account" - }, - { - "name": "Key" - }, - { - "name": "DID" - }, - { - "name": "Resource" - }, - { - "name": "Credential" - }, - { - "name": "Presentation" - }, - { - "name": "Credential Status" - } - ], - "externalDocs": { - "description": "Credential Service API Documentation", - "url": "https://docs.cheqd.io/identity" - }, - "paths": { - "/account": { - "get": { - "tags": ["Account"], - "summary": "Fetch custodian-mode client details.", - "description": "This endpoint returns the custodian-mode client details for authenticated users.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Customer" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/idtoken": { - "get": { - "tags": ["Account"], - "summary": "Fetch IdToken.", - "description": "This endpoint returns IdToken as JWT with list of user roles inside", - "deprecated": true, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/account/create": { - "post": { - "tags": ["Account"], - "summary": "Create an client for an authenticated user.", - "description": "This endpoint creates a client in the custodian-mode for an authenticated user", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccountCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "idToken": { - "type": "string" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/unencrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Create an unencrypted StatusList2021 credential status list.", - "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/create/encrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Create an encrypted StatusList2021 credential status list.", - "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", - "required": true, - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/unencrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Update an existing unencrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": ["revoke", "suspend", "reinstate"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/update/encrypted": { - "post": { - "tags": ["Credential Status"], - "summary": "Update an existing encrypted StatusList2021 credential status list.", - "parameters": [ - { - "in": "query", - "name": "statusAction", - "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", - "required": true, - "schema": { - "type": "string", - "enum": ["revoke", "suspend", "reinstate"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/check": { - "post": { - "tags": ["Credential Status"], - "summary": "Check a StatusList2021 index for a given Verifiable Credential.", - "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", - "parameters": [ - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "required": true, - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusCheckResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential-status/search": { - "get": { - "tags": ["Credential Status"], - "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", - "parameters": [ - { - "in": "query", - "name": "did", - "description": "The DID of the issuer of the status list.", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "statusPurpose", - "description": "The purpose of the status list. Can be either revocation or suspension.", - "schema": { - "type": "string", - "enum": ["revocation", "suspension"] - } - }, - { - "in": "query", - "name": "statusListName", - "description": "The name of the StatusList2021 DID-Linked Resource.", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialStatusListSearchResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/issue": { - "post": { - "tags": ["Credential"], - "summary": "Issue a Verifiable Credential", - "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Credential" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/verify": { - "post": { - "tags": ["Credential"], - "summary": "Verify a Verifiable Credential.", - "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "verify", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyCredentialResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/revoke": { - "post": { - "tags": ["Credential"], - "summary": "Revoke a Verifiable Credential.", - "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", - "operationId": "revoke", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RevocationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/suspend": { - "post": { - "tags": ["Credential"], - "summary": "Suspend a Verifiable Credential.", - "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", - "operationId": "suspend", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/credential/reinstate": { - "post": { - "tags": ["Credential"], - "summary": "Reinstate a suspended Verifiable Credential.", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "operationId": "reinstate", - "parameters": [ - { - "in": "query", - "name": "publish", - "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", - "schema": { - "type": "boolean" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialRevokeRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnsuspensionResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/create": { - "post": { - "tags": ["DID"], - "summary": "Create a DID Document.", - "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestFormBased" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidCreateRequestJson" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/update": { - "post": { - "tags": ["DID"], - "summary": "Update a DID Document.", - "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidUpdateResponse" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/import": { - "post": { - "tags": ["DID"], - "summary": "Import a DID Document.", - "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/did/deactivate/{did}": { - "post": { - "tags": ["DID"], - "summary": "Deactivate a DID Document.", - "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to deactivate.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidDeactivateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeactivatedDidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/list": { - "get": { - "tags": ["DID"], - "summary": "Fetch DIDs associated with an account.", - "description": "This endpoint returns the list of DIDs controlled by the account.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/did/search/{did}": { - "get": { - "tags": ["DID"], - "summary": "Resolve a DID Document.", - "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to resolve.", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "metadata", - "description": "Return only metadata of DID Document instead of actual DID Document.", - "schema": { - "type": "boolean" - } - }, - { - "in": "query", - "name": "versionId", - "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "versionTime", - "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "transformKeys", - "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", - "schema": { - "type": "string", - "enum": ["Ed25519VerificationKey2018", "Ed25519VerificationKey2020", "JsonWebKey2020"] - } - }, - { - "in": "query", - "name": "service", - "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", - "schema": { - "type": "string" - }, - "example": "service-1" - }, - { - "in": "query", - "name": "relativeRef", - "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", - "schema": { - "type": "string" - }, - "example": "/path/to/file" - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DidResolution" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/key/create": { - "post": { - "tags": ["Key"], - "summary": "Create an identity key pair.", - "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/import": { - "post": { - "tags": ["Key"], - "summary": "Import an identity key pair.", - "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/key/read/{kid}": { - "get": { - "tags": ["Key"], - "summary": "Fetch an identity key pair.", - "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", - "parameters": [ - { - "name": "kid", - "description": "Key ID of the identity key pair to fetch.", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/KeyResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/presentation/create": { - "post": { - "tags": ["Presentation"], - "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", - "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationCreateResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/presentation/verify": { - "post": { - "tags": ["Presentation"], - "summary": "Verify a Verifiable Presentation generated from credential(s).", - "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", - "parameters": [ - { - "in": "query", - "name": "verifyStatus", - "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "fetchRemoteContexts", - "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", - "schema": { - "type": "boolean", - "default": false - } - }, - { - "in": "query", - "name": "allowDeactivatedDid", - "description": "If set to `true` allow to verify credential which based on deactivated DID.", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PresentationVerifyRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyPresentationResult" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/create/{did}": { - "post": { - "tags": ["Resource"], - "summary": "Create a DID-Linked Resource.", - "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier to link the resource to.", - "schema": { - "type": "string" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" - } - } - } - }, - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - }, - "/resource/search/{did}": { - "get": { - "tags": ["Resource"], - "summary": "Get a DID-Linked Resource.", - "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", - "parameters": [ - { - "in": "path", - "name": "did", - "description": "DID identifier", - "schema": { - "type": "string" - }, - "required": true, - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - { - "in": "query", - "name": "resourceId", - "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "uuid" - }, - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - { - "in": "query", - "name": "resourceName", - "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "cheqd-issuer-logo" - }, - { - "in": "query", - "name": "resourceType", - "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "CredentialArtwork" - }, - { - "in": "query", - "name": "resourceVersion", - "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "v1" - }, - { - "in": "query", - "name": "resourceVersionTime", - "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", - "schema": { - "type": "string", - "format": "date-time" - }, - "example": "1970-01-01T00:00:00Z" - }, - { - "in": "query", - "name": "checksum", - "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", - "schema": { - "type": "string" - }, - "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" - }, - { - "in": "query", - "name": "resourceMetadata", - "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "any": { - "schema": { - "type": "object" - } - } - } - }, - "400": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "$ref": "#/components/schemas/InternalError" - } - } - } - } - }, - "components": { - "schemas": { - "AlsoKnownAs": { - "type": "object", - "properties": { - "alsoKnownAs": { - "type": "array", - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string", - "format": "uri", - "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" - }, - "description": { - "type": "string", - "description": "Optional description of the URI." - } - } - } - } - } - }, - "CredentialRequest": { - "description": "Input fields for the creating a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "issuerDid": { - "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "subjectDid": { - "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", - "type": "string", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - }, - "attributes": { - "description": "JSON object containing the attributes to be included in the credential.", - "type": "object", - "example": { - "name": "Bob", - "gender": "male" - } - }, - "@context": { - "description": "Optional properties to be included in the `@context` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": ["https://schema.org/schema.jsonld", "https://veramo.io/contexts/profile/v1"] - }, - "type": { - "description": "Optional properties to be included in the `type` property of the credential.", - "type": "array", - "items": { - "type": "string" - }, - "example": ["Person"] - }, - "expirationDate": { - "description": "Optional expiration date according to the VC Data Model specification.", - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "format": { - "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", - "type": "string", - "enum": ["jwt", "jsonld"], - "example": "jwt" - }, - "credentialStatus": { - "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", - "type": "object", - "required": ["statusPurpose", "statusListName"], - "properties": { - "statusPurpose": { - "type": "string", - "enum": ["revocation", "suspension"] - }, - "statusListName": { - "type": "string" - }, - "statusListIndex": { - "type": "number" - }, - "statusListVersion": { - "type": "string", - "format": "date-time" - }, - "statusListRangeStart": { - "type": "number" - }, - "statusListRangeEnd": { - "type": "number" - }, - "indexNotIn": { - "type": "number" - } - }, - "example": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials" - } - } - }, - "required": ["issuerDid", "subjectDid", "attributes"], - "example": { - "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "attributes": { - "gender": "male", - "name": "Bob" - }, - "@context": ["https://schema.org"], - "type": ["Person"], - "format": "jwt", - "credentialStatus": { - "statusPurpose": "revocation", - "statusListName": "employee-credentials", - "statusListIndex": 10 - } - } - }, - "Credential": { - "description": "Input fields for revoking/suspending a Verifiable Credential.", - "type": "object", - "additionalProperties": false, - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ] - }, - "type": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["VerifiableCredential", "Person"] - }, - "expirationDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "issuer": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - } - } - }, - "credentialSubject": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "DID", - "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - } - } - }, - "credentialStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" - }, - "statusListIndex": { - "type": "number", - "example": 20 - }, - "statusPurpose": { - "type": "string", - "enum": ["revocation", "suspension"], - "example": "suspension" - }, - "type": { - "type": "string", - "enum": ["StatusList2021Entry"] - } - } - }, - "issuanceDate": { - "type": "string", - "format": "date-time", - "example": "2023-06-08T13:49:28.000Z" - }, - "proof": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "jwt": { - "type": "string" - } - }, - "example": { - "type": "JwtProof2020", - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" - } - } - }, - "example": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.org", - "https://veramo.io/contexts/profile/v1" - ], - "credentialSubject": { - "gender": "male", - "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "name": "Bob" - }, - "credentialStatus": { - "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", - "statusIndex": 20, - "statusPurpose": "suspension", - "type": "StatusList2021Entry" - }, - "issuanceDate": "2023-06-08T13:49:28.000Z", - "issuer": { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "proof": { - "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", - "type": "JwtProof2020" - }, - "type": ["VerifiableCredential", "Person"] - } - }, - "CredentialRevokeRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", - "oneOf": [ - { - "type": "object" - }, - { - "type": "string" - } - ] - }, - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", - "type": "string" - } - } - }, - "RevocationResult": { - "properties": { - "revoked": { - "type": "boolean", - "example": true - } - } - }, - "SuspensionResult": { - "properties": { - "suspended": { - "type": "boolean", - "example": true - } - } - }, - "UnsuspensionResult": { - "properties": { - "unsuspended": { - "type": "boolean", - "example": true - } - } - }, - "CredentialVerifyRequest": { - "type": "object", - "properties": { - "credential": { - "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", - "type": "object" - }, - "policies": { - "description": "Custom verification policies to execute when verifying credential.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "VerifyPresentationResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - } - }, - "VerifyCredentialResult": { - "type": "object", - "properties": { - "verified": { - "type": "boolean" - }, - "issuer": { - "type": "string" - }, - "signer": { - "type": "object" - }, - "jwt": { - "type": "string" - }, - "verifiableCredential": { - "type": "object" - } - }, - "example": { - "verified": true, - "polices": {}, - "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "signer": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - } - }, - "PresentationCreateRequest": { - "type": "object", - "required": ["credentials"], - "properties": { - "credentials": { - "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", - "type": "array", - "items": { - "type": "object" - } - }, - "holderDid": { - "description": "DID of holder", - "type": "string" - }, - "verifierDid": { - "description": "DID of verifier", - "type": "string" - } - } - }, - "PresentationVerifyRequest": { - "type": "object", - "required": ["presentation"], - "properties": { - "presentation": { - "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", - "type": "object" - }, - "verifierDid": { - "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", - "type": "string" - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": false - }, - "policies": { - "description": "Custom verification policies to execute when verifying presentation.", - "type": "object", - "properties": { - "issuanceDate": { - "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "expirationDate": { - "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", - "type": "boolean", - "default": true - }, - "audience": { - "description": "Policy to skip the audience check when set to `false`.", - "type": "boolean", - "default": false - } - } - } - } - }, - "CredentialStatusCreateBody": { - "allOf": [ - { - "type": "object", - "required": ["did", "statusListName"], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be created.", - "type": "string" - }, - "length": { - "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true, - "default": 140000 - }, - "encoding": { - "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", - "type": "string", - "default": "base64url", - "enum": ["base64url", "base64", "hex"] - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/AlsoKnownAs" - } - ] - }, - "CredentialStatusCreateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "length": 140000, - "encoding": "base64url" - } - }, - "CredentialStatusUnencryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": false - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-employee-credentials", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "1.0.0", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - } - } - }, - "CredentialStatusCreateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusEncryptedPaymentConditionsBody": { - "type": "object", - "properties": { - "feePaymentAddress": { - "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 20 - }, - "feePaymentWindow": { - "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 10 - } - } - }, - "CredentialStatusEncryptedPaymentConditionsJson": { - "type": "object", - "properties": { - "paymentConditions": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - } - } - }, - "CredentialStatusCreateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - }, - { - "type": "object", - "required": ["feePaymentAddress", "feePaymentAmount", "feePaymentWindow"] - } - ] - }, - "CredentialStatusCreateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCreateBody" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - }, - { - "type": "object", - "required": ["paymentConditions"] - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "paymentConditions": [ - { - "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", - "feePaymentAmount": 20, - "feePaymentWindow": 10 - } - ] - } - }, - "CredentialStatusEncryptedResult": { - "type": "object", - "properties": { - "resource": { - "type": "object", - "properties": { - "StatusList2021": { - "type": "object", - "properties": { - "encodedList": { - "type": "string", - "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" - }, - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "validFrom": { - "type": "string", - "format": "date-time", - "example": "2023-06-26T11:45:19.349Z" - } - } - }, - "metadata": { - "type": "object", - "properties": { - "type": { - "type": "string", - "example": "StatusList2021Revocation" - }, - "encoding": { - "type": "string", - "example": "base64url" - }, - "encrypted": { - "type": "boolean", - "example": true - }, - "encryptedSymmetricKey": { - "type": "string", - "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" - }, - "paymentConditions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feePaymentAddress": { - "type": "string", - "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" - }, - "feePaymentAmount": { - "type": "string", - "example": "20000000000ncheq" - }, - "intervalInSeconds": { - "type": "number", - "example": 600 - }, - "type": { - "type": "string", - "example": "timelockPayment" - } - } - } - } - } - }, - "resourceMetadata": { - "type": "object", - "example": { - "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", - "resourceName": "cheqd-revocation-encrypted-1", - "resourceType": "StatusList2021Revocation", - "mediaType": "application/json", - "resourceVersion": "2023-06-26T11:45:19.349Z", - "created": "2023-06-26T11:45:20Z", - "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", - "previousVersionId": null, - "nextVersionId": null - } - }, - "symmetricKey": { - "type": "string", - "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - } - } - } - }, - "CredentialStatusCreateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "created": { - "type": "boolean", - "example": true - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusUpdateBody": { - "type": "object", - "required": ["did", "statusListName", "indices"], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", - "type": "string" - }, - "indices": { - "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", - "type": "array", - "items": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - } - }, - "statusListVersion": { - "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - "CredentialStatusUpdateUnencryptedRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials", - "indices": [10, 3199, 12109, 130999] - } - }, - "CredentialStatusUpdateUnencryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - } - ] - }, - "CredentialStatusUpdateEncryptedFormRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": ["symmetricKey"], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" - } - ] - }, - "CredentialStatusUpdateEncryptedJsonRequest": { - "allOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUpdateBody" - }, - { - "type": "object", - "required": ["symmetricKey"], - "properties": { - "symmetricKey": { - "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", - "type": "string" - } - } - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" - } - ], - "example": { - "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", - "statusListName": "cheqd-employee-credentials-encrypted", - "indices": [10, 3199, 12109, 130999], - "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" - } - }, - "CredentialStatusUpdateEncryptedResult": { - "allOf": [ - { - "type": "object", - "properties": { - "updated": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/RevocationResult" - }, - { - "$ref": "#/components/schemas/SuspensionResult" - }, - { - "$ref": "#/components/schemas/UnsuspensionResult" - } - ] - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - }, - "CredentialStatusCheckRequest": { - "type": "object", - "required": ["did", "statusListName", "index"], - "properties": { - "did": { - "description": "DID of the StatusList2021 publisher.", - "type": "string", - "format": "uri" - }, - "statusListName": { - "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", - "type": "string" - }, - "index": { - "description": "Credential status index to be checked for revocation or suspension.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": false - }, - "makeFeePayment": { - "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", - "type": "boolean", - "default": true - } - } - }, - "CredentialStatusCheckResult": { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" - } - ] - }, - "CredentialStatusCheckRevocationResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "revoked": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusCheckSuspensionResult": { - "type": "object", - "properties": { - "checked": { - "type": "boolean", - "example": true - }, - "suspended": { - "type": "boolean", - "example": false - } - } - }, - "CredentialStatusListSearchResult": { - "allOf": [ - { - "type": "object", - "properties": { - "found": { - "type": "boolean", - "example": true - } - } - }, - { - "oneOf": [ - { - "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" - }, - { - "$ref": "#/components/schemas/CredentialStatusEncryptedResult" - } - ] - } - ] - }, - "KeyImportRequest": { - "type": "object", - "properties": { - "alias": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["Ed25519", "Secp256k1"] - }, - "privateKeyHex": { - "type": "string" - } - } - }, - "KeyResult": { - "type": "object", - "properties": { - "kid": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["Ed25519", "Secp256k1"] - }, - "publicKeyHex": { - "type": "string" - } - } - }, - "DidDocument": { - "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "verificationMethod": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": ["https://www.w3.org/ns/did/v1"], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], - "verificationMethod": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "type": "Ed25519VerificationKey2018", - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" - } - ], - "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": ["https://example.com"] - } - ] - } - }, - "DidDocumentWithoutVerificationMethod": { - "type": "object", - "properties": { - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "controllers": { - "type": "array", - "items": { - "type": "string" - } - }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "authentication": { - "type": "array", - "items": { - "type": "string" - } - }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } - }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } - }, - "keyAgreement": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "@context": ["https://www.w3.org/ns/did/v1"], - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "controller": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0"], - "authentication": ["did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1"], - "service": [ - { - "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", - "type": "LinkedDomains", - "serviceEndpoint": ["https://example.com"] - } - ] - } - }, - "DidCreateRequestFormBased": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": ["testnet", "mainnet"] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": ["uuid", "base58btc"] - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] - }, - "service": { - "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", - "type": "array", - "items": { - "type": "object", - "properties": { - "idFragment": { - "type": "string" - }, - "type": { - "type": "string" - }, - "serviceEndpoint": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "example": [ - { - "idFragment": "service-1", - "type": "LinkedDomains", - "serviceEndpoint": ["https://example.com"] - } - ] - }, - "key": { - "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", - "type": "string" - }, - "@context": { - "type": "array", - "items": { - "type": "string" - }, - "example": ["https://www.w3.org/ns/did/v1"] - } - } - }, - "DidCreateRequestJson": { - "type": "object", - "properties": { - "network": { - "description": "Network to create the DID on (testnet or mainnet)", - "type": "string", - "enum": ["testnet", "mainnet"] - }, - "identifierFormatType": { - "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", - "type": "string", - "enum": ["uuid", "base58btc"] - }, - "assertionMethod": { - "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", - "type": "boolean", - "default": true - }, - "options": { - "type": "object", - "properties": { - "key": { - "type": "string", - "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" - }, - "verificationMethodType": { - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "type": "string", - "enum": ["Ed25519VerificationKey2018", "JsonWebKey2020", "Ed25519VerificationKey2020"] - } - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" - } - } - }, - "DidImportRequest": { - "type": "object", - "properties": { - "did": { - "type": "string", - "description": "DID to be imported", - "format": "uri", - "required": true - }, - "keys": { - "type": "array", - "description": "List of keys required to import the DID", - "required": true, - "items": { - "$ref": "#/components/schemas/KeyImportRequest" - } - } - } - }, - "PresentationCreateResult": { - "type": "object", - "properties": { - "vp": { - "type": "object", - "description": "Verifiable Presentation which could be provided to the verifier." - }, - "nbf": { - "type": "integer", - "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." - }, - "iss": { - "type": "string", - "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" - }, - "aud": { - "type": "array", - "items": { - "type": "string" - }, - "description": "DID of the verifier of the Verifiable Presentation." - } - }, - "example": { - "vp": { - "@context": ["https://www.w3.org/2018/credentials/v1"], - "type": ["VerifiablePresentation"], - "verifiableCredential": [ - "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" - ] - }, - "nbf": 1700744275, - "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", - "aud": ["did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22"] - } - }, - "DidResult": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string" - }, - "keys": { - "type": "array", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - } - } - }, - "DidUpdateResponse": { - "type": "object", - "properties": { - "did": { - "type": "string" - }, - "controllerKeyId": { - "type": "string", - "description": "The default key id of which is the key associated with the first verificationMethod" - }, - "keys": { - "type": "array", - "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", - "items": { - "type": "object" - } - }, - "services": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "controllerKeyRefs": { - "type": "array", - "description": "The list of keyRefs which were used for signing the transaction", - "items": { - "type": "string" - } - }, - "controllerKeys": { - "type": "array", - "description": "The list of all possible keys, inlcuding all controller's keys", - "items": { - "type": "string" - } - } - } - }, - "VerificationMethod": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - }, - "controller": { - "type": "string" - }, - "publicKeyMultibase": { - "type": "string" - }, - "publicKeyJwk": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", - "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", - "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", - "type": "Ed25519VerificationKey2018" - } - }, - "Service": { - "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", - "type": "object", - "properties": { - "id": { - "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", - "type": "string", - "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" - }, - "type": { - "description": "Service type as defined in DID Specification Registries.", - "type": "string", - "example": "LinkedDomains" - }, - "serviceEndpoint": { - "description": "Service endpoint as defined in DID Core Specification.", - "type": "array", - "items": { - "type": "string", - "example": "https://example.com" - } - } - } - }, - "DidUpdateRequest": { - "type": "object", - "properties": { - "did": { - "description": "DID identifier to be updated.", - "type": "string", - "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" - }, - "service": { - "type": "array", - "description": "Service section of the DID Document.", - "items": { - "$ref": "#/components/schemas/Service" - } - }, - "verificationMethod": { - "type": "array", - "description": "Verification Method section of the DID Document.", - "items": { - "$ref": "#/components/schemas/VerificationMethod" - } - }, - "authentication": { - "description": "Authentication section of the DID Document.", - "type": "array", - "items": { - "type": "string" - } - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - } - } - }, - "DidDeactivateRequest": { - "type": "object", - "properties": { - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "CreateResourceRequest": { - "description": "Input fields for DID-Linked Resource creation.", - "type": "object", - "additionalProperties": false, - "required": ["name", "type", "data", "encoding"], - "properties": { - "data": { - "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", - "type": "string" - }, - "encoding": { - "description": "Encoding format used to encode the data.", - "type": "string", - "enum": ["base64url", "base64", "hex"] - }, - "name": { - "description": "Name of DID-Linked Resource.", - "type": "string" - }, - "type": { - "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", - "type": "string" - }, - "alsoKnownAs": { - "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", - "type": "array", - "items": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "description": { - "type": "string" - } - } - } - }, - "version": { - "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", - "type": "string" - }, - "publicKeyHexs": { - "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "example": { - "data": "SGVsbG8gV29ybGQ=", - "encoding": "base64url", - "name": "ResourceName", - "type": "TextDocument" - } - }, - "ResourceList": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "contentMetadata": { - "type": "object" - }, - "contentStream": { - "type": "object" - }, - "dereferencingMetadata": { - "$ref": "#/components/schemas/DereferencingMetadata" - } - } - }, - "DereferencingMetadata": { - "type": "object", - "properties": { - "contentType": { - "type": "string", - "example": "application/did+ld+json" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - } - } - }, - "DidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DidDocumentMetadata" - } - } - }, - "DeactivatedDidResolution": { - "type": "object", - "properties": { - "@context": { - "type": "string", - "example": "https://w3id.org/did-resolution/v1" - }, - "didDidResolutionMetadata": { - "$ref": "#/components/schemas/DidResolutionMetadata" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" - }, - "didDocumentMetadata": { - "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" - } - } - }, - "DidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": false - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "DeactivatedDidDocumentMetadata": { - "type": "object", - "properties": { - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "deactivated": { - "type": "boolean", - "example": true - }, - "updated": { - "type": "string", - "example": "2021-09-10T12:00:00Z" - }, - "versionId": { - "type": "string", - "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" - }, - "linkedResourceMetadata": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceMetadata" - } - } - } - }, - "ResourceMetadata": { - "type": "object", - "properties": { - "resourceURI": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceCollectionId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "resourceId": { - "type": "string", - "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" - }, - "resourceName": { - "type": "string", - "example": "cheqd-issuer-logo" - }, - "resourceType": { - "type": "string", - "example": "CredentialArtwork" - }, - "mediaType": { - "type": "string", - "example": "image/png" - }, - "resourceVersion": { - "type": "string", - "example": "1.0" - }, - "checksum": { - "type": "string", - "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" - }, - "created": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "nextVersionId": { - "type": "string", - "example": "d4829ac7-4566-478c-a408-b44767eddadc" - }, - "previousVersionId": { - "type": "string", - "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" - } - } - }, - "DidResolutionMetadata": { - "type": "object", - "properties": { - "contentType": { - "allOf": [ - { - "$ref": "#/components/schemas/ContentType" - } - ], - "example": "application/did+ld+json" - }, - "retrieved": { - "type": "string", - "example": "2021-09-01T12:00:00Z" - }, - "did": { - "$ref": "#/components/schemas/DidProperties" - } - } - }, - "ContentType": { - "type": "string", - "enum": ["application/did+json", "application/did+ld+json", "application/ld+json", "application/json"] - }, - "DidProperties": { - "type": "object", - "properties": { - "didString": { - "type": "string", - "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - }, - "method": { - "type": "string", - "example": "cheqd" - }, - "methodSpecificId": { - "type": "string", - "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" - } - } - }, - "Customer": { - "type": "object", - "properties": { - "customerId": { - "type": "string", - "example": "6w5drpiiwhhs" - }, - "address": { - "type": "string", - "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" - } - } - }, - "AccountCreateRequest": { - "type": "object", - "properties": { - "user": { - "type": "object", - "properties": { - "primaryEmail": { - "type": "string" - } - } - } - } - }, - "InvalidRequest": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "InvalidRequest" - } - } - }, - "InternalError": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Internal Error" - } - } - }, - "UnauthorizedError": { - "description": "Access token is missing or invalid", - "type": "object", - "properties": { - "error": { - "type": "string", - "example": "Unauthorized Error" - } - } - } - } - } -} + "openapi": "3.0.0", + "info": { + "title": "Credential Service API for cheqd network", + "version": "2.0.0", + "description": "API service to create and manage DIDs, Verifiable Credentials, and DID-Linked Resources", + "contact": { + "name": "Cheqd Foundation Limited", + "url": "https://github.com/cheqd/credential-service", + "email": "support-github@cheqd.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/cheqd/credential-service/blob/main/LICENSE" + } + }, + "tags": [ + { + "name": "Account" + }, + { + "name": "Key" + }, + { + "name": "DID" + }, + { + "name": "Resource" + }, + { + "name": "Credential" + }, + { + "name": "Presentation" + }, + { + "name": "Credential Status" + } + ], + "externalDocs": { + "description": "Credential Service API Documentation", + "url": "https://docs.cheqd.io/identity" + }, + "paths": { + "/account": { + "get": { + "tags": [ + "Account" + ], + "summary": "Fetch custodian-mode client details.", + "description": "This endpoint returns the custodian-mode client details for authenticated users.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Customer" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/idtoken": { + "get": { + "tags": [ + "Account" + ], + "summary": "Fetch IdToken.", + "description": "This endpoint returns IdToken as JWT with list of user roles inside", + "deprecated": true, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/account/create": { + "post": { + "tags": [ + "Account" + ], + "summary": "Create an client for an authenticated user.", + "description": "This endpoint creates a client in the custodian-mode for an authenticated user", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "idToken": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/unencrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Create an unencrypted StatusList2021 credential status list.", + "description": "This endpoint creates an unencrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/create/encrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Create an encrypted StatusList2021 credential status list.", + "description": "This endpoint creates an encrypted StatusList2021 credential status list. The StatusList is published as a DID-Linked Resource on ledger. As input, it can can take input parameters needed to create the status list via a form, or a pre-assembled status list in JSON format. Status lists can be created as either encrypted or unencrypted; and with purpose as either revocation or suspension.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension. Once this is set, it cannot be changed. A new status list must be created to change the purpose.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCreateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/unencrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Update an existing unencrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the unencrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revoke", + "suspend", + "reinstate" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateUnencryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/update/encrypted": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Update an existing encrypted StatusList2021 credential status list.", + "parameters": [ + { + "in": "query", + "name": "statusAction", + "description": "The update action to be performed on the encrypted status list, can be revoke, suspend or reinstate", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revoke", + "suspend", + "reinstate" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedFormRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedJsonRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusUpdateEncryptedResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/check": { + "post": { + "tags": [ + "Credential Status" + ], + "summary": "Check a StatusList2021 index for a given Verifiable Credential.", + "description": "This endpoint checks a StatusList2021 index for a given Verifiable Credential and reports whether it is revoked or suspended. It offers a standalone method for checking an index without passing the entire Verifiable Credential or Verifiable Presentation.", + "parameters": [ + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "required": true, + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusCheckResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential-status/search": { + "get": { + "tags": [ + "Credential Status" + ], + "summary": "Fetch StatusList2021 DID-Linked Resource based on search criteria.", + "parameters": [ + { + "in": "query", + "name": "did", + "description": "The DID of the issuer of the status list.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "statusPurpose", + "description": "The purpose of the status list. Can be either revocation or suspension.", + "schema": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + } + }, + { + "in": "query", + "name": "statusListName", + "description": "The name of the StatusList2021 DID-Linked Resource.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialStatusListSearchResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/issue": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Issue a Verifiable Credential", + "description": "This endpoint issues a Verifiable Credential. As input it takes the list of issuerDid, subjectDid, attributes, and other parameters of the credential to be issued.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Credential" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/verify": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Verify a Verifiable Credential.", + "description": "This endpoint verifies a Verifiable Credential passed to it. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "verify", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the credential. Requires the VC to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyCredentialResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/revoke": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Revoke a Verifiable Credential.", + "description": "This endpoint revokes a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself. The StatusList2021 resource should already be setup in the VC and `credentialStatus` property present in the VC.", + "operationId": "revoke", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RevocationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/suspend": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Suspend a Verifiable Credential.", + "description": "This endpoint suspends a given Verifiable Credential. As input, it can take the VC-JWT as a string or the entire credential itself.", + "operationId": "suspend", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/credential/reinstate": { + "post": { + "tags": [ + "Credential" + ], + "summary": "Reinstate a suspended Verifiable Credential.", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "operationId": "reinstate", + "parameters": [ + { + "in": "query", + "name": "publish", + "description": "Set whether the StatusList2021 resource should be published to the ledger or not. If set to `false`, the StatusList2021 publisher should manually publish the resource.", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRevokeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnsuspensionResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/create": { + "post": { + "tags": [ + "DID" + ], + "summary": "Create a DID Document.", + "description": "This endpoint creates a DID and associated DID Document. As input, it can take the DID Document parameters via a form, or the fully-assembled DID Document itself.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestFormBased" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidCreateRequestJson" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/update": { + "post": { + "tags": [ + "DID" + ], + "summary": "Update a DID Document.", + "description": "This endpoint updates a DID Document. As an input, it can take JUST the sections/parameters that need to be updated in the DID Document (in this scenario, it fetches the current DID Document and applies the updated section). Alternatively, it take the fully-assembled DID Document with updated sections as well as unchanged sections.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidUpdateResponse" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/import": { + "post": { + "tags": [ + "DID" + ], + "summary": "Import a DID Document.", + "description": "This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/did/deactivate/{did}": { + "post": { + "tags": [ + "DID" + ], + "summary": "Deactivate a DID Document.", + "description": "This endpoint deactivates a DID Document by taking the DID identifier as input. Must be called and signed by the DID owner.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to deactivate.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidDeactivateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeactivatedDidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/list": { + "get": { + "tags": [ + "DID" + ], + "summary": "Fetch DIDs associated with an account.", + "description": "This endpoint returns the list of DIDs controlled by the account.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/did/search/{did}": { + "get": { + "tags": [ + "DID" + ], + "summary": "Resolve a DID Document.", + "description": "Resolve a DID Document by DID identifier. Also supports DID Resolution Queries as defined in the W3C DID Resolution specification.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to resolve.", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "metadata", + "description": "Return only metadata of DID Document instead of actual DID Document.", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "versionId", + "description": "Unique UUID version identifier of DID Document. Allows for fetching a specific version of the DID Document. See cheqd DID Method Specification for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "versionTime", + "description": "Returns the closest version of the DID Document *at* or *before* specified time. See DID Resolution handling for `did:cheqd` for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "transformKeys", + "description": "This directive transforms the Verification Method key format from the version in the DID Document to the specified format chosen below.", + "schema": { + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "Ed25519VerificationKey2020", + "JsonWebKey2020" + ] + } + }, + { + "in": "query", + "name": "service", + "description": "Query DID Document for a specific Service Endpoint by Service ID (e.g., `service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`). This will typically redirect to the Service Endpoint based on DID Resolution specification algorithm.", + "schema": { + "type": "string" + }, + "example": "service-1" + }, + { + "in": "query", + "name": "relativeRef", + "description": "Relative reference is a query fragment appended to the Service Endpoint URL. **Must** be used along with the `service` query property above. See DID Resolution specification algorithm for more details.", + "schema": { + "type": "string" + }, + "example": "/path/to/file" + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DidResolution" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/key/create": { + "post": { + "tags": [ + "Key" + ], + "summary": "Create an identity key pair.", + "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/import": { + "post": { + "tags": [ + "Key" + ], + "summary": "Import an identity key pair.", + "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/read/{kid}": { + "get": { + "tags": [ + "Key" + ], + "summary": "Fetch an identity key pair.", + "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", + "parameters": [ + { + "name": "kid", + "description": "Key ID of the identity key pair to fetch.", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/presentation/create": { + "post": { + "tags": [ + "Presentation" + ], + "summary": "!!! WARN. Such endpoint is made mostly for testing purposes and it is not supposed to be used in production !!! Create a Verifiable Presentation from credential(s).", + "description": "This endpoint creates a Verifiable Presentation from credential(s). As input, it can take the credential(s) as a string or the entire credential(s) itself. \n !!! WARN. Such endpoint is made only for testing purposes !!!", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationCreateResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/presentation/verify": { + "post": { + "tags": [ + "Presentation" + ], + "summary": "Verify a Verifiable Presentation generated from credential(s).", + "description": "This endpoint verifies the Verifiable Presentation generated from credential(s). As input, it can take the Verifiable Presentation JWT as a string or the entire Verifiable Presentation itself.", + "parameters": [ + { + "in": "query", + "name": "verifyStatus", + "description": "If set to `true` the verification will also check the status of the presentation. Requires the VP to have a `credentialStatus` property.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "fetchRemoteContexts", + "description": "When dealing with JSON-LD you also MUST provide the proper contexts. * Set this to `true` ONLY if you want the `@context` URLs to be fetched in case they are a custom context.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "allowDeactivatedDid", + "description": "If set to `true` allow to verify credential which based on deactivated DID.", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresentationVerifyRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPresentationResult" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/create/{did}": { + "post": { + "tags": [ + "Resource" + ], + "summary": "Create a DID-Linked Resource.", + "description": "This endpoint creates a DID-Linked Resource. As input, it can take the DID identifier and the resource parameters via a form, or the fully-assembled resource itself.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier to link the resource to.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/resource/search/{did}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get a DID-Linked Resource.", + "description": "This endpoint returns the DID-Linked Resource for a given DID identifier and resource identifier.", + "parameters": [ + { + "in": "path", + "name": "did", + "description": "DID identifier", + "schema": { + "type": "string" + }, + "required": true, + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + { + "in": "query", + "name": "resourceId", + "description": "Fetch a DID-Linked Resource by Resource ID unique identifier. Since this is a unique identifier, other Resource query parameters are not required. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + { + "in": "query", + "name": "resourceName", + "description": "Filter a DID-Linked Resource query by Resource Name. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "cheqd-issuer-logo" + }, + { + "in": "query", + "name": "resourceType", + "description": "Filter a DID-Linked Resource query by Resource Type. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "CredentialArtwork" + }, + { + "in": "query", + "name": "resourceVersion", + "description": "Filter a DID-Linked Resource query by Resource Version, which is an optional free-text field used by issuers (e.g., \"v1\", \"Final Version\", \"1st January 1970\" etc). See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "v1" + }, + { + "in": "query", + "name": "resourceVersionTime", + "description": "Filter a DID-Linked Resource query which returns the closest version of the Resource *at* or *before* specified time. See DID-Linked Resources for more details.", + "schema": { + "type": "string", + "format": "date-time" + }, + "example": "1970-01-01T00:00:00Z" + }, + { + "in": "query", + "name": "checksum", + "description": "Request integrity check against a given DID-Linked Resource by providing a SHA-256 checksum hash. See DID-Linked Resources for more details.", + "schema": { + "type": "string" + }, + "example": "dc64474d062ed750a66bad58cb609928de55ed0d81defd231a4a4bf97358e9ed" + }, + { + "in": "query", + "name": "resourceMetadata", + "description": "Return only metadata of DID-Linked Resource instead of actual DID-Linked Resource. Mutually exclusive with some of the other parameters.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "any": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + } + }, + "components": { + "schemas": { + "AlsoKnownAs": { + "type": "object", + "properties": { + "alsoKnownAs": { + "type": "array", + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "uri", + "description": "URI where the DID-Linked Resource can be fetched from. Can be any type of URI (e.g., DID, HTTPS, IPFS, etc.)" + }, + "description": { + "type": "string", + "description": "Optional description of the URI." + } + } + } + } + } + }, + "CredentialRequest": { + "description": "Input fields for the creating a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "issuerDid": { + "description": "DID of the Verifiable Credential issuer. This needs to be a `did:cheqd` DID.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "subjectDid": { + "description": "DID of the Verifiable Credential holder/subject. This needs to be a `did:key` DID.", + "type": "string", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + }, + "attributes": { + "description": "JSON object containing the attributes to be included in the credential.", + "type": "object", + "example": { + "name": "Bob", + "gender": "male" + } + }, + "@context": { + "description": "Optional properties to be included in the `@context` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://schema.org/schema.jsonld", + "https://veramo.io/contexts/profile/v1" + ] + }, + "type": { + "description": "Optional properties to be included in the `type` property of the credential.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "Person" + ] + }, + "expirationDate": { + "description": "Optional expiration date according to the VC Data Model specification.", + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "format": { + "description": "Format of the Verifiable Credential. Defaults to VC-JWT.", + "type": "string", + "enum": [ + "jwt", + "jsonld" + ], + "example": "jwt" + }, + "credentialStatus": { + "description": "Optional `credentialStatus` properties for VC revocation or suspension. Takes `statusListName` and `statusListPurpose` as inputs.", + "type": "object", + "required": [ + "statusPurpose", + "statusListName" + ], + "properties": { + "statusPurpose": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ] + }, + "statusListName": { + "type": "string" + }, + "statusListIndex": { + "type": "number" + }, + "statusListVersion": { + "type": "string", + "format": "date-time" + }, + "statusListRangeStart": { + "type": "number" + }, + "statusListRangeEnd": { + "type": "number" + }, + "indexNotIn": { + "type": "number" + } + }, + "example": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials" + } + } + }, + "required": [ + "issuerDid", + "subjectDid", + "attributes" + ], + "example": { + "issuerDid": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "subjectDid": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "attributes": { + "gender": "male", + "name": "Bob" + }, + "@context": [ + "https://schema.org" + ], + "type": [ + "Person" + ], + "format": "jwt", + "credentialStatus": { + "statusPurpose": "revocation", + "statusListName": "employee-credentials", + "statusListIndex": 10 + } + } + }, + "Credential": { + "description": "Input fields for revoking/suspending a Verifiable Credential.", + "type": "object", + "additionalProperties": false, + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ] + }, + "type": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "VerifiableCredential", + "Person" + ] + }, + "expirationDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "issuer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + } + } + }, + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "DID", + "example": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + } + } + }, + "credentialStatus": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20" + }, + "statusListIndex": { + "type": "number", + "example": 20 + }, + "statusPurpose": { + "type": "string", + "enum": [ + "revocation", + "suspension" + ], + "example": "suspension" + }, + "type": { + "type": "string", + "enum": [ + "StatusList2021Entry" + ] + } + } + }, + "issuanceDate": { + "type": "string", + "format": "date-time", + "example": "2023-06-08T13:49:28.000Z" + }, + "proof": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "jwt": { + "type": "string" + } + }, + "example": { + "type": "JwtProof2020", + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org", + "https://veramo.io/contexts/profile/v1" + ], + "credentialSubject": { + "gender": "male", + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "name": "Bob" + }, + "credentialStatus": { + "id": "https://resolver.cheqd.net/1.0/identifiers/did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e?resourceName=cheqd-suspension-1&resourceType=StatusList2021Suspension#20", + "statusIndex": 20, + "statusPurpose": "suspension", + "type": "StatusList2021Entry" + }, + "issuanceDate": "2023-06-08T13:49:28.000Z", + "issuer": { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "proof": { + "jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6Y2hlcWQ6dGVzdG5ldDo3YmY4MWEyMC02MzNjLTRjYzctYmM0YS01YTQ1ODAxMDA1ZTAiLCJuYmYiOjE2ODYyMzIxNjgsInN1YiI6ImRpZDprZXk6ejZNa2hhWGdCWkR2b3REa0w1MjU3ZmFpenRpR2lDMlF0S0xHcGJubkVHdGEyZG9LIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3NjaGVtYS5vcmciLCJodHRwczovL3ZlcmFtby5pby9jb250ZXh0cy9wcm9maWxlL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImdlbmRlciI6Im1hbGUiLCJuYW1lIjoiQm9iIn0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJQZXJzb24iXX19.wMfdR6RtyAZA4eoWya5Aw97wwER2Cm5Guk780Xw8H9fA3sfudIJeLRLboqixpTchqSbYeA7KbuCTAnLgXTD_Cg", + "type": "JwtProof2020" + }, + "type": [ + "VerifiableCredential", + "Person" + ] + } + }, + "CredentialRevokeRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be revoked as a VC-JWT string or a JSON object.", + "oneOf": [ + { + "type": "object" + }, + { + "type": "string" + } + ] + }, + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource. Required if the StatusList2021 DID-Linked Resource is encrypted.", + "type": "string" + } + } + }, + "RevocationResult": { + "properties": { + "revoked": { + "type": "boolean", + "example": true + } + } + }, + "SuspensionResult": { + "properties": { + "suspended": { + "type": "boolean", + "example": true + } + } + }, + "UnsuspensionResult": { + "properties": { + "unsuspended": { + "type": "boolean", + "example": true + } + } + }, + "CredentialVerifyRequest": { + "type": "object", + "properties": { + "credential": { + "description": "Verifiable Credential to be verified as a VC-JWT string or a JSON object.", + "type": "object" + }, + "policies": { + "description": "Custom verification policies to execute when verifying credential.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "VerifyPresentationResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + } + }, + "VerifyCredentialResult": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + }, + "issuer": { + "type": "string" + }, + "signer": { + "type": "object" + }, + "jwt": { + "type": "string" + }, + "verifiableCredential": { + "type": "object" + } + }, + "example": { + "verified": true, + "polices": {}, + "issuer": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "signer": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + } + }, + "PresentationCreateRequest": { + "type": "object", + "required": [ + "credentials" + ], + "properties": { + "credentials": { + "description": "Verifiable Credentials to be used for VP-JWT creation as a VP-JWT strings or a JSON objectsf.", + "type": "array", + "items": { + "type": "object" + } + }, + "holderDid": { + "description": "DID of holder", + "type": "string" + }, + "verifierDid": { + "description": "DID of verifier", + "type": "string" + } + } + }, + "PresentationVerifyRequest": { + "type": "object", + "required": [ + "presentation" + ], + "properties": { + "presentation": { + "description": "Verifiable Presentation to be verified as a VP-JWT string or a JSON object.", + "type": "object" + }, + "verifierDid": { + "description": "Provide an optional verifier DID (also known as 'domain' parameter), if the verifier DID in the presentation is not managed in the wallet.", + "type": "string" + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": false + }, + "policies": { + "description": "Custom verification policies to execute when verifying presentation.", + "type": "object", + "properties": { + "issuanceDate": { + "description": "Policy to skip the `issuanceDate` (`nbf`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "expirationDate": { + "description": "Policy to skip the `expirationDate` (`exp`) timestamp check when set to `false`.", + "type": "boolean", + "default": true + }, + "audience": { + "description": "Policy to skip the audience check when set to `false`.", + "type": "boolean", + "default": false + } + } + } + } + }, + "CredentialStatusCreateBody": { + "allOf": [ + { + "type": "object", + "required": [ + "did", + "statusListName" + ], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be created.", + "type": "string" + }, + "length": { + "description": "The length of the status list to be created. The default and minimum length is 140000 which is 16kb.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "default": 140000 + }, + "encoding": { + "description": "The encoding format of the StatusList2021 DiD-Linked Resource to be created.", + "type": "string", + "default": "base64url", + "enum": [ + "base64url", + "base64", + "hex" + ] + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/AlsoKnownAs" + } + ] + }, + "CredentialStatusCreateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "length": 140000, + "encoding": "base64url" + } + }, + "CredentialStatusUnencryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "H4sIAAAAAAAAA-3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAAAAAAAADwaDhDr_xcRAAA" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": false + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-employee-credentials", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "1.0.0", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + } + } + }, + "CredentialStatusCreateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusEncryptedPaymentConditionsBody": { + "type": "object", + "properties": { + "feePaymentAddress": { + "description": "The cheqd/Cosmos payment address where payments to unlock the encrypted StatusList2021 DID-Linked Resource need to be sent.", + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "description": "Amount in CHEQ tokens to unlock the encrypted StatusList2021 DID-Linked Resource.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 20 + }, + "feePaymentWindow": { + "description": "Time window (in minutes) within which the payment to unlock the encrypted StatusList2021 DID-Linked Resource is considered valid.", + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 10 + } + } + }, + "CredentialStatusEncryptedPaymentConditionsJson": { + "type": "object", + "properties": { + "paymentConditions": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + } + } + }, + "CredentialStatusCreateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + }, + { + "type": "object", + "required": [ + "feePaymentAddress", + "feePaymentAmount", + "feePaymentWindow" + ] + } + ] + }, + "CredentialStatusCreateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCreateBody" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + }, + { + "type": "object", + "required": [ + "paymentConditions" + ] + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "paymentConditions": [ + { + "feePaymentAddress": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg", + "feePaymentAmount": 20, + "feePaymentWindow": 10 + } + ] + } + }, + "CredentialStatusEncryptedResult": { + "type": "object", + "properties": { + "resource": { + "type": "object", + "properties": { + "StatusList2021": { + "type": "object", + "properties": { + "encodedList": { + "type": "string", + "example": "496fdfbeb745b4db03fcdb40566f9c4c4a1c0f184b31255e641b6e7bdfb9b6946c12be87ca3763be0393c00b67ac1e8737c106b32f46ef59c765754415b5e8cc7c65fccaa3374620430ea476301a5e0dd63340e7a27a68bc627518471f22e4a2" + }, + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "example": "2023-06-26T11:45:19.349Z" + } + } + }, + "metadata": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "StatusList2021Revocation" + }, + "encoding": { + "type": "string", + "example": "base64url" + }, + "encrypted": { + "type": "boolean", + "example": true + }, + "encryptedSymmetricKey": { + "type": "string", + "example": "b11182dc524b8181f9a6aef4c4ad0a1c14e40033b9112dffd8d1bcf6cc3b85abc07ded2205ee94068a99f4202502cb0855f322583fa6ce1534d3a05bf36891766ea2c5f90a982b3040680762977d404d758a2370224a239c8279aa7d21e980931c42055b17ca4c7dbffa4782480a8b6279cf989b2f166d5fdb4b2c1b5a63927200000000000000203018dcaba26df45a415bb599218b27ca853a70289d7a3ed3ed0e3730452e8f8d9af91b6e71312565d2c069341f6660ab" + }, + "paymentConditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feePaymentAddress": { + "type": "string", + "example": "cheqd1qs0nhyk868c246defezhz5eymlt0dmajna2csg" + }, + "feePaymentAmount": { + "type": "string", + "example": "20000000000ncheq" + }, + "intervalInSeconds": { + "type": "number", + "example": 600 + }, + "type": { + "type": "string", + "example": "timelockPayment" + } + } + } + } + } + }, + "resourceMetadata": { + "type": "object", + "example": { + "resourceURI": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e/resources/5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceCollectionId": "7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "resourceId": "5945233a-a4b5-422b-b893-eaed5cedd2dc", + "resourceName": "cheqd-revocation-encrypted-1", + "resourceType": "StatusList2021Revocation", + "mediaType": "application/json", + "resourceVersion": "2023-06-26T11:45:19.349Z", + "created": "2023-06-26T11:45:20Z", + "checksum": "909e22e371a41afbb96c330a97752cf7c8856088f1f937f87decbef06cbe9ca2", + "previousVersionId": null, + "nextVersionId": null + } + }, + "symmetricKey": { + "type": "string", + "example": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + } + } + } + }, + "CredentialStatusCreateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "created": { + "type": "boolean", + "example": true + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusUpdateBody": { + "type": "object", + "required": [ + "did", + "statusListName", + "indices" + ], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be updated.", + "type": "string" + }, + "indices": { + "description": "List of credential status indices to be updated. The indices must be in the range of the status list.", + "type": "array", + "items": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + } + }, + "statusListVersion": { + "description": "Optional field to assign a human-readable version in the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + "CredentialStatusUpdateUnencryptedRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials", + "indices": [ + 10, + 3199, + 12109, + 130999 + ] + } + }, + "CredentialStatusUpdateUnencryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + } + ] + }, + "CredentialStatusUpdateEncryptedFormRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": [ + "symmetricKey" + ], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsBody" + } + ] + }, + "CredentialStatusUpdateEncryptedJsonRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUpdateBody" + }, + { + "type": "object", + "required": [ + "symmetricKey" + ], + "properties": { + "symmetricKey": { + "description": "The symmetric key used to encrypt the StatusList2021 DID-Linked Resource.", + "type": "string" + } + } + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedPaymentConditionsJson" + } + ], + "example": { + "did": "did:cheqd:testnet:7c2b990c-3d05-4ebf-91af-f4f4d0091d2e", + "statusListName": "cheqd-employee-credentials-encrypted", + "indices": [ + 10, + 3199, + 12109, + 130999 + ], + "symmetricKey": "dfe204ee95ae74ea5d74b94c3d8ff782273905b07fbc9f8c3d961c3b43849f18" + } + }, + "CredentialStatusUpdateEncryptedResult": { + "allOf": [ + { + "type": "object", + "properties": { + "updated": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/RevocationResult" + }, + { + "$ref": "#/components/schemas/SuspensionResult" + }, + { + "$ref": "#/components/schemas/UnsuspensionResult" + } + ] + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + }, + "CredentialStatusCheckRequest": { + "type": "object", + "required": [ + "did", + "statusListName", + "index" + ], + "properties": { + "did": { + "description": "DID of the StatusList2021 publisher.", + "type": "string", + "format": "uri" + }, + "statusListName": { + "description": "The name of the StatusList2021 DID-Linked Resource to be checked.", + "type": "string" + }, + "index": { + "description": "Credential status index to be checked for revocation or suspension.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": false + }, + "makeFeePayment": { + "description": "Automatically make fee payment (if required) based on payment conditions to unlock encrypted StatusList2021 DID-Linked Resource.", + "type": "boolean", + "default": true + } + } + }, + "CredentialStatusCheckResult": { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusCheckRevocationResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusCheckSuspensionResult" + } + ] + }, + "CredentialStatusCheckRevocationResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "revoked": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusCheckSuspensionResult": { + "type": "object", + "properties": { + "checked": { + "type": "boolean", + "example": true + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, + "CredentialStatusListSearchResult": { + "allOf": [ + { + "type": "object", + "properties": { + "found": { + "type": "boolean", + "example": true + } + } + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/CredentialStatusUnencryptedResult" + }, + { + "$ref": "#/components/schemas/CredentialStatusEncryptedResult" + } + ] + } + ] + }, + "KeyImportRequest": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Ed25519", + "Secp256k1" + ] + }, + "privateKeyHex": { + "type": "string" + } + } + }, + "KeyResult": { + "type": "object", + "properties": { + "kid": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "Ed25519", + "Secp256k1" + ] + }, + "publicKeyHex": { + "type": "string" + } + } + }, + "DidDocument": { + "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See Universal DID Registrar specification.", + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationMethod": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + ], + "verificationMethod": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "publicKeyBase58": "z6MkkVbyHJLLjdjU5B62DaJ4mkdMdUkttf9UqySSkA9bVTeZ" + } + ], + "authentication": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" + ], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + } + }, + "DidDocumentWithoutVerificationMethod": { + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + ], + "authentication": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" + ], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + } + }, + "DidCreateRequestFormBased": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": [ + "testnet", + "mainnet" + ] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": [ + "uuid", + "base58btc" + ] + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "JsonWebKey2020", + "Ed25519VerificationKey2020" + ] + }, + "service": { + "description": "It's a list of special objects which are designed to build the actual service. It's almost the same as in DID Core specification, but instead of `id` it utilises `idFragment` field for making the right `id` for each service. !!! WARN. Cause swagger-ui does not handle x-ww-form based arrays correctly, please frame all your services in brackets while using swagger UI. !!!", + "type": "array", + "items": { + "type": "object", + "properties": { + "idFragment": { + "type": "string" + }, + "type": { + "type": "string" + }, + "serviceEndpoint": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "example": [ + { + "idFragment": "service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + }, + "key": { + "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/ns/did/v1" + ] + } + } + }, + "DidCreateRequestJson": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": [ + "testnet", + "mainnet" + ] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": [ + "uuid", + "base58btc" + ] + }, + "assertionMethod": { + "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", + "type": "boolean", + "default": true + }, + "options": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "JsonWebKey2020", + "Ed25519VerificationKey2020" + ] + } + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" + } + } + }, + "DidImportRequest": { + "type": "object", + "properties": { + "did": { + "type": "string", + "description": "DID to be imported", + "format": "uri", + "required": true + }, + "keys": { + "type": "array", + "description": "List of keys required to import the DID", + "required": true, + "items": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "PresentationCreateResult": { + "type": "object", + "properties": { + "vp": { + "type": "object", + "description": "Verifiable Presentation which could be provided to the verifier." + }, + "nbf": { + "type": "integer", + "description": "Unix timestamp of the earliest time that the Verifiable Presentation is valid." + }, + "iss": { + "type": "string", + "description": "DID of the issuer of the Verifiable Presentation. (Here it's supposed to be a holder DID)" + }, + "aud": { + "type": "array", + "items": { + "type": "string" + }, + "description": "DID of the verifier of the Verifiable Presentation." + } + }, + "example": { + "vp": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc2NoZW1hLm9yZy9zY2hlbWEuanNvbmxkIiwiaHR0cHM6Ly92ZXJhbW8uaW8vY29udGV4dHMvcHJvZmlsZS92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMtc3RhdHVzLWxpc3QtMjAyMS92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiUGVyc29uIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7Im5hbWUiOiJCb2IiLCJnZW5kZXIiOiJtYWxlIn0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL3Jlc29sdmVyLmNoZXFkLm5ldC8xLjAvaWRlbnRpZmllcnMvZGlkOmNoZXFkOnRlc3RuZXQ6OTBkNWMxNDEtNzI0Zi00N2FkLTlhZTctYTdjMzNhOWU1NjQzP3Jlc291cmNlTmFtZT1zdXNwZW5zaW9uRW4mcmVzb3VyY2VUeXBlPVN0YXR1c0xpc3QyMDIxU3VzcGVuc2lvbiMxMzMzOCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InN1c3BlbnNpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxMzMzOCJ9fSwic3ViIjoiZGlkOmtleTp6Nk1raGFYZ0JaRHZvdERrTDUyNTdmYWl6dGlHaUMyUXRLTEdwYm5uRUd0YTJkb0siLCJuYmYiOjE3MDA0NzM0MTYsImlzcyI6ImRpZDpjaGVxZDp0ZXN0bmV0OjkwZDVjMTQxLTcyNGYtNDdhZC05YWU3LWE3YzMzYTllNTY0MyJ9.-14Ril1pZEy2HEEo48gTJr2yOtGxBhUGTFmzVdjAtyhFRsW5zZg9onHt6V9JQ8BaiYBlTkP9GzTnJ-O6hdiyCw" + ] + }, + "nbf": 1700744275, + "iss": "did:cheqd:testnet:4b846d0f-2f6c-4ab6-9fe2-5b8db301c83c", + "aud": [ + "did:cheqd:testnet:8c71e9b6-c5a3-4250-8c58-fa591533cd22" + ] + } + }, + "DidResult": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string" + }, + "keys": { + "type": "array", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + } + } + }, + "DidUpdateResponse": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "controllerKeyId": { + "type": "string", + "description": "The default key id of which is the key associated with the first verificationMethod" + }, + "keys": { + "type": "array", + "description": "The list of keys associated with the list of verificationMethod's of DIDDocument", + "items": { + "type": "object" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "controllerKeyRefs": { + "type": "array", + "description": "The list of keyRefs which were used for signing the transaction", + "items": { + "type": "string" + } + }, + "controllerKeys": { + "type": "array", + "description": "The list of all possible keys, inlcuding all controller's keys", + "items": { + "type": "string" + } + } + } + }, + "VerificationMethod": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "controller": { + "type": "string" + }, + "publicKeyMultibase": { + "type": "string" + }, + "publicKeyJwk": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "controller": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "id": "did:cheqd:testnet :7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1", + "publicKeyBase58": "BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt", + "type": "Ed25519VerificationKey2018" + } + }, + "Service": { + "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", + "type": "object", + "properties": { + "id": { + "description": "DID appended with Service fragment ID (e.g., `#service-1` in `did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1`)", + "type": "string", + "example": "did:cheqd:mainnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1" + }, + "type": { + "description": "Service type as defined in DID Specification Registries.", + "type": "string", + "example": "LinkedDomains" + }, + "serviceEndpoint": { + "description": "Service endpoint as defined in DID Core Specification.", + "type": "array", + "items": { + "type": "string", + "example": "https://example.com" + } + } + } + }, + "DidUpdateRequest": { + "type": "object", + "properties": { + "did": { + "description": "DID identifier to be updated.", + "type": "string", + "example": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + }, + "service": { + "type": "array", + "description": "Service section of the DID Document.", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "verificationMethod": { + "type": "array", + "description": "Verification Method section of the DID Document.", + "items": { + "$ref": "#/components/schemas/VerificationMethod" + } + }, + "authentication": { + "description": "Authentication section of the DID Document.", + "type": "array", + "items": { + "type": "string" + } + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + } + } + }, + "DidDeactivateRequest": { + "type": "object", + "properties": { + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CreateResourceRequest": { + "description": "Input fields for DID-Linked Resource creation.", + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "type", + "data", + "encoding" + ], + "properties": { + "data": { + "description": "Encoded string containing the data to be stored in the DID-Linked Resource.", + "type": "string" + }, + "encoding": { + "description": "Encoding format used to encode the data.", + "type": "string", + "enum": [ + "base64url", + "base64", + "hex" + ] + }, + "name": { + "description": "Name of DID-Linked Resource.", + "type": "string" + }, + "type": { + "description": "Type of DID-Linked Resource. This is NOT the same as the media type, which is calculated automatically ledger-side.", + "type": "string" + }, + "alsoKnownAs": { + "description": "Optional field to assign a set of alternative URIs where the DID-Linked Resource can be fetched from.", + "type": "array", + "items": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "version": { + "description": "Optional field to assign a human-readable version in the DID-Linked Resource.", + "type": "string" + }, + "publicKeyHexs": { + "description": "List of key references (publicKeys) which will be used for signing the message. The should be in hexadecimal format and placed in the wallet of current user.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "data": "SGVsbG8gV29ybGQ=", + "encoding": "base64url", + "name": "ResourceName", + "type": "TextDocument" + } + }, + "ResourceList": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "contentMetadata": { + "type": "object" + }, + "contentStream": { + "type": "object" + }, + "dereferencingMetadata": { + "$ref": "#/components/schemas/DereferencingMetadata" + } + } + }, + "DereferencingMetadata": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "example": "application/did+ld+json" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + } + } + }, + "DidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DidDocumentMetadata" + } + } + }, + "DeactivatedDidResolution": { + "type": "object", + "properties": { + "@context": { + "type": "string", + "example": "https://w3id.org/did-resolution/v1" + }, + "didDidResolutionMetadata": { + "$ref": "#/components/schemas/DidResolutionMetadata" + }, + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + }, + "didDocumentMetadata": { + "$ref": "#/components/schemas/DeactivatedDidDocumentMetadata" + } + } + }, + "DidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": false + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "DeactivatedDidDocumentMetadata": { + "type": "object", + "properties": { + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "deactivated": { + "type": "boolean", + "example": true + }, + "updated": { + "type": "string", + "example": "2021-09-10T12:00:00Z" + }, + "versionId": { + "type": "string", + "example": "3ccde6ba-6ba5-56f2-9f4f-8825561a9860" + }, + "linkedResourceMetadata": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceMetadata" + } + } + } + }, + "ResourceMetadata": { + "type": "object", + "properties": { + "resourceURI": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47/resources/398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceCollectionId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "resourceId": { + "type": "string", + "example": "398cee0a-efac-4643-9f4c-74c48c72a14b" + }, + "resourceName": { + "type": "string", + "example": "cheqd-issuer-logo" + }, + "resourceType": { + "type": "string", + "example": "CredentialArtwork" + }, + "mediaType": { + "type": "string", + "example": "image/png" + }, + "resourceVersion": { + "type": "string", + "example": "1.0" + }, + "checksum": { + "type": "string", + "example": "a95380f460e63ad939541a57aecbfd795fcd37c6d78ee86c885340e33a91b559" + }, + "created": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "nextVersionId": { + "type": "string", + "example": "d4829ac7-4566-478c-a408-b44767eddadc" + }, + "previousVersionId": { + "type": "string", + "example": "ad7a8442-3531-46eb-a024-53953ec6e4ff" + } + } + }, + "DidResolutionMetadata": { + "type": "object", + "properties": { + "contentType": { + "allOf": [ + { + "$ref": "#/components/schemas/ContentType" + } + ], + "example": "application/did+ld+json" + }, + "retrieved": { + "type": "string", + "example": "2021-09-01T12:00:00Z" + }, + "did": { + "$ref": "#/components/schemas/DidProperties" + } + } + }, + "ContentType": { + "type": "string", + "enum": [ + "application/did+json", + "application/did+ld+json", + "application/ld+json", + "application/json" + ] + }, + "DidProperties": { + "type": "object", + "properties": { + "didString": { + "type": "string", + "example": "did:cheqd:testnet:55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + }, + "method": { + "type": "string", + "example": "cheqd" + }, + "methodSpecificId": { + "type": "string", + "example": "55dbc8bf-fba3-4117-855c-1e0dc1d3bb47" + } + } + }, + "Customer": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "example": "6w5drpiiwhhs" + }, + "address": { + "type": "string", + "example": "cheqd1wgsvqwlkmdp60f4dek26ak0sjw6au3ytd3pz7f" + } + } + }, + "AccountCreateRequest": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "primaryEmail": { + "type": "string" + } + } + } + } + }, + "InvalidRequest": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "InvalidRequest" + } + } + }, + "InternalError": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Internal Error" + } + } + }, + "UnauthorizedError": { + "description": "Access token is missing or invalid", + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized Error" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/types/authentication.ts b/src/types/authentication.ts index afb8c8f7..72d6f35e 100644 --- a/src/types/authentication.ts +++ b/src/types/authentication.ts @@ -1,49 +1,51 @@ -export enum Namespaces { - Testnet = 'testnet', - Mainnet = 'mainnet', +import { CheqdNetwork } from '@cheqd/sdk'; +import type { Request } from 'express'; +import { DefaultNetworkPattern } from './shared.js'; +import stringify from 'json-stringify-safe'; +import { InvalidTokenError, jwtDecode } from 'jwt-decode'; + +export type AuthRuleOptions = { + allowUnauthorized?: boolean; + skipNamespace?: boolean; } -export class MethodToScopeRule { +export class AuthRule { private route: string; private method: string; private scope: string; - private options: { - allowUnauthorized: boolean; - skipNamespace: boolean; - }; + private options: AuthRuleOptions; - constructor(route: string, method: string, scope: string, { allowUnauthorized = false, skipNamespace = false }) { + constructor(route: string, method: string, scope: string, options: AuthRuleOptions = { allowUnauthorized: false, skipNamespace: false }) { this.route = route; this.method = method; this.scope = scope; - this.options = { - allowUnauthorized, - skipNamespace, - }; + this.options = options; } public validate(scope: string): boolean { return this.scope === scope; } - public doesRuleMatches(route: string, method: string, namespace = Namespaces.Testnet): boolean { - // If route and method are exactly the same - check scope - if (this.route === route && this.method === method) { - return this.checkScope(namespace); - } - // If route is not exactly the same - check if it matches as an regexp - const matches = route.match(this.route); - if (matches && matches.length > 0 && this.method === method) { - return this.checkScope(namespace); + public match(request: Request): boolean { + const { path, method } = request; + // Route may be /account/create which matches as is + // Also it may be /did/deactivate/:did which matches only with regexp + const directMatch = this.route === path && this.method === method; + const matches = path.match(this.route); + const regexpMatch = matches && matches.length > 0 && this.method === method; + + if (directMatch || regexpMatch) { + return this.matchScope(request); } return false; } - private checkScope(namespace: string): boolean { - // If scope is empty or namespace is not needed - return true - if (this.scope === '' || this.isSkipNamespace()) { + private matchScope(request: Request): boolean { + // if namespace is not required - return true + if (this.isSkipNamespace()) { return true; } + const namespace = this.getNamespaceFromRequest(request); // If namespace is required and it's not provided - return false if (!namespace) { return false; @@ -51,16 +53,99 @@ export class MethodToScopeRule { return this.scope.includes(namespace); } - public getScope(): string { - return this.scope; + public isValidScope(scope: string): boolean { + return this.validate(scope); + } + + public areValidScopes(scopes: string[]): boolean { + for (const scope of scopes) { + if (this.isValidScope(scope)) { + return true; + } + } + return false; + } + + // Utils + protected getNamespaceFromRequest(req: Request): CheqdNetwork | null { + let network: string | null = ''; + + if (req && req.body && req.body.credential) { + const { credential } = req.body; + let decoded = ''; + let issuerDid = ''; + // Try to get issuer DID + if (credential && credential.issuer) { + issuerDid = credential.issuer.id; + } + network = this.findNetworkInBody(issuerDid); + if (network) { + return this.switchNetwork(network); + } + try { + decoded = jwtDecode(req.body.credential); + } catch (e) { + // If it's not a JWT - just skip it + if (!(e instanceof InvalidTokenError)) { + throw e; + } + } + // if not - try to search for decoded credential + network = this.findNetworkInBody(stringify(decoded)); + if (network) { + return this.switchNetwork(network); + } + } + // Try to search in request body + if (req && req.body) { + network = this.findNetworkInBody(stringify(req.body)); + if (network) { + return this.switchNetwork(network); + } + } + // Try to search in request path + if (req && req.path) { + network = this.findNetworkInBody(decodeURIComponent(req.path)); + if (network) { + return this.switchNetwork(network); + } + } + // For DID create we specify it as a separate parameter in body + if (req.body && req.body.network) { + return this.switchNetwork(req.body.network); + } + + return null; + } + + protected findNetworkInBody(body: string): string | null { + const matches = body.match(DefaultNetworkPattern); + if (matches && matches.length > 0) { + return matches[1]; + } + return null; + } + + protected switchNetwork(network: string): CheqdNetwork | null { + switch (network) { + case 'testnet': { + return CheqdNetwork.Testnet; + } + case 'mainnet': { + return CheqdNetwork.Mainnet; + } + default: { + return null; + } + } } public isAllowedUnauthorized(): boolean { - return this.options.allowUnauthorized; + return this.options.allowUnauthorized || false; } public isSkipNamespace(): boolean { - return this.options.skipNamespace; + return this.options.skipNamespace || false; } public isEmpty(): boolean { @@ -68,17 +153,17 @@ export class MethodToScopeRule { } } -export interface IAuthResponse extends ICommonErrorResponse { - status: number; - data: { - userId: string; - customerId: string; - scopes: string[]; - namespace: Namespaces; - isAllowedUnauthorized: boolean; - }; - error: string; -} +// export interface IAuthResponse extends ICommonErrorResponse { +// status: number; +// data: { +// userId: string; +// customerId: string; +// scopes: string[]; +// namespace: CheqdNetwork; +// isAllowedUnauthorized: boolean; +// }; +// error: string; +// } export interface ICommonErrorResponse { status: number; From be14478ef258bdd1affdea42f71c00a3419612d9 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 16 Apr 2024 10:44:24 +0200 Subject: [PATCH 36/49] Change response text and fix tests --- src/middleware/auth/auth-gaurd.ts | 2 +- tests/e2e/credential-status/create-encrypted.spec.ts | 4 +++- tests/e2e/credential-status/create-unencrypted.spec.ts | 4 +++- tests/e2e/credential-status/update-encrypted.spec.ts | 4 +++- tests/e2e/credential-status/update-unencrypted.spec.ts | 4 +++- tests/e2e/credential/issue-negative.spec.ts | 4 +++- tests/e2e/credential/reinstate-negative.spec.ts | 4 +++- tests/e2e/credential/revoke-negative.spec.ts | 4 +++- tests/e2e/credential/suspend-negative.spec.ts | 4 +++- tests/e2e/did/create-negative.spec.ts | 4 +++- tests/e2e/did/deactivate-negative.spec.ts | 4 +++- tests/e2e/did/update-negative.spec.ts | 4 +++- tests/e2e/resource/create-negative.spec.ts | 4 +++- 13 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/middleware/auth/auth-gaurd.ts b/src/middleware/auth/auth-gaurd.ts index 0fb5143f..a61a8380 100644 --- a/src/middleware/auth/auth-gaurd.ts +++ b/src/middleware/auth/auth-gaurd.ts @@ -58,7 +58,7 @@ export class APIGuard { // Checks if the list of scopes from user enough to make an action if (!authRule.areValidScopes(response.locals.scopes)) { return response.status(StatusCodes.FORBIDDEN).send({ - error: `Forbidden. Your account is not authorized to carry out this action.` + error: `Unauthorized error: Your account is not authorized to carry out this action.` } satisfies ValidationErrorResponseBody); } diff --git a/tests/e2e/credential-status/create-encrypted.spec.ts b/tests/e2e/credential-status/create-encrypted.spec.ts index 030e24e3..6bf36a80 100644 --- a/tests/e2e/credential-status/create-encrypted.spec.ts +++ b/tests/e2e/credential-status/create-encrypted.spec.ts @@ -7,6 +7,7 @@ import { import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: STORAGE_STATE_AUTHENTICATED }); @@ -21,5 +22,6 @@ test('[Negative] It cannot create an encrypted statusList2021 in mainnet network }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/credential-status/create-unencrypted.spec.ts b/tests/e2e/credential-status/create-unencrypted.spec.ts index 7a1de0c9..bb571c52 100644 --- a/tests/e2e/credential-status/create-unencrypted.spec.ts +++ b/tests/e2e/credential-status/create-unencrypted.spec.ts @@ -7,6 +7,7 @@ import { import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: STORAGE_STATE_AUTHENTICATED }); @@ -21,5 +22,6 @@ test('[Negative] It cannot create an unencrypted statusList2021 in mainnet netwo }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/credential-status/update-encrypted.spec.ts b/tests/e2e/credential-status/update-encrypted.spec.ts index d74fb299..ca234262 100644 --- a/tests/e2e/credential-status/update-encrypted.spec.ts +++ b/tests/e2e/credential-status/update-encrypted.spec.ts @@ -7,6 +7,7 @@ import { import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: STORAGE_STATE_AUTHENTICATED }); @@ -21,5 +22,6 @@ test('[Negative] It cannot update an encrypted statusList2021 in mainnet network }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/credential-status/update-unencrypted.spec.ts b/tests/e2e/credential-status/update-unencrypted.spec.ts index f1df1e2e..e91b6b4e 100644 --- a/tests/e2e/credential-status/update-unencrypted.spec.ts +++ b/tests/e2e/credential-status/update-unencrypted.spec.ts @@ -7,6 +7,7 @@ import { import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: STORAGE_STATE_AUTHENTICATED }); @@ -21,5 +22,6 @@ test('[Negative] It cannot update an unencrypted statusList2021 in mainnet netwo }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/credential/issue-negative.spec.ts b/tests/e2e/credential/issue-negative.spec.ts index 13f94d15..85e6b9a7 100644 --- a/tests/e2e/credential/issue-negative.spec.ts +++ b/tests/e2e/credential/issue-negative.spec.ts @@ -7,6 +7,7 @@ import { import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: STORAGE_STATE_AUTHENTICATED }); @@ -19,5 +20,6 @@ test('[Negative] It cannot issue credential in mainnet network for user with tes }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/credential/reinstate-negative.spec.ts b/tests/e2e/credential/reinstate-negative.spec.ts index 21b93367..fbba0760 100644 --- a/tests/e2e/credential/reinstate-negative.spec.ts +++ b/tests/e2e/credential/reinstate-negative.spec.ts @@ -7,6 +7,7 @@ import { import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: STORAGE_STATE_AUTHENTICATED }); @@ -19,5 +20,6 @@ test('[Negative] It cannot reinstate credential in mainnet network for user with }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/credential/revoke-negative.spec.ts b/tests/e2e/credential/revoke-negative.spec.ts index 435261c1..307b6a08 100644 --- a/tests/e2e/credential/revoke-negative.spec.ts +++ b/tests/e2e/credential/revoke-negative.spec.ts @@ -7,6 +7,7 @@ import { import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: STORAGE_STATE_AUTHENTICATED }); @@ -19,5 +20,6 @@ test('[Negative] It cannot revoke credential in mainnet network for user with te }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/credential/suspend-negative.spec.ts b/tests/e2e/credential/suspend-negative.spec.ts index 7014288e..f01fae74 100644 --- a/tests/e2e/credential/suspend-negative.spec.ts +++ b/tests/e2e/credential/suspend-negative.spec.ts @@ -7,6 +7,7 @@ import { import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: STORAGE_STATE_AUTHENTICATED }); @@ -19,5 +20,6 @@ test('[Negative] It cannot suspend credential in mainnet network for user with t }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/did/create-negative.spec.ts b/tests/e2e/did/create-negative.spec.ts index 9fead56d..02638bde 100644 --- a/tests/e2e/did/create-negative.spec.ts +++ b/tests/e2e/did/create-negative.spec.ts @@ -13,6 +13,7 @@ import { v4 } from 'uuid'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; import { CheqdNetwork, VerificationMethods } from '@cheqd/sdk'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: 'playwright/.auth/user.json' }); @@ -180,5 +181,6 @@ test('[Negative] It cannot create DID in mainnet network for user with testnet r }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/did/deactivate-negative.spec.ts b/tests/e2e/did/deactivate-negative.spec.ts index 0ed1482f..f4c9fa35 100644 --- a/tests/e2e/did/deactivate-negative.spec.ts +++ b/tests/e2e/did/deactivate-negative.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; import { DEFAULT_DOES_NOT_HAVE_PERMISSIONS, DEFAULT_MAINNET_DID } from '../constants'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: 'playwright/.auth/user.json' }); @@ -8,5 +9,6 @@ test('[Negative] It cannot deactivated DID in mainnet network for user with test const response = await request.post(`/did/deactivate/${DEFAULT_MAINNET_DID}`, {}); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/did/update-negative.spec.ts b/tests/e2e/did/update-negative.spec.ts index 3f921e22..fbe76981 100644 --- a/tests/e2e/did/update-negative.spec.ts +++ b/tests/e2e/did/update-negative.spec.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; import { CONTENT_TYPE, DEFAULT_DOES_NOT_HAVE_PERMISSIONS, PAYLOADS_PATH } from '../constants'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: 'playwright/.auth/user.json' }); @@ -14,5 +15,6 @@ test('[Negative] It cannot update DID in mainnet network for user with testnet r }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); diff --git a/tests/e2e/resource/create-negative.spec.ts b/tests/e2e/resource/create-negative.spec.ts index 73aacec7..44dead77 100644 --- a/tests/e2e/resource/create-negative.spec.ts +++ b/tests/e2e/resource/create-negative.spec.ts @@ -8,6 +8,7 @@ import { import * as fs from 'fs'; import { test, expect } from '@playwright/test'; import { StatusCodes } from 'http-status-codes'; +import { UnsuccessfulResponseBody } from '@cheqd/credential-service/src/types/shared.js'; test.use({ storageState: STORAGE_STATE_AUTHENTICATED }); @@ -18,5 +19,6 @@ test('[Negative] It cannot create resource in mainnet network for user with test }); expect(response).not.toBeOK(); expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); + const { error} = (await response.json()) as UnsuccessfulResponseBody; + expect(error).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); }); From a612f2daebdb8b6af659bb0d36722b94398178aa Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 16 Apr 2024 11:22:51 +0200 Subject: [PATCH 37/49] Add some logs --- src/controllers/admin/api-key.ts | 6 +-- src/controllers/admin/webhook.ts | 16 +++---- src/controllers/api/account.ts | 45 ++++++++++++++++++- src/middleware/auth/logto-helper.ts | 2 +- src/services/admin/stripe.ts | 10 ++--- .../track/admin/subscription-submitter.ts | 20 ++++----- 6 files changed, 71 insertions(+), 28 deletions(-) diff --git a/src/controllers/admin/api-key.ts b/src/controllers/admin/api-key.ts index 3786cc55..047b8768 100644 --- a/src/controllers/admin/api-key.ts +++ b/src/controllers/admin/api-key.ts @@ -134,7 +134,7 @@ export class APIKeyController { message: 'Cannot create a new API key', }); } - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `API key for customer: ${response.locals.customer.customerId} has been created`, OperationNameEnum.API_KEY_CREATE @@ -203,7 +203,7 @@ export class APIKeyController { } satisfies APIKeyUpdateUnsuccessfulResponseBody); } - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `API key for customer: ${response.locals.customer.customerId} has been updated`, OperationNameEnum.API_KEY_UPDATE @@ -263,7 +263,7 @@ export class APIKeyController { error: 'Cannot revoke API key', } satisfies APIKeyRevokeUnsuccessfulResponseBody); } - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `API key for customer: ${response.locals.customer.customerId} has been revoked`, OperationNameEnum.API_KEY_REVOKE diff --git a/src/controllers/admin/webhook.ts b/src/controllers/admin/webhook.ts index 8b5f0007..96a11e5c 100644 --- a/src/controllers/admin/webhook.ts +++ b/src/controllers/admin/webhook.ts @@ -18,7 +18,7 @@ export class WebhookController { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); if (!process.env.STRIPE_WEBHOOK_SECRET) { - await eventTracker.notify({ + await await eventTracker.notify({ message: 'Stripe webhook secret not found. Webhook ID: ${request.body.id}.', severity: 'error', } satisfies INotifyMessage); @@ -27,7 +27,7 @@ export class WebhookController { const signature = request.headers['stripe-signature']; if (!signature) { - await eventTracker.notify({ + await await eventTracker.notify({ message: 'Webhook signature not found. Webhook ID: ${request.body.id}.', severity: 'error', } satisfies INotifyMessage); @@ -37,7 +37,7 @@ export class WebhookController { try { event = stripe.webhooks.constructEvent(request.rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { - await eventTracker.notify({ + await await eventTracker.notify({ message: `Webhook signature verification failed. Webhook ID: ${request.body.id}. Error: ${(err as Record)?.message || err}`, severity: 'error', } satisfies INotifyMessage); @@ -48,7 +48,7 @@ export class WebhookController { case 'customer.subscription.trial_will_end': subscription = event.data.object; status = subscription.status; - await eventTracker.notify({ + await await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.trial_will_end' @@ -62,7 +62,7 @@ export class WebhookController { case 'customer.subscription.deleted': subscription = event.data.object; status = subscription.status; - await eventTracker.notify({ + await await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.deleted' @@ -74,7 +74,7 @@ export class WebhookController { case 'customer.subscription.created': subscription = event.data.object; status = subscription.status; - await eventTracker.notify({ + await await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.created' @@ -86,7 +86,7 @@ export class WebhookController { case 'customer.subscription.updated': subscription = event.data.object; status = subscription.status; - await eventTracker.notify({ + await await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.updated' @@ -97,7 +97,7 @@ export class WebhookController { break; default: // Unexpected event type - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Unexpected event: ${event} with type: ${event?.type}`, 'Stripe Webhook: unexpected' diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 16af974a..ddf1d128 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -21,7 +21,7 @@ import type { } from '../../types/customer.js'; import type { UnsuccessfulResponseBody } from '../../types/shared.js'; import { check, validationResult } from 'express-validator'; -import { eventTracker } from '../../services/track/tracker.js'; +import { EventTracker, eventTracker } from '../../services/track/tracker.js'; import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from '../../services/track/submitter.js'; import * as dotenv from 'dotenv'; dotenv.config(); @@ -194,6 +194,13 @@ export class AccountController { error: 'User is not found in db: Customer was not created', } satisfies UnsuccessfulResponseBody); } + // Notify + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + 'User was not found in db: Customer with customerId: ' + customer.customerId + ' was created', + ), + severity: 'info' + }) // 2.2. Create user user = await UserService.instance.create(logToUserId, customer, defaultRole); if (!user) { @@ -201,6 +208,13 @@ export class AccountController { error: 'User is not found in db: User was not created', } satisfies UnsuccessfulResponseBody); } + // Notify + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + 'User was not found in db: User with userId: ' + user.logToId + ' was created', + ), + severity: 'info' + }) } // 3. If yes - check that there is customer associated with such user if (!user.customer) { @@ -212,6 +226,13 @@ export class AccountController { error: 'User exists in db: Customer was not created', } satisfies UnsuccessfulResponseBody); } + // Notify + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + 'User exists in db: Customer with customerId: ' + customer.customerId + ' was created', + ), + severity: 'info' + }) // 3.1.2. Assign customer to the user user.customer = customer; await UserService.instance.update(user.logToId, customer); @@ -242,6 +263,13 @@ export class AccountController { error: 'PaymentAccount is not found in db: Payment account was not created', } satisfies UnsuccessfulResponseBody); } + // Notify + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + 'PaymentAccount was not found in db: Payment account with address: ' + paymentAccount.address + ' was created', + ), + severity: 'info' + }) } else { paymentAccount = accounts[0]; } @@ -270,6 +298,14 @@ export class AccountController { error: _r.error, } satisfies UnsuccessfulResponseBody); } + + // Notify + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Default role with id: ${process.env.LOGTO_DEFAULT_ROLE_ID} was assigned to user with id: ${user.logToId}`, + ), + severity: 'info' + }) } const customDataFromLogTo = await logToHelper.getCustomData(user.logToId); @@ -305,6 +341,13 @@ export class AccountController { error: resp.error, } satisfies UnsuccessfulResponseBody); } + // Notify + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Testnet account with address: ${paymentAccount.address} was funded with ${TESTNET_MINIMUM_BALANCE}`, + ), + severity: 'info' + }) } } // 8. Add the Stripe account to the Customer diff --git a/src/middleware/auth/logto-helper.ts b/src/middleware/auth/logto-helper.ts index e20de9a0..b8fba6f6 100644 --- a/src/middleware/auth/logto-helper.ts +++ b/src/middleware/auth/logto-helper.ts @@ -50,7 +50,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { if (response.status === StatusCodes.OK) { return this.m2mToken; } - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( 'Failed to get M2M token, Attempt ' + i + ' of ' + this.m2mGetTokenAttempts, 'M2M token issuing' diff --git a/src/services/admin/stripe.ts b/src/services/admin/stripe.ts index a3bba0f0..7cda2d87 100644 --- a/src/services/admin/stripe.ts +++ b/src/services/admin/stripe.ts @@ -42,7 +42,7 @@ export class StripeService { await this.createSubscription(subscription); } } - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription syncronization completed`, 'Subscription syncronization' @@ -74,7 +74,7 @@ export class StripeService { const local = await SubscriptionService.instance.findCurrent(customer); if (!local) { - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Active subscription not found for customer with id ${customer.customerId}`, 'Subscription syncronization' @@ -91,7 +91,7 @@ export class StripeService { }); const subs = [...activeSubs.data, ...trialSubs.data]; if (subs.length > 1) { - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Multiple active subscriptions found for customer with id ${customer.customerId}`, 'Subscription syncronization' @@ -108,7 +108,7 @@ export class StripeService { const subscriptionId = local.subscriptionId; const remote = await stripe.subscriptions.retrieve(subscriptionId); if (!remote) { - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription with id ${subscriptionId} could not be retrieved from Stripe`, 'Subscription syncronization' @@ -136,7 +136,7 @@ export class StripeService { async updateSubscription(subscription: Stripe.Subscription, current: SubscriptionEntity): Promise { // Update only if there are changes if (SubscriptionService.instance.equals(current, subscription)) { - eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription with id ${subscription.id} has no changes`, 'Subscription syncronization' diff --git a/src/services/track/admin/subscription-submitter.ts b/src/services/track/admin/subscription-submitter.ts index bf808a77..8e9d0a11 100644 --- a/src/services/track/admin/subscription-submitter.ts +++ b/src/services/track/admin/subscription-submitter.ts @@ -44,7 +44,7 @@ export class SubscriptionSubmitter implements IObserver { }); if (customers.length !== 1) { - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `It should be only 1 Stripe account associated with CaaS customer. Stripe accountId: ${data.paymentProviderId}.`, operation.operation @@ -65,7 +65,7 @@ export class SubscriptionSubmitter implements IObserver { data.trialEnd as Date ); if (!subscription) { - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `Failed to create a new subscription with id: ${data.subscriptionId}.`, operation.operation @@ -74,7 +74,7 @@ export class SubscriptionSubmitter implements IObserver { }); } - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `Subscription created with id: ${data.subscriptionId}.`, operation.operation @@ -82,7 +82,7 @@ export class SubscriptionSubmitter implements IObserver { severity: 'info', }); } catch (error) { - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `Failed to create a new subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation @@ -104,7 +104,7 @@ export class SubscriptionSubmitter implements IObserver { data.trialEnd as Date ); if (!subscription) { - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `Failed to update subscription with id: ${data.subscriptionId}.`, operation.operation @@ -113,7 +113,7 @@ export class SubscriptionSubmitter implements IObserver { }); } - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `Subscription updated with id: ${data.subscriptionId}.`, operation.operation @@ -121,7 +121,7 @@ export class SubscriptionSubmitter implements IObserver { severity: 'info', }); } catch (error) { - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `Failed to update subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation @@ -136,7 +136,7 @@ export class SubscriptionSubmitter implements IObserver { try { const subscription = await SubscriptionService.instance.update(data.subscriptionId, data.status); if (!subscription) { - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `Failed to cancel subscription with id: ${data.subscriptionId}.`, operation.operation @@ -145,7 +145,7 @@ export class SubscriptionSubmitter implements IObserver { }); } - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `Subscription canceled with id: ${data.subscriptionId}.`, operation.operation @@ -153,7 +153,7 @@ export class SubscriptionSubmitter implements IObserver { severity: 'info', }); } catch (error) { - this.notify({ + await this.notify({ message: EventTracker.compileBasicNotification( `Failed to cancel subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation From 29f5b6f3217919766a14977c7d09c72048e9e049 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 16 Apr 2024 13:19:33 +0200 Subject: [PATCH 38/49] Some refactorings --- src/app.ts | 2 +- src/controllers/api/account.ts | 11 +-- src/controllers/api/credential-status.ts | 70 +++---------------- src/controllers/api/credential.ts | 52 ++------------ src/controllers/api/did.ts | 48 ++----------- src/controllers/api/key.ts | 11 +++ src/controllers/api/presentation.ts | 25 ++----- src/controllers/api/resource.ts | 25 ++----- src/middleware/auth/auth-gaurd.ts | 3 +- .../auth/routes/admin/admin-auth.ts | 2 +- .../auth/routes/api/account-auth.ts | 2 +- .../auth/routes/api/auth-user-info.ts | 2 +- .../auth/routes/api/credential-auth.ts | 2 +- .../auth/routes/api/credential-status-auth.ts | 2 +- src/middleware/auth/routes/api/did-auth.ts | 2 +- src/middleware/auth/routes/api/key-auth.ts | 2 +- .../auth/routes/api/presentation-auth.ts | 2 +- .../auth/routes/api/resource-auth.ts | 2 +- .../auth/{ => routes}/auth-rule-provider.ts | 31 +------- .../auth/routes/auth-rule-repository.ts | 34 +++++++++ src/middleware/auth/routine.ts | 9 --- .../auth/user-info-fetcher/api-token.ts | 12 +++- .../auth/user-info-fetcher/idtoken.ts | 7 ++ .../auth/user-info-fetcher/m2m-creds-token.ts | 7 ++ .../auth/user-info-fetcher/portal-token.ts | 7 ++ .../auth/user-info-fetcher/swagger-ui.ts | 7 ++ src/middleware/authentication.ts | 2 +- src/types/shared.ts | 6 +- 28 files changed, 134 insertions(+), 253 deletions(-) rename src/middleware/auth/{ => routes}/auth-rule-provider.ts (62%) create mode 100644 src/middleware/auth/routes/auth-rule-repository.ts delete mode 100644 src/middleware/auth/routine.ts diff --git a/src/app.ts b/src/app.ts index 0cb65d33..b17b6d7a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -183,7 +183,7 @@ class App { // Keys API app.post('/key/create', new KeyController().createKey); app.post('/key/import', KeyController.keyImportValidator, new KeyController().importKey); - app.get('/key/read/:kid', new KeyController().getKey); + app.get('/key/read/:kid',KeyController.keyGetValidator, new KeyController().getKey); // DIDs API app.post('/did/create', DIDController.createDIDValidator, new DIDController().createDid); diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index ddf1d128..ea3c402f 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -20,10 +20,11 @@ import type { UnsuccessfulQueryIdTokenResponseBody, } from '../../types/customer.js'; import type { UnsuccessfulResponseBody } from '../../types/shared.js'; -import { check, validationResult } from 'express-validator'; +import { check } from 'express-validator'; import { EventTracker, eventTracker } from '../../services/track/tracker.js'; import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from '../../services/track/submitter.js'; import * as dotenv from 'dotenv'; +import { validate } from '../validator/decorator.js'; dotenv.config(); export class AccountController { @@ -394,6 +395,7 @@ export class AccountController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async create(request: Request, response: Response) { // For now we keep temporary 1-1 relation between user and customer // So the flow is: @@ -407,13 +409,6 @@ export class AccountController { let paymentAccount: PaymentAccountEntity | null; // 1. Get logTo UserId from request body - // validate request - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ error: result.array().pop()?.msg }); - } - const { username } = request.body; try { diff --git a/src/controllers/api/credential-status.ts b/src/controllers/api/credential-status.ts index 480c2493..484915ca 100644 --- a/src/controllers/api/credential-status.ts +++ b/src/controllers/api/credential-status.ts @@ -1,9 +1,8 @@ import type { Request, Response } from 'express'; -import { check, validationResult, query } from '../validator/index.js'; +import { check, query } from '../validator/index.js'; import { fromString } from 'uint8arrays'; import { StatusCodes } from 'http-status-codes'; import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; -import type { ValidationErrorResponseBody } from '../../types/shared.js'; import type { CheckStatusListSuccessfulResponseBody, FeePaymentOptions } from '../../types/credential-status.js'; import { DefaultStatusAction, @@ -49,6 +48,7 @@ import { eventTracker } from '../../services/track/tracker.js'; import type { ICredentialStatusTrack, ITrackOperation, IFeePaymentOptions } from '../../types/track.js'; import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; import { FeeAnalyzer } from '../../helpers/fee-analyzer.js'; +import { validate } from '../validator/decorator.js'; export class CredentialStatusController { static createUnencryptedValidator = [ @@ -492,17 +492,8 @@ export class CredentialStatusController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate async createUnencryptedStatusList(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // collect request parameters - case: body const { did, encodedList, statusListName, alsoKnownAs, statusListVersion, length, encoding } = request.body as CreateUnencryptedStatusListRequestBody; @@ -619,17 +610,8 @@ export class CredentialStatusController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate async createEncryptedStatusList(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // collect request parameters - case: body const { did, @@ -746,17 +728,8 @@ export class CredentialStatusController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate async updateUnencryptedStatusList(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // collect request parameters - case: body const { did, statusListName, statusListVersion, indices } = request.body as UpdateUnencryptedStatusListRequestBody; @@ -923,17 +896,8 @@ export class CredentialStatusController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate async updateEncryptedStatusList(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // collect request parameters - case: body const { did, @@ -1116,17 +1080,8 @@ export class CredentialStatusController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate async checkStatusList(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array()[0].msg, - } satisfies ValidationErrorResponseBody); - } - const feePaymentOptions: IFeePaymentOptions[] = []; // Make the base body for tracking @@ -1322,17 +1277,8 @@ export class CredentialStatusController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate async searchStatusList(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // collect request parameters - case: query const { did, statusListName, statusPurpose } = request.query as SearchStatusListQuery; diff --git a/src/controllers/api/credential.ts b/src/controllers/api/credential.ts index 7cbea5f1..040a90e3 100644 --- a/src/controllers/api/credential.ts +++ b/src/controllers/api/credential.ts @@ -2,11 +2,10 @@ import type { Request, Response } from 'express'; import type { VerifiableCredential } from '@veramo/core'; import { StatusCodes } from 'http-status-codes'; -import { check, validationResult, query } from '../validator/index.js'; +import { check, query } from '../validator/index.js'; import { Credentials } from '../../services/api/credentials.js'; import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; -import type { ValidationErrorResponseBody } from '../../types/shared.js'; import { CheqdW3CVerifiableCredential } from '../../services/w3c-credential.js'; import { isCredentialIssuerDidDeactivated } from '../../services/helpers.js'; import type { @@ -35,6 +34,7 @@ import { Cheqd } from '@cheqd/did-provider-cheqd'; import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; import { eventTracker } from '../../services/track/tracker.js'; import type { ICredentialStatusTrack, ICredentialTrack, ITrackOperation } from '../../types/track.js'; +import { validate } from '../validator/decorator.js'; export class CredentialController { public static issueValidator = [ @@ -144,16 +144,8 @@ export class CredentialController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async issue(request: Request, response: Response) { - // validate request - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // Get request body const requestBody = request.body as IssueCredentialRequestBody; @@ -260,16 +252,8 @@ export class CredentialController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async verify(request: Request, response: Response) { - // validate request - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // Get params from request const { credential, policies } = request.body as VerifyCredentialRequestBody; const { verifyStatus, allowDeactivatedDid } = request.query as VerifyCredentialRequestQuery; @@ -363,15 +347,8 @@ export class CredentialController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async revoke(request: Request, response: Response) { - // validate request - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies UnsuccesfulRevokeCredentialResponseBody); - } // Get publish flag const { publish } = request.query as RevokeCredentialRequestQuery; // Get symmetric key @@ -463,16 +440,8 @@ export class CredentialController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async suspend(request: Request, response: Response) { - // validate request - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies UnsuccesfulSuspendCredentialResponseBody); - } - // Get publish flag const { publish } = request.query as SuspendCredentialRequestQuery; // Get symmetric key @@ -565,15 +534,8 @@ export class CredentialController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async reinstate(request: Request, response: Response) { - // validate request - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies UnsuccesfulUnsuspendCredentialResponseBody); - } // Get publish flag const { publish } = request.query as UnsuspendCredentialRequestQuery; // Get symmetric key diff --git a/src/controllers/api/did.ts b/src/controllers/api/did.ts index d41e06f9..b58588be 100644 --- a/src/controllers/api/did.ts +++ b/src/controllers/api/did.ts @@ -33,10 +33,9 @@ import type { ResolveDIDRequestParams, DeactivateDIDRequestBody, } from '../../types/did.js'; -import { check, validationResult, param } from '../validator/index.js'; +import { check, param } from '../validator/index.js'; import type { IKey, RequireOnly } from '@veramo/core'; import { extractPublicKeyHex } from '@veramo/utils'; -import type { ValidationErrorResponseBody } from '../../types/shared.js'; import type { KeyImport } from '../../types/key.js'; import { eventTracker } from '../../services/track/tracker.js'; import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; @@ -44,6 +43,7 @@ import type { IDIDTrack, ITrackOperation } from '../../types/track.js'; import { arePublicKeyHexsInWallet } from '../../services/helpers.js'; import { CheqdProviderErrorCodes } from '@cheqd/did-provider-cheqd'; import type { CheqdProviderError } from '@cheqd/did-provider-cheqd'; +import { validate } from '../validator/decorator.js'; export class DIDController { public static createDIDValidator = [ @@ -169,17 +169,8 @@ export class DIDController { * example: * error: Internal Error */ + @validate public async createDid(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // handle request params const { identifierFormatType, network, verificationMethodType, service, key, options } = request.body satisfies CreateDidRequestBody; @@ -320,17 +311,8 @@ export class DIDController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async updateDid(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // handle request params const { did, service, verificationMethod, authentication, publicKeyHexs } = request.body as UpdateDidRequestBody; @@ -462,17 +444,8 @@ export class DIDController { * example: * error: Internal Error */ + @validate public async importDid(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - try { // Get the params from body const { did, controllerKeyId, keys } = request.body as ImportDidRequestBody; @@ -584,17 +557,8 @@ export class DIDController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async deactivateDid(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // Extract did from request params const { did } = request.params as DeactivateDIDRequestParams; const { publicKeyHexs } = request.body as DeactivateDIDRequestBody; diff --git a/src/controllers/api/key.ts b/src/controllers/api/key.ts index d480178d..07f3850c 100644 --- a/src/controllers/api/key.ts +++ b/src/controllers/api/key.ts @@ -17,10 +17,19 @@ import { check } from '../validator/index.js'; import { eventTracker } from '../../services/track/tracker.js'; import type { IKeyTrack, ITrackOperation } from '../../types/track.js'; import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; +import { validate } from '../validator/decorator.js'; // ToDo: Make the format of /key/create and /key/read the same // ToDo: Add valdiation for /key/import export class KeyController { + public static keyGetValidator = [ + check('kid') + .exists() + .withMessage('keyId was not provided') + .isHexadecimal() + .withMessage('keyId should be a hexadecimal string') + .bail(), + ] public static keyImportValidator = [ check('privateKeyHex') .exists() @@ -140,6 +149,7 @@ export class KeyController { * example: * error: Internal Error */ + @validate public async importKey(request: Request, response: Response) { // Get parameters requeired for key importing const { type, encrypted = false, ivHex, salt, alias, privateKeyHex } = request.body as ImportKeyRequestBody; @@ -224,6 +234,7 @@ export class KeyController { * example: * error: Internal Error */ + @validate public async getKey(request: Request, response: Response) { const { kid } = request.params as GetKeyRequestBody; // Get strategy e.g. postgres or local diff --git a/src/controllers/api/presentation.ts b/src/controllers/api/presentation.ts index d3ec020e..6d49cc47 100644 --- a/src/controllers/api/presentation.ts +++ b/src/controllers/api/presentation.ts @@ -1,7 +1,7 @@ import type { Request, Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import { check, validationResult, query } from '../validator/index.js'; +import { check, query } from '../validator/index.js'; import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; import { CheqdW3CVerifiablePresentation } from '../../services/w3c-presentation.js'; import type { @@ -14,10 +14,10 @@ import type { VerifyPresentationResponseQuery, } from '../../types/presentation.js'; import { isIssuerDidDeactivated } from '../../services/helpers.js'; -import type { ValidationErrorResponseBody } from '../../types/shared.js'; import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; import { eventTracker } from '../../services/track/tracker.js'; import type { IFeePaymentOptions, IPresentationTrack, ITrackOperation } from '../../types/track.js'; +import { validate } from '../validator/decorator.js'; export class PresentationController { public static presentationCreateValidator = [ @@ -95,17 +95,8 @@ export class PresentationController { * 500: * $ref: '#/components/schemas/InternalError' */ - + @validate public async createPresentation(request: Request, response: Response) { - // Validate request - const result = validationResult(request); - // Handle validation error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array()[0].msg, - } satisfies ValidationErrorResponseBody); - } - const { credentials, holderDid, verifierDid } = request.body as CreatePresentationRequestBody; try { @@ -183,16 +174,10 @@ export class PresentationController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async verifyPresentation(request: Request, response: Response) { - const result = validationResult(request); - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array()[0].msg, - } satisfies UnsuccessfulVerifyCredentialResponseBody); - } - + // Set fee payment options let feePaymentOptions: IFeePaymentOptions[] = []; - // Make the base body for tracking const trackInfo = { name: OperationNameEnum.PRESENTATION_VERIFY, diff --git a/src/controllers/api/resource.ts b/src/controllers/api/resource.ts index 5251fd5b..858613ba 100644 --- a/src/controllers/api/resource.ts +++ b/src/controllers/api/resource.ts @@ -6,10 +6,9 @@ import { StatusCodes } from 'http-status-codes'; import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; import { getQueryParams } from '../../helpers/helpers.js'; import { DIDMetadataDereferencingResult, DefaultResolverUrl } from '@cheqd/did-provider-cheqd'; -import type { ValidationErrorResponseBody } from '../../types/shared.js'; import type { IResourceTrack, ITrackOperation } from '../../types/track.js'; import { OperationCategoryNameEnum, OperationNameEnum } from '../../types/constants.js'; -import { check, validationResult, param, query } from '../validator/index.js'; +import { check, param, query } from '../validator/index.js'; import type { CreateResourceRequestBody, CreateResourceResponseBody, @@ -20,6 +19,7 @@ import type { } from '../../types/resource.js'; import { eventTracker } from '../../services/track/tracker.js'; import { arePublicKeyHexsInWallet } from '../../services/helpers.js'; +import { validate } from '../validator/decorator.js'; export class ResourceController { public static createResourceValidator = [ @@ -142,17 +142,8 @@ export class ResourceController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async createResource(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } - // Extract the did from the request const { did } = request.params; // Extract the resource parameters from the request @@ -306,16 +297,8 @@ export class ResourceController { * 500: * $ref: '#/components/schemas/InternalError' */ + @validate public async searchResource(request: Request, response: Response) { - // validate request - const result = validationResult(request); - - // handle error - if (!result.isEmpty()) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: result.array().pop()?.msg, - } satisfies ValidationErrorResponseBody); - } const { did } = request.params as SearchResourceRequestParams; // Get strategy e.g. postgres or local const identityServiceStrategySetup = new IdentityServiceStrategySetup(); diff --git a/src/middleware/auth/auth-gaurd.ts b/src/middleware/auth/auth-gaurd.ts index a61a8380..2fefa32d 100644 --- a/src/middleware/auth/auth-gaurd.ts +++ b/src/middleware/auth/auth-gaurd.ts @@ -1,5 +1,5 @@ import { StatusCodes } from "http-status-codes"; -import type { AuthRuleRepository } from "./auth-rule-provider.js"; +import type { AuthRuleRepository } from "./routes/auth-rule-repository.js"; import type { NextFunction, Request, Response } from "express"; import type { ValidationErrorResponseBody } from "../../types/shared.js"; import type { IUserInfoFetcher } from "./user-info-fetcher/base.js"; @@ -42,6 +42,7 @@ export class APIGuard { } satisfies ValidationErrorResponseBody); } + // There some requests where API guarding is not needed if (authRule.isAllowedUnauthorized()) { return next(); } diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index a9e4d7e0..b2c720b4 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -1,4 +1,4 @@ -import { AuthRuleProvider } from '../../auth-rule-provider.js'; +import { AuthRuleProvider } from '../auth-rule-provider.js'; export class AdminAuthRuleProvider extends AuthRuleProvider { /** diff --git a/src/middleware/auth/routes/api/account-auth.ts b/src/middleware/auth/routes/api/account-auth.ts index 74c7dd90..016efad6 100644 --- a/src/middleware/auth/routes/api/account-auth.ts +++ b/src/middleware/auth/routes/api/account-auth.ts @@ -1,4 +1,4 @@ -import { AuthRuleProvider } from '../../auth-rule-provider.js'; +import { AuthRuleProvider } from '../auth-rule-provider.js'; export class AccountAuthProvider extends AuthRuleProvider { constructor() { diff --git a/src/middleware/auth/routes/api/auth-user-info.ts b/src/middleware/auth/routes/api/auth-user-info.ts index e662c923..79489fa8 100644 --- a/src/middleware/auth/routes/api/auth-user-info.ts +++ b/src/middleware/auth/routes/api/auth-user-info.ts @@ -1,4 +1,4 @@ -import { AuthRuleProvider } from '../../auth-rule-provider.js'; +import { AuthRuleProvider } from '../auth-rule-provider.js'; export class AuthInfoProvider extends AuthRuleProvider { constructor() { diff --git a/src/middleware/auth/routes/api/credential-auth.ts b/src/middleware/auth/routes/api/credential-auth.ts index a5a74b9c..28c7badd 100644 --- a/src/middleware/auth/routes/api/credential-auth.ts +++ b/src/middleware/auth/routes/api/credential-auth.ts @@ -1,4 +1,4 @@ -import { AuthRuleProvider } from "../../auth-rule-provider.js"; +import { AuthRuleProvider } from "../auth-rule-provider.js"; export class CredentialAuthRuleProvider extends AuthRuleProvider { constructor() { diff --git a/src/middleware/auth/routes/api/credential-status-auth.ts b/src/middleware/auth/routes/api/credential-status-auth.ts index 2e03a3b0..dc838c84 100644 --- a/src/middleware/auth/routes/api/credential-status-auth.ts +++ b/src/middleware/auth/routes/api/credential-status-auth.ts @@ -1,4 +1,4 @@ -import { AuthRuleProvider } from "../../auth-rule-provider.js"; +import { AuthRuleProvider } from "../auth-rule-provider.js"; export class CredentialStatusAuthRuleProvider extends AuthRuleProvider { constructor() { diff --git a/src/middleware/auth/routes/api/did-auth.ts b/src/middleware/auth/routes/api/did-auth.ts index af24927a..332d6465 100644 --- a/src/middleware/auth/routes/api/did-auth.ts +++ b/src/middleware/auth/routes/api/did-auth.ts @@ -1,4 +1,4 @@ -import { AuthRuleProvider } from "../../auth-rule-provider.js"; +import { AuthRuleProvider } from "../auth-rule-provider.js"; export class DidAuthRuleProvider extends AuthRuleProvider { constructor() { diff --git a/src/middleware/auth/routes/api/key-auth.ts b/src/middleware/auth/routes/api/key-auth.ts index 79a5acf1..e4338956 100644 --- a/src/middleware/auth/routes/api/key-auth.ts +++ b/src/middleware/auth/routes/api/key-auth.ts @@ -1,4 +1,4 @@ -import { AuthRuleProvider } from "../../auth-rule-provider.js"; +import { AuthRuleProvider } from "../auth-rule-provider.js"; export class KeyAuthProvider extends AuthRuleProvider { constructor() { diff --git a/src/middleware/auth/routes/api/presentation-auth.ts b/src/middleware/auth/routes/api/presentation-auth.ts index 9ca96ea1..27310169 100644 --- a/src/middleware/auth/routes/api/presentation-auth.ts +++ b/src/middleware/auth/routes/api/presentation-auth.ts @@ -1,4 +1,4 @@ -import { AuthRuleProvider } from "../../auth-rule-provider.js"; +import { AuthRuleProvider } from "../auth-rule-provider.js"; export class PresentationAuthRuleProvider extends AuthRuleProvider { constructor() { diff --git a/src/middleware/auth/routes/api/resource-auth.ts b/src/middleware/auth/routes/api/resource-auth.ts index eaf23ae7..68e7b5a1 100644 --- a/src/middleware/auth/routes/api/resource-auth.ts +++ b/src/middleware/auth/routes/api/resource-auth.ts @@ -1,4 +1,4 @@ -import { AuthRuleProvider } from "../../auth-rule-provider.js"; +import { AuthRuleProvider } from "../auth-rule-provider.js"; export class ResourceAuthRuleProvider extends AuthRuleProvider { constructor() { diff --git a/src/middleware/auth/auth-rule-provider.ts b/src/middleware/auth/routes/auth-rule-provider.ts similarity index 62% rename from src/middleware/auth/auth-rule-provider.ts rename to src/middleware/auth/routes/auth-rule-provider.ts index 0849dda0..a7b217ba 100644 --- a/src/middleware/auth/auth-rule-provider.ts +++ b/src/middleware/auth/routes/auth-rule-provider.ts @@ -1,4 +1,4 @@ -import { AuthRule, AuthRuleOptions } from "../../types/authentication.js"; +import { AuthRule, AuthRuleOptions } from "../../../types/authentication.js"; import type { Request } from "express"; export interface IAuthRuleProvider { @@ -47,32 +47,3 @@ export class AuthRuleProvider implements IAuthRuleProvider { return null; } } - -export class AuthRuleRepository { - private providers: AuthRuleProvider[] = []; - /** - * Adds a new provider to the providers list. - * - * @param {AuthRuleProvider} provider - The provider to push. - * @return {void} - */ - public push(provider: AuthRuleProvider): void { - this.providers.push(provider); - } - - /** - * Matches the request against the rules and returns the matching rule, if found. - * - * @param {Request} request - The request to match against the rules. - * @return {AuthRule | null} The matching rule, if found; otherwise, null. - */ - public match(request: Request): AuthRule | null { - for (const provider of this.providers) { - const rule = provider.match(request); - if (rule) { - return rule; - } - } - return null; - } -} \ No newline at end of file diff --git a/src/middleware/auth/routes/auth-rule-repository.ts b/src/middleware/auth/routes/auth-rule-repository.ts new file mode 100644 index 00000000..be113dd2 --- /dev/null +++ b/src/middleware/auth/routes/auth-rule-repository.ts @@ -0,0 +1,34 @@ +import type { AuthRuleProvider } from "./auth-rule-provider.js"; + +import type { Request } from "express"; +import type { AuthRule } from "../../../types/authentication.js"; + + +export class AuthRuleRepository { + private providers: AuthRuleProvider[] = []; + /** + * Adds a new provider to the providers list. + * + * @param {AuthRuleProvider} provider - The provider to push. + * @return {void} + */ + public push(provider: AuthRuleProvider): void { + this.providers.push(provider); + } + + /** + * Matches the request against the rules and returns the matching rule, if found. + * + * @param {Request} request - The request to match against the rules. + * @return {AuthRule | null} The matching rule, if found; otherwise, null. + */ + public match(request: Request): AuthRule | null { + for (const provider of this.providers) { + const rule = provider.match(request); + if (rule) { + return rule; + } + } + return null; + } +} diff --git a/src/middleware/auth/routine.ts b/src/middleware/auth/routine.ts deleted file mode 100644 index 6e3c6a71..00000000 --- a/src/middleware/auth/routine.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as dotenv from 'dotenv'; -import type { ICommonErrorResponse } from '../../types/authentication.js'; -dotenv.config(); - -// Simple interface for building the response/result -export interface IReturn { - returnOk(): ICommonErrorResponse; - returnError(status: number, error: string): ICommonErrorResponse; -} diff --git a/src/middleware/auth/user-info-fetcher/api-token.ts b/src/middleware/auth/user-info-fetcher/api-token.ts index c366abe9..27330ced 100644 --- a/src/middleware/auth/user-info-fetcher/api-token.ts +++ b/src/middleware/auth/user-info-fetcher/api-token.ts @@ -20,12 +20,18 @@ export class APITokenUserInfoFetcher extends UserInfoHelper implements IUserInfo } async fetch(request: Request, response: Response): Promise { - return this.verifyToken(this.token as string, response); + return this.verifyToken(response); } - public async verifyToken(token: string, response: Response): Promise { + /** + * Verifies the token and sets the user's scopes and customer entity in the global context. + * + * @param {Response} response - The response object to send the response. + * @return {Promise} The response object with the appropriate status code and error message, or undefined if successful. + */ + public async verifyToken(response: Response): Promise { try { - const apiEntity = await APIKeyService.instance.get(token); + const apiEntity = await APIKeyService.instance.get(this.token); if (!apiEntity) { return response.status(StatusCodes.UNAUTHORIZED).json({ error: `Unauthorized error: API Key not found.` diff --git a/src/middleware/auth/user-info-fetcher/idtoken.ts b/src/middleware/auth/user-info-fetcher/idtoken.ts index cd4c7552..ba9aef79 100644 --- a/src/middleware/auth/user-info-fetcher/idtoken.ts +++ b/src/middleware/auth/user-info-fetcher/idtoken.ts @@ -22,6 +22,13 @@ export class IdTokenUserInfoFetcher extends UserInfoHelper implements IUserInfoF return this.verifyJWTToken(request, response); } + /** + * Verifies the JWT token and sets the user's scopes and customer entity in the global context. + * + * @param {Request} request - The request object. + * @param {Response} response - The response object. + * @return {Promise} The response object with the appropriate status code and error message, or undefined if successful. + */ public async verifyJWTToken(request: Request, response: Response) { try { const { payload } = await jwtVerify( diff --git a/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts b/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts index 672bc973..076b371f 100644 --- a/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts +++ b/src/middleware/auth/user-info-fetcher/m2m-creds-token.ts @@ -17,6 +17,13 @@ export class M2MCredsTokenUserInfoFetcher extends UserInfoHelper implements IUse this.oauthProvider = oauthProvider; } + /** + * Verify M2M token + * + * @param {Request} request - The request object. + * @param {Response} response - The response object. + * @return {Promise} The result of verifying the M2M token. + */ async fetch(request: Request, response: Response) { // Verify M2M token return this.verifyJWTToken(request, response); diff --git a/src/middleware/auth/user-info-fetcher/portal-token.ts b/src/middleware/auth/user-info-fetcher/portal-token.ts index c56ceb33..7908ec5b 100644 --- a/src/middleware/auth/user-info-fetcher/portal-token.ts +++ b/src/middleware/auth/user-info-fetcher/portal-token.ts @@ -30,6 +30,13 @@ export class PortalUserInfoFetcher extends UserInfoHelper implements IUserInfoFe return this.verifyM2MToken(request, response); } + /** + * Verifies the ID token for the portal. + * + * @param {Request} request - The request object. + * @param {Response} response - The response object. + * @return {Promise} The result of verifying the ID token. + */ public async verifyIdToken(request: Request, response: Response) { try { const { payload } = await jwtVerify( diff --git a/src/middleware/auth/user-info-fetcher/swagger-ui.ts b/src/middleware/auth/user-info-fetcher/swagger-ui.ts index 719bfa17..7d453de1 100644 --- a/src/middleware/auth/user-info-fetcher/swagger-ui.ts +++ b/src/middleware/auth/user-info-fetcher/swagger-ui.ts @@ -12,6 +12,13 @@ export class SwaggerUserInfoFetcher extends UserInfoHelper implements IUserInfoF this.oauthProvider = oauthProvider; } + /** + * Tries to fetch user information based on the request and sets the appropriate response. + * + * @param {Request} request - The request object containing user information. + * @param {Response} response - The response object to be set based on user authentication status. + * @return {Promise} The response object with user information or an error message. + */ async fetch(request: Request, response: Response) { try { // If the user is not authenticated - return error diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 5cc118ff..43dc54b3 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -10,7 +10,7 @@ import { handleAuthRoutes, withLogto } from '@logto/express'; import { LogToProvider } from './auth/oauth/logto-provider.js'; import { AdminAuthRuleProvider } from './auth/routes/admin/admin-auth.js'; import { APIGuard } from './auth/auth-gaurd.js'; -import { AuthRuleRepository } from './auth/auth-rule-provider.js'; +import { AuthRuleRepository } from "./auth/routes/auth-rule-repository.js"; import type { IOAuthProvider } from './auth/oauth/abstract.js'; import { DidAuthRuleProvider } from './auth/routes/api/did-auth.js'; import { PresentationAuthRuleProvider } from './auth/routes/api/presentation-auth.js'; diff --git a/src/types/shared.ts b/src/types/shared.ts index 16a5544b..294c3225 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -14,7 +14,6 @@ import type { AbstractIdentifierProvider } from '@veramo/did-manager'; import type { AbstractKeyManagementSystem } from '@veramo/key-manager'; import type { DataSource } from 'typeorm'; import { CheqdNetwork } from '@cheqd/sdk'; -import type { IReturn } from '../middleware/auth/routine.js'; import type { ICommonErrorResponse } from './authentication.js'; import { StatusCodes } from 'http-status-codes'; @@ -65,6 +64,11 @@ export type UnsuccessfulQueryResponseBody = UnsuccessfulResponseBody; export type ValidationErrorResponseBody = UnsuccessfulResponseBody; +export interface IReturn { + returnOk(): ICommonErrorResponse; + returnError(status: number, error: string): ICommonErrorResponse; +} + export class CommonReturn implements IReturn { returnOk(data = {}): ICommonErrorResponse { return { From 5e03a89540a32f37cb21ac888e16b1f2f0d945b2 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 16 Apr 2024 22:38:20 +0200 Subject: [PATCH 39/49] Add an ability to update organisation (customer) --- src/app.ts | 6 + src/controllers/admin/api-key.ts | 2 +- src/controllers/admin/organisation.ts | 142 ++++++++++++++++++ src/controllers/admin/portal-customer.ts | 27 ---- src/controllers/admin/prices.ts | 2 +- src/controllers/admin/product.ts | 2 +- src/controllers/admin/subscriptions.ts | 2 +- src/controllers/api/account.ts | 2 + src/database/entities/customer.entity.ts | 16 +- .../migrations/AlterCustomerTableAddEmail.ts | 29 ++++ src/database/types/types.ts | 3 + src/middleware/auth/auth-gaurd.ts | 2 +- .../auth/routes/admin/admin-auth.ts | 59 +++----- src/services/admin/api-key.ts | 2 +- src/services/api/customer.ts | 14 +- src/services/identity/postgres.ts | 2 +- src/static/swagger-admin-options.json | 3 + src/static/swagger-admin.json | 125 +++++++++++++++ src/types/{portal.ts => admin.ts} | 20 +++ src/types/swagger-admin-types.ts | 25 +++ 20 files changed, 407 insertions(+), 78 deletions(-) create mode 100644 src/controllers/admin/organisation.ts delete mode 100644 src/controllers/admin/portal-customer.ts create mode 100644 src/database/migrations/AlterCustomerTableAddEmail.ts rename src/types/{portal.ts => admin.ts} (87%) diff --git a/src/app.ts b/src/app.ts index b17b6d7a..c659f241 100644 --- a/src/app.ts +++ b/src/app.ts @@ -32,6 +32,7 @@ import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; import { WebhookController } from './controllers/admin/webhook.js'; import { APIKeyController } from './controllers/admin/api-key.js'; +import { OrganisationController } from './controllers/admin/organisation.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -278,6 +279,11 @@ class App { // Webhook app.post('/admin/webhook', new WebhookController().handleWebhook); + + // Customer + app.post('/admin/organisation/update', OrganisationController.organisationUpdatevalidator, new OrganisationController().update); + app.get('/admin/organisation/get', new OrganisationController().get); + } // 404 for all other requests diff --git a/src/controllers/admin/api-key.ts b/src/controllers/admin/api-key.ts index 047b8768..64d9556b 100644 --- a/src/controllers/admin/api-key.ts +++ b/src/controllers/admin/api-key.ts @@ -20,7 +20,7 @@ import type { APIKeyUpdateResponseBody, APIKeyUpdateUnsuccessfulResponseBody, APIServiceOptions, -} from '../../types/portal.js'; +} from '../../types/admin.js'; import { EventTracker, eventTracker } from '../../services/track/tracker.js'; import { OperationNameEnum } from '../../types/constants.js'; diff --git a/src/controllers/admin/organisation.ts b/src/controllers/admin/organisation.ts new file mode 100644 index 00000000..875e0680 --- /dev/null +++ b/src/controllers/admin/organisation.ts @@ -0,0 +1,142 @@ +import * as dotenv from 'dotenv'; +import type { Request, Response } from 'express'; +import { check } from 'express-validator'; +import { validate } from '../validator/decorator.js'; +import { CustomerService } from '../../services/api/customer.js'; +import { StatusCodes } from 'http-status-codes'; +import type { AdminCustomerGetUnsuccessfulResponseBody, AdminCustomerUpdateResponseBody, AdminCustomerUpdateUnsuccessfulResponseBody } from '../../types/admin.js'; +import { PaymentAccountService } from '../../services/api/payment-account.js'; + +dotenv.config(); + +export class OrganisationController { + static organisationUpdatevalidator = [ + check('name'). + optional(). + isString(). + withMessage('name should be a valid string'), + check('email'). + optional(). + isString(). + withMessage('email should be a valid string'), + check('description'). + optional(). + isString(). + withMessage('description should be a valid string'), + ]; + + + /** + * @openapi + * + * /admin/organisation/update: + * post: + * summary: Update an organisation + * description: Update an organisation + * tags: [Organisation] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * example: Cheqd + * email: + * type: string + * example: cheqd@example.com + * format: email + * description: + * type: string + * example: Cheqd organisation + * responses: + * 200: + * description: A successful response + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrganisationResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + * */ + @validate + async update(request: Request, response: Response) { + const { name, email, description } = request.body; + try { + const customer = await CustomerService.instance.update(response.locals.customer.customerId, name, email, description); + const paymentAccount = await PaymentAccountService.instance.find({ customer: customer }); + + if (!customer || !paymentAccount) { + response.status(StatusCodes.NOT_FOUND).json({ + error: 'Customer for updating not found', + } satisfies AdminCustomerUpdateUnsuccessfulResponseBody); + } + + return response.status(StatusCodes.OK).json({ + name: customer.name, + email: customer.email, + description: customer.description, + cosmosAddress: paymentAccount[0].address as string + } satisfies AdminCustomerUpdateResponseBody); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies AdminCustomerUpdateUnsuccessfulResponseBody); + } + } + + /** + * @openapi + * + * /admin/organisation/get: + * get: + * summary: Get an organisation + * description: Get an organisation + * tags: [Organisation] + * responses: + * 200: + * description: A successful response + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/OrganisationResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + async get(request: Request, response: Response) { + try { + const customer = response.locals.customer; + const paymentAccount = await PaymentAccountService.instance.find({ customer: response.locals.customer }); + + if (!customer || !paymentAccount) { + response.status(StatusCodes.NOT_FOUND).json({ + error: 'Customer for current user was not found or did not setup properly. Please contact administrator.', + } satisfies AdminCustomerGetUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + name: customer.name, + email: customer.email, + description: customer.description, + cosmosAddress: paymentAccount[0].address as string, + }); + } catch (error) { + return response.status(500).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies AdminCustomerGetUnsuccessfulResponseBody); + } + } +} diff --git a/src/controllers/admin/portal-customer.ts b/src/controllers/admin/portal-customer.ts deleted file mode 100644 index 4c1d1c4d..00000000 --- a/src/controllers/admin/portal-customer.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as dotenv from 'dotenv'; -import type { Request, Response } from 'express'; -import { check } from 'express-validator'; -import { validationResult } from '../validator'; -import type { PortalCustomerGetUnsuccessfulResponseBody } from '../../types/portal.js'; - -dotenv.config(); - -export class PortalCustomerController { - static portalCustomerGetValidator = [ - check('logToUserId').optional().isString().withMessage('logToUserId should be a string').bail(), - ]; - - async get(request: Request, response: Response) { - const result = validationResult(request); - // handle error - if (!result.isEmpty()) { - return response.status(400).json({ - error: result.array().pop()?.msg, - } satisfies PortalCustomerGetUnsuccessfulResponseBody); - } - - return response.status(500).json({ - error: 'Not implemented yet', - }); - } -} diff --git a/src/controllers/admin/prices.ts b/src/controllers/admin/prices.ts index 935b554d..9a463257 100644 --- a/src/controllers/admin/prices.ts +++ b/src/controllers/admin/prices.ts @@ -1,7 +1,7 @@ import type { Stripe } from 'stripe'; import type { Request, Response } from 'express'; import * as dotenv from 'dotenv'; -import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../../types/portal.js'; +import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../../types/admin.js'; import { StatusCodes } from 'http-status-codes'; import { check } from '../validator/index.js'; import { validate } from '../validator/decorator.js'; diff --git a/src/controllers/admin/product.ts b/src/controllers/admin/product.ts index 920695a0..31c36102 100644 --- a/src/controllers/admin/product.ts +++ b/src/controllers/admin/product.ts @@ -7,7 +7,7 @@ import type { ProductListResponseBody, ProductListUnsuccessfulResponseBody, ProductWithPrices, -} from '../../types/portal.js'; +} from '../../types/admin.js'; import { StatusCodes } from 'http-status-codes'; import { check } from '../validator/index.js'; import { validate } from '../validator/decorator.js'; diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 9c21b2bf..0e822833 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -18,7 +18,7 @@ import type { SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, SubscriptionCancelRequestBody, -} from '../../types/portal.js'; +} from '../../types/admin.js'; import { StatusCodes } from 'http-status-codes'; import { check } from '../validator/index.js'; import { SubscriptionService } from '../../services/admin/subscription.js'; diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index ea3c402f..582a3e36 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -357,6 +357,7 @@ export class AccountController { operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { name: customer.name, + email: customer.email, customerId: customer.customerId, } satisfies ISubmitStripeCustomerCreateData, } satisfies ISubmitOperation); @@ -471,6 +472,7 @@ export class AccountController { operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { name: customer.name, + email: customer.email, customerId: customer.customerId, } satisfies ISubmitStripeCustomerCreateData, } satisfies ISubmitOperation); diff --git a/src/database/entities/customer.entity.ts b/src/database/entities/customer.entity.ts index de774c1f..fd0a6ba7 100644 --- a/src/database/entities/customer.entity.ts +++ b/src/database/entities/customer.entity.ts @@ -14,6 +14,18 @@ export class CustomerEntity { }) name!: string; + @Column({ + type: 'text', + nullable: true, + }) + email?: string; + + @Column({ + type: 'text', + nullable: true, + }) + description?: string; + @Column({ type: 'timestamptz', nullable: false, @@ -42,9 +54,11 @@ export class CustomerEntity { this.updatedAt = new Date(); } - constructor(customerId: string, name: string) { + constructor(customerId: string, name: string, email?: string, description?: string) { this.customerId = customerId; this.name = name; + this.email = email; + this.description = description; } public isEqual(customer: CustomerEntity): boolean { diff --git a/src/database/migrations/AlterCustomerTableAddEmail.ts b/src/database/migrations/AlterCustomerTableAddEmail.ts new file mode 100644 index 00000000..36f8c94e --- /dev/null +++ b/src/database/migrations/AlterCustomerTableAddEmail.ts @@ -0,0 +1,29 @@ +import { TableColumn, type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AlterCustomerTableAddEmail1695740346005 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const table_name = 'customer'; + + await queryRunner.addColumn( + table_name, + new TableColumn({ + name: 'email', + type: 'text', + isNullable: true, + }) + ); + + await queryRunner.addColumn( + table_name, + new TableColumn({ + name: 'description', + type: 'text', + isNullable: true, + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + throw new Error('illegal_operation: cannot roll back initial migration'); + } +} diff --git a/src/database/types/types.ts b/src/database/types/types.ts index 28c6fa1b..95bbff70 100644 --- a/src/database/types/types.ts +++ b/src/database/types/types.ts @@ -37,6 +37,7 @@ import { AlterOperationTable1695740346001 } from '../migrations/AlterOperationTa import { SubscriptionEntity } from '../entities/subscription.entity.js'; import { CreateSubscritpionTable1695740346003 } from '../migrations/CreateSubscriptionTable.js'; import { AlterAPIKeyTable1695740346004 } from '../migrations/AlterAPIKeyTable.js'; +import { AlterCustomerTableAddEmail1695740346005 } from '../migrations/AlterCustomerTableAddEmail.js'; dotenv.config(); const { EXTERNAL_DB_CONNECTION_URL, EXTERNAL_DB_CERT } = process.env; @@ -109,6 +110,8 @@ export class Postgres implements AbstractDatabase { CreateSubscritpionTable1695740346003, // Add revoked field to APIKey table AlterAPIKeyTable1695740346004, + // Add email and description fields + AlterCustomerTableAddEmail1695740346005, ], entities: [ ...Entities, diff --git a/src/middleware/auth/auth-gaurd.ts b/src/middleware/auth/auth-gaurd.ts index 2fefa32d..321573e8 100644 --- a/src/middleware/auth/auth-gaurd.ts +++ b/src/middleware/auth/auth-gaurd.ts @@ -17,7 +17,7 @@ export class APIGuard { private userInfoFetcher: IUserInfoFetcher; private oauthProvider: IOAuthProvider; private static bearerTokenIdentifier = 'Bearer'; - private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap', '/admin/webhook', '/admin/swagger']; + private pathSkip = ['/swagger', '/static', '/logto', '/account/bootstrap', '/admin/webhook']; constructor(authRuleRepository: AuthRuleRepository, oauthProvider: IOAuthProvider) { this.authRuleRepository = authRuleRepository; diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index b2c720b4..9a25f9bf 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -7,61 +7,40 @@ export class AdminAuthRuleProvider extends AuthRuleProvider { constructor() { super(); // Main swagger route - this.registerRule('/admin/swagger', 'GET', 'admin:swagger:testnet', { skipNamespace: true }); - this.registerRule('/admin/swagger', 'GET', 'admin:swagger:mainnet', { skipNamespace: true }); + this.registerRule('/admin/swagger', 'GET', 'admin:swagger', { skipNamespace: true }); // Subscriptions // skipNamespace is set to true cause we don't have the information about the namespace in the request - this.registerRule('/admin/subscription/create', 'POST', 'admin:subscription:create:testnet', { + this.registerRule('/admin/subscription/create', 'POST', 'admin:subscription:create', { skipNamespace: true, }); - this.registerRule('/admin/subscription/create', 'POST', 'admin:subscription:create:mainnet', { + this.registerRule('/admin/subscription/list', 'GET', 'admin:subscription:list', { skipNamespace: true, }); - this.registerRule('/admin/subscription/list', 'GET', 'admin:subscription:list:testnet', { + this.registerRule('/admin/subscription/update', 'POST', 'admin:subscription:update', { skipNamespace: true, }); - this.registerRule('/admin/subscription/list', 'GET', 'admin:subscription:list:mainnet', { + this.registerRule('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel', { skipNamespace: true, }); - this.registerRule('/admin/subscription/update', 'POST', 'admin:subscription:update:testnet', { + this.registerRule('/admin/subscription/resume', 'POST', 'admin:subscription:resume', { skipNamespace: true, }); - this.registerRule('/admin/subscription/update', 'POST', 'admin:subscription:update:mainnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:testnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/cancel', 'POST', 'admin:subscription:cancel:mainnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/resume', 'POST', 'admin:subscription:resume:testnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/resume', 'POST', 'admin:subscription:resume:mainnet', { - skipNamespace: true, - }); - this.registerRule('/admin/subscription/get', 'GET', 'admin:subscription:get:testnet', { skipNamespace: true }); - this.registerRule('/admin/subscription/get', 'GET', 'admin:subscription:get:mainnet', { skipNamespace: true }); + this.registerRule('/admin/subscription/get', 'GET', 'admin:subscription:get', { skipNamespace: true }); // Prices - this.registerRule('/admin/price/list', 'GET', 'admin:price:list:testnet', { skipNamespace: true }); - this.registerRule('/admin/price/list', 'GET', 'admin:price:list:mainnet', { skipNamespace: true }); + this.registerRule('/admin/price/list', 'GET', 'admin:price:list', { skipNamespace: true }); // Products - this.registerRule('/admin/product/list', 'GET', 'admin:product:list:testnet', { skipNamespace: true }); - this.registerRule('/admin/product/list', 'GET', 'admin:product:list:mainnet', { skipNamespace: true }); - this.registerRule('/admin/product/get', 'GET', 'admin:product:get:testnet', { skipNamespace: true }); - this.registerRule('/admin/product/get', 'GET', 'admin:product:get:mainnet', { skipNamespace: true }); + this.registerRule('/admin/product/list', 'GET', 'admin:product:list', { skipNamespace: true }); + this.registerRule('/admin/product/get', 'GET', 'admin:product:get', { skipNamespace: true }); // API Key - this.registerRule('/admin/api-key/create', 'POST', 'admin:api-key:create:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/create', 'POST', 'admin:api-key:create:mainnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/update', 'POST', 'admin:api-key:update:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/update', 'POST', 'admin:api-key:update:mainnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke:mainnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get:mainnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list:testnet', { skipNamespace: true }); - this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list:mainnet', { skipNamespace: true }); + this.registerRule('/admin/api-key/create', 'POST', 'admin:api-key:create', { skipNamespace: true }); + this.registerRule('/admin/api-key/update', 'POST', 'admin:api-key:update', { skipNamespace: true }); + this.registerRule('/admin/api-key/revoke', 'DELETE', 'admin:api-key:revoke', { skipNamespace: true }); + this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get', { skipNamespace: true }); + this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list', { skipNamespace: true }); + // Customer + this.registerRule('/admin/organisation/update', 'POST', 'admin:organisation:update', { skipNamespace: true }); + this.registerRule('/admin/organisation/get', 'GET', 'admin:organisation:get', { skipNamespace: true }); + } } diff --git a/src/services/admin/api-key.ts b/src/services/admin/api-key.ts index adc843b7..59075bcd 100644 --- a/src/services/admin/api-key.ts +++ b/src/services/admin/api-key.ts @@ -10,7 +10,7 @@ import { APIKeyEntity } from '../../database/entities/api.key.entity.js'; import type { UserEntity } from '../../database/entities/user.entity.js'; import { SecretBox } from '@veramo/kms-local'; import { API_SECRET_KEY_LENGTH, API_KEY_PREFIX, API_KEY_EXPIRATION } from '../../types/constants.js'; -import type { APIServiceOptions } from '../../types/portal.js'; +import type { APIServiceOptions } from '../../types/admin.js'; dotenv.config(); export class APIKeyService { diff --git a/src/services/api/customer.ts b/src/services/api/customer.ts index 254290c4..0b965199 100644 --- a/src/services/api/customer.ts +++ b/src/services/api/customer.ts @@ -20,7 +20,7 @@ export class CustomerService { this.customerRepository = Connection.instance.dbConnection.getRepository(CustomerEntity); } - public async create(name: string) { + public async create(name: string, email?: string, description?: string) { // The sequence for creating a customer is supposed to be: // 1. Create a new customer entity in the database; // 2. Create new cosmos keypair @@ -30,7 +30,7 @@ export class CustomerService { if (await this.isExist({ name: name })) { throw new Error(`Cannot create a new customer since the customer with same name ${name} already exists`); } - const customerEntity = new CustomerEntity(uuidv4(), name); + const customerEntity = new CustomerEntity(uuidv4(), name, email, description); await this.customerRepository.insert(customerEntity); // Create a new Cosmos account for the customer and make a link with customer entity; @@ -42,7 +42,7 @@ export class CustomerService { }; } - public async update(customerId: string, name?: string, paymentProviderId?: string) { + public async update(customerId: string, name?: string, email?: string, description?: string, paymentProviderId?: string) { const existingCustomer = await this.customerRepository.findOneBy({ customerId }); if (!existingCustomer) { throw new Error(`CustomerId not found`); @@ -51,6 +51,14 @@ export class CustomerService { existingCustomer.name = name; } + if (email) { + existingCustomer.email = email; + } + + if (description) { + existingCustomer.description = description; + } + if (paymentProviderId) { existingCustomer.paymentProviderId = paymentProviderId; } diff --git a/src/services/identity/postgres.ts b/src/services/identity/postgres.ts index 2b1c3fe9..02204553 100644 --- a/src/services/identity/postgres.ts +++ b/src/services/identity/postgres.ts @@ -55,7 +55,7 @@ import type { AbstractIdentifierProvider } from '@veramo/did-manager'; import type { CheqdProviderError } from '@cheqd/did-provider-cheqd'; import type { TPublicKeyEd25519 } from '@cheqd/did-provider-cheqd'; import { toTPublicKeyEd25519 } from '../helpers.js'; -import type { APIServiceOptions } from '../../types/portal.js'; +import type { APIServiceOptions } from '../../types/admin.js'; dotenv.config(); diff --git a/src/static/swagger-admin-options.json b/src/static/swagger-admin-options.json index 1b041cf1..bd8c4a09 100644 --- a/src/static/swagger-admin-options.json +++ b/src/static/swagger-admin-options.json @@ -26,6 +26,9 @@ }, { "name": "API Key" + }, + { + "name": "Organisation" } ] } diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index f8d03627..e079172e 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -26,6 +26,9 @@ }, { "name": "API Key" + }, + { + "name": "Organisation" } ], "paths": { @@ -222,6 +225,97 @@ } } }, + "/admin/organisation/update": { + "post": { + "summary": "Update an organisation", + "description": "Update an organisation", + "tags": [ + "Organisation" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Cheqd" + }, + "email": { + "type": "string", + "example": "cheqd@example.com", + "format": "email" + }, + "description": { + "type": "string", + "example": "Cheqd organisation" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "A successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/organisation/get": { + "get": { + "summary": "Get an organisation", + "description": "Get an organisation", + "tags": [ + "Organisation" + ], + "responses": { + "200": { + "description": "A successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, "/admin/price/list": { "get": { "summary": "Get a list of prices", @@ -1034,6 +1128,37 @@ "ref": "#/components/schemas/APIKeyResponse" } }, + "OrganisationResponseBody": { + "description": "The response body for an organisation", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the organisation", + "example": "Cheqd" + }, + "email": { + "type": "string", + "description": "The email of the organisation", + "example": "cheqd@example.com", + "format": "email", + "nullable": true, + "default": null + }, + "description": { + "type": "string", + "description": "The description of the organisation", + "example": "Cheqd organisation", + "nullable": true, + "default": null + }, + "cosmosAddress": { + "type": "string", + "description": "The cosmos address of the organisation", + "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" + } + } + }, "NotFoundError": { "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", "type": "object", diff --git a/src/types/portal.ts b/src/types/admin.ts similarity index 87% rename from src/types/portal.ts rename to src/types/admin.ts index 8ab6f0ba..5c048edc 100644 --- a/src/types/portal.ts +++ b/src/types/admin.ts @@ -147,6 +147,26 @@ export type APIKeyGetRequestBody = APIKeyResponseBody; export type APIKeyGetResponseBody = APIKeyCreateResponseBody; export type APIKeyGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; +// Organisation +export type AdminCustomerResponseBody = { + name: string; + email?: string; + description?: string; + cosmosAddress?: string; +}; + +export type AdminCustomerGetResponseBody = AdminCustomerResponseBody; +export type AdminCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; + +export type AdminCustomerUpdateRequestBody = { + name?: string; + email?: string; + description?: string; +}; +export type AdminCustomerUpdateResponseBody = AdminCustomerGetResponseBody; +export type AdminCustomerUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; + + // Utils export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 8f320a75..b0f66e22 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -316,6 +316,31 @@ * type: object * schema: * ref: '#/components/schemas/APIKeyResponse' + * OrganisationResponseBody: + * description: The response body for an organisation + * type: object + * properties: + * name: + * type: string + * description: The name of the organisation + * example: Cheqd + * email: + * type: string + * description: The email of the organisation + * example: cheqd@example.com + * format: email + * nullable: true + * default: null + * description: + * type: string + * description: The description of the organisation + * example: Cheqd organisation + * nullable: true + * default: null + * cosmosAddress: + * type: string + * description: The cosmos address of the organisation + * example: cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. * type: object From c11d3e3ae2aad6315b4cef59e5108104226fcfe1 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 16 Apr 2024 23:11:10 +0200 Subject: [PATCH 40/49] Fix typo --- src/app.ts | 6 +-- .../{organisation.ts => organization.ts} | 38 +++++++++---------- .../auth/routes/admin/admin-auth.ts | 4 +- src/static/swagger-admin-options.json | 2 +- src/static/swagger-admin.json | 38 +++++++++---------- src/types/admin.ts | 14 +++---- src/types/swagger-admin-types.ts | 14 +++---- 7 files changed, 58 insertions(+), 58 deletions(-) rename src/controllers/admin/{organisation.ts => organization.ts} (78%) diff --git a/src/app.ts b/src/app.ts index c659f241..7fd1a79e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -32,7 +32,7 @@ import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; import { WebhookController } from './controllers/admin/webhook.js'; import { APIKeyController } from './controllers/admin/api-key.js'; -import { OrganisationController } from './controllers/admin/organisation.js'; +import { OrganizationController } from './controllers/admin/organization.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -281,8 +281,8 @@ class App { app.post('/admin/webhook', new WebhookController().handleWebhook); // Customer - app.post('/admin/organisation/update', OrganisationController.organisationUpdatevalidator, new OrganisationController().update); - app.get('/admin/organisation/get', new OrganisationController().get); + app.post('/admin/organization/update', OrganizationController.organizationUpdatevalidator, new OrganizationController().update); + app.get('/admin/organization/get', new OrganizationController().get); } diff --git a/src/controllers/admin/organisation.ts b/src/controllers/admin/organization.ts similarity index 78% rename from src/controllers/admin/organisation.ts rename to src/controllers/admin/organization.ts index 875e0680..9db6c8d0 100644 --- a/src/controllers/admin/organisation.ts +++ b/src/controllers/admin/organization.ts @@ -4,13 +4,13 @@ import { check } from 'express-validator'; import { validate } from '../validator/decorator.js'; import { CustomerService } from '../../services/api/customer.js'; import { StatusCodes } from 'http-status-codes'; -import type { AdminCustomerGetUnsuccessfulResponseBody, AdminCustomerUpdateResponseBody, AdminCustomerUpdateUnsuccessfulResponseBody } from '../../types/admin.js'; +import type { AdminOrganizationGetUnsuccessfulResponseBody, AdminOrganizationUpdateResponseBody, AdminOrganizationUpdateUnsuccessfulResponseBody } from '../../types/admin.js'; import { PaymentAccountService } from '../../services/api/payment-account.js'; dotenv.config(); -export class OrganisationController { - static organisationUpdatevalidator = [ +export class OrganizationController { + static organizationUpdatevalidator = [ check('name'). optional(). isString(). @@ -29,11 +29,11 @@ export class OrganisationController { /** * @openapi * - * /admin/organisation/update: + * /admin/organization/update: * post: - * summary: Update an organisation - * description: Update an organisation - * tags: [Organisation] + * summary: Update an organization + * description: Update an organization + * tags: [Organization] * requestBody: * required: true * content: @@ -50,14 +50,14 @@ export class OrganisationController { * format: email * description: * type: string - * example: Cheqd organisation + * example: Cheqd organization * responses: * 200: * description: A successful response * content: * application/json: * schema: - * $ref: '#/components/schemas/OrganisationResponseBody' + * $ref: '#/components/schemas/OrganizationResponseBody' * 400: * $ref: '#/components/schemas/InvalidRequest' * 401: @@ -77,7 +77,7 @@ export class OrganisationController { if (!customer || !paymentAccount) { response.status(StatusCodes.NOT_FOUND).json({ error: 'Customer for updating not found', - } satisfies AdminCustomerUpdateUnsuccessfulResponseBody); + } satisfies AdminOrganizationUpdateUnsuccessfulResponseBody); } return response.status(StatusCodes.OK).json({ @@ -85,29 +85,29 @@ export class OrganisationController { email: customer.email, description: customer.description, cosmosAddress: paymentAccount[0].address as string - } satisfies AdminCustomerUpdateResponseBody); + } satisfies AdminOrganizationUpdateResponseBody); } catch (error) { return response.status(500).json({ error: `Internal error: ${(error as Error)?.message || error}`, - } satisfies AdminCustomerUpdateUnsuccessfulResponseBody); + } satisfies AdminOrganizationUpdateUnsuccessfulResponseBody); } } /** * @openapi * - * /admin/organisation/get: + * /admin/organization/get: * get: - * summary: Get an organisation - * description: Get an organisation - * tags: [Organisation] + * summary: Get an organization + * description: Get an organization + * tags: [Organization] * responses: * 200: * description: A successful response * content: * application/json: * schema: - * $ref: '#/components/schemas/OrganisationResponseBody' + * $ref: '#/components/schemas/OrganizationResponseBody' * 400: * $ref: '#/components/schemas/InvalidRequest' * 401: @@ -125,7 +125,7 @@ export class OrganisationController { if (!customer || !paymentAccount) { response.status(StatusCodes.NOT_FOUND).json({ error: 'Customer for current user was not found or did not setup properly. Please contact administrator.', - } satisfies AdminCustomerGetUnsuccessfulResponseBody); + } satisfies AdminOrganizationGetUnsuccessfulResponseBody); } return response.status(StatusCodes.OK).json({ name: customer.name, @@ -136,7 +136,7 @@ export class OrganisationController { } catch (error) { return response.status(500).json({ error: `Internal error: ${(error as Error)?.message || error}`, - } satisfies AdminCustomerGetUnsuccessfulResponseBody); + } satisfies AdminOrganizationGetUnsuccessfulResponseBody); } } } diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index 9a25f9bf..f09ec377 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -39,8 +39,8 @@ export class AdminAuthRuleProvider extends AuthRuleProvider { this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get', { skipNamespace: true }); this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list', { skipNamespace: true }); // Customer - this.registerRule('/admin/organisation/update', 'POST', 'admin:organisation:update', { skipNamespace: true }); - this.registerRule('/admin/organisation/get', 'GET', 'admin:organisation:get', { skipNamespace: true }); + this.registerRule('/admin/organization/update', 'POST', 'admin:organization:update', { skipNamespace: true }); + this.registerRule('/admin/organization/get', 'GET', 'admin:organization:get', { skipNamespace: true }); } } diff --git a/src/static/swagger-admin-options.json b/src/static/swagger-admin-options.json index bd8c4a09..0ab9f2e4 100644 --- a/src/static/swagger-admin-options.json +++ b/src/static/swagger-admin-options.json @@ -28,7 +28,7 @@ "name": "API Key" }, { - "name": "Organisation" + "name": "Organization" } ] } diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index e079172e..5d849b77 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -28,7 +28,7 @@ "name": "API Key" }, { - "name": "Organisation" + "name": "Organization" } ], "paths": { @@ -225,12 +225,12 @@ } } }, - "/admin/organisation/update": { + "/admin/organization/update": { "post": { - "summary": "Update an organisation", - "description": "Update an organisation", + "summary": "Update an organization", + "description": "Update an organization", "tags": [ - "Organisation" + "Organization" ], "requestBody": { "required": true, @@ -250,7 +250,7 @@ }, "description": { "type": "string", - "example": "Cheqd organisation" + "example": "Cheqd organization" } } } @@ -263,7 +263,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganisationResponseBody" + "$ref": "#/components/schemas/OrganizationResponseBody" } } } @@ -283,12 +283,12 @@ } } }, - "/admin/organisation/get": { + "/admin/organization/get": { "get": { - "summary": "Get an organisation", - "description": "Get an organisation", + "summary": "Get an organization", + "description": "Get an organization", "tags": [ - "Organisation" + "Organization" ], "responses": { "200": { @@ -296,7 +296,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganisationResponseBody" + "$ref": "#/components/schemas/OrganizationResponseBody" } } } @@ -1128,18 +1128,18 @@ "ref": "#/components/schemas/APIKeyResponse" } }, - "OrganisationResponseBody": { - "description": "The response body for an organisation", + "OrganizationResponseBody": { + "description": "The response body for an organization", "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the organisation", + "description": "The name of the organization", "example": "Cheqd" }, "email": { "type": "string", - "description": "The email of the organisation", + "description": "The email of the organization", "example": "cheqd@example.com", "format": "email", "nullable": true, @@ -1147,14 +1147,14 @@ }, "description": { "type": "string", - "description": "The description of the organisation", - "example": "Cheqd organisation", + "description": "The description of the organization", + "example": "Cheqd organization", "nullable": true, "default": null }, "cosmosAddress": { "type": "string", - "description": "The cosmos address of the organisation", + "description": "The cosmos address of the organization", "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" } } diff --git a/src/types/admin.ts b/src/types/admin.ts index 5c048edc..6890ce33 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -147,24 +147,24 @@ export type APIKeyGetRequestBody = APIKeyResponseBody; export type APIKeyGetResponseBody = APIKeyCreateResponseBody; export type APIKeyGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; -// Organisation -export type AdminCustomerResponseBody = { +// Organization +export type AdminOrganizationResponseBody = { name: string; email?: string; description?: string; cosmosAddress?: string; }; -export type AdminCustomerGetResponseBody = AdminCustomerResponseBody; -export type AdminCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type AdminOrganizationGetResponseBody = AdminOrganizationResponseBody; +export type AdminOrganizationGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; -export type AdminCustomerUpdateRequestBody = { +export type AdminOrganizationUpdateRequestBody = { name?: string; email?: string; description?: string; }; -export type AdminCustomerUpdateResponseBody = AdminCustomerGetResponseBody; -export type AdminCustomerUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type AdminOrganizationUpdateResponseBody = AdminOrganizationGetResponseBody; +export type AdminOrganizationUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Utils diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index b0f66e22..282ab918 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -316,30 +316,30 @@ * type: object * schema: * ref: '#/components/schemas/APIKeyResponse' - * OrganisationResponseBody: - * description: The response body for an organisation + * OrganizationResponseBody: + * description: The response body for an organization * type: object * properties: * name: * type: string - * description: The name of the organisation + * description: The name of the organization * example: Cheqd * email: * type: string - * description: The email of the organisation + * description: The email of the organization * example: cheqd@example.com * format: email * nullable: true * default: null * description: * type: string - * description: The description of the organisation - * example: Cheqd organisation + * description: The description of the organization + * example: Cheqd organization * nullable: true * default: null * cosmosAddress: * type: string - * description: The cosmos address of the organisation + * description: The cosmos address of the organization * example: cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. From 2f103ea6d3e53bbf09ee4a7581648ead47ed7375 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 16 Apr 2024 23:13:29 +0200 Subject: [PATCH 41/49] Revert "Fix typo" This reverts commit c11d3e3ae2aad6315b4cef59e5108104226fcfe1. --- src/app.ts | 6 +-- .../{organization.ts => organisation.ts} | 38 +++++++++---------- .../auth/routes/admin/admin-auth.ts | 4 +- src/static/swagger-admin-options.json | 2 +- src/static/swagger-admin.json | 38 +++++++++---------- src/types/admin.ts | 14 +++---- src/types/swagger-admin-types.ts | 14 +++---- 7 files changed, 58 insertions(+), 58 deletions(-) rename src/controllers/admin/{organization.ts => organisation.ts} (78%) diff --git a/src/app.ts b/src/app.ts index 7fd1a79e..c659f241 100644 --- a/src/app.ts +++ b/src/app.ts @@ -32,7 +32,7 @@ import { SubscriptionController } from './controllers/admin/subscriptions.js'; import { PriceController } from './controllers/admin/prices.js'; import { WebhookController } from './controllers/admin/webhook.js'; import { APIKeyController } from './controllers/admin/api-key.js'; -import { OrganizationController } from './controllers/admin/organization.js'; +import { OrganisationController } from './controllers/admin/organisation.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -281,8 +281,8 @@ class App { app.post('/admin/webhook', new WebhookController().handleWebhook); // Customer - app.post('/admin/organization/update', OrganizationController.organizationUpdatevalidator, new OrganizationController().update); - app.get('/admin/organization/get', new OrganizationController().get); + app.post('/admin/organisation/update', OrganisationController.organisationUpdatevalidator, new OrganisationController().update); + app.get('/admin/organisation/get', new OrganisationController().get); } diff --git a/src/controllers/admin/organization.ts b/src/controllers/admin/organisation.ts similarity index 78% rename from src/controllers/admin/organization.ts rename to src/controllers/admin/organisation.ts index 9db6c8d0..875e0680 100644 --- a/src/controllers/admin/organization.ts +++ b/src/controllers/admin/organisation.ts @@ -4,13 +4,13 @@ import { check } from 'express-validator'; import { validate } from '../validator/decorator.js'; import { CustomerService } from '../../services/api/customer.js'; import { StatusCodes } from 'http-status-codes'; -import type { AdminOrganizationGetUnsuccessfulResponseBody, AdminOrganizationUpdateResponseBody, AdminOrganizationUpdateUnsuccessfulResponseBody } from '../../types/admin.js'; +import type { AdminCustomerGetUnsuccessfulResponseBody, AdminCustomerUpdateResponseBody, AdminCustomerUpdateUnsuccessfulResponseBody } from '../../types/admin.js'; import { PaymentAccountService } from '../../services/api/payment-account.js'; dotenv.config(); -export class OrganizationController { - static organizationUpdatevalidator = [ +export class OrganisationController { + static organisationUpdatevalidator = [ check('name'). optional(). isString(). @@ -29,11 +29,11 @@ export class OrganizationController { /** * @openapi * - * /admin/organization/update: + * /admin/organisation/update: * post: - * summary: Update an organization - * description: Update an organization - * tags: [Organization] + * summary: Update an organisation + * description: Update an organisation + * tags: [Organisation] * requestBody: * required: true * content: @@ -50,14 +50,14 @@ export class OrganizationController { * format: email * description: * type: string - * example: Cheqd organization + * example: Cheqd organisation * responses: * 200: * description: A successful response * content: * application/json: * schema: - * $ref: '#/components/schemas/OrganizationResponseBody' + * $ref: '#/components/schemas/OrganisationResponseBody' * 400: * $ref: '#/components/schemas/InvalidRequest' * 401: @@ -77,7 +77,7 @@ export class OrganizationController { if (!customer || !paymentAccount) { response.status(StatusCodes.NOT_FOUND).json({ error: 'Customer for updating not found', - } satisfies AdminOrganizationUpdateUnsuccessfulResponseBody); + } satisfies AdminCustomerUpdateUnsuccessfulResponseBody); } return response.status(StatusCodes.OK).json({ @@ -85,29 +85,29 @@ export class OrganizationController { email: customer.email, description: customer.description, cosmosAddress: paymentAccount[0].address as string - } satisfies AdminOrganizationUpdateResponseBody); + } satisfies AdminCustomerUpdateResponseBody); } catch (error) { return response.status(500).json({ error: `Internal error: ${(error as Error)?.message || error}`, - } satisfies AdminOrganizationUpdateUnsuccessfulResponseBody); + } satisfies AdminCustomerUpdateUnsuccessfulResponseBody); } } /** * @openapi * - * /admin/organization/get: + * /admin/organisation/get: * get: - * summary: Get an organization - * description: Get an organization - * tags: [Organization] + * summary: Get an organisation + * description: Get an organisation + * tags: [Organisation] * responses: * 200: * description: A successful response * content: * application/json: * schema: - * $ref: '#/components/schemas/OrganizationResponseBody' + * $ref: '#/components/schemas/OrganisationResponseBody' * 400: * $ref: '#/components/schemas/InvalidRequest' * 401: @@ -125,7 +125,7 @@ export class OrganizationController { if (!customer || !paymentAccount) { response.status(StatusCodes.NOT_FOUND).json({ error: 'Customer for current user was not found or did not setup properly. Please contact administrator.', - } satisfies AdminOrganizationGetUnsuccessfulResponseBody); + } satisfies AdminCustomerGetUnsuccessfulResponseBody); } return response.status(StatusCodes.OK).json({ name: customer.name, @@ -136,7 +136,7 @@ export class OrganizationController { } catch (error) { return response.status(500).json({ error: `Internal error: ${(error as Error)?.message || error}`, - } satisfies AdminOrganizationGetUnsuccessfulResponseBody); + } satisfies AdminCustomerGetUnsuccessfulResponseBody); } } } diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index f09ec377..9a25f9bf 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -39,8 +39,8 @@ export class AdminAuthRuleProvider extends AuthRuleProvider { this.registerRule('/admin/api-key/get', 'GET', 'admin:api-key:get', { skipNamespace: true }); this.registerRule('/admin/api-key/list', 'GET', 'admin:api-key:list', { skipNamespace: true }); // Customer - this.registerRule('/admin/organization/update', 'POST', 'admin:organization:update', { skipNamespace: true }); - this.registerRule('/admin/organization/get', 'GET', 'admin:organization:get', { skipNamespace: true }); + this.registerRule('/admin/organisation/update', 'POST', 'admin:organisation:update', { skipNamespace: true }); + this.registerRule('/admin/organisation/get', 'GET', 'admin:organisation:get', { skipNamespace: true }); } } diff --git a/src/static/swagger-admin-options.json b/src/static/swagger-admin-options.json index 0ab9f2e4..bd8c4a09 100644 --- a/src/static/swagger-admin-options.json +++ b/src/static/swagger-admin-options.json @@ -28,7 +28,7 @@ "name": "API Key" }, { - "name": "Organization" + "name": "Organisation" } ] } diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 5d849b77..e079172e 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -28,7 +28,7 @@ "name": "API Key" }, { - "name": "Organization" + "name": "Organisation" } ], "paths": { @@ -225,12 +225,12 @@ } } }, - "/admin/organization/update": { + "/admin/organisation/update": { "post": { - "summary": "Update an organization", - "description": "Update an organization", + "summary": "Update an organisation", + "description": "Update an organisation", "tags": [ - "Organization" + "Organisation" ], "requestBody": { "required": true, @@ -250,7 +250,7 @@ }, "description": { "type": "string", - "example": "Cheqd organization" + "example": "Cheqd organisation" } } } @@ -263,7 +263,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResponseBody" + "$ref": "#/components/schemas/OrganisationResponseBody" } } } @@ -283,12 +283,12 @@ } } }, - "/admin/organization/get": { + "/admin/organisation/get": { "get": { - "summary": "Get an organization", - "description": "Get an organization", + "summary": "Get an organisation", + "description": "Get an organisation", "tags": [ - "Organization" + "Organisation" ], "responses": { "200": { @@ -296,7 +296,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrganizationResponseBody" + "$ref": "#/components/schemas/OrganisationResponseBody" } } } @@ -1128,18 +1128,18 @@ "ref": "#/components/schemas/APIKeyResponse" } }, - "OrganizationResponseBody": { - "description": "The response body for an organization", + "OrganisationResponseBody": { + "description": "The response body for an organisation", "type": "object", "properties": { "name": { "type": "string", - "description": "The name of the organization", + "description": "The name of the organisation", "example": "Cheqd" }, "email": { "type": "string", - "description": "The email of the organization", + "description": "The email of the organisation", "example": "cheqd@example.com", "format": "email", "nullable": true, @@ -1147,14 +1147,14 @@ }, "description": { "type": "string", - "description": "The description of the organization", - "example": "Cheqd organization", + "description": "The description of the organisation", + "example": "Cheqd organisation", "nullable": true, "default": null }, "cosmosAddress": { "type": "string", - "description": "The cosmos address of the organization", + "description": "The cosmos address of the organisation", "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" } } diff --git a/src/types/admin.ts b/src/types/admin.ts index 6890ce33..5c048edc 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -147,24 +147,24 @@ export type APIKeyGetRequestBody = APIKeyResponseBody; export type APIKeyGetResponseBody = APIKeyCreateResponseBody; export type APIKeyGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; -// Organization -export type AdminOrganizationResponseBody = { +// Organisation +export type AdminCustomerResponseBody = { name: string; email?: string; description?: string; cosmosAddress?: string; }; -export type AdminOrganizationGetResponseBody = AdminOrganizationResponseBody; -export type AdminOrganizationGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type AdminCustomerGetResponseBody = AdminCustomerResponseBody; +export type AdminCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; -export type AdminOrganizationUpdateRequestBody = { +export type AdminCustomerUpdateRequestBody = { name?: string; email?: string; description?: string; }; -export type AdminOrganizationUpdateResponseBody = AdminOrganizationGetResponseBody; -export type AdminOrganizationUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type AdminCustomerUpdateResponseBody = AdminCustomerGetResponseBody; +export type AdminCustomerUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Utils diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 282ab918..b0f66e22 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -316,30 +316,30 @@ * type: object * schema: * ref: '#/components/schemas/APIKeyResponse' - * OrganizationResponseBody: - * description: The response body for an organization + * OrganisationResponseBody: + * description: The response body for an organisation * type: object * properties: * name: * type: string - * description: The name of the organization + * description: The name of the organisation * example: Cheqd * email: * type: string - * description: The email of the organization + * description: The email of the organisation * example: cheqd@example.com * format: email * nullable: true * default: null * description: * type: string - * description: The description of the organization - * example: Cheqd organization + * description: The description of the organisation + * example: Cheqd organisation * nullable: true * default: null * cosmosAddress: * type: string - * description: The cosmos address of the organization + * description: The cosmos address of the organisation * example: cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. From d9c0fa65208c4ef4ef7bc62f9ac48508f0685310 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Tue, 16 Apr 2024 23:16:00 +0200 Subject: [PATCH 42/49] Get rid of Customer in admin names --- src/controllers/admin/organisation.ts | 12 ++++++------ src/types/admin.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/controllers/admin/organisation.ts b/src/controllers/admin/organisation.ts index 875e0680..e3e9eee1 100644 --- a/src/controllers/admin/organisation.ts +++ b/src/controllers/admin/organisation.ts @@ -4,7 +4,7 @@ import { check } from 'express-validator'; import { validate } from '../validator/decorator.js'; import { CustomerService } from '../../services/api/customer.js'; import { StatusCodes } from 'http-status-codes'; -import type { AdminCustomerGetUnsuccessfulResponseBody, AdminCustomerUpdateResponseBody, AdminCustomerUpdateUnsuccessfulResponseBody } from '../../types/admin.js'; +import type { AdminOrganisationGetUnsuccessfulResponseBody, AdminOrganisationUpdateResponseBody, AdminOrganisationUpdateUnsuccessfulResponseBody } from '../../types/admin.js'; import { PaymentAccountService } from '../../services/api/payment-account.js'; dotenv.config(); @@ -77,7 +77,7 @@ export class OrganisationController { if (!customer || !paymentAccount) { response.status(StatusCodes.NOT_FOUND).json({ error: 'Customer for updating not found', - } satisfies AdminCustomerUpdateUnsuccessfulResponseBody); + } satisfies AdminOrganisationUpdateUnsuccessfulResponseBody); } return response.status(StatusCodes.OK).json({ @@ -85,11 +85,11 @@ export class OrganisationController { email: customer.email, description: customer.description, cosmosAddress: paymentAccount[0].address as string - } satisfies AdminCustomerUpdateResponseBody); + } satisfies AdminOrganisationUpdateResponseBody); } catch (error) { return response.status(500).json({ error: `Internal error: ${(error as Error)?.message || error}`, - } satisfies AdminCustomerUpdateUnsuccessfulResponseBody); + } satisfies AdminOrganisationUpdateUnsuccessfulResponseBody); } } @@ -125,7 +125,7 @@ export class OrganisationController { if (!customer || !paymentAccount) { response.status(StatusCodes.NOT_FOUND).json({ error: 'Customer for current user was not found or did not setup properly. Please contact administrator.', - } satisfies AdminCustomerGetUnsuccessfulResponseBody); + } satisfies AdminOrganisationGetUnsuccessfulResponseBody); } return response.status(StatusCodes.OK).json({ name: customer.name, @@ -136,7 +136,7 @@ export class OrganisationController { } catch (error) { return response.status(500).json({ error: `Internal error: ${(error as Error)?.message || error}`, - } satisfies AdminCustomerGetUnsuccessfulResponseBody); + } satisfies AdminOrganisationGetUnsuccessfulResponseBody); } } } diff --git a/src/types/admin.ts b/src/types/admin.ts index 5c048edc..cb16979e 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -148,23 +148,23 @@ export type APIKeyGetResponseBody = APIKeyCreateResponseBody; export type APIKeyGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Organisation -export type AdminCustomerResponseBody = { +export type AdminOrganisationResponseBody = { name: string; email?: string; description?: string; cosmosAddress?: string; }; -export type AdminCustomerGetResponseBody = AdminCustomerResponseBody; -export type AdminCustomerGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type AdminOrganisationGetResponseBody = AdminOrganisationResponseBody; +export type AdminOrganisationGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; -export type AdminCustomerUpdateRequestBody = { +export type AdminOrganisationUpdateRequestBody = { name?: string; email?: string; description?: string; }; -export type AdminCustomerUpdateResponseBody = AdminCustomerGetResponseBody; -export type AdminCustomerUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type AdminOrganisationUpdateResponseBody = AdminOrganisationGetResponseBody; +export type AdminOrganisationUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Utils From c0f7c86ece6c7795fcb0712e49322490dcb66b49 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Wed, 17 Apr 2024 10:16:10 +0530 Subject: [PATCH 43/49] Remove duplicate await statement --- src/controllers/admin/webhook.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controllers/admin/webhook.ts b/src/controllers/admin/webhook.ts index 96a11e5c..4b205fae 100644 --- a/src/controllers/admin/webhook.ts +++ b/src/controllers/admin/webhook.ts @@ -18,7 +18,7 @@ export class WebhookController { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); if (!process.env.STRIPE_WEBHOOK_SECRET) { - await await eventTracker.notify({ + await eventTracker.notify({ message: 'Stripe webhook secret not found. Webhook ID: ${request.body.id}.', severity: 'error', } satisfies INotifyMessage); @@ -27,7 +27,7 @@ export class WebhookController { const signature = request.headers['stripe-signature']; if (!signature) { - await await eventTracker.notify({ + await eventTracker.notify({ message: 'Webhook signature not found. Webhook ID: ${request.body.id}.', severity: 'error', } satisfies INotifyMessage); @@ -37,7 +37,7 @@ export class WebhookController { try { event = stripe.webhooks.constructEvent(request.rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { - await await eventTracker.notify({ + await eventTracker.notify({ message: `Webhook signature verification failed. Webhook ID: ${request.body.id}. Error: ${(err as Record)?.message || err}`, severity: 'error', } satisfies INotifyMessage); @@ -48,7 +48,7 @@ export class WebhookController { case 'customer.subscription.trial_will_end': subscription = event.data.object; status = subscription.status; - await await eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.trial_will_end' @@ -62,7 +62,7 @@ export class WebhookController { case 'customer.subscription.deleted': subscription = event.data.object; status = subscription.status; - await await eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.deleted' @@ -74,7 +74,7 @@ export class WebhookController { case 'customer.subscription.created': subscription = event.data.object; status = subscription.status; - await await eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.created' @@ -86,7 +86,7 @@ export class WebhookController { case 'customer.subscription.updated': subscription = event.data.object; status = subscription.status; - await await eventTracker.notify({ + await eventTracker.notify({ message: EventTracker.compileBasicNotification( `Subscription status is ${status} for subscription with id: ${subscription.id}`, 'Stripe Webhook: customer.subscription.updated' From 9bf2ebcd5e46ea69106179070ac289ea3a2cd747 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Wed, 17 Apr 2024 10:43:14 +0200 Subject: [PATCH 44/49] Return back user info strategy switcher --- src/middleware/auth/auth-gaurd.ts | 38 +++++++++++-------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/src/middleware/auth/auth-gaurd.ts b/src/middleware/auth/auth-gaurd.ts index 321573e8..2ca1c5c8 100644 --- a/src/middleware/auth/auth-gaurd.ts +++ b/src/middleware/auth/auth-gaurd.ts @@ -73,34 +73,22 @@ export class APIGuard { * @return {void} This function does not return a value. */ private chooseUserFetcherStrategy(request: Request): void { - const authorizationHeader = request.headers.authorization as string; - const apiKeyHeader = request.headers['x-api-key'] as string; - - if (authorizationHeader && apiKeyHeader) { - const token = authorizationHeader.replace('Bearer ', ''); - this.setUserInfoStrategy(new PortalUserInfoFetcher(token, apiKeyHeader, this.oauthProvider)); - return; - } - - if (authorizationHeader) { - const token = authorizationHeader.replace('Bearer ', ''); - const payload = decodeJwt(token); - - if (payload.aud === process.env.LOGTO_APP_ID) { - this.setUserInfoStrategy(new IdTokenUserInfoFetcher(token, this.oauthProvider)); + const token = APIGuard.extractBearerTokenFromHeaders(request.headers) as string; + const headerIdToken = request.headers['id-token'] as string; + if (headerIdToken && token) { + this.setUserInfoStrategy(new PortalUserInfoFetcher(token, headerIdToken, this.oauthProvider)); + } else { + if (token) { + const payload = decodeJwt(token); + if (payload.aud === process.env.LOGTO_APP_ID) { + this.setUserInfoStrategy(new APITokenUserInfoFetcher(token, this.oauthProvider)); + } else { + this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token, this.oauthProvider)); + } } else { - this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token, this.oauthProvider)); + this.setUserInfoStrategy(new SwaggerUserInfoFetcher(this.oauthProvider)); } - - return; - } - - if (apiKeyHeader) { - this.setUserInfoStrategy(new APITokenUserInfoFetcher(apiKeyHeader, this.oauthProvider)); - return; } - - this.setUserInfoStrategy(new SwaggerUserInfoFetcher(this.oauthProvider)); } From a40283ba6ef5b00e137a5f660bc201c88a555a7e Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Wed, 17 Apr 2024 11:57:36 +0200 Subject: [PATCH 45/49] Fix the user info strategy picking --- src/middleware/auth/auth-gaurd.ts | 45 ++++++++++++++++++------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/middleware/auth/auth-gaurd.ts b/src/middleware/auth/auth-gaurd.ts index 2ca1c5c8..8d35424b 100644 --- a/src/middleware/auth/auth-gaurd.ts +++ b/src/middleware/auth/auth-gaurd.ts @@ -10,7 +10,6 @@ import { PortalUserInfoFetcher } from "./user-info-fetcher/portal-token.js"; import { IdTokenUserInfoFetcher } from "./user-info-fetcher/idtoken.js"; import { M2MCredsTokenUserInfoFetcher } from "./user-info-fetcher/m2m-creds-token.js"; import { APITokenUserInfoFetcher } from "./user-info-fetcher/api-token.js"; -import { decodeJwt } from 'jose'; export class APIGuard { private authRuleRepository: AuthRuleRepository; @@ -73,23 +72,33 @@ export class APIGuard { * @return {void} This function does not return a value. */ private chooseUserFetcherStrategy(request: Request): void { - const token = APIGuard.extractBearerTokenFromHeaders(request.headers) as string; - const headerIdToken = request.headers['id-token'] as string; - if (headerIdToken && token) { - this.setUserInfoStrategy(new PortalUserInfoFetcher(token, headerIdToken, this.oauthProvider)); - } else { - if (token) { - const payload = decodeJwt(token); - if (payload.aud === process.env.LOGTO_APP_ID) { - this.setUserInfoStrategy(new APITokenUserInfoFetcher(token, this.oauthProvider)); - } else { - this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(token, this.oauthProvider)); - } - } else { - this.setUserInfoStrategy(new SwaggerUserInfoFetcher(this.oauthProvider)); - } - } - } + const bearerToken = APIGuard.extractBearerTokenFromHeaders(request.headers) as string; + const portalToken = request.headers['id-token'] as string; + const m2mCreds = request.headers['customer-id'] as string; + const apiToken = request.headers['x-api-key'] as string; + + if (apiToken) { + this.setUserInfoStrategy(new APITokenUserInfoFetcher(apiToken, this.oauthProvider)); + return; + } + + if (m2mCreds) { + this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(m2mCreds, this.oauthProvider)); + return; + } + + if (portalToken && bearerToken) { + this.setUserInfoStrategy(new PortalUserInfoFetcher(bearerToken, portalToken, this.oauthProvider)); + return; + } + + if (bearerToken) { + this.setUserInfoStrategy(new IdTokenUserInfoFetcher(bearerToken, this.oauthProvider)); + return; + } + + this.setUserInfoStrategy(new SwaggerUserInfoFetcher(this.oauthProvider)); + } /** From ba87c9c97abe9971bb538a17d8afc02fe5f71d56 Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Wed, 17 Apr 2024 17:35:16 +0200 Subject: [PATCH 46/49] Review comments --- README.md | 3 +-- docker/Dockerfile | 2 -- src/controllers/admin/organisation.ts | 8 ++++---- src/services/track/tracker.ts | 6 +----- src/types/constants.ts | 2 -- src/types/environment.d.ts | 1 - 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e6070e0f..04211448 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,7 @@ The application allows configuring the following parameters using environment va #### Events tracking -1. `ENABLE_DATADOG`: enables sending information about events to datadog -2. `LOG_LEVEL`: specifies log level, like 'trace', 'debug', 'info', 'warn' or 'error'; +1. `LOG_LEVEL`: specifies log level, like 'trace', 'debug', 'info', 'warn' or 'error'; #### Network API endpoints diff --git a/docker/Dockerfile b/docker/Dockerfile index 5eb787ed..29630d59 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,7 +68,6 @@ ARG LOGTO_M2M_APP_SECRET ARG LOGTO_MANAGEMENT_API ARG LOGTO_DEFAULT_ROLE_ID ARG LOGTO_WEBHOOK_SECRET -ARG ENABLE_DATADOG=false ARG LOG_LEVEL=info # API generation @@ -118,7 +117,6 @@ ENV LOGTO_M2M_APP_SECRET ${LOGTO_M2M_APP_SECRET} ENV LOGTO_MANAGEMENT_API ${LOGTO_MANAGEMENT_API} ENV LOGTO_DEFAULT_ROLE_ID ${LOGTO_DEFAULT_ROLE_ID} ENV LOGTO_WEBHOOK_SECRET ${LOGTO_WEBHOOK_SECRET} -ENV ENABLE_DATADOG ${ENABLE_DATADOG} ENV LOG_LEVEL ${LOG_LEVEL} # API generatioin diff --git a/src/controllers/admin/organisation.ts b/src/controllers/admin/organisation.ts index e3e9eee1..deb5f65e 100644 --- a/src/controllers/admin/organisation.ts +++ b/src/controllers/admin/organisation.ts @@ -17,8 +17,8 @@ export class OrganisationController { withMessage('name should be a valid string'), check('email'). optional(). - isString(). - withMessage('email should be a valid string'), + isEmail(). + withMessage('email value should a well-formatted string'), check('description'). optional(). isString(). @@ -74,7 +74,7 @@ export class OrganisationController { const customer = await CustomerService.instance.update(response.locals.customer.customerId, name, email, description); const paymentAccount = await PaymentAccountService.instance.find({ customer: customer }); - if (!customer || !paymentAccount) { + if (!customer || paymentAccount.length === 0) { response.status(StatusCodes.NOT_FOUND).json({ error: 'Customer for updating not found', } satisfies AdminOrganisationUpdateUnsuccessfulResponseBody); @@ -122,7 +122,7 @@ export class OrganisationController { const customer = response.locals.customer; const paymentAccount = await PaymentAccountService.instance.find({ customer: response.locals.customer }); - if (!customer || !paymentAccount) { + if (!customer || paymentAccount.length === 0) { response.status(StatusCodes.NOT_FOUND).json({ error: 'Customer for current user was not found or did not setup properly. Please contact administrator.', } satisfies AdminOrganisationGetUnsuccessfulResponseBody); diff --git a/src/services/track/tracker.ts b/src/services/track/tracker.ts index b854f2f5..864b5b87 100644 --- a/src/services/track/tracker.ts +++ b/src/services/track/tracker.ts @@ -1,6 +1,6 @@ import EventEmitter from 'node:events'; import type { INotifyMessage, ITrackOperation } from '../../types/track.js'; -import { DatadogNotifier, LoggerNotifier } from './notifiers.js'; +import { LoggerNotifier } from './notifiers.js'; import { DBOperationSubscriber } from './operation-subscriber.js'; import { ResourceSubscriber } from './api/resource-subscriber.js'; import { PresentationSubscriber } from './api/presentation-subscriber.js'; @@ -8,7 +8,6 @@ import { CredentialStatusSubscriber } from './api/credential-status-subscriber.j import { DIDSubscriber } from './api/did-subscriber.js'; import { CredentialSubscriber } from './api/credential-subscriber.js'; import type { ITrackType } from './types.js'; -import { ENABLE_DATADOG } from '../../types/constants.js'; import { SubmitSubject, TrackSubject } from './observer.js'; import type { ISubmitOperation } from './submitter.js'; import { PortalAccountCreateSubmitter } from './admin/account-submitter.js'; @@ -49,9 +48,6 @@ export class EventTracker { setupDefaultNotifiers() { this.notifier.attach(new LoggerNotifier()); - if (ENABLE_DATADOG) { - this.notifier.attach(new DatadogNotifier()); - } } setupDefaultSubmitters() { diff --git a/src/types/constants.ts b/src/types/constants.ts index 34c30e24..2279f964 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -14,8 +14,6 @@ export const CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || APPLICAT export const API_KEY_PREFIX = 'caas'; export const API_SECRET_KEY_LENGTH = 64; export const API_KEY_EXPIRATION = 30; -// By default we don't send events to datadog -export const ENABLE_DATADOG = process.env.ENABLE_DATADOG === 'true' ? true : false; // Possible cases 'trace' 'debug' 'info' 'warn' 'error'; export const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index f5584c72..3e4d3065 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -7,7 +7,6 @@ declare global { NODE_ENV: EnvironmentType; PORT: string; NPM_CONFIG_LOGLEVEL: string; - ENABLE_DATADOG: string | 'false'; LOG_LEVEL: string | 'info'; // Network API endpoints From d3345d689a80a7b1130d3fb7d3db1a09c978b6ae Mon Sep 17 00:00:00 2001 From: Andrew Nikitin Date: Wed, 17 Apr 2024 18:06:40 +0200 Subject: [PATCH 47/49] update swagger-admin.json --- src/static/swagger-admin.json | 122 ++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index f8d03627..8dd4307f 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -222,6 +222,97 @@ } } }, + "/admin/organisation/update": { + "post": { + "summary": "Update an organisation", + "description": "Update an organisation", + "tags": [ + "Organisation" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Cheqd" + }, + "email": { + "type": "string", + "example": "cheqd@example.com", + "format": "email" + }, + "description": { + "type": "string", + "example": "Cheqd organisation" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "A successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, + "/admin/organisation/get": { + "get": { + "summary": "Get an organisation", + "description": "Get an organisation", + "tags": [ + "Organisation" + ], + "responses": { + "200": { + "description": "A successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrganisationResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } + }, "/admin/price/list": { "get": { "summary": "Get a list of prices", @@ -1034,6 +1125,37 @@ "ref": "#/components/schemas/APIKeyResponse" } }, + "OrganisationResponseBody": { + "description": "The response body for an organisation", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the organisation", + "example": "Cheqd" + }, + "email": { + "type": "string", + "description": "The email of the organisation", + "example": "cheqd@example.com", + "format": "email", + "nullable": true, + "default": null + }, + "description": { + "type": "string", + "description": "The description of the organisation", + "example": "Cheqd organisation", + "nullable": true, + "default": null + }, + "cosmosAddress": { + "type": "string", + "description": "The cosmos address of the organisation", + "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" + } + } + }, "NotFoundError": { "description": "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.", "type": "object", From cd6ec32eb7c0dd3f6d79be8441cac88e637e38ce Mon Sep 17 00:00:00 2001 From: Ankur Banerjee Date: Wed, 17 Apr 2024 17:13:58 +0100 Subject: [PATCH 48/49] npm run format --- src/app.ts | 7 ++- src/controllers/admin/organisation.ts | 33 ++++++----- .../migrations/AlterCustomerTableAddEmail.ts | 2 +- src/middleware/auth/auth-gaurd.ts | 55 +++++++++---------- .../auth/routes/admin/admin-auth.ts | 1 - src/middleware/auth/user-info-fetcher/base.ts | 9 ++- src/services/api/customer.ts | 8 ++- src/types/admin.ts | 1 - 8 files changed, 60 insertions(+), 56 deletions(-) diff --git a/src/app.ts b/src/app.ts index efa795b4..c996c566 100644 --- a/src/app.ts +++ b/src/app.ts @@ -285,9 +285,12 @@ class App { app.post('/admin/webhook', new WebhookController().handleWebhook); // Customer - app.post('/admin/organisation/update', OrganisationController.organisationUpdatevalidator, new OrganisationController().update); + app.post( + '/admin/organisation/update', + OrganisationController.organisationUpdatevalidator, + new OrganisationController().update + ); app.get('/admin/organisation/get', new OrganisationController().get); - } // 404 for all other requests diff --git a/src/controllers/admin/organisation.ts b/src/controllers/admin/organisation.ts index deb5f65e..49c210f7 100644 --- a/src/controllers/admin/organisation.ts +++ b/src/controllers/admin/organisation.ts @@ -4,28 +4,22 @@ import { check } from 'express-validator'; import { validate } from '../validator/decorator.js'; import { CustomerService } from '../../services/api/customer.js'; import { StatusCodes } from 'http-status-codes'; -import type { AdminOrganisationGetUnsuccessfulResponseBody, AdminOrganisationUpdateResponseBody, AdminOrganisationUpdateUnsuccessfulResponseBody } from '../../types/admin.js'; +import type { + AdminOrganisationGetUnsuccessfulResponseBody, + AdminOrganisationUpdateResponseBody, + AdminOrganisationUpdateUnsuccessfulResponseBody, +} from '../../types/admin.js'; import { PaymentAccountService } from '../../services/api/payment-account.js'; dotenv.config(); export class OrganisationController { static organisationUpdatevalidator = [ - check('name'). - optional(). - isString(). - withMessage('name should be a valid string'), - check('email'). - optional(). - isEmail(). - withMessage('email value should a well-formatted string'), - check('description'). - optional(). - isString(). - withMessage('description should be a valid string'), + check('name').optional().isString().withMessage('name should be a valid string'), + check('email').optional().isEmail().withMessage('email value should a well-formatted string'), + check('description').optional().isString().withMessage('description should be a valid string'), ]; - /** * @openapi * @@ -71,7 +65,12 @@ export class OrganisationController { async update(request: Request, response: Response) { const { name, email, description } = request.body; try { - const customer = await CustomerService.instance.update(response.locals.customer.customerId, name, email, description); + const customer = await CustomerService.instance.update( + response.locals.customer.customerId, + name, + email, + description + ); const paymentAccount = await PaymentAccountService.instance.find({ customer: customer }); if (!customer || paymentAccount.length === 0) { @@ -84,7 +83,7 @@ export class OrganisationController { name: customer.name, email: customer.email, description: customer.description, - cosmosAddress: paymentAccount[0].address as string + cosmosAddress: paymentAccount[0].address as string, } satisfies AdminOrganisationUpdateResponseBody); } catch (error) { return response.status(500).json({ @@ -116,7 +115,7 @@ export class OrganisationController { * $ref: '#/components/schemas/InternalError' * 404: * $ref: '#/components/schemas/NotFoundError' - */ + */ async get(request: Request, response: Response) { try { const customer = response.locals.customer; diff --git a/src/database/migrations/AlterCustomerTableAddEmail.ts b/src/database/migrations/AlterCustomerTableAddEmail.ts index 36f8c94e..fcf40bef 100644 --- a/src/database/migrations/AlterCustomerTableAddEmail.ts +++ b/src/database/migrations/AlterCustomerTableAddEmail.ts @@ -13,7 +13,7 @@ export class AlterCustomerTableAddEmail1695740346005 implements MigrationInterfa }) ); - await queryRunner.addColumn( + await queryRunner.addColumn( table_name, new TableColumn({ name: 'description', diff --git a/src/middleware/auth/auth-gaurd.ts b/src/middleware/auth/auth-gaurd.ts index 838e69df..0aac2f4b 100644 --- a/src/middleware/auth/auth-gaurd.ts +++ b/src/middleware/auth/auth-gaurd.ts @@ -70,35 +70,34 @@ export class APIGuard { * @param {Request} request - The request object containing the headers. * @return {void} This function does not return a value. */ - private chooseUserFetcherStrategy(request: Request): void { - const bearerToken = APIGuard.extractBearerTokenFromHeaders(request.headers) as string; - const portalToken = request.headers['id-token'] as string; - const m2mCreds = request.headers['customer-id'] as string; - const apiToken = request.headers['x-api-key'] as string; - - if (apiToken) { - this.setUserInfoStrategy(new APITokenUserInfoFetcher(apiToken, this.oauthProvider)); - return; - } - - if (m2mCreds) { - this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(m2mCreds, this.oauthProvider)); - return; - } - - if (portalToken && bearerToken) { - this.setUserInfoStrategy(new PortalUserInfoFetcher(bearerToken, portalToken, this.oauthProvider)); - return; - } - - if (bearerToken) { - this.setUserInfoStrategy(new IdTokenUserInfoFetcher(bearerToken, this.oauthProvider)); - return; - } - - this.setUserInfoStrategy(new SwaggerUserInfoFetcher(this.oauthProvider)); - } + private chooseUserFetcherStrategy(request: Request): void { + const bearerToken = APIGuard.extractBearerTokenFromHeaders(request.headers) as string; + const portalToken = request.headers['id-token'] as string; + const m2mCreds = request.headers['customer-id'] as string; + const apiToken = request.headers['x-api-key'] as string; + + if (apiToken) { + this.setUserInfoStrategy(new APITokenUserInfoFetcher(apiToken, this.oauthProvider)); + return; + } + + if (m2mCreds) { + this.setUserInfoStrategy(new M2MCredsTokenUserInfoFetcher(m2mCreds, this.oauthProvider)); + return; + } + + if (portalToken && bearerToken) { + this.setUserInfoStrategy(new PortalUserInfoFetcher(bearerToken, portalToken, this.oauthProvider)); + return; + } + + if (bearerToken) { + this.setUserInfoStrategy(new IdTokenUserInfoFetcher(bearerToken, this.oauthProvider)); + return; + } + this.setUserInfoStrategy(new SwaggerUserInfoFetcher(this.oauthProvider)); + } /** * Sets the user info strategy for the API guard. diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index 9a25f9bf..cebd79ff 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -41,6 +41,5 @@ export class AdminAuthRuleProvider extends AuthRuleProvider { // Customer this.registerRule('/admin/organisation/update', 'POST', 'admin:organisation:update', { skipNamespace: true }); this.registerRule('/admin/organisation/get', 'GET', 'admin:organisation:get', { skipNamespace: true }); - } } diff --git a/src/middleware/auth/user-info-fetcher/base.ts b/src/middleware/auth/user-info-fetcher/base.ts index fba9ef50..d7f170cb 100644 --- a/src/middleware/auth/user-info-fetcher/base.ts +++ b/src/middleware/auth/user-info-fetcher/base.ts @@ -18,7 +18,6 @@ export interface IUserInfoFetcher { } export class UserInfoHelper { - setScopes(scopes: string[], response: Response) { response.locals.scopes = scopes; return; @@ -28,13 +27,13 @@ export class UserInfoHelper { if (!customerEntity) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: `Unexpected error: Customer entity for handling such request is not found in internal storage. CustomerId: ${customerId}`, - }) + }); } const userEntity = await UserService.instance.findOne({ customer: customerEntity }); if (!userEntity) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: `Unexpected error: User entity for handling such request is not found in internal storage. CustomerId: ${customerId}`, - }) + }); } response.locals.customer = customerEntity; response.locals.user = userEntity; @@ -46,10 +45,10 @@ export class UserInfoHelper { if (!entity) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: `Unexpected error: User entity for handling such request is not found in internal storage`, - }) + }); } response.locals.user = entity; response.locals.customer = entity.customer; return; } -} \ No newline at end of file +} diff --git a/src/services/api/customer.ts b/src/services/api/customer.ts index 0b965199..10191648 100644 --- a/src/services/api/customer.ts +++ b/src/services/api/customer.ts @@ -42,7 +42,13 @@ export class CustomerService { }; } - public async update(customerId: string, name?: string, email?: string, description?: string, paymentProviderId?: string) { + public async update( + customerId: string, + name?: string, + email?: string, + description?: string, + paymentProviderId?: string + ) { const existingCustomer = await this.customerRepository.findOneBy({ customerId }); if (!existingCustomer) { throw new Error(`CustomerId not found`); diff --git a/src/types/admin.ts b/src/types/admin.ts index cb16979e..6836476c 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -166,7 +166,6 @@ export type AdminOrganisationUpdateRequestBody = { export type AdminOrganisationUpdateResponseBody = AdminOrganisationGetResponseBody; export type AdminOrganisationUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; - // Utils export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; From a9a2aac646bb0207d45ebcd683b9cf2160d3c9ba Mon Sep 17 00:00:00 2001 From: Ankur Banerjee Date: Wed, 17 Apr 2024 17:16:05 +0100 Subject: [PATCH 49/49] Fix text of error message --- src/controllers/admin/api-key.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/admin/api-key.ts b/src/controllers/admin/api-key.ts index bb39d5b5..82edd873 100644 --- a/src/controllers/admin/api-key.ts +++ b/src/controllers/admin/api-key.ts @@ -201,7 +201,7 @@ export class APIKeyController { ); if (!apiKeyEntity) { return response.status(StatusCodes.NOT_FOUND).json({ - error: "Cannot update API key cause it's not found", + error: 'Update failed: API key does not exist', } satisfies APIKeyUpdateUnsuccessfulResponseBody); }