diff --git a/README.md b/README.md index 4ee759a..62c0875 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,17 @@ The purpose of this service is to provide a [Universal Registrar driver](https:/ ## 📖 Endpoints -- `/create` -- `/update` -- `/deactivate` -- `/create-resource` -- `/api-docs` +- POST `/create` +- POST `/update` +- POST `/deactivate` +- POST `/{did}/create-resource` +- POST `/createResource` +- POST `/updateResource` +- GET `/key-pair` +- GET `/did-document` +- GET `/properties` +- GET `/methods` +- GET `/traits` ## 🧑‍💻🛠 Developer Guide @@ -65,6 +71,24 @@ npm run build npm start ``` +### 🛠 Testing + +This repository contains the playwright tests for unit and integration testing. +Add any additional tests in the `tests` directory. + +You must set up these two env vars before running test: + +1. `TEST_PRIVATE_KEY` : Private key for signing the requests +2. `TEST_PUBLIC_KEY` : Corresponding public key + +Then execute the tests + +```bash +npm run test +# if tests faile because of parallelism, run +npm run test -- --workers=1 +``` + ## 🐞 Bug reports & 🤔 feature requests If you notice anything not behaving how you expected, or would like to make a suggestion / request for a new feature, please create a [**new issue**](https://github.com/cheqd/did-registrar/issues/new/choose) and let us know. diff --git a/package-lock.json b/package-lock.json index 3c519c9..4d1ffba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cheqd/did-registrar", - "version": "2.1.5", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cheqd/did-registrar", - "version": "2.1.5", + "version": "2.2.0", "license": "Apache-2.0", "dependencies": { "@cheqd/sdk": "^5.0.1", diff --git a/src/app.ts b/src/app.ts index e702e37..002ec7d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import express, { Request, Response } from 'express'; import Helmet from 'helmet'; import swaggerUI from 'swagger-ui-express'; @@ -7,7 +7,8 @@ import swaggerDocument from './static/swagger.json' with { type: 'json' }; import { DidController } from './controllers/did.js'; import { CheqdController } from './controllers/cheqd.js'; import { ResourceController } from './controllers/resource.js'; -import { CheqdRegistrar } from './service/cheqd.js'; +import { CheqdRegistrar, DefaultResolverUrl, DefaultRPCUrl } from './service/cheqd.js'; +import { MethodSpecificIdAlgo, VerificationMethods } from '@cheqd/sdk'; class App { public express: express.Application; @@ -29,6 +30,44 @@ class App { private routes() { const app = this.express; const URL_PREFIX = '/1.0'; + const staticTraits = { + name: 'cheqd', + updateable: true, + updateableServiceEndpoints: true, + deactivatable: true, + deletable: false, + transactionalFees: true, + selfCertifying: true, + updateableVerificationMethods: true, + prerotationOfKeys: false, + multisigVerificationMethod: false, + humanreadable: false, + enumerable: true, + resolvableLocally: false, + resolvableGlobally: true, + history: true, + historySigned: true, + hostingNotRequired: false, + hostedCentrally: false, + hostedDecentrally: true, + cryptographyPrivacyPreserving: false, + cryptographyGovernmentApproved: false, + dataProtectionCompliant: false, + }; + const properties = { + cheqd: { + supportedVerificationMethods: [ + VerificationMethods.Ed255192020, + VerificationMethods.Ed255192018, + VerificationMethods.JWK, + ], + supportedAlgorithm: [MethodSpecificIdAlgo.Base58, MethodSpecificIdAlgo.Uuid], + localStoreTTL: process.env.LOCAL_STORE_TTL, + rpcUrl: DefaultRPCUrl.Mainnet, + resolverUrl: DefaultResolverUrl.Cheqd, + }, + }; + const didMethods = ['cheqd']; app.get('/', (req, res) => res.redirect('api-docs')); @@ -58,6 +97,29 @@ class App { new ResourceController().create ); + app.post( + `${URL_PREFIX}/createResource`, + ResourceController.createResourceValidator, + DidController.commonValidator, + new ResourceController().createResource + ); + app.post( + `${URL_PREFIX}/updateResource`, + ResourceController.updateResourceValidator, + DidController.commonValidator, + new ResourceController().updateResource + ); + + app.get(`${URL_PREFIX}/methods`, (req: Request, res: Response) => { + res.status(200).json(didMethods); + }); + app.get(`${URL_PREFIX}/properties`, (req: Request, res: Response) => { + res.status(200).json(properties); + }); + app.get(`${URL_PREFIX}/traits`, (req: Request, res: Response) => { + res.status(200).json(staticTraits); + }); + // cheqd-helpers app.get(`${URL_PREFIX}/key-pair`, new CheqdController().generateKeys); app.get(`${URL_PREFIX}/did-document`, CheqdController.didDocValidator, new CheqdController().generateDidDoc); diff --git a/src/controllers/resource.ts b/src/controllers/resource.ts index 63497e4..0f1edbd 100644 --- a/src/controllers/resource.ts +++ b/src/controllers/resource.ts @@ -7,7 +7,13 @@ import { v4 } from 'uuid'; import { fromString } from 'uint8arrays'; import { CheqdRegistrar, CheqdResolver, NetworkType } from '../service/cheqd.js'; -import { IResourceCreateRequest, IState } from '../types/types.js'; +import { + ContentOperation, + IResourceCreateRequest, + IResourceCreateRequestV1, + IResourceUpdateRequest, + IState, +} from '../types/types.js'; import { Messages } from '../types/constants.js'; import { convertToSignInfo } from '../helpers/helpers.js'; import { Responses } from '../helpers/response.js'; @@ -29,13 +35,56 @@ export class ResourceController { check('alsoKnownAs.*.uri').isString().withMessage(Messages.Invalid), check('alsoKnownAs.*.description').isString().withMessage(Messages.Invalid), ]; + public static createResourceValidator = [ + check('did').exists().isString().contains('did:cheqd').withMessage(Messages.InvalidDid), + check('jobId') + .custom((value, { req }) => { + if (!value && !(req.body.name && req.body.type && req.body.content)) return false; + return true; + }) + .withMessage('name, type and content are required'), + check('content').optional().isString().withMessage(Messages.Invalid), + check('name').optional().isString().withMessage(Messages.Invalid), + check('type').optional().isString().withMessage(Messages.Invalid), + check('version').optional().isString().withMessage(Messages.Invalid), + check('relativeDidUrl').optional().isString().contains('/resources/').withMessage(Messages.InvalidDidUrl), + check('alsoKnownAs').optional().isArray().withMessage(Messages.Invalid), + check('alsoKnownAs.*.uri').isString().withMessage(Messages.Invalid), + check('alsoKnownAs.*.description').isString().withMessage(Messages.Invalid), + ]; + public static updateResourceValidator = [ + check('did').exists().isString().contains('did:cheqd').withMessage(Messages.InvalidDid), + check('jobId') + .custom((value, { req }) => { + if (!value && !(req.body.name && req.body.type && req.body.content)) return false; + return true; + }) + .withMessage('name, type and content are required'), + check('name').optional().isString().withMessage(Messages.Invalid), + check('type').optional().isString().withMessage(Messages.Invalid), + check('content') + .optional() + .isArray() + .custom((value) => { + if (value.length !== 1) return false; + if (typeof value[0] !== 'string') return false; + return true; + }) + .withMessage('The content array must be provided and must have exactly one string'), + check('relativeDidUrl').optional().isString().contains('/resources/').withMessage(Messages.InvalidDidUrl), + check('contentOperation') + .optional() + .isArray() + .custom((value) => value[0] === ContentOperation.Set && value.length == 1) + .withMessage('Only Set operation is supported'), + ]; public async create(request: Request, response: Response) { const result = validationResult(request); if (!result.isEmpty()) { return response .status(400) - .json(Responses.GetInvalidResourceResponse({}, request.body.secret, result.array()[0].msg)); + .json(Responses.GetInvalidResourceResponseV1({}, request.body.secret, result.array()[0].msg)); } const { did } = request.params; @@ -48,7 +97,7 @@ export class ResourceController { version, secret = {}, options = {}, - } = request.body as IResourceCreateRequest; + } = request.body as IResourceCreateRequestV1; let resourcePayload: Partial = {}; try { @@ -84,7 +133,7 @@ export class ResourceController { } else if (!data) { return response .status(400) - .json(Responses.GetInvalidResourceResponse({}, secret, Messages.InvalidResource)); + .json(Responses.GetInvalidResourceResponseV1({}, secret, Messages.InvalidResource)); } else { jobId = v4(); @@ -108,7 +157,7 @@ export class ResourceController { return response .status(200) .json( - Responses.GetResourceActionSignatureResponse( + Responses.GetResourceActionSignatureResponseV1( jobId, resolvedDocument.verificationMethod, resourcePayload @@ -120,11 +169,13 @@ export class ResourceController { await CheqdRegistrar.instance.connect(options); const result = await CheqdRegistrar.instance.createResource(signInputs, resourcePayload); if (result.code == 0) { - return response.status(201).json(Responses.GetResourceSuccessResponse(jobId, secret, resourcePayload)); + return response + .status(201) + .json(Responses.GetResourceSuccessResponseV1(jobId, secret, resourcePayload)); } else { return response .status(400) - .json(Responses.GetInvalidResourceResponse(resourcePayload, secret, Messages.InvalidResource)); + .json(Responses.GetInvalidResourceResponseV1(resourcePayload, secret, Messages.InvalidResource)); } } catch (error) { return response.status(500).json({ @@ -139,4 +190,299 @@ export class ResourceController { }); } } + + // function to get resource by using name and type + public static async checkResourceStatus( + did: string, + name: string, + type: string + ): Promise<{ existingResource: any }> { + let existingResource; + let queryString = did + '?resourceName=' + name + '&resourceType=' + type + '&resourceMetadata=true'; + let resource = await CheqdResolver(queryString); + if (resource) + if (resource.contentStream) { + let metadata = resource.contentStream.linkedResourceMetadata || []; + if (metadata.length >= 1) { + return { + existingResource: metadata[0], + }; + } + } + return { existingResource: existingResource }; + } + + public async createResource(request: Request, response: Response) { + const result = validationResult(request); + if (!result.isEmpty()) { + return response + .status(400) + .json(Responses.GetInvalidResourceResponse('', {}, request.body.secret, result.array()[0].msg)); + } + + let { + did, + jobId, + content, + name, + type, + version, + secret = {}, + options = {}, + } = request.body as IResourceCreateRequest; + + let resourcePayload: Partial = {}; + + try { + // check if did is registered on the ledger + let resolvedDocument = await CheqdResolver(did); + if (!resolvedDocument?.didDocument || resolvedDocument.didDocumentMetadata.deactivated) { + return response + .status(400) + .send(Responses.GetInvalidResourceResponse(did, {}, secret, Messages.DidNotFound)); + } + resolvedDocument = resolvedDocument.didDocument; + // Validate and get store data if any + if (jobId) { + const storeData = LocalStore.instance.getResource(jobId); + if (!storeData) { + return response.status(400).json(Responses.GetJobExpiredResponse(jobId)); + } else if (storeData.state == IState.Finished) { + return response.status(201).json({ + jobId, + didUrlState: { + didUrl: storeData.resource.id, + state: IState.Finished, + secret, + resource: storeData.resource, + }, + }); + } + + resourcePayload = storeData.resource; + resourcePayload.data = new Uint8Array(Object.values(resourcePayload.data!)); + } else if (!content) { + return response + .status(400) + .json(Responses.GetInvalidResourceResponse('', {}, secret, Messages.InvalidContent)); + } else { + const checkResource = await ResourceController.checkResourceStatus(did, name, type); + if (checkResource.existingResource) { + return response + .status(400) + .send(Responses.GetInvalidResourceResponse(did, {}, secret, Messages.ResourceExists)); + } + jobId = v4(); + + resourcePayload = { + collectionId: did.split(':').pop()!, + id: v4(), + name, + resourceType: type, + version: version, + data: fromString(content, 'base64'), + }; + } + + let signInputs: SignInfo[]; + + if (secret.signingResponse) { + signInputs = convertToSignInfo(secret.signingResponse); + } else { + LocalStore.instance.setResource(jobId, { resource: resourcePayload, state: IState.Action }); + return response + .status(200) + .json( + Responses.GetResourceActionSignatureResponse( + jobId, + resolvedDocument.verificationMethod, + did, + resourcePayload + ) + ); + } + + options.network = options.network || (did.split(':')[2] as NetworkType); + await CheqdRegistrar.instance.connect(options); + const result = await CheqdRegistrar.instance.createResource(signInputs, resourcePayload); + if (result.code == 0) { + return response + .status(201) + .json(Responses.GetResourceSuccessResponse(jobId, secret, did, resourcePayload)); + } else { + return response + .status(400) + .json(Responses.GetInvalidResourceResponse(did, resourcePayload, secret, Messages.InvalidResource)); + } + } catch (error) { + return response.status(500).json({ + jobId, + didUrlState: { + state: IState.Failed, + reason: Messages.Internal, + description: Messages.TryAgain + error, + secret, + resourcePayload, + }, + }); + } + } + public async updateResource(request: Request, response: Response) { + const result = validationResult(request); + if (!result.isEmpty()) { + return response + .status(400) + .json(Responses.GetInvalidResourceResponse('', {}, request.body.secret, result.array()[0].msg)); + } + + let { + did, + jobId, + content, + relativeDidUrl, + name, + type, + version, + secret = {}, + options = {}, + } = request.body as IResourceUpdateRequest; + + let resourcePayload: Partial = {}; + + try { + // check if did is registered on the ledger + let resolvedDocument = await CheqdResolver(did); + if (!resolvedDocument?.didDocument || resolvedDocument.didDocumentMetadata.deactivated) { + return response + .status(400) + .send(Responses.GetInvalidResourceResponse(did, {}, secret, Messages.DidNotFound)); + } + const resolvedDidDocument = resolvedDocument.didDocument; + // Validate and get store data if any + if (jobId) { + const storeData = LocalStore.instance.getResource(jobId); + if (!storeData) { + return response.status(400).json(Responses.GetJobExpiredResponse(jobId)); + } else if (storeData.state == IState.Finished) { + return response.status(201).json({ + jobId, + didUrlState: { + didUrl: storeData.resource.id, + state: IState.Finished, + secret, + resource: storeData.resource, + }, + }); + } + + resourcePayload = storeData.resource; + resourcePayload.data = new Uint8Array(Object.values(resourcePayload.data!)); + } else if (!content) { + return response + .status(400) + .json(Responses.GetInvalidResourceResponse('', {}, secret, Messages.InvalidContent)); + } else { + let existingResource; + const linkedResourceMetadata = resolvedDocument.didDocumentMetadata.linkedResourceMetadata || []; + + if (relativeDidUrl) { + // search resource using relativeDidUrl + const didUrlIndex = linkedResourceMetadata.findIndex( + (resource: { resourceURI: string }) => resource.resourceURI === did + relativeDidUrl + ); + if (didUrlIndex !== -1) { + // if resource is found using relativeDidUrl + existingResource = linkedResourceMetadata[didUrlIndex]; + // passed name and type must match + if (existingResource.resourceName !== name || existingResource.resourceType !== type) + return response + .status(400) + .send( + Responses.GetInvalidResourceResponse( + did, + { id: relativeDidUrl.split('resources/')[1] }, + secret, + Messages.InvalidUpdateResource + ) + ); + // If resource has a nextVersionId, then return error + if (existingResource.nextVersionId) { + return response + .status(400) + .send( + Responses.GetInvalidResourceResponse( + did, + {}, + secret, + 'Only latest version of resource can be updated' + ) + ); + } + } + } else { + // if not relativeDidUrl, find by name and type + const checkResource = await ResourceController.checkResourceStatus(did, name, type); + existingResource = checkResource.existingResource; + } + if (!existingResource) { + return response + .status(400) + .send(Responses.GetInvalidResourceResponse(did, {}, secret, Messages.ResourceNotFound)); + } + + jobId = v4(); + + resourcePayload = { + collectionId: did.split(':').pop()!, + id: v4(), + name, + resourceType: type, + version: version, + data: fromString(content[0], 'base64'), + }; + } + + let signInputs: SignInfo[]; + + if (secret.signingResponse) { + signInputs = convertToSignInfo(secret.signingResponse); + } else { + LocalStore.instance.setResource(jobId, { resource: resourcePayload, state: IState.Action }); + return response + .status(200) + .json( + Responses.GetResourceActionSignatureResponse( + jobId, + resolvedDidDocument.verificationMethod, + did, + resourcePayload + ) + ); + } + + options.network = options.network || (did.split(':')[2] as NetworkType); + await CheqdRegistrar.instance.connect(options); + const result = await CheqdRegistrar.instance.createResource(signInputs, resourcePayload); + if (result.code == 0) { + return response + .status(201) + .json(Responses.GetResourceSuccessResponse(jobId, secret, did, resourcePayload)); + } else { + return response + .status(400) + .json(Responses.GetInvalidResourceResponse(did, resourcePayload, secret, Messages.InvalidResource)); + } + } catch (error) { + return response.status(500).json({ + jobId, + didUrlState: { + state: IState.Failed, + reason: Messages.Internal, + description: Messages.TryAgain + error, + secret, + resourcePayload, + }, + }); + } + } } diff --git a/src/helpers/response.ts b/src/helpers/response.ts index 34cd2b9..4949b32 100644 --- a/src/helpers/response.ts +++ b/src/helpers/response.ts @@ -98,7 +98,7 @@ export class Responses { }; } - static GetResourceActionSignatureResponse( + static GetResourceActionSignatureResponseV1( jobId: string, verificationMethod: VerificationMethod[], resource: Partial @@ -129,6 +129,38 @@ export class Responses { }, }; } + static GetResourceActionSignatureResponse( + jobId: string, + verificationMethod: VerificationMethod[], + did: string, + resource: Partial + ) { + const signingRequest = verificationMethod.map((method) => { + return { + kid: method.id, + type: method.type, + alg: 'EdDSA', + serializedPayload: toString( + MsgCreateResourcePayload.encode(MsgCreateResourcePayload.fromPartial(resource)).finish(), + 'base64pad' + ), + }; + }); + + return { + jobId, + didUrlState: { + didUrl: did + '/resources/' + resource.id, + state: IState.Action, + action: IAction.GetSignature, + description: Messages.GetSignature, + signingRequest, + secret: { + signingResponse: [Messages.SigingResponse], + }, + }, + }; + } static GetInvalidResponse(didDocument: DIDDocument | undefined, secret: Record = {}, error: string) { return { @@ -167,7 +199,7 @@ export class Responses { }; } - static GetResourceSuccessResponse( + static GetResourceSuccessResponseV1( jobId: string, secret: Record, resourcePayload: Partial @@ -182,8 +214,29 @@ export class Responses { }, }; } + static GetResourceSuccessResponse( + jobId: string, + secret: Record, + did: string, + resourcePayload: Partial + ) { + return { + jobId, + didUrlState: { + didUrl: did + '/resources/' + resourcePayload.id || '', + state: IState.Finished, + secret, + content: resourcePayload.data, + name: resourcePayload.name, + type: resourcePayload.resourceType, + version: resourcePayload.version, + }, + didRegistrationMetadata: {}, + contentMetadata: {}, + }; + } - static GetInvalidResourceResponse( + static GetInvalidResourceResponseV1( resourcePayload: Partial = {}, secret: Record = {}, error: string @@ -200,4 +253,22 @@ export class Responses { }, }; } + static GetInvalidResourceResponse( + did: string, + resourcePayload: Partial = {}, + secret: Record = {}, + error: string + ) { + return { + jobId: null, + didUrlState: { + didUrl: did + '/resources/' + resourcePayload.id, + state: IState.Failed, + reason: Messages.Invalid, + description: Messages.Invalid + ': ' + error, + secret, + resourcePayload, + }, + }; + } } diff --git a/src/static/swagger.json b/src/static/swagger.json index d81f0c0..8dca7ee 100644 --- a/src/static/swagger.json +++ b/src/static/swagger.json @@ -6,9 +6,9 @@ } ], "info": { + "title": "Universal Registrar driver for did:cheqd", "description": "Universal Registrar driver for did:cheqd", - "version": "1.0.0", - "title": "Universal Registrar driver for did:cheqd" + "version": "1.0.0" }, "tags": [ { @@ -16,20 +16,17 @@ "externalDocs": { "url": "https://github.com/cheqd/did-registrar#readme" } - }, - { - "name": "Universal Registrar driver for did:cheqd" } ], "paths": { "/create": { "post": { - "tags": [ - "DID" - ], "summary": "Create a DID.", "description": "

This endpoint creates a DID. As input it takes the DID plus options, a DID document, and optionally secrets needed to create a DID. The output is a state object that represents the current state of the DID creation process.

See the DID Registration specification for additional details.

", "operationId": "create", + "tags": [ + "DID" + ], "requestBody": { "content": { "application/json": { @@ -85,12 +82,12 @@ }, "/update": { "post": { - "tags": [ - "DID" - ], "summary": "Update a DID.", "description": "

This endpoint updates a DID. As input it takes the existing DID plus options, a DID document, and optionally secrets needed to update a DID. The output is a state object that represents the current state of the DID update process.

See the DID Registration specification for additional details.

", "operationId": "update", + "tags": [ + "DID" + ], "requestBody": { "content": { "application/json": { @@ -136,12 +133,12 @@ }, "/deactivate": { "post": { - "tags": [ - "DID" - ], "summary": "Deactivate a DID.", "description": "

This endpoint deactivates a DID. As input it takes the existing DID plus options, and optionally secrets needed to deactivate a DID. The output is a state object that represents the current state of the DID deactivation process.

See the DID Registration specification for additional details.

", "operationId": "deactivate", + "tags": [ + "DID" + ], "requestBody": { "content": { "application/json": { @@ -187,10 +184,12 @@ }, "/{did}/create-resource": { "post": { + "summary": "Create a Resource (Deprecated)", + "description": "This endpoint is deprecated and will be removed in future versions. Please use `/createResource` instead.", + "deprecated": true, "tags": [ "Resource" ], - "summary": "Create a Resource", "parameters": [ { "in": "path", @@ -205,7 +204,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateResourceRequest" + "$ref": "#/components/schemas/CreateResourceRequestV1" } } } @@ -217,6 +216,118 @@ } } }, + "/createResource": { + "post": { + "summary": "Create a DID URL and associated resource", + "description": "

This endpoint creates a DID URL and associated resource. As input it takes an existing DID, a relative DID URL, a resource, plus optional DID registration options and secrets needed to create the DID URL. The output is a state object that represents the current state of the DID URL creation process.

See the DID Registration specification for additional details.

", + "operationId": "createResource", + "tags": [ + "Resource" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful, but the DID URL may not be fully created yet, as indicated by the \"didState.state\" and \"jobId\" output fields. Additional state information plus metadata are available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceState" + } + } + } + }, + "201": { + "description": "The DID URL has been successfully created, as indicated by the \"didState.state\" output field. Additional state information (including the created DID URL) plus metadata are available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceState" + } + } + } + }, + "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" + } + } + } + }, + "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" + } + } + } + } + } + } + }, + "/updateResource": { + "post": { + "summary": "Update a DID URL and associated resource", + "description": "

This endpoint updates a DID URL and associated resource. As input it takes an existing DID, a relative DID URL, a resource, plus optional DID registration options and secrets needed to update the DID URL. The output is a state object that represents the current state of the DID URL update process.

See the DID Registration specification for additional details.

", + "operationId": "updateResource", + "tags": [ + "Resource" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateResourceRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful, and the DID URL may or may not be fully updated yet, as indicated by the \"didState.state\" and \"jobId\" output fields. Additional state information plus metadata are available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateResourceState" + } + } + } + }, + "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" + } + } + } + }, + "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" + } + } + } + } + } + } + }, "/key-pair": { "get": { "tags": [ @@ -303,66 +414,198 @@ } } } + }, + "/properties": { + "get": { + "summary": "Return a map of configuration properties", + "description": "

This endpoint returns a map of the configuration properties of the DID Registrar, including of its drivers.

", + "operationId": "universalRegistrarGetProperties", + "tags": [ + "Properties" + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/did+json": { + "schema": { + "type": "object", + "description": "A map of properties.", + "example": { + "cheqd": { + "someprop": "/lib", + "poolConfigs": "..." + } + } + } + } + } + } + } + } + }, + "/methods": { + "get": { + "summary": "Return a list of supported DID methods", + "description": "

This endpoint returns a list of DID methods supported by the DID Registrar.

", + "operationId": "universalRegistrarGetMethods", + "tags": [ + "Properties" + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/did+json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The list of supported DID methods.", + "example": [ + "cheqd" + ] + } + } + } + } + } + } + }, + "/traits": { + "get": { + "summary": "Return a map of DID traits", + "description": "

This endpoint returns a map of the DID traits of the DID methods supported by the DID Registrar.

", + "operationId": "universalRegistrarGetTraits", + "tags": [ + "Properties" + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/did+json": { + "schema": { + "type": "object", + "description": "A map of DID traits.", + "example": { + "cheqd": { + "updatable": true, + "deactivatable": true, + "enumerable": true, + "historyAvailable": true, + "humanReadable": false + } + } + } + } + } + } + } + } } }, "components": { "schemas": { - "CreateRequest": { - "description": "Input fields for the create operation.", + "RegistrarRequest": { + "description": "Input fields for a DID operation.", "type": "object", - "additionalProperties": false, "properties": { "jobId": { - "description": "This input field is used to keep track of an ongoing DID creation process. See https://identity.foundation/did-registration/#jobid.", - "type": "string" + "description": "This input field is used to keep track of an ongoing DID operation process. See https://identity.foundation/did-registration/#jobid.", + "type": "string", + "example": "6d85bcd0-2ea3-4288-ab00-15afadd8a156" }, "options": { - "$ref": "#/components/schemas/Options" + "$ref": "#/components/schemas/RequestOptions" }, "secret": { - "$ref": "#/components/schemas/Secret" - }, - "didDocument": { - "$ref": "#/components/schemas/DidDocument" + "$ref": "#/components/schemas/RequestSecret" } } }, - "UpdateRequest": { - "description": "Input fields for the update operation.", + "RequestOptions": { + "description": "This input field contains an object with various options for the DID operation, such as the network where the DID operation should be executed. See https://identity.foundation/did-registration/#options.", "type": "object", - "additionalProperties": false, "properties": { - "jobId": { - "description": "This input field is used to keep track of an ongoing DID update process. See https://identity.foundation/did-registration/#jobid.", - "type": "string" + "clientSecretMode": { + "$ref": "#/components/schemas/Boolean" }, - "did": { - "description": "This input field indicates the DID that is the target of the DID update operation.", - "type": "string", - "example": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" + "storeSecrets": { + "$ref": "#/components/schemas/Boolean" }, - "options": { - "$ref": "#/components/schemas/Options" + "returnSecrets": { + "$ref": "#/components/schemas/Boolean" }, - "secret": { - "$ref": "#/components/schemas/SigningResponse" + "requestVerificationMethod": { + "$ref": "#/components/schemas/RequestOptionsRequestVerificationMethod" + } + }, + "additionalProperties": {}, + "example": { + "network": "testnet" + } + }, + "RequestOptionsRequestVerificationMethod": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethodTemplate" + } + }, + "CreateRequest": { + "description": "Input fields for the DID create operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarRequest" }, - "didDocumentOperation": { - "type": "array", - "items": { - "type": "string" - }, - "default": [ - "setDidDocument" - ] + { + "type": "object", + "properties": { + "didDocument": { + "$ref": "#/components/schemas/DidDocument" + } + } + } + ] + }, + "UpdateRequest": { + "description": "Input fields for the DID update operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarRequest" }, - "didDocument": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DidDocument" + { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "description": "This input field indicates the DID that is the target of the DID update operation.", + "type": "string", + "example": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" + }, + "didDocumentOperation": { + "description": "This input field indicates which update operation(s) should be applied to a DID’s associated DID document. See https://identity.foundation/did-registration/#diddocumentoperation.", + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "setDidDocument" + ] + }, + "didDocument": { + "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See https://identity.foundation/did-registration/#diddocument.", + "type": "array", + "items": { + "$ref": "#/components/schemas/DidDocument" + } + } } } - }, + ], "example": { "did": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09", "didDocumentOperation": [ @@ -400,31 +643,28 @@ } }, "DeactivateRequest": { - "description": "Input fields for the deactivate operation.", - "type": "object", - "additionalProperties": false, - "properties": { - "jobId": { - "description": "This input field is used to keep track of an ongoing DID deactivation process. See https://identity.foundation/did-registration/#jobid.", - "type": "string" - }, - "did": { - "description": "This input field indicates the DID that is the target of the DID deactivation operation.", - "type": "string" - }, - "options": { - "$ref": "#/components/schemas/Options" + "description": "Input fields for the DID deactivate operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarRequest" }, - "secret": { - "$ref": "#/components/schemas/Secret" + { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "description": "This input field indicates the DID that is the target of the DID deactivation operation.", + "type": "string", + "example": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" + } + } } - }, - "example": { - "did": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" - } + ] }, - "CreateResourceRequest": { - "description": "Input fields for the resource creation", + "CreateResourceRequestV1": { + "description": "Input fields for a DID URL create operation.", "type": "object", "additionalProperties": false, "properties": { @@ -483,239 +723,790 @@ "type": "TextDocument" } }, - "CreateState": { - "description": "The state after a create operation.", - "type": "object", - "additionalProperties": false, - "required": [ - "didState" - ], - "properties": { - "jobId": { - "type": "string", - "example": "null" + "CreateResourceRequest": { + "description": "Input fields for a DID URL create operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarRequest" }, - "didState": { - "$ref": "#/components/schemas/DidState" + { + "type": "object", + "required": [ + "did", + "relativeDidUrl" + ], + "properties": { + "did": { + "description": "This input field indicates the DID that is the target of the DID URL create operation.", + "type": "string", + "example": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" + }, + "relativeDidUrl": { + "description": "This input field indicates a relative DID URL that is the target of the DID URL create operation.", + "type": "string", + "example": "/resources/123" + }, + "content": { + "description": "This input field contains Base64-encoded data that is the content of the resource associated with the DID URL.", + "type": "string", + "example": "SGVsbG8gV29ybGQ=" + } + } + } + ] + }, + "UpdateResourceRequest": { + "description": "Input fields for a DID URL update operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarRequest" }, - "didRegistrationMetadata": { - "type": "object" + { + "type": "object", + "required": [ + "did", + "relativeDidUrl" + ], + "properties": { + "did": { + "description": "This input field indicates the DID that is the target of the DID URL update operation.", + "type": "string", + "example": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" + }, + "relativeDidUrl": { + "description": "This input field indicates a relative DID URL that is the target of the DID URL update operation.", + "type": "string", + "example": "/resources/123" + }, + "contentOperation": { + "description": "This input field indicates which update operation(s) should be applied to the content of a DID URL’s associated resource. See https://identity.foundation/did-registration/#resourceoperation.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "setContent" + ] + }, + "content": { + "description": "This input field contains Base64-encoded data that is the content of the resource associated with the DID URL.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "SGVsbG8gV29ybGQ=" + ] + } + } + } + ] + }, + "DidDocument": { + "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See https://identity.foundation/did-registration/#diddocument.", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/String" }, - "didDocumentMetadata": { - "type": "object" + "controller": { + "$ref": "#/components/schemas/DidDocumentController" + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09#key-0" + ] + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationMethod": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethodTemplate" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DidDocumentService" + } } + }, + "additionalProperties": {}, + "example": { + "id": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09", + "controller": [ + "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" + ], + "verificationMethod": [ + { + "id": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09#key-1", + "type": "Ed25519VerificationKey2020", + "controller": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09", + "publicKeyMultibase": "z6Mkt9Vg1a1Jbg5a1NkToUeWH23Z33TwGUua5MrqAYUz2AL3" + } + ], + "authentication": [ + "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09#key-1" + ] } }, - "UpdateState": { - "description": "The state after an update operation.", + "DidDocumentController": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" + ] + }, + "DidDocumentService": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "RegistrarState": { + "description": "The state after a DID operation.", "type": "object", - "additionalProperties": false, "required": [ "didState" ], "properties": { "jobId": { "type": "string", - "example": "null" + "example": "6d85bcd0-2ea3-4288-ab00-15afadd8a156" }, "didState": { "$ref": "#/components/schemas/DidState" }, "didRegistrationMetadata": { - "type": "object" + "type": "object", + "additionalProperties": {} }, "didDocumentMetadata": { - "type": "object" + "type": "object", + "additionalProperties": {} } } }, - "DeactivateState": { - "description": "The state after a deactivate operation.", + "RegistrarResourceState": { + "description": "The state after a DID URL operation.", "type": "object", - "additionalProperties": false, "required": [ - "didState" + "didUrlState" ], "properties": { "jobId": { "type": "string", - "example": "null" + "example": "6d85bcd0-2ea3-4288-ab00-15afadd8a156" }, - "didState": { - "$ref": "#/components/schemas/DidState" + "didUrlState": { + "$ref": "#/components/schemas/DidUrlState" }, "didRegistrationMetadata": { + "type": "object", + "additionalProperties": {} + }, + "contentMetadata": { + "type": "object", + "additionalProperties": {} + } + } + }, + "CreateState": { + "description": "The state after a DID create operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarState" + }, + { "type": "object" + } + ] + }, + "UpdateState": { + "description": "The state after a DID update operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarState" }, - "didDocumentMetadata": { + { "type": "object" } - } + ] + }, + "DeactivateState": { + "description": "The state after a DID deactivate operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarState" + }, + { + "type": "object" + } + ] + }, + "CreateResourceState": { + "description": "The state after a DID URL create operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarResourceState" + }, + { + "type": "object" + } + ] + }, + "UpdateResourceState": { + "description": "The state after a DID URL update operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarResourceState" + }, + { + "type": "object" + } + ] + }, + "DeactivateResourceState": { + "description": "The state after a DID URL deactivate operation.", + "allOf": [ + { + "$ref": "#/components/schemas/RegistrarResourceState" + }, + { + "type": "object" + } + ] }, "DidState": { "description": "The current state of a DID.", "type": "object", - "additionalProperties": true, + "required": [ + "state" + ], + "discriminator": { + "propertyName": "state", + "mapping": { + "finished": "#/components/schemas/DidStateFinished", + "failed": "#/components/schemas/DidStateFailed", + "action": "#/components/schemas/DidStateAction", + "wait": "#/components/schemas/DidStateWait" + } + }, "properties": { "state": { - "type": "string", - "example": "finished" + "$ref": "#/components/schemas/String" }, "did": { - "type": "string", - "example": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" + "$ref": "#/components/schemas/String" }, "secret": { - "$ref": "#/components/schemas/Secret" + "$ref": "#/components/schemas/DidStateSecret" }, "didDocument": { "$ref": "#/components/schemas/DidDocument" } - } + }, + "additionalProperties": true }, - "Secret": { - "description": "This input field contains an object with DID controller keys and other secrets needed for performing the DID deactivate operation. See https://identity.foundation/did-registration/#secret.", + "DidUrlState": { + "description": "The current state of a DID URL and associated resource.", "type": "object", + "required": [ + "state" + ], + "discriminator": { + "propertyName": "state", + "mapping": { + "finished": "#/components/schemas/DidUrlStateFinished", + "failed": "#/components/schemas/DidUrlStateFailed", + "action": "#/components/schemas/DidUrlStateAction", + "wait": "#/components/schemas/DidUrlStateWait" + } + }, "properties": { - "signingResponse": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SigningResponse" - }, - "example": [ - { - "kid": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09", - "signature": "keaiUef4jaPvupBpC2IK9/H/yd/OPsgxCADOgftuLQc" + "state": { + "$ref": "#/components/schemas/String" + }, + "didUrl": { + "$ref": "#/components/schemas/String" + }, + "secret": { + "$ref": "#/components/schemas/DidStateSecret" + }, + "content": { + "$ref": "#/components/schemas/String" + } + }, + "additionalProperties": {} + }, + "DidStateFinished": { + "allOf": [ + { + "$ref": "#/components/schemas/DidState" + }, + { + "$ref": "#/components/schemas/StateFinished" + } + ] + }, + "DidStateFailed": { + "allOf": [ + { + "$ref": "#/components/schemas/DidState" + }, + { + "$ref": "#/components/schemas/StateFailed" + } + ] + }, + "DidStateAction": { + "allOf": [ + { + "$ref": "#/components/schemas/DidState" + }, + { + "$ref": "#/components/schemas/StateAction" + } + ] + }, + "DidStateWait": { + "allOf": [ + { + "$ref": "#/components/schemas/DidState" + }, + { + "$ref": "#/components/schemas/StateWait" + } + ] + }, + "DidUrlStateFinished": { + "allOf": [ + { + "$ref": "#/components/schemas/DidUrlState" + }, + { + "$ref": "#/components/schemas/StateFinished" + } + ] + }, + "DidUrlStateFailed": { + "allOf": [ + { + "$ref": "#/components/schemas/DidUrlState" + }, + { + "$ref": "#/components/schemas/StateFailed" + } + ] + }, + "DidUrlStateAction": { + "allOf": [ + { + "$ref": "#/components/schemas/DidUrlState" + }, + { + "$ref": "#/components/schemas/StateAction" + } + ] + }, + "DidUrlStateWait": { + "allOf": [ + { + "$ref": "#/components/schemas/DidUrlState" + }, + { + "$ref": "#/components/schemas/StateWait" + } + ] + }, + "StateFinished": { + "description": "This state indicates that the DID operation has been completed. See https://identity.foundation/did-registration/#didstatestatefinished.", + "allOf": [ + { + "type": "object" + } + ] + }, + "StateFailed": { + "description": "This state indicates that the DID operation has failed. See https://identity.foundation/did-registration/#didstatestatefailed.", + "allOf": [ + { + "type": "object", + "properties": { + "error": { + "$ref": "#/components/schemas/String" + }, + "reason": { + "$ref": "#/components/schemas/String" } - ] + } } + ] + }, + "StateAction": { + "description": "This state indicates that the client needs to perform an action, before the DID operation can be continued. See https://identity.foundation/did-registration/#didstatestateaction.", + "allOf": [ + { + "type": "object", + "properties": { + "action": { + "$ref": "#/components/schemas/String" + }, + "verificationMethodTemplate": { + "$ref": "#/components/schemas/StateActionVerificationMethodTemplate" + }, + "signingRequest": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SigningRequest" + } + }, + "decryptionRequest": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DecryptionRequest" + } + } + } + } + ] + }, + "StateActionVerificationMethodTemplate": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethodTemplate" } }, - "Options": { - "description": "This input field contains an object with various options for the DID create operation, such as the network where the DID should be created. See https://identity.foundation/did-registration/#options.", + "StateWait": { + "description": "This state indicates that the client needs to wait, before the DID operation can be continued. See https://identity.foundation/did-registration/#didstatestatewait.", + "allOf": [ + { + "type": "object", + "properties": { + "wait": { + "$ref": "#/components/schemas/String" + }, + "waittime": { + "$ref": "#/components/schemas/String" + } + } + } + ] + }, + "Secret": { + "description": "Secrets in requests and states.", "type": "object", "properties": { - "network": { - "type": "string" + "verificationMethod": { + "$ref": "#/components/schemas/SecretVerificationMethod" } }, - "example": { - "network": "testnet" + "additionalProperties": {} + }, + "SecretVerificationMethod": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/VerificationMethodPublicData" + }, + { + "$ref": "#/components/schemas/VerificationMethodPrivateData" + }, + { + "$ref": "#/components/schemas/SecretVerificationMethodVerificationMethodTemplate" + } + ] + }, + "example": [ + { + "id": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09#key-1", + "type": "JsonWebKey2020", + "privateKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "d": "NzJXR3A3TmdGUjFPcWRpOHpsdDdqUVE0MzRYUjBjTlE", + "x": "jpIKKU2b77lNXKTNW2NGvw1GUMjU6v_l_tLJAH5uYz0" + } + } + ] + }, + "SecretVerificationMethodVerificationMethodTemplate": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VerificationMethodTemplate" } }, - "KeyPair": { + "RequestSecret": { + "description": "This input field contains an object with DID controller keys and other secrets needed for performing the DID operation. See https://identity.foundation/did-registration/#secret.", + "allOf": [ + { + "$ref": "#/components/schemas/Secret" + }, + { + "type": "object", + "properties": { + "signingResponse": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SigningResponse" + } + }, + "decryptionResponse": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DecryptionResponse" + } + } + } + } + ] + }, + "DidStateSecret": { + "description": "This output field contains an object with DID controller keys and other secrets. See https://identity.foundation/did-registration/#didstatesecret.", + "allOf": [ + { + "$ref": "#/components/schemas/Secret" + }, + { + "type": "object", + "example": { + "seed": "72WGp7NgFR1Oqdi8zlt7jQQ434XR0cNQ" + } + } + ] + }, + "SigningRequest": { + "description": "A signing request, see https://identity.foundation/did-registration/#signing-request-set.", "type": "object", + "required": [ + "serializedPayload", + "alg" + ], "properties": { + "payload": { + "type": "object" + }, + "serializedPayload": { + "$ref": "#/components/schemas/String" + }, "kid": { - "type": "string" + "$ref": "#/components/schemas/String" }, - "privateKeyHex": { - "type": "string" + "alg": { + "$ref": "#/components/schemas/String" }, - "publicKeyHex": { - "type": "string" + "purpose": { + "$ref": "#/components/schemas/String" } + }, + "additionalProperties": {} + }, + "SigningRequestSet": { + "description": "A signing request set, see https://identity.foundation/did-registration/#signing-request-set.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/SigningRequest" } }, "SigningResponse": { + "description": "A signing response, see https://identity.foundation/did-registration/#signing-response-set.", "type": "object", + "required": [ + "signature" + ], "properties": { + "signature": { + "$ref": "#/components/schemas/String" + }, "kid": { - "type": "string" + "$ref": "#/components/schemas/String" }, - "signature": { - "type": "string" + "alg": { + "$ref": "#/components/schemas/String" + }, + "purpose": { + "$ref": "#/components/schemas/String" } + }, + "additionalProperties": {}, + "example": { + "kid": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09#key-1", + "signature": "keaiUef4jaPvupBpC2IK9/H/yd/OPsgxCADOgftuLQc" } }, - "DidDocument": { - "description": "This input field contains either a complete DID document, or an incremental change (diff) to a DID document. See https://identity.foundation/did-registration/#diddocument.", + "SigningResponseSet": { + "description": "A signing response set, see https://identity.foundation/did-registration/#signing-response-set.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/SigningResponse" + } + }, + "DecryptionRequest": { + "description": "A decryption request, see https://identity.foundation/did-registration/#decryption-request-set.", "type": "object", + "required": [ + "encryptedPayload", + "enc" + ], "properties": { - "context": { - "type": "array", - "items": { - "type": "string" - } + "payload": { + "$ref": "#/components/schemas/String" }, - "id": { - "type": "string", - "example": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" + "encryptedPayload": { + "$ref": "#/components/schemas/String" }, - "controller": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" - ] + "kid": { + "$ref": "#/components/schemas/String" }, - "authentication": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09#key-0" - ] + "enc": { + "$ref": "#/components/schemas/String" }, - "assertionMethod": { - "type": "array", - "items": { - "type": "string" - } + "purpose": { + "$ref": "#/components/schemas/String" + } + }, + "additionalProperties": {} + }, + "DecryptionRequestSet": { + "description": "A decryption request set, see https://identity.foundation/did-registration/#decryption-request-set.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/DecryptionRequest" + } + }, + "DecryptionResponse": { + "description": "A decryption response, see https://identity.foundation/did-registration/#decryption-response-set.", + "type": "object", + "required": [ + "decryptedPayload" + ], + "properties": { + "decryptedPayload": { + "$ref": "#/components/schemas/String" }, - "capabilityInvocation": { - "type": "array", - "items": { - "type": "string" - } + "kid": { + "$ref": "#/components/schemas/String" }, - "capabilityDelegation": { - "type": "array", - "items": { - "type": "string" - } + "enc": { + "$ref": "#/components/schemas/String" }, - "keyAgreement": { + "purpose": { + "$ref": "#/components/schemas/String" + } + }, + "additionalProperties": {}, + "example": { + "kid": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09#key-1", + "decryptedPayload": "keaiUef4jaPvupBpC2IK9/H/yd/OPsgxCADOgftuLQc" + } + }, + "DecryptionResponseSet": { + "description": "A decryption response set, see https://identity.foundation/did-registration/#decryption-response-set.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/DecryptionResponse" + } + }, + "VerificationMethodData": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "controller": { + "type": "string" + }, + "purpose": { "type": "array", "items": { "type": "string" } + } + } + }, + "VerificationMethodPublicData": { + "description": "A verification method public data object, see https://identity.foundation/did-registration/#verification-method-public-data", + "allOf": [ + { + "$ref": "#/components/schemas/VerificationMethodData" }, - "verificationMethod": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VerificationMethod" + { + "type": "object", + "properties": { + "publicKeyJwk": { + "type": "array", + "items": { + "type": "string" + } + }, + "publicKeyMultibase": { + "type": "string" + } } + } + ] + }, + "VerificationMethodPrivateData": { + "description": "A verification method private data object, see https://identity.foundation/did-registration/#verification-method-private-data", + "allOf": [ + { + "$ref": "#/components/schemas/VerificationMethodData" }, - "service": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Service" + { + "type": "object", + "properties": { + "privateKeyJwk": { + "type": "array", + "items": { + "type": "string" + } + }, + "privateKeyMultibase": { + "type": "string" + } } } - }, - "example": { - "id": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09", - "controller": [ - "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" - ], - "verificationMethod": [ - { - "id": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09#key-1", - "type": "Ed25519VerificationKey2020", - "controller": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09", - "publicKeyMultibase": "z6Mkt9Vg1a1Jbg5a1NkToUeWH23Z33TwGUua5MrqAYUz2AL3" - } - ], - "authentication": [ - "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09#key-1" - ] - } + ] }, - "VerificationMethod": { + "VerificationMethodTemplate": { + "description": "A verification method template, see https://identity.foundation/did-registration/#verification-method-template", "type": "object", "properties": { "id": { @@ -730,6 +1521,12 @@ "type": "string", "example": "did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09" }, + "purpose": { + "type": "array", + "items": { + "type": "string" + } + }, "publicKeyMultibase": { "type": "string", "example": "z6Mkt9Vg1a1Jbg5a1NkToUeWH23Z33TwGUua5MrqAYUz2AL3" @@ -742,6 +1539,12 @@ } } }, + "String": { + "type": "string" + }, + "Boolean": { + "type": "boolean" + }, "Service": { "type": "object", "properties": { @@ -764,6 +1567,32 @@ } } }, + "Options": { + "description": "This input field contains an object with various options for the DID create operation, such as the network where the DID should be created. See https://identity.foundation/did-registration/#options.", + "type": "object", + "properties": { + "network": { + "type": "string" + } + }, + "example": { + "network": "testnet" + } + }, + "KeyPair": { + "type": "object", + "properties": { + "kid": { + "type": "string" + }, + "privateKeyHex": { + "type": "string" + }, + "publicKeyHex": { + "type": "string" + } + } + }, "InvalidRequest": { "type": "object", "properties": { diff --git a/src/types/constants.ts b/src/types/constants.ts index 0142872..957edba 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -3,12 +3,17 @@ export enum Messages { Internal = 'Internal server error', TryAgain = 'The operation failed due to an internal error. Please try again', GetSignature = 'Please sign the following payload with the keys in verificationMethod and Add the signingResponse in secret', - DidNotFound = 'The DID does not exist or Deactivated', + DidNotFound = 'The DID does not exist or is Deactivated', InvalidDidDocument = 'Provide a DID Document with at least one valid verification method', InvalidDid = 'The DID is not valid', + InvalidDidUrl = 'The DID URL is not valid', InvalidJob = 'The jobId is either expired or not found', SecretValidation = 'Provide either a valid KeyPair or Signature', InvalidResource = 'Resource Data is invalid', + ResourceNotFound = 'Resource does not exist', + ResourceExists = 'Resource already exists', + InvalidContent = 'Resource Content is invalid', + InvalidUpdateResource = 'Update resource name or type does not match existing resource', TestnetFaucet = 'sketch mountain erode window enact net enrich smoke claim kangaroo another visual write meat latin bacon pulp similar forum guilt father state erase bright', SigingResponse = 'e.g. { kid: did:cheqd:testnet:qsqdcansoica#key-1, signature: aca1s12q14213casdvaadcfas }', InvalidOptions = 'The provided options are invalid', diff --git a/src/types/types.ts b/src/types/types.ts index 0ff85c0..61dfb57 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,24 +1,24 @@ -import type { DIDDocument, DidStdFee } from '@cheqd/sdk'; +import type { DIDDocument, DidStdFee, VerificationMethod } from '@cheqd/sdk'; import type { AlternativeUri } from '@cheqd/ts-proto/cheqd/resource/v2/index.js'; import type { NetworkType } from '../service/cheqd.js'; export interface IDIDCreateRequest { jobId: string | null; options?: IOptions; - secret: ISecret; + secret?: ISecret; didDocument: DIDDocument; } export interface IDIDUpdateRequest { jobId: string | null; did: string; - options: IOptions; - secret: ISecret; + options?: IOptions; + secret?: ISecret; didDocumentOperation: DidDocumentOperation[]; didDocument: DIDDocument[]; } -export interface IResourceCreateRequest { +export interface IResourceCreateRequestV1 { jobId: string | null; secret: ISecret; options: IOptions; @@ -39,8 +39,8 @@ export enum DidDocumentOperation { export interface IDIDDeactivateRequest { jobId: string | null; did: string; - options: Record; - secret: ISecret; + options?: IOptions; + secret?: ISecret; } export interface IDidResponse { @@ -70,15 +70,27 @@ export enum IAction { GetSignature = 'signPayload', Redirect = 'redirect', Wait = 'wait', + DecryptionRequest = 'decryptionRequest', } export interface ISignInfo { kid: string; signature: string; + alg?: string; + purpose?: string; +} + +export interface IDecryptionInfo { + kid: string; + decryptedPayload: string; + enc?: string; + purpose?: string; } export interface ISecret { + verificationMethod?: VerificationMethod[]; signingResponse?: ISignInfo[]; + decryptionResponse?: IDecryptionInfo[]; } export interface IOptions { @@ -87,3 +99,48 @@ export interface IOptions { fee?: DidStdFee; versionId?: string; } + +export interface IResourceCreateRequest { + jobId: string | null; + options?: IOptions; + secret?: ISecret; + did: string; + relativeDidUrl: string; + content: any; + name: string; + type: string; + version: string; +} +export interface IResourceUpdateRequest { + jobId: string | null; + options?: IOptions; + secret?: ISecret; + did: string; + relativeDidUrl: string; + content: any; + contentOperation: ContentOperation[]; + name: string; + type: string; + version: string; +} + +export enum ContentOperation { + Set = 'setContent', + Add = 'addContent', + Remove = 'removeContent', +} + +export interface IResourceResponse { + jobId: null; + didUrlState: IDidUrlState; + didRegistratonMetatdata?: Record; + contentMetadata?: Record; +} + +export interface IDidUrlState { + state: IState; + action?: IAction; + didUrl: string; + secret: ISecret; + content: string; +} diff --git a/tests/did/validateDid.spec.ts b/tests/did/validateDid.spec.ts new file mode 100644 index 0000000..aac2ffa --- /dev/null +++ b/tests/did/validateDid.spec.ts @@ -0,0 +1,141 @@ +import { test, expect } from '@playwright/test'; +import { toString, fromString } from 'uint8arrays'; +import * as dotenv from 'dotenv'; +import { assert } from 'console'; + +dotenv.config(); + +const pub_key_base_64 = process.env.TEST_PUBLIC_KEY; + +assert(pub_key_base_64, 'TEST_PUBLIC_KEY is not defined'); + +const pubKeyHex = toString(fromString(pub_key_base_64 as string, 'base64pad'), 'base16'); + +let didPayload; +let indyDid = 'did:indy:sovrin:WRfXPg8dantKVubE3HX8pw'; +let deactiveDid = 'did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09'; + +test('did-document. Generate the payload', async ({ request }) => { + const payload = await request.get( + `/1.0/did-document?verificationMethod=JsonWebKey2020&methodSpecificIdAlgo=uuid&network=testnet&publicKeyHex=${pubKeyHex}` + ); + + expect(payload.status()).toBe(200); + + const body = await payload.json(); + expect(body.didDoc).toBeDefined(); + expect(body.key).toBeDefined(); + expect(body.key.kid).toBeDefined(); + expect(body.key.publicKeyHex).toBeDefined(); + + didPayload = body.didDoc; +}); + +test('did-create. wrong didDocument', async ({ request }) => { + const payload = await request.post('/1.0/create', { + data: { + didDocument: {}, + secret: {}, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didState).toBeDefined(); + expect(body.didState.description).toEqual( + 'Invalid payload: Provide a DID Document with at least one valid verification method' + ); +}); + +test('did-update. invalid did', async ({ request }) => { + const payload = await request.post(`/1.0/update`, { + data: { + did: indyDid, + didDocument: [didPayload], + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didState).toBeDefined(); + expect(body.didState.description).toEqual('Invalid payload: The DID is not valid'); +}); + +test('did-update. Send deactivated did', async ({ request }) => { + const payload = await request.post(`/1.0/update`, { + data: { + did: deactiveDid, + didDocument: [didPayload], + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didState).toBeDefined(); + expect(body.didState.description).toEqual('Invalid payload: The DID does not exist or is Deactivated'); +}); + +test('did-update. Send wrong operation', async ({ request }) => { + const payload = await request.post(`/1.0/update`, { + data: { + didDocument: [didPayload], + did: didPayload.id, + didDocumentOperation: ['removeFromDidDocument'], + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didState).toBeDefined(); + expect(body.didState.description).toEqual('Invalid payload: Only Set operation is supported'); +}); + +test('did-deactivate. invalid did', async ({ request }) => { + const payload = await request.post(`/1.0/deactivate`, { + data: { + did: indyDid, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didState).toBeDefined(); + expect(body.didState.description).toEqual('Invalid payload: The DID is not valid'); +}); + +test('did-deactivate. Send deactivated did', async ({ request }) => { + const payload = await request.post(`/1.0/deactivate`, { + data: { + did: deactiveDid, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didState).toBeDefined(); + expect(body.didState.description).toEqual('Invalid payload: The DID does not exist or is Deactivated'); +}); diff --git a/tests/resource/createResource.spec.ts b/tests/resource/createResource.spec.ts new file mode 100644 index 0000000..5c1c60d --- /dev/null +++ b/tests/resource/createResource.spec.ts @@ -0,0 +1,181 @@ +import { test, expect } from '@playwright/test'; +import { sign } from '@stablelib/ed25519'; +import { toString, fromString } from 'uint8arrays'; +import base64url from 'base64url'; + +import * as dotenv from 'dotenv'; +import { assert } from 'console'; + +dotenv.config(); + +const pub_key_base_64 = process.env.TEST_PUBLIC_KEY; +const priv_key_base_64 = process.env.TEST_PRIVATE_KEY; + +assert(pub_key_base_64, 'TEST_PUBLIC_KEY is not defined'); +assert(priv_key_base_64, 'TEST_PRIVATE_KEY is not defined'); + +const pubKeyHex = toString(fromString(pub_key_base_64 as string, 'base64pad'), 'base16'); +const privKeyBytes = base64url.toBuffer(priv_key_base_64 as string); + +let didPayload; +let didState; +let didUrlState; +let jobId; +let resourceJobId; + +test('did-document. Generate the payload', async ({ request }) => { + const payload = await request.get( + `/1.0/did-document?verificationMethod=JsonWebKey2020&methodSpecificIdAlgo=uuid&network=testnet&publicKeyHex=${pubKeyHex}` + ); + + expect(payload.status()).toBe(200); + + const body = await payload.json(); + expect(body.didDoc).toBeDefined(); + expect(body.key).toBeDefined(); + expect(body.key.kid).toBeDefined(); + expect(body.key.publicKeyHex).toBeDefined(); + + didPayload = body.didDoc; +}); + +test('resource-create. Initiate DID Create procedure', async ({ request }) => { + const payload = await request.post('/1.0/create', { + data: { + didDocument: didPayload, + secret: {}, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(200); + + const body = await payload.json(); + + expect(body.jobId).toBeDefined(); + expect(body.didState).toBeDefined(); + expect(body.didState.did).toBeDefined(); + expect(body.didState.state).toBeDefined(); + expect(body.didState.secret).toBeDefined(); + + didState = body.didState; + jobId = body.jobId; +}); + +test('resource-create. Send the final request for DID creation', async ({ request }) => { + const serializedPayload = didState.signingRequest[0].serializedPayload; + const serializedBytes = Buffer.from(serializedPayload, 'base64'); + const signature = sign(privKeyBytes, serializedBytes); + + const secret = { + signingResponse: [ + { + kid: didState.signingRequest[0].kid, + signature: toString(signature, 'base64'), + }, + ], + }; + + const didCreate = await request.post(`/1.0/create`, { + data: { + jobId: jobId, + secret: secret, + options: { + network: 'testnet', + }, + didDocument: didPayload, + }, + }); + + expect(didCreate.status()).toBe(201); +}); + +test('resource-create. Initiate Resource creation procedure', async ({ request }) => { + const payload = await request.post(`/1.0/createResource`, { + data: { + did: didPayload.id, + content: 'SGVsbG8gV29ybGQ=', + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(200); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.didUrl).toBeDefined(); + expect(body.didUrlState.state).toBeDefined(); + expect(body.didUrlState.signingRequest).toBeDefined(); + + didUrlState = body.didUrlState; + resourceJobId = body.jobId; +}); + +test('resource-create. Send the final request for Resource creation', async ({ request }) => { + const serializedPayload = didUrlState.signingRequest[0].serializedPayload; + const serializedBytes = Buffer.from(serializedPayload, 'base64'); + const signature = sign(privKeyBytes, serializedBytes); + + const secret = { + signingResponse: [ + { + kid: didUrlState.signingRequest[0].kid, + signature: toString(signature, 'base64'), + }, + ], + }; + + const resourceCreate = await request.post(`/1.0/createResource`, { + data: { + did: didPayload.id, + content: 'SGVsbG8gV29ybGQ=', + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + jobId: resourceJobId, + secret: secret, + options: { + network: 'testnet', + }, + }, + }); + const response = await resourceCreate.json(); + expect(resourceCreate.status()).toBe(201); + expect(response.didUrlState).toBeDefined(); + expect(response.didUrlState.didUrl).toBeDefined(); + expect(response.didUrlState.state).toBeDefined(); + expect(response.didUrlState.state).toEqual('finished'); + expect(response.didUrlState.name).toEqual('ResourceName'); + expect(response.didUrlState.type).toEqual('TextDocument'); + expect(response.didUrlState.version).toEqual('1.0'); +}); + +test('resource-create. Fail second create with same name and type', async ({ request }) => { + const payload = await request.post(`/1.0/createResource`, { + data: { + did: didPayload.id, + content: 'SGVsbG8gV29ybGQ=', + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.state).toBeDefined(); + expect(body.didUrlState.state).toEqual('failed'); + expect(body.didUrlState.description).toEqual('Invalid payload: Resource already exists'); +}); diff --git a/tests/resource/resource-create.spec.ts b/tests/resource/resource-create.spec.ts index d109927..951283f 100644 --- a/tests/resource/resource-create.spec.ts +++ b/tests/resource/resource-create.spec.ts @@ -63,7 +63,7 @@ test('resource-create. Initiate DID Create procedure', async ({ request }) => { jobId = body.jobId; }); -test('resource-create. Send the final request for DID creating', async ({ request }) => { +test('resource-create. Send the final request for DID creation', async ({ request }) => { const serializedPayload = didState.signingRequest[0].serializedPayload; const serializedBytes = Buffer.from(serializedPayload, 'base64'); const signature = sign(privKeyBytes, serializedBytes); @@ -111,7 +111,7 @@ test('resource-create. Initiate Resource creation procedure', async ({ request } jobId = body.jobId; }); -test('resource-create. Send the final request for Resource creating', async ({ request }) => { +test('resource-create. Send the final request for Resource creation', async ({ request }) => { const serializedPayload = resourceState.signingRequest[0].serializedPayload; const serializedBytes = Buffer.from(serializedPayload, 'base64'); const signature = sign(privKeyBytes, serializedBytes); diff --git a/tests/resource/updateResource.spec.ts b/tests/resource/updateResource.spec.ts new file mode 100644 index 0000000..3d5304b --- /dev/null +++ b/tests/resource/updateResource.spec.ts @@ -0,0 +1,313 @@ +import { test, expect } from '@playwright/test'; +import { sign } from '@stablelib/ed25519'; +import { toString, fromString } from 'uint8arrays'; +import base64url from 'base64url'; + +import * as dotenv from 'dotenv'; +import { assert } from 'console'; + +dotenv.config(); + +const pub_key_base_64 = process.env.TEST_PUBLIC_KEY; +const priv_key_base_64 = process.env.TEST_PRIVATE_KEY; + +assert(pub_key_base_64, 'TEST_PUBLIC_KEY is not defined'); +assert(priv_key_base_64, 'TEST_PRIVATE_KEY is not defined'); + +const pubKeyHex = toString(fromString(pub_key_base_64 as string, 'base64pad'), 'base16'); +const privKeyBytes = base64url.toBuffer(priv_key_base_64 as string); + +let didPayload; +let didState; +let didUrlState; +let jobId; +let resourceJobId; +let resourceId; + +test('did-document. Generate the payload', async ({ request }) => { + const payload = await request.get( + `/1.0/did-document?verificationMethod=JsonWebKey2020&methodSpecificIdAlgo=uuid&network=testnet&publicKeyHex=${pubKeyHex}` + ); + + expect(payload.status()).toBe(200); + + const body = await payload.json(); + expect(body.didDoc).toBeDefined(); + expect(body.key).toBeDefined(); + expect(body.key.kid).toBeDefined(); + expect(body.key.publicKeyHex).toBeDefined(); + + didPayload = body.didDoc; +}); + +test('resource-update. Initiate DID Create procedure', async ({ request }) => { + const payload = await request.post('/1.0/create', { + data: { + didDocument: didPayload, + secret: {}, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(200); + + const body = await payload.json(); + + expect(body.jobId).toBeDefined(); + expect(body.didState).toBeDefined(); + expect(body.didState.did).toBeDefined(); + expect(body.didState.state).toBeDefined(); + expect(body.didState.secret).toBeDefined(); + + didState = body.didState; + jobId = body.jobId; +}); + +test('resource-update. Send the final request for DID creation', async ({ request }) => { + const serializedPayload = didState.signingRequest[0].serializedPayload; + const serializedBytes = Buffer.from(serializedPayload, 'base64'); + const signature = sign(privKeyBytes, serializedBytes); + + const secret = { + signingResponse: [ + { + kid: didState.signingRequest[0].kid, + signature: toString(signature, 'base64'), + }, + ], + }; + + const didCreate = await request.post(`/1.0/create`, { + data: { + jobId: jobId, + secret: secret, + options: { + network: 'testnet', + }, + didDocument: didPayload, + }, + }); + + expect(didCreate.status()).toBe(201); +}); + +test('resource-update. Initiate Resource creation procedure', async ({ request }) => { + const payload = await request.post(`/1.0/createResource`, { + data: { + did: didPayload.id, + content: 'SGVsbG8gV29ybGQ=', + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(200); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.didUrl).toBeDefined(); + expect(body.didUrlState.state).toBeDefined(); + expect(body.didUrlState.signingRequest).toBeDefined(); + + didUrlState = body.didUrlState; + resourceJobId = body.jobId; +}); + +test('resource-update. Send the final request for Resource creation', async ({ request }) => { + const serializedPayload = didUrlState.signingRequest[0].serializedPayload; + const serializedBytes = Buffer.from(serializedPayload, 'base64'); + const signature = sign(privKeyBytes, serializedBytes); + + const secret = { + signingResponse: [ + { + kid: didUrlState.signingRequest[0].kid, + signature: toString(signature, 'base64'), + }, + ], + }; + + const resourceCreate = await request.post(`/1.0/createResource`, { + data: { + did: didPayload.id, + content: 'SGVsbG8gV29ybGQ=', + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + jobId: resourceJobId, + secret: secret, + options: { + network: 'testnet', + }, + }, + }); + const response = await resourceCreate.json(); + expect(resourceCreate.status()).toBe(201); + expect(response.didUrlState).toBeDefined(); + expect(response.didUrlState.didUrl).toBeDefined(); + expect(response.didUrlState.state).toBeDefined(); + expect(response.didUrlState.state).toEqual('finished'); + expect(response.didUrlState.name).toEqual('ResourceName'); + expect(response.didUrlState.type).toEqual('TextDocument'); + expect(response.didUrlState.version).toEqual('1.0'); + console.log('DIDUrl:' + response.didUrlState.didUrl); + resourceId = response.didUrlState.didUrl.split('/resources/')[1]; +}); + +test('resource-update. Initiate Resource update procedure', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: didPayload.id, + name: 'ResourceName', + type: 'TextDocument', + content: ['SGVsbG8gV29ybGQ='], + version: '2.0', + relativeDidUrl: '/resources/' + resourceId, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(200); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.didUrl).toBeDefined(); + expect(body.didUrlState.state).toBeDefined(); + expect(body.didUrlState.signingRequest).toBeDefined(); + + didUrlState = body.didUrlState; + resourceJobId = body.jobId; +}); + +test('resource-update. Send the final request for Resource update', async ({ request }) => { + const serializedPayload = didUrlState.signingRequest[0].serializedPayload; + const serializedBytes = Buffer.from(serializedPayload, 'base64'); + const signature = sign(privKeyBytes, serializedBytes); + + const secret = { + signingResponse: [ + { + kid: didUrlState.signingRequest[0].kid, + signature: toString(signature, 'base64'), + }, + ], + }; + + const resourceUpdate = await request.post(`/1.0/updateResource`, { + data: { + did: didPayload.id, + content: ['SGVsbG8gV29ybGQ='], + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + jobId: resourceJobId, + relativeDidUrl: '/resources/' + resourceId, + secret: secret, + options: { + network: 'testnet', + }, + }, + }); + const response = await resourceUpdate.json(); + expect(resourceUpdate.status()).toBe(201); + expect(response.didUrlState).toBeDefined(); + expect(response.didUrlState.didUrl).toBeDefined(); + expect(response.didUrlState.state).toBeDefined(); + expect(response.didUrlState.state).toEqual('finished'); + expect(response.didUrlState.name).toEqual('ResourceName'); + expect(response.didUrlState.type).toEqual('TextDocument'); + expect(response.didUrlState.version).toEqual('2.0'); +}); + +test('resource-update. Resource update without relativeDidUrl', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: didPayload.id, + name: 'ResourceName', + type: 'TextDocument', + content: ['SGVsbG8gV29ybGQ='], + version: '3.0', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(200); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.didUrl).toBeDefined(); + expect(body.didUrlState.state).toBeDefined(); + expect(body.didUrlState.signingRequest).toBeDefined(); + + didUrlState = body.didUrlState; + resourceJobId = body.jobId; +}); + +test('resource-update. Send the final update without relativeDidUrl', async ({ request }) => { + const serializedPayload = didUrlState.signingRequest[0].serializedPayload; + const serializedBytes = Buffer.from(serializedPayload, 'base64'); + const signature = sign(privKeyBytes, serializedBytes); + + const secret = { + signingResponse: [ + { + kid: didUrlState.signingRequest[0].kid, + signature: toString(signature, 'base64'), + }, + ], + }; + + const resourceUpdate = await request.post(`/1.0/updateResource`, { + data: { + did: didPayload.id, + jobId: resourceJobId, + secret: secret, + options: { + network: 'testnet', + }, + }, + }); + const response = await resourceUpdate.json(); + expect(resourceUpdate.status()).toBe(201); + expect(response.didUrlState).toBeDefined(); + expect(response.didUrlState.didUrl).toBeDefined(); + expect(response.didUrlState.state).toBeDefined(); + expect(response.didUrlState.state).toEqual('finished'); + expect(response.didUrlState.name).toEqual('ResourceName'); + expect(response.didUrlState.type).toEqual('TextDocument'); + expect(response.didUrlState.version).toEqual('3.0'); +}); + +test('resource-update. Fail Resource update with existing nextVersionId', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: didPayload.id, + name: 'ResourceName', + type: 'TextDocument', + content: ['SGVsbG8gV29ybGQ='], + version: '4.0', + relativeDidUrl: '/resources/' + resourceId, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.state).toBeDefined(); + expect(body.didUrlState.state).toEqual('failed'); + expect(body.didUrlState.description).toEqual('Invalid payload: Only latest version of resource can be updated'); +}); diff --git a/tests/resource/validateResource.spec.ts b/tests/resource/validateResource.spec.ts new file mode 100644 index 0000000..c780999 --- /dev/null +++ b/tests/resource/validateResource.spec.ts @@ -0,0 +1,282 @@ +import { test, expect } from '@playwright/test'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +let activeDid = 'did:cheqd:testnet:a9ed7bb4-d706-4454-bbbc-feabebe801b8'; +let indyDid = 'did:indy:sovrin:WRfXPg8dantKVubE3HX8pw'; +let deactiveDid = 'did:cheqd:testnet:ca9ff47c-0286-4614-a4be-8ffa83911e09'; +let didUrl = '/resources/bf94eb78-228b-4e4a-88be-1ef2fa44be48'; + +test('resource-create. wrong did', async ({ request }) => { + const payload = await request.post(`/1.0/createResource`, { + data: { + did: indyDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: The DID is not valid'); +}); + +test('resource-create. Fail to send content', async ({ request }) => { + const payload = await request.post(`/1.0/createResource`, { + data: { + did: activeDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: name, type and content are required'); +}); + +test('resource-create. Send wrong content type', async ({ request }) => { + const payload = await request.post(`/1.0/createResource`, { + data: { + did: activeDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + content: 50, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: Invalid payload'); +}); +test('resource-create. Send deactivated did', async ({ request }) => { + const payload = await request.post(`/1.0/createResource`, { + data: { + did: deactiveDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + content: 'Test Data', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: The DID does not exist or is Deactivated'); +}); + +test('resource-update. wrong did', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: indyDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: The DID is not valid'); +}); + +test('resource-update. Fail to send content', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: activeDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: name, type and content are required'); +}); + +test('resource-update. Send wrong content type', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: activeDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + content: [50], + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual( + 'Invalid payload: The content array must be provided and must have exactly one string' + ); +}); +test('resource-update. Send wrong didUrl', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: deactiveDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + content: ['Test Data'], + relativeDidUrl: 'abcdef', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: The DID URL is not valid'); +}); +test('resource-update. Send wrong operation', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: deactiveDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + content: ['Test Data'], + relativeDidUrl: didUrl, + contentOperation: ['removeContent'], + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: Only Set operation is supported'); +}); +test('resource-update. Send deactivated did', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: deactiveDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + content: ['Test Data'], + relativeDidUrl: didUrl, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: The DID does not exist or is Deactivated'); +}); +test('resource-update. Send wrong name/type', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: activeDid, + name: 'ResourceName1', + type: 'TextDocument', + version: '1.0', + content: ['Test Data'], + relativeDidUrl: didUrl, + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual( + 'Invalid payload: Update resource name or type does not match existing resource' + ); +}); +test('resource-update. Resource not found', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: activeDid, + name: 'ResourceName', + type: 'TextDocument', + version: '1.0', + content: ['Test Data'], + relativeDidUrl: '/resources/1234567', + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: Resource does not exist'); +}); + +test('resource-update. Send wrong name/type without relativeDidUrl', async ({ request }) => { + const payload = await request.post(`/1.0/updateResource`, { + data: { + did: activeDid, + name: 'ResourceName2', + type: 'TextDocument2', + version: '1.0', + content: ['Test Data'], + options: { + network: 'testnet', + }, + }, + }); + + expect(payload.status()).toBe(400); + + const body = await payload.json(); + expect(body.didUrlState).toBeDefined(); + expect(body.didUrlState.description).toEqual('Invalid payload: Resource does not exist'); +});