diff --git a/README.md b/README.md index d3b4158d..844e60d7 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,15 @@ 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_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 The app supports 3rd party connectors for credential storage and delivery. diff --git a/docker/Dockerfile b/docker/Dockerfile index 1c554d37..3e51763a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -80,6 +80,12 @@ ARG ENABLE_ACCOUNT_TOPUP=false 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 + # Environment variables: base configuration ENV NPM_CONFIG_LOGLEVEL ${NPM_CONFIG_LOGLEVEL} ENV PORT ${PORT} @@ -123,6 +129,12 @@ 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} +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/package-lock.json b/package-lock.json index e33aa35c..37467a9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cheqd/credential-service", - "version": "2.19.0-develop.1", + "version": "2.19.1-develop.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cheqd/credential-service", - "version": "2.19.0-develop.1", + "version": "2.19.1-develop.1", "license": "Apache-2.0", "dependencies": { "@cheqd/did-provider-cheqd": "^4.1.0", @@ -53,6 +53,7 @@ "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", @@ -32098,6 +32099,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 a0465822..de1aa43a 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}'", @@ -94,6 +96,7 @@ "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", diff --git a/src/app.ts b/src/app.ts index f5580293..ab0ccf33 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,12 +20,17 @@ 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 { FailedResponseTracker } from './middleware/event-tracker.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 { 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'; +import { WebhookController } from './controllers/admin/webhook.js'; let swaggerOptions = {}; if (process.env.ENABLE_AUTHENTICATION === 'true') { @@ -71,7 +76,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( @@ -96,7 +101,19 @@ class App { } this.express.use(express.text()); - this.express.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument, swaggerOptions)); + this.express.use( + '/swagger', + swaggerUi.serveFiles(swaggerAPIDocument, swaggerOptions), + swaggerUi.setup(swaggerAPIDocument, swaggerOptions) + ); + 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)); } @@ -206,6 +223,55 @@ class App { express.static(path.join(process.cwd(), '/dist'), { extensions: ['js'], index: false }) ); + // Portal + // Product + 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); + + // 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); + } + // 404 for all other requests app.all('*', (_req, res) => res.status(StatusCodes.BAD_REQUEST).send('Bad request')); } diff --git a/src/controllers/admin/portal-customer.ts b/src/controllers/admin/portal-customer.ts new file mode 100644 index 00000000..4c1d1c4d --- /dev/null +++ b/src/controllers/admin/portal-customer.ts @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000..935b554d --- /dev/null +++ b/src/controllers/admin/prices.ts @@ -0,0 +1,73 @@ +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 { StatusCodes } from 'http-status-codes'; +import { check } from '../validator/index.js'; +import { validate } from '../validator/decorator.js'; + +dotenv.config(); + +export class PriceController { + 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: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + @validate + async getListPrices(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; + // 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); + } + } +} diff --git a/src/controllers/admin/product.ts b/src/controllers/admin/product.ts new file mode 100644 index 00000000..920695a0 --- /dev/null +++ b/src/controllers/admin/product.ts @@ -0,0 +1,172 @@ +import type { 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 { check } from '../validator/index.js'; +import { validate } from '../validator/decorator.js'; + +dotenv.config(); + +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: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + @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; + + 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' + */ + @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; + + 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); + } + } +} diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts new file mode 100644 index 00000000..9c21b2bf --- /dev/null +++ b/src/controllers/admin/subscriptions.ts @@ -0,0 +1,514 @@ +import type 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 { check } from '../validator/index.js'; +import { SubscriptionService } from '../../services/admin/subscription.js'; +import { stripeService } from '../../services/admin/stripe.js'; +import type { UnsuccessfulResponseBody } from '../../types/shared.js'; +import { validate } from '../validator/decorator.js'; + +dotenv.config(); + +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) { + 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 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') + .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('trialPeriodDays').optional().isInt().withMessage('trialPeriodDays 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(), + ]; + + static subscriptionListValidator = [ + check('paymentProviderId').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(), + ]; + + /** + * @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' + */ + + @validate + async create(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; + + const { price, successURL, cancelURL, quantity, idempotencyKey, trialPeriodDays } = + request.body satisfies SubscriptionCreateRequestBody; + try { + const session = await stripe.checkout.sessions.create( + { + mode: 'subscription', + customer: response.locals.customer.paymentProviderId, + line_items: [ + { + price: price, + quantity: quantity || 1, + }, + ], + success_url: successURL, + cancel_url: cancelURL, + subscription_data: { + trial_period_days: trialPeriodDays, + }, + }, + { + idempotencyKey, + } + ); + + if (session.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Checkout session was not created', + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + + if (!session.url) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Checkout session URL was not provided', + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + + return response.json({ + sessionURL: session.url as string, + } 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' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + */ + @validate + @syncOne + 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) { + 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.paymentProviderId, + return_url: returnUrl, + }); + + if (session.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Billing portal session for upgrading the subscription was not created', + } satisfies SubscriptionUpdateUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + sessionURL: session.url, + } satisfies SubscriptionUpdateResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies SubscriptionUpdateUnsuccessfulResponseBody); + } + } + + /** + * @openapi + * + * /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' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + + @validate + @syncCustomer + public async list(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; + const paymentProviderId = response.locals.customer.paymentProviderId; + try { + // Get the subscriptions + const subscriptions = paymentProviderId + ? await stripe.subscriptions.list({ + customer: paymentProviderId 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: + * 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' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + @validate + @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.findCurrent(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`, + } 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' + */ + @validate + @syncOne + async cancel(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; + 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.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' + */ + @validate + @syncOne + async resume(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; + 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); + } + } +} diff --git a/src/controllers/admin/webhook.ts b/src/controllers/admin/webhook.ts new file mode 100644 index 00000000..8b5f0007 --- /dev/null +++ b/src/controllers/admin/webhook.ts @@ -0,0 +1,111 @@ +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 { buildSubmitOperation } from '../../services/track/helpers.js'; + +dotenv.config(); +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; + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + + 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( + buildSubmitOperation(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(buildSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CANCEL)); + 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(buildSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CREATE)); + 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(buildSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE)); + break; + default: + // Unexpected event type + eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `Unexpected event: ${event} with 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/account.ts b/src/controllers/api/account.ts similarity index 90% rename from src/controllers/account.ts rename to src/controllers/api/account.ts index 739e4943..65fdc083 100644 --- a/src/controllers/account.ts +++ b/src/controllers/api/account.ts @@ -1,26 +1,28 @@ 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, 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 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/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'; +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'; +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.paymentProviderId === 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/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..7cbea5f1 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/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 { 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 97% rename from src/controllers/did.ts rename to src/controllers/api/did.ts index a4f064b0..d41e06f9 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'; @@ -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/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/controllers/validator/decorator.ts b/src/controllers/validator/decorator.ts new file mode 100644 index 00000000..2657d46c --- /dev/null +++ b/src/controllers/validator/decorator.ts @@ -0,0 +1,30 @@ +import type { ValidationErrorResponseBody } from '../../types/shared.js'; +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; + } + + 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; +} diff --git a/src/database/entities/customer.entity.ts b/src/database/entities/customer.entity.ts index 6426c24d..de774c1f 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, + }) + paymentProviderId!: 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..5c2dd197 --- /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; + if (trialStart) this.trialStart = trialStart; + if (trialEnd) this.trialEnd = trialEnd; + } +} diff --git a/src/database/migrations/AlterCustomerTable.ts b/src/database/migrations/AlterCustomerTable.ts new file mode 100644 index 00000000..c19df230 --- /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: 'paymentProviderId', + 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..fc55bc0f --- /dev/null +++ b/src/database/migrations/CreateSubscriptionTable.ts @@ -0,0 +1,35 @@ +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'); + } +} diff --git a/src/database/types/enum.ts b/src/database/types/enum.ts index 72f3bfae..92ff2180 100644 --- a/src/database/types/enum.ts +++ b/src/database/types/enum.ts @@ -5,9 +5,18 @@ 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..eb896d4c 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 paymentProviderId 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/helpers/helpers.ts b/src/helpers/helpers.ts index 09ad386f..f65eb0b3 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 c479bb25..927f98c1 100644 --- a/src/middleware/auth/base-auth-handler.ts +++ b/src/middleware/auth/base-auth-handler.ts @@ -8,8 +8,9 @@ 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'; export class BaseAPIGuard extends RuleRoutine implements IAPIGuard { userInfoFetcher: IUserInfoFetcher = {} as IUserInfoFetcher; @@ -24,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 auth rules for handling such request: ${request.method} ${request.path}` ); } // If the rule is not found - skip the auth check @@ -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,20 +131,26 @@ 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 M2MCredsTokenUserInfoFetcher(token)); + } } else { - this.setUserInfoStrategy(new M2MTokenUserInfoFetcher(token)); + this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); } - } else { - this.setUserInfoStrategy(new SwaggerUserInfoFetcher()); } } - 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/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts new file mode 100644 index 00000000..4c10144f --- /dev/null +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -0,0 +1,58 @@ +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(); + // 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, + }); + this.registerRoute('/admin/subscription/create', 'POST', 'admin:subscription:create: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/update', 'POST', 'admin:subscription:update:testnet', { + skipNamespace: true, + }); + 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 + 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/')) { + 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..ad373936 100644 --- a/src/middleware/auth/user-info-fetcher/base.ts +++ b/src/middleware/auth/user-info-fetcher/base.ts @@ -2,6 +2,10 @@ 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-creds-token.ts similarity index 85% rename from src/middleware/auth/user-info-fetcher/m2m-token.ts rename to src/middleware/auth/user-info-fetcher/m2m-creds-token.ts index 4c6df508..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) { @@ -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..2eaf358f --- /dev/null +++ b/src/middleware/auth/user-info-fetcher/portal-token.ts @@ -0,0 +1,84 @@ +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.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 scopes = payload.scope ? (payload.scope as string).split(' ') : []; + 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..695be967 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -11,25 +11,26 @@ 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(); 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(); } @@ -41,37 +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(); - - // 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); - - // Set chain of responsibility - this.authHandler - .setNext(didAuthHandler) - .setNext(keyAuthHandler) - .setNext(credentialAuthHandler) - .setNext(credentialStatusAuthHandler) - .setNext(resourceAuthHandler) - .setNext(presentationAuthHandler) - .setNext(authInfoHandler); + 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; } @@ -88,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({ @@ -110,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) { @@ -129,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, 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/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 new file mode 100644 index 00000000..a3bba0f0 --- /dev/null +++ b/src/services/admin/stripe.ts @@ -0,0 +1,154 @@ +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 type { SubscriptionEntity } from '../../database/entities/subscription.entity.js'; +import { buildSubmitOperation } 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 + 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', + }); + } + + // 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.paymentProviderId, + 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 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( + buildSubmitOperation(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; + } + await this.submitter.submitSubscriptionUpdate( + buildSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE) + ); + } +} + +export const stripeService = new StripeService(); diff --git a/src/services/admin/subscription.ts b/src/services/admin/subscription.ts new file mode 100644 index 00000000..6e4b8eb3 --- /dev/null +++ b/src/services/admin/subscription.ts @@ -0,0 +1,107 @@ +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 Stripe from 'stripe'; + +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'], + }); + } + + 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 && + 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; + } + if (subscription.trial_end) { + if (!subscriptionEntity.trialEnd || subscriptionEntity.trialEnd.getTime() !== subscription.trial_end * 1000) + return false; + } + return true; + } +} 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 80% rename from src/services/credentials.ts rename to src/services/api/credentials.ts index 2fdd7cd2..9da2b91a 100644 --- a/src/services/credentials.ts +++ b/src/services/api/credentials.ts @@ -1,12 +1,12 @@ 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 { VeridaDIDValidator } from '../controllers/validator/did.js'; +import type { CustomerEntity } from '../../database/entities/customer.entity.js'; +import { VeridaDIDValidator } from '../../controllers/validator/did.js'; dotenv.config(); const { ENABLE_VERIDA_CONNECTOR } = process.env; @@ -42,7 +42,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( isVeridaDid.namespace, diff --git a/src/services/customer.ts b/src/services/api/customer.ts similarity index 71% rename from src/services/customer.ts rename to src/services/api/customer.ts index 4f103dad..254290c4 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, paymentProviderId?: 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 (paymentProviderId) { + existingCustomer.paymentProviderId = paymentProviderId; + } return await this.customerRepository.save(existingCustomer); } @@ -56,12 +61,28 @@ 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 findbyPaymentProviderId(paymentProviderId: string) { + return await this.customerRepository.findOne({ + where: { paymentProviderId: paymentProviderId }, + }); + } + 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..de5346fa --- /dev/null +++ b/src/services/track/admin/account-submitter.ts @@ -0,0 +1,67 @@ +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; + + constructor(emitter: EventEmitter) { + this.emitter = emitter; + } + + 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; + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + + try { + // Create a new Stripe account + const account = await 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..bf808a77 --- /dev/null +++ b/src/services/track/admin/subscription-submitter.ts @@ -0,0 +1,165 @@ +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 } from '../tracker.js'; +import type { IObserver } from '../types.js'; + +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; + } + } + + async submitSubscriptionCreate(operation: ISubmitOperation): Promise { + const data = operation.data as ISubmitSubscriptionData; + let customer: CustomerEntity | undefined = operation.options?.customer; + try { + if (!customer) { + const customers = await CustomerService.instance.find({ + 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.paymentProviderId}.`, + operation.operation + ), + severity: 'error', + }); + } + customer = customers[0]; + } + + const subscription = await SubscriptionService.instance.create( + data.subscriptionId, + customer, + 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', + }); + } + } + + 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', + }); + } + } + + 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 new file mode 100644 index 00000000..762d543b --- /dev/null +++ b/src/services/track/api/credential-status-subscriber.ts @@ -0,0 +1,40 @@ +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..7ca1515c --- /dev/null +++ b/src/services/track/api/credential-subscriber.ts @@ -0,0 +1,40 @@ +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..5bc38685 --- /dev/null +++ b/src/services/track/api/did-subscriber.ts @@ -0,0 +1,38 @@ +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..1e46b6f9 --- /dev/null +++ b/src/services/track/api/key-subscriber.ts @@ -0,0 +1,40 @@ +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..daee2c07 --- /dev/null +++ b/src/services/track/api/presentation-subscriber.ts @@ -0,0 +1,40 @@ +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..f26dbe05 --- /dev/null +++ b/src/services/track/api/resource-subscriber.ts @@ -0,0 +1,135 @@ +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/helpers.ts b/src/services/track/helpers.ts index 1b720b9b..638f4718 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 buildSubmitOperation(subscription: Stripe.Subscription, name: string, options?: ISubmitOptions) { + return { + operation: name, + data: { + subscriptionId: subscription.id, + paymentProviderId: 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/observer.ts b/src/services/track/observer.ts index 2672d504..61dc82f0 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,30 @@ 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..57409171 --- /dev/null +++ b/src/services/track/operation-subscriber.ts @@ -0,0 +1,156 @@ +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..207660e2 --- /dev/null +++ b/src/services/track/submitter.ts @@ -0,0 +1,26 @@ +import type { ISubmitOptions } from './types'; + +// Type: Interface +export type ISubmitData = ISubmitStripeCustomerCreateData | ISubmitSubscriptionData; + +export interface ISubmitStripeCustomerCreateData { + customerId: string; + name: string; + email?: string; +} + +export interface ISubmitSubscriptionData { + paymentProviderId: string; + status: string; + currentPeriodStart: Date; + currentPeriodEnd: Date; + trialStart?: Date; + trialEnd?: Date; + subscriptionId: string; +} + +export interface ISubmitOperation { + operation: string; + data: ISubmitData; + options?: ISubmitOptions; +} diff --git a/src/services/track/subscribers.ts b/src/services/track/subscribers.ts deleted file mode 100644 index 290b4231..00000000 --- a/src/services/track/subscribers.ts +++ /dev/null @@ -1,464 +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; - } - const resourceOperation = { ...trackOperation }; - resourceOperation.category = OperationCategoryNameEnum.RESOURCE; - resourceOperation.name = OperationNameEnum.RESOURCE_CREATE; - // tracking resource creation in DB - const result = await this.trackResourceOperation(resourceOperation); - // 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..b854f2f5 100644 --- a/src/services/track/tracker.ts +++ b/src/services/track/tracker.ts @@ -1,27 +1,30 @@ import EventEmitter from 'node:events'; import type { INotifyMessage, ITrackOperation } from '../../types/track.js'; import { DatadogNotifier, LoggerNotifier } from './notifiers.js'; -import { - DBOperationSubscriber, - ResourceSubscriber, - CredentialSubscriber, - DIDSubscriber, - CredentialStatusSubscriber, - PresentationSubscriber, -} from './subscribers.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'; +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 +32,9 @@ export class EventTracker { if (!notifier) { this.setupDefaultNotifiers(); } + if (!submitter) { + this.setupDefaultSubmitters(); + } this.setupBaseEvents(); } @@ -48,6 +54,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 +66,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 +77,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..2b61b251 100644 --- a/src/services/track/types.ts +++ b/src/services/track/types.ts @@ -1,6 +1,8 @@ +import type { CustomerEntity } from '../../database/entities/customer.entity'; 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; @@ -16,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-options.json b/src/static/swagger-admin-options.json new file mode 100644 index 00000000..b3382775 --- /dev/null +++ b/src/static/swagger-admin-options.json @@ -0,0 +1,34 @@ +{ + "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" + } + ] +} diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json new file mode 100644 index 00000000..7643a41b --- /dev/null +++ b/src/static/swagger-admin.json @@ -0,0 +1,644 @@ +{ + "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" + } + } + } + } + } +} 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-api.json b/src/static/swagger-api.json new file mode 100644 index 00000000..a490ba8a --- /dev/null +++ b/src/static/swagger-api.json @@ -0,0 +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" + } + } + } + } + } +} diff --git a/src/static/swagger.json b/src/static/swagger.json deleted file mode 100644 index d8d26cfe..00000000 --- a/src/static/swagger.json +++ /dev/null @@ -1,3549 +0,0 @@ -{ - "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 diff --git a/src/types/constants.ts b/src/types/constants.ts index e67b0e9d..1c09245a 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -79,6 +79,7 @@ export enum OperationCategoryNameEnum { CREDENTIAL = 'credential', PRESENTATION = 'presentation', KEY = 'key', + SUBSCRIPTION = 'subscription', } export enum OperationDefaultFeeEnum { @@ -126,6 +127,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 63ce0445..54897834 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -56,6 +56,12 @@ declare global { // Creds 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 new file mode 100644 index 00000000..2e4fcaa7 --- /dev/null +++ b/src/types/portal.ts @@ -0,0 +1,99 @@ +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 = { + price: string; + successURL: string; + cancelURL: string; + quantity?: number; + trialPeriodDays?: number; + idempotencyKey?: string; +}; + +export type SubscriptionCreateResponseBody = { + sessionURL: Stripe.Checkout.Session['client_secret']; +}; + +export type SubscriptionUpdateResponseBody = { + sessionURL: string; +}; + +// Update +export type SubscriptionUpdateRequestBody = { + returnUrl: string; +}; + +// Get +export type SubscriptionGetRequestBody = { + subscriptionId: string; +}; + +export type SubscriptionGetResponseBody = { + subscription: Stripe.Response; +}; + +// List +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; + +// Utils + +export type PaymentBehavior = Stripe.SubscriptionCreateParams.PaymentBehavior; diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts new file mode 100644 index 00000000..33249863 --- /dev/null +++ b/src/types/swagger-admin-types.ts @@ -0,0 +1,181 @@ +/** + * @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: + * 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 + */ 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 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 =